Birdfolio
by @tonbistudio
Bird identification, life list tracking, and trading card generation. Use this skill when the user: sends a bird photo to identify, says "set up my Birdfolio...
clawhub install birdfolioπ About This Skill
name: birdfolio description: > Bird identification, life list tracking, and trading card generation. Use this skill when the user: sends a bird photo to identify, says "set up my Birdfolio" or "set my region", asks "how's my checklist", asks "Birdfolio progress", asks "how many birds have I found", asks "show my Birdfolio" or "show my life list", asks "what's my rarest bird", or asks "tell me about [bird species]". Handles everything from first-time setup through ongoing life list tracking and visual trading card generation.
Birdfolio
Birdfolio turns bird photos into a personal life list. Users photograph birds in the wild, send the photo to you, and you identify the species with Vision. You.com provides real-time rarity and regional data. Each sighting is logged to a life list with a PokΓ©mon-inspired rarity tier (Common / Rare / Super Rare) and gets a visual trading card sent back via Telegram.
Data lives in: Railway PostgreSQL (via API) + local birdfolio/ folder (cards, birds, config)
Scripts live in: {baseDir}/scripts/
API: https://api-production-d0e2.up.railway.app (also saved to birdfolio/config.json after init)
Schema reference: {baseDir}/references/data-schema.md
Search queries: {baseDir}/references/you-search-queries.md
> Note on --workspace & --api-url: Every data script accepts --workspace (absolute path to birdfolio/) and --api-url (API base URL). After init_birdfolio.py runs, both the API URL and Telegram ID are saved to birdfolio/config.json and read automatically β subsequent scripts only need --workspace.
>
> Telegram ID: Read from the inbound message metadata (sender_id). Pass as --telegram-id to init_birdfolio.py on first setup.
1. Setup Flow
Trigger: User says "Set up my Birdfolio", "set my region", or sends a photo before setup exists.
Check first: If birdfolio/config.json exists in your workspace, setup is already done β skip to the relevant flow.
Steps:
1. Ask: *"What's your home region? (e.g. California, Texas, United Kingdom)"*
2. Run to create the workspace folder structure and register the user in the API:
exec: python {baseDir}/scripts/init_birdfolio.py \
--telegram-id {senderTelegramId} \
--region "{region}" \
--api-url "https://api-production-d0e2.up.railway.app" \
--workspace
3. Search You.com (run all three):
"{region} most common backyard birds eBird species list"
"{region} uncommon seasonal rare birds eBird checklist"
"{region} rare vagrant endangered birds eBird"
4. From results, build a checklist with 10 common, 5 rare, 1 super rare species. Use classification signals from {baseDir}/references/you-search-queries.md.
5. Write the populated checklist to birdfolio/checklist.json in your workspace:
{
"{region}": {
"common": [
{ "species": "American Robin", "slug": "american-robin", "found": false, "dateFound": null }
],
"rare": [...],
"superRare": [...]
}
}
6. Reply with a welcome message and checklist preview:
π¦
Birdfolio is set up for {region}! Your checklist:
Common (10): American Robin, House Sparrow, ...
Rare (5): Great Blue Heron, ...
Super Rare: California Condor
Send me a bird photo to start collecting!
2. Bird Identification Flow
Trigger: User sends a photo.
> Getting the photo file path: When a user sends a photo via Telegram, OpenClaw downloads it and makes the local file path available in the message attachment metadata. Capture this path β you'll need it for card generation in Step 5. If OpenClaw provides the image inline without a path, use exec to find the most recently downloaded file in OpenClaw's temp/media folder, or check %APPDATA%\openclaw\media\ on Windows. Save the photo to birdfolio/birds/{slug}-{timestamp}.jpg for permanent storage:
>
> exec: copy "" "birdfolio/birds/-.jpg"
> Step 1 β Identify with Vision
The submitted photo is directly visible in your context. Analyze it (or use the image tool if it's not inline):
Identify the bird species in this photo. Return JSON only:
{
"commonName": "...",
"scientificName": "...",
"confidence": "high|medium|low",
"features": ["visible feature 1", "visible feature 2"]
}
Rarity rules:
common, rare, or superRarebonus (shows a neutral "Bonus Find" badge, no rarity assigned)Confidence rules:
"high" β proceed automatically, no confirmation needed"medium" β ask: *"I think this might be a [species] β based on [features]. Does that look right to you?"* β wait for confirmation before continuing"low" β reply: *"This photo isn't clear enough for me to be confident. Could you send a clearer shot?"* β stop, do not log anythingStep 2 β Rarity lookup
Search You.com:
"{commonName} {homeRegion} eBird frequency how common rare"
Classify using these signals:
| Tier | Script value | Signals |
|------|-------------|---------|
| Common π’ | common | "abundant", "widespread", "year-round resident", >50% of checklists |
| Rare π‘ | rare | "uncommon", "seasonal", "migratory", "occasional", 5β50% of checklists |
| Super Rare π΄ | superRare | "rare", "vagrant", "accidental", "endangered", <5% of checklists |
When unsure β default to rare. Always use the script value (e.g. superRare, not Super Rare) when passing --rarity to any script.
Step 3 β Get a fun fact
Search You.com:
"{commonName} bird interesting facts habitat behavior"
Extract one punchy fact (1β2 sentences).
Step 4 β Log the sighting
Save the sighting to birdfolio/lifeList.json in your workspace:
exec: python {baseDir}/scripts/log_sighting.py \
--species "{commonName}" \
--scientific-name "{scientificName}" \
--rarity "{rarity}" \
--region "{homeRegion}" \
--notes "" \
--workspace
Capture from output: isLifer, totalSightings, totalSpecies.
Step 5 β Update checklist
Mark the species as found in birdfolio/checklist.json:
exec: python {baseDir}/scripts/update_checklist.py \
--species "{commonName}" \
--region "{homeRegion}" \
--workspace
Step 6 β Generate trading card
The card is a two-column design: the user's photo fills the left panel (280px), a solid dark info panel sits on the right. Always use the user's actual submitted photo β not a stock image.
Step 6a β Detect bird position with Vision:
Use the image tool on the submitted photo:
> "Where is the bird positioned horizontally in this photo? Give me approximately what percentage from the left edge the bird's center is (0β100)."
Convert the answer to a CSS value: "40% center", "60% center", "center center", etc. Use this as --object-position.
Step 6b β Generate the card HTML with the embedded photo:
exec: python {baseDir}/scripts/generate_card.py \
--species "{commonName}" \
--scientific-name "{scientificName}" \
--rarity "{rarity}" \
--region "{homeRegion}" \
--date "{YYYY-MM-DD}" \
--fun-fact "{funFact}" \
--image-path "" \
--object-position "{objectPosition}" \
--life-count {totalSpecies} \
--workspace
--image-path embeds the user's actual photo as base64 directly into the HTML. No separate embed step needed.
Fallback if photo path is unavailable: omit --image-path and pass --image-url " instead (find a URL via You.com: "{commonName} bird photo wildlife").
Capture cardPath from output.
Step 6c β Screenshot, save, upload, and send: Run the screenshot script to render the card at 600Γ400 and save a PNG:
exec: node {baseDir}/scripts/screenshot_card.js ""
Capture pngPath from output.Upload to Cloudflare R2 and get a public URL:
exec: python {baseDir}/scripts/upload_card.py ""
Capture url from output.Update the sighting's card URL in the API (use the id from the log_sighting output):
PATCH /users/{telegram_id}/sightings/{sighting_id}/card
Body: {"card_png_url": ""}
Send the PNG via Telegram:
message(action="send", media="")
Step 7 β Reply
isLifer is true: If totalSpecies == 1 (this is their very first bird ever): also send their personal PWA link:
*"π¦
Your Birdfolio is live! Bookmark this link to see your life list:
https://birdfolio.tonbistudio.com/app/[telegram_id]"*
The telegram_id is the sender's Telegram ID from the inbound message metadata (sender_id). This is also stored in birdfolio/config.json after init.
Include: rarity badge emoji, the fun fact, checklist status (if species was on checklist, mention it).
Fallback if screenshot fails: Send a formatted text card:
π¦
[RARITY_EMOJI] [Common Name]
Scientific: [Scientific Name]
Region: [Region] | Spotted: [Date]
Rarity: [Rarity]
π‘ [Fun Fact]
Bird #[N] in your Birdfolio
3. Checklist & Stats
Trigger: "How's my checklist?", "Birdfolio progress", "How many birds have I found?"
exec: python {baseDir}/scripts/get_stats.py \
--workspace
Format response using checklistProgress from output:
π {region} ChecklistCommon β
β
β
β¬β¬β¬β¬β¬β¬β¬ 3/10
Rare β
β¬β¬β¬β¬ 1/5
Super Rare β¬ 0/1
π¦ {totalSpecies} species | {totalSightings} total sightings
π Last spotted: {mostRecentSighting.commonName} on {date}
π Rarest find: {rarestBird.commonName} ({rarity})
Use β for found, β¬ for not found. One box per species.
Optional visual checklist card: Generate a visual HTML checklist card and screenshot it:
exec: python {baseDir}/scripts/generate_checklist_card.py \
--workspace
Then screenshot with screenshot_card.js and send the PNG.4. Life List View
Trigger: "Show my Birdfolio", "Show my life list"
Read birdfolio/lifeList.json from your workspace.
Group lifers by rarity (Super Rare first, then Rare, then Common). Format as a text list or generate an HTML gallery, save it to birdfolio/my-birdfolio.html in your workspace, and screenshot it.
5. Species Lookup (no logging)
Trigger: "Tell me about [species]"
Search You.com:
"{species} bird facts habitat range behavior diet"
"{species} bird {homeRegion} eBird frequency resident or migratory"
Return a conversational summary. Do not log a sighting or generate a card.
6. Rarest Bird
Trigger: "What's my rarest bird?", "Show my best find"
exec: python {baseDir}/scripts/get_stats.py \
--workspace
Read rarestBird from output and reply with species name, rarity, date spotted, and region.
Quick Reference
| Script | Key args | Returns |
|--------|----------|---------|
| init_birdfolio.py | --telegram-id, --region, --api-url, --workspace | {status, workspace, files_created, next} |
| log_sighting.py | --species, --scientific-name, --rarity, --region, --date, --workspace | {status, sighting, isLifer, totalSightings, totalSpecies} |
| update_checklist.py | --species, --region, --workspace | {status, tier, dateFound} or {status: not_on_checklist} |
| get_stats.py | --workspace | {totalSightings, totalSpecies, checklistProgress, mostRecentSighting, rarestBird} |
| generate_card.py | --species, --scientific-name, --rarity, --region, --date, --fun-fact, --image-path (preferred) OR --image-url, --object-position, --life-count, --workspace | {status, cardPath, filename} |
| generate_checklist_card.py | --workspace | {status, cardPath} β visual HTML checklist card |
| screenshot_card.js | [outputPath] | {status, pngPath} β saves PNG to birdfolio/cards/ |
| upload_card.py | [--secrets path] | {status, url} β uploads to R2, returns public URL |
All Python scripts output JSON to stdout. Always pass absolute --workspace path.
screenshot_card.js uses OpenClaw's bundled playwright-core + system Chrome/Edge (no separate install needed).