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 tobirdfolio/) and--api-url(API base URL). Afterinit_birdfolio.pyruns, both the API URL and Telegram ID are saved tobirdfolio/config.jsonand read automatically — subsequent scripts only need--workspace.Telegram ID: Read from the inbound message metadata (
sender_id). Pass as--telegram-idtoinit_birdfolio.pyon 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:
-
Ask: "What's your home region? (e.g. California, Texas, United Kingdom)"
-
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 <absolute path to birdfolio/ in your workspace> -
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" -
From results, build a checklist with 10 common, 5 rare, 1 super rare species. Use classification signals from
{baseDir}/references/you-search-queries.md. -
Write the populated checklist to
birdfolio/checklist.jsonin your workspace:{ "{region}": { "common": [ { "species": "American Robin", "slug": "american-robin", "found": false, "dateFound": null } ], "rare": [...], "superRare": [...] } } -
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
execto find the most recently downloaded file in OpenClaw's temp/media folder, or check%APPDATA%\openclaw\media\on Windows. Save the photo tobirdfolio/birds/{slug}-{timestamp}.jpgfor permanent storage:exec: copy "<attachment path>" "birdfolio/birds/<slug>-<timestamp>.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:
- Bird IS on the checklist → use its tier:
common,rare, orsuperRare - Bird is NOT on the checklist → use
bonus(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 anything
Step 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 <absolute path to birdfolio/ in your 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 <absolute path to birdfolio/ in your 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 "<absolute path to submitted photo>" \
--object-position "{objectPosition}" \
--life-count {totalSpecies} \
--workspace <absolute path to birdfolio/ in your 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 "<stock photo 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 "<cardPath>"
Capture pngPath from output.
Upload to Cloudflare R2 and get a public URL:
exec: python {baseDir}/scripts/upload_card.py "<pngPath>"
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": "<url>"}
Send the PNG via Telegram:
message(action="send", media="<pngPath>")
Step 7 — Reply
-
If
isLiferis true: "🎉 New lifer! That's your first ever [commonName]! Bird #[totalSpecies] in your Birdfolio."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_idis the sender's Telegram ID from the inbound message metadata (sender_id). This is also stored inbirdfolio/config.jsonafter init. -
Otherwise: "[commonName] spotted! You've now seen [N] species in your Birdfolio."
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 <absolute path to birdfolio/ in your workspace>
Format response using checklistProgress from output:
📋 {region} Checklist
Common ✅✅✅⬜⬜⬜⬜⬜⬜⬜ 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 <absolute path to birdfolio/ in your 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 <absolute path to birdfolio/ in your 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 | <cardPath> [outputPath] | {status, pngPath} — saves PNG to birdfolio/cards/ |
upload_card.py | <pngPath> [--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).