================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Birder's Life List
## 1. Project
**Birder's Life List** is a personal field-companion and lifetime-list
builder for amateur ornithologists who keep returning to the same few
patches of water and woodland. The user logs each sighting by a
six-second voice note pressed out in the field, sometimes with a
photograph clipped onto the entry if the bird sat still long enough,
and the app turns the messy stream of voice notes into a structured
lifetime list, a map of every patch the user has walked, and a quiet
suggestion engine that knows what they have not yet seen at this site
this season. Each entry keeps the original audio, the verbatim
transcript, the photograph if there is one, and structured metadata:
the species (or candidates, if the user wasn't sure), the location, the
date and time, the weather, the behaviour described, and the
confidence band the app assigns to its own identification.
This is the kind of app a birder builds for himself after his
hundredth walk around the same reservoir — because his sightings live
in three notebooks, a damp birthday card with names scribbled on the
back, two Excel files he stopped maintaining in 2019, and the voice
notes app on his phone, and he wants the list of his life as one
thing he can hold. It is also the kind of app a retired
schoolteacher in a tower block in Naivasha builds the year after she
takes up birding on her morning walks to the lake, with a small pair
of borrowed Nikon binoculars and an old Android phone, and finds that
the printed regional field guides she can afford do not cover the
last fifteen species she has half-identified. Same shape of moment,
different continent, different gear, different list.
The single demo that proves the magic: the user is standing at the
willow scrub at the south end of the reservoir at 7:12 am, presses
the record button, says "flycatcher, female, near the willows, low
in the willows, called twice, tail-flicked, didn't get a clear look
at the eye ring" → in under twenty seconds the app posts a
structured entry to the lifetime list: candidate species "Spotted
Flycatcher (Muscicapa striata)" with confidence 0.78 and an
alternative "Pied Flycatcher (Ficedula hypoleuca), female" at 0.18.
The map updates with a pin at the willows. A quiet panel surfaces
underneath: "You have not yet recorded a Garden Warbler at this site
this spring — listen for a fast, rich warble from inside the
hawthorn."
And in the harder cases — a confusing autumn warbler at last light, a
calling Acrocephalus heard but not seen, a high-flying raptor in
silhouette over a Kenyan rift — the app refuses to invent a species
ID. It surfaces the top three candidates with what would distinguish
them in the field, asks the user one question they can answer the
next time they go out, and stores the entry as "candidates" until the
user resolves it themselves. The lifetime list never silently fills
in a sighting the user wasn't sure of.
**Tagline:** _Turn six-second voice notes into a lifetime list — in
any patch, any continent, with every behaviour, weather note, and
honest uncertainty preserved._
## 2. Target audience
- Amateur ornithologists who walk the same patch (reservoir, urban park, allotment, coastline, garden hide) three or more times a week and have stopped using printed field notebooks because their hands are full
- Retirees in their fifties to eighties who took up birding after work or after raising children and want a structured lifetime list without learning a new SaaS UI
- Diaspora birders watching unfamiliar species at a new home — Pakistani-British birder at a Manchester reservoir, Vietnamese-American birder in the South Bay, Polish-Brazilian birder in a São Paulo park — comparing what they see now with what they remember from home
- Birders in the global majority working with field guides that are out of date, partially translated, or cover the next country over — Kenyan, Tanzanian, Sri Lankan, Indonesian, Colombian, Peruvian, Filipino birders who need an app that is honest about what it does not know
- School and university bird-club organisers logging group sightings on Saturday morning walks who want a shared archive their teenage members can search by patch
- Citizen-science contributors who want to keep their personal life list in their own database first and choose deliberately what to share with eBird, BirdTrack, the Atlas of Living Australia, or the Kenya Bird Map
- People returning to birding after a long pause (illness, bereavement, retirement, returning travel) who want to rebuild a list from memory plus new walks
- Bird-walk guides who keep separate per-client lists across the year and want one place that holds every site, every season, every group
- Single-site obsessives — the reservoir-only birder, the garden-only birder, the cemetery-only birder — for whom the lifetime list and the patch list are the same thing
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Six-second voice in, structured entry out** — the user does not stop birding to type. Gemini 3.5 Flash does the multimodal audio reasoning + species candidate ranking + behaviour parse in one call, even when the voice note is fragmentary, whispered into a sleeve, or includes the dog. Background bird vocalisations in the voice note itself are noted as candidate clues but never silently promoted to confirmed sightings.
- **Never invents a species ID** — this is the hard rule. If the description is ambiguous, the app surfaces two or three candidates with confidence bands and a single distinguishing question ("did you see the wingbars clearly?") instead of guessing. A sighting can sit in `candidates` for weeks until the user resolves it.
- **The lifetime list is a real list** — chronological, filterable, exportable. It is yours, in your database, before it is anyone else's. Sharing to eBird, BirdTrack, or the local atlas is a deliberate act, not a default.
- **Patch-aware suggestions, not gamified prompts** — the app knows what the user has and has not recorded at this site this season, and it surfaces "you haven't seen X yet this autumn at the reservoir; X is most likely in the hawthorn after the first cold front" — phrased as a fieldcraft prompt, never as a streak nudge.
- **Maps every patch across any continent** — postmarked by the GPS at the moment of the voice note (with manual override and a "rough pin" mode for users who don't want exact coordinates published) and rendered with patch-level detail. A Manchester reservoir, Lake Naivasha, the Coyote Hills shoreline, Sungei Buloh, a private allotment hide — all first-class patches with their own list.
- **Multilingual fieldcraft** — the voice note can be in English, Swahili, Urdu, Tagalog, Vietnamese, Mandarin, Cantonese, Bengali, Tamil, Hindi, Punjabi, Amharic, Khmer, Farsi, Spanish, Portuguese, French, German, Polish. Common-name disambiguation handles regional names ("Maina" parsed as "Common Myna" in Karachi context, "Crested Myna" in Hanoi context); the scientific binomial is always also recorded so the list is portable across languages.
- **Honest about uncertainty** — every entry has a confidence band; the list view shows you which entries are confirmed, which are candidates, and which are heard-only. Heard-only entries are kept as `heard_only` and never silently merged with sight records.
- **Yours, private, exportable** — the database is your Firestore project; export to eBird CSV, BirdTrack XML, Atlas of Living Australia, Kenya Bird Map, or a plain CSV with one column per field. The original audio is yours; we never use your voice for training.
## 4. Features to build
- Single-tap record button on every screen (mobile-first) that records up to 30 seconds of audio with a press-and-hold gesture, releases to stop, and starts the structured-parse call before the user has put their phone back in their pocket
- A "quick photo" gesture on the same screen that snaps a photograph attached to the same entry (camera holds focus, optimises for distant subjects in low light)
- Multimodal parse — audio reasoning + multilingual transcript + species candidate ranking + behaviour parse + weather inference (if the user mentioned it) — in a single Gemini 3.5 Flash call per entry
- Image-only entry — when the user only got a photo, the app parses the photograph alone and offers candidates, with a follow-up voice note prompt for the behavioural detail
- Heard-only entry — when the user heard a call but didn't see the bird, the entry is kept as `heard_only`. If a recording of the call is in the audio, the app surfaces it as a separate candidate cue ("the recording contains a high-pitched single note repeated three times")
- Candidate-surface UI — every uncertain sighting gets two or three candidate species with a distinguishing question; resolving the question merges the candidates into one confirmed sighting or keeps the entry open
- Lifetime list view — chronological by date of first sighting, filterable by site, season, behaviour, family, candidate-vs-confirmed
- Site list view — per-patch list of every species recorded at this site, with seasonal occurrence (which months you've seen this species here)
- Map view — every patch as a circle of varying density (more sightings = denser); zooming in shows individual pins for each sighting, including the GPS at the moment of the voice note
- "You haven't seen yet" panel — surfaces species you have not yet recorded at this site this season, with a one-line fieldcraft note for each (where to look, what time, what to listen for)
- Audio playback — every entry retains the original audio; tap to replay. The transcript is shown alongside, with the species mentions highlighted
- Voice-note search — semantic search across the entire archive ("the time I saw a swift at dusk near the willows"), structured search ("Ficedula species at the reservoir in autumn"), and audio-content search ("clips where I described a wingbar")
- Confidence-band rendering — every entry shows a small badge: "confirmed", "high-confidence", "candidates", "heard-only", "review needed"
- Manual override — the user can change any field on any entry. The original AI parse is preserved in `original_parse[]` so the user can see what the model said before they overrode it
- eBird / BirdTrack / Kenya Bird Map / Atlas of Living Australia export — selectable per-entry, per-site, or full-list; CSV for eBird, XML for BirdTrack, JSON for the others
- Group-walk mode — multiple birders walk together and one phone records the group's sightings; each member can later "claim" entries they themselves saw to add to their personal list
- Private patches — a user can mark a patch as private (precise GPS hidden, only a rough region shown on shared exports); useful for nesting raptors, sensitive species, or private property
- Calendar view — a year-at-a-glance heatmap showing on which days the user went birding and how many species they recorded
- Patch comparison — side-by-side comparison of two patches showing species in common, species unique to each, and seasonality differences
- Field-guide grounding — when a candidate species is surfaced, the app pulls a short, grounded description from the relevant regional avifauna (with citation) so the user has the right field marks in hand without leaving the entry
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Audio reasoning** (Gemini 3.5 Flash) — accepts the user's voice note as `inlineData` (base64-encoded audio) or via the Files API (`files/*` resource name) and reasons about it directly. The model hears whispered words, slurred field shorthand, and the user's own description of behaviour ("tail-flicked", "underwing flash", "long undulating flight"). It does NOT transcribe the bird call in the background and infer the species from that audio alone — bird vocalisation classification is not a Gemini capability and any such inference must be flagged as user-described, not machine-heard. The model may note "the recording contains a high-pitched repeated note in the background" as a candidate cue.
- **Multimodal image input** (Gemini 3.5 Flash) — when the user attaches a photograph, the model reads field marks: eye ring, wing bars, primary projection, leg colour, posture, habitat clues, comparison size if other birds are in frame. Combines with the audio reasoning in a single call.
- **Multilingual transcription + reasoning** (Gemini 3.5 Flash) — the voice note can be in English, Swahili, Urdu, Hindi, Tamil, Bengali, Punjabi, Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Khmer, Amharic, Farsi, Arabic, Spanish, Portuguese, French, German, Polish. Regional common names ("Maina", "Bulbul", "Drongo") are disambiguated by the geographic context of the entry's GPS.
- **Structured output / JSON Schema** — the response matches the `Sighting` schema below. Every field is typed; the schema is included verbatim in the system instruction and as `responseSchema`.
- **Long context (1M tokens)** — the patch-suggestion call ("what haven't I seen at this site this season") reads the user's full lifetime list for that site (typically a few thousand sightings even after twenty years of patch birding) plus a baseline of expected species for the site's region and season in one call. **Guardrail**: a parsed Sighting averages ~600 tokens; a user with 5,000 lifetime sightings ≈ ~3M tokens (over the limit). Chunk by site for the patch-suggestion call and by year for the full-list summary call. The 1M ceiling is real and a heavy-patcher with twenty years of records will exceed it.
- **Search grounding** — for the field-guide grounding call, which pulls a short description of a candidate species's field marks from a trusted source (Birds of the World, Cornell All About Birds, Africa Bird Atlas, Avibase) with citations. Grounded search prevents the model from confidently inventing field marks for a species it has never seen.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — narrates the day's sightings on the walk back, in the user's chosen language and voice, at a relaxed pace. Reads each sighting's structured summary, not the original voice note (which the user already heard themselves speak).
- **Nano Banana 2** (`gemini-3.1-flash-image`) — generates a small printable card per species the first time the user adds it to their lifetime list ("a watercolour-style illustration of a male Spotted Flycatcher on a willow branch, soft afternoon light"). The card is for the user's own list cover; never used as a field-identification reference.
- **Thinking levels** — `medium` for the primary multimodal parse (audio + image + species candidate ranking). `low` for transcript-only multilingual cleanup, patch-suggestion, and field-guide grounding. Surface `thoughtSummary` only when the user taps the small "(i) how the model heard this" icon on a low-confidence entry.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for group-walk invitations) also requires the sender domain to be authorised in Firebase Auth. Lifetime lists are private to the owner; group walks share only what the owner explicitly opts into.
- **Database — Required.** Firestore for `users`, `patches`, `sightings`, `species_cards`, `walks`, `group_walk_members`, `exports`. Real-time listeners on the lifetime-list view so a sighting recorded on phone appears on tablet within seconds.
- **File storage — Required.** Firebase Storage for original voice notes (kept at upload bitrate, forever) + attached photographs + Nano Banana 2 species cards. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before the first voice note. Pre-signed URLs only; the audio is never publicly addressable.
- **Email — Required (transactional).** Group-walk invitations via email link (Firebase Auth magic links). Weekly digest emails are opt-in only and never sent by default.
- **Payments — Not needed for v1.** Free for personal use. A future "printed lifetime list book" tier could pipe to a print-on-demand partner (Lulu, Blurb) and charge for that physical artefact only.
- **External APIs:** Gemini API for all intelligence. Optional Mapbox or MapTiler for the satellite layer of the patch map. Optional eBird API for the species-baseline lookup; if absent, the app falls back to a bundled GeoJSON of regional checklists shipped with the template.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Mapbox token if used, eBird API token if used) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's voice notes are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · GPS coordinates of sightings are private by default; sharing to eBird or other citizen-science platforms is explicit per-export · private patches obscure precise coordinates on every export.
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Parse voice note (+ optional photo) → `Sighting` schema | `gemini-3.5-flash` | medium | (none) |
| Multilingual transcript cleanup (long voice note) | `gemini-3.5-flash` | low | (none) |
| Patch suggestion ("you haven't seen X yet this season") | `gemini-3.5-flash` | medium | (none) — long-context over the site's lifetime list |
| Field-guide grounding (one candidate species' field marks) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Lifetime-list summary narration (daily / weekly TTS) | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Species card illustration (first-time-on-list) | `gemini-3.1-flash-image` | n/a | n/a |
| Group-walk attribution (which member saw what) | `gemini-3.5-flash` | low | (none) |
*Note for builders:* on TTS and image-generation calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const SpeciesCandidate = z.object({
common_name: z.string(), // "Spotted Flycatcher"
common_name_local: z.string().nullable(), // user's-language form if applicable
scientific_name: z.string(), // "Muscicapa striata"
family: z.string(), // "Muscicapidae"
confidence: z.number().min(0).max(1),
reasoning_short: z.string(), // one sentence: what cues led here
distinguishing_question: z.string().nullable(), // one yes/no question the user could answer next time
});
const HeardCue = z.object({
description: z.string(), // "a high-pitched single note repeated three times"
source: z.enum(["user_described", "background_in_recording"]),
source_note: z.string().nullable(),
});
const PhotoMark = z.object({
feature: z.string(), // "wing bars visible on closed wing"
position_in_frame: z.string().nullable(), // "upper-right quadrant"
confidence: z.number().min(0).max(1),
});
const WeatherInferred = z.object({
conditions_described_by_user: z.string().nullable(), // verbatim quote from the audio
wind_direction: z.string().nullable(),
precipitation: z.enum(["none", "light", "heavy", "fog", "unknown"]),
notes: z.string().nullable(),
});
const Behaviour = z.object({
description_verbatim: z.string(), // user's own words, untranslated
description_translation: z.string().nullable(), // if voice note was not in English
category: z.enum([
"feeding", "calling", "singing", "displaying", "nesting",
"perched", "flight", "interaction", "alarm", "unknown",
]),
});
const Sighting = z.object({
sighting_id: z.string(),
audio_uri: z.string().nullable(), // gs:// URI for the voice note
photo_uris: z.array(z.string()), // gs:// URIs for attached photos
artefact_type: z.enum([
"voice_only", "voice_and_photo", "photo_only", "heard_only_audio", "manual_entry",
]),
recorded_at_iso: z.string(), // ISO timestamp from device
recorded_at_iso_user_overridden: z.boolean(),
patch_id: z.string(), // foreign key into patches
gps_lat: z.number().nullable(),
gps_lng: z.number().nullable(),
gps_accuracy_metres: z.number().nullable(),
gps_user_overridden: z.boolean(),
patch_sub_location: z.string().nullable(), // "willows at south end"
voice_note_language: z.string(), // BCP-47, "en-GB", "sw", "ur"
transcript_verbatim: z.string(), // exactly what the user said
transcript_translation_en: z.string().nullable(), // if not English
candidates: z.array(SpeciesCandidate), // 1-3 species candidates
confirmed_species_index: z.number().int().nullable(), // index into candidates, null until user resolves
confidence_band: z.enum([
"confirmed",
"high_confidence",
"candidates",
"heard_only",
"review_needed",
]),
identification_basis: z.enum([
"sight_only", "sight_and_call", "call_only", "photo_only",
"behaviour_and_habitat", "user_asserted_no_AI_inference",
]),
behaviour: z.array(Behaviour),
weather_inferred: WeatherInferred,
heard_cues: z.array(HeardCue),
photo_marks: z.array(PhotoMark),
count_min: z.number().int(), // user said "a pair" → min 2
count_max: z.number().int(), // user said "a pair" → max 2
count_inference_source: z.enum([
"user_stated_number", "user_stated_group_term", "photo_count", "estimate",
]),
ageing_sex_notes: z.string().nullable(), // "female-type", "first-winter"
ageing_sex_certainty: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(), // "candidates[0].confidence"
reason: z.string(),
})),
original_parse_preserved: z.boolean(), // true once the user manually overrides any field
notes_user_added: z.string().nullable(),
});
type Sighting = z.infer;
const PatchSuggestion = z.object({
patch_id: z.string(),
season: z.enum(["spring", "summer", "autumn", "winter", "year-round"]),
unseen_candidates: z.array(z.object({
common_name: z.string(),
scientific_name: z.string(),
likelihood_band: z.enum(["very_likely", "likely", "possible", "unlikely_but_possible"]),
fieldcraft_note: z.string(), // one sentence: when, where, what to listen for
last_seen_at_this_site_ever: z.string().nullable(), // ISO date or null
})),
notes_on_user_record: z.string().nullable(), // any patterns the model noticed
});
type PatchSuggestion = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the voice-note parse call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash silently downgrades the multilingual audio handling and loses dialect markers; it also tends to over-confidently assign single species rather than surfacing candidates.
- Model classifies bird vocalisations in the background of the recording — Gemini cannot reliably identify a bird from a brief call in a noisy field recording, and the model will sometimes try. Hard rule in the system instruction: "do not infer a species from background audio alone; surface it as a `heard_cue` with `source: background_in_recording`."
- Model translates "Maina" or "Drongo" into a different English common name across cultures — pin the geographic context (the entry's GPS or, if missing, the patch's country) in the user message; instruct the model to use the regional common name appropriate to that geography and always also record the scientific binomial.
- Confidence inflation — a brief whispered note becomes a high-confidence sighting. Cap confidence: if `audio_duration_seconds < 4` and no photo, confidence cannot exceed 0.85.
- Patch suggestion blurs into a generic checklist — without the user's actual lifetime-list-at-this-site, the suggestions become a regional field-guide list. The patch-suggestion call MUST include the user's lifetime list for that patch in its context; without that, return "insufficient data — log more sightings here first".
- Single-species lock-in — model picks one candidate and refuses to consider alternatives. Force structured output to `candidates: SpeciesCandidate[]` (array, minimum 1, typical 2-3 when confidence is below 0.9); even a confirmed sighting can have a single-element candidates array.
- Scientific binomial errors — model invents a genus or misspells a species epithet. Cross-check against the bundled IOC World Bird List taxonomy server-side; if the binomial isn't in the list, flag the entry for review and surface the common name only until resolved.
- Field-guide grounding cites blogspot or pinterest — pin the allowed source domains in the system instruction (Cornell, Birds of the World, eBird, Avibase, BirdLife International, national atlas projects). Reject citations from sources not on the list.
- TTS reads scientific names in English phonemes — `Muscicapa` should be read as Latin, not as "muss-eh-kay-pah-uh". TTS in Gemini 2.5 Flash Preview does not support custom phoneme overrides; use the input-text style directive to instruct the model to "read scientific binomials in classical Latin pronunciation, slowed slightly" and prepend the IPA in brackets for the most-likely-to-be-mangled binomials in the input.
- GPS leak — the app accidentally exposes precise coordinates of a sensitive species (e.g. a nesting raptor) on a shared export. Always check the patch's `privacy_level` before any export; private patches export rough region only.
### Negative constraints (hard rules)
- Do NOT invent a species ID. If the description is ambiguous, surface candidates with a distinguishing question. A single-element `candidates[]` array is fine; an entry committed as a confirmed sighting with confidence below 0.9 is not.
- Do NOT identify a bird from the audio of its call alone unless the user explicitly states they heard the call (the audio in `audio_uri` is the user's voice note, not a field recording of the bird; treat background vocalisations as cues, not evidence).
- Do NOT extrapolate to behaviour the user did not describe. If the user said "perched on the willows", do not annotate "likely defending breeding territory". Behaviour is what the user said.
- Do NOT silently promote a candidate to confirmed. The user must resolve the candidate themselves; the app surfaces a one-question prompt that resolves it.
- Do NOT translate proper nouns: place names, patch names, species common names in regional usage that the user provided in the voice note. "Sungei Buloh" stays "Sungei Buloh"; "Naivasha" stays "Naivasha"; "Maina" stays in the user's language form, with the agreed English common name + scientific binomial alongside.
- Do NOT modernise or smooth the user's verbatim transcript. "Flycatcher near the willows, didn't get the eye ring" stays exactly as said. Transcripts are sacred to the user's memory of the moment.
- Do NOT use the user's voice notes or photographs to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT auto-publish or auto-share. Lifetime lists are private by default. Export to eBird, BirdTrack, atlases, or any public destination is explicit, per-export.
- Do NOT auto-claim that a patch is a known hotspot, that a species is rare, or that the sighting is "of regional importance" without grounding. Such phrases require search grounding with citation.
- Do NOT surface a patch-suggestion that contradicts a recent confirmed sighting at that patch. If the user logged Garden Warbler yesterday, do not suggest Garden Warbler as "not yet seen this season" today.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`).
### Call: Parse voice note (+ optional photo) → `Sighting` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are reading an amateur ornithologist's field voice note, with or
without an attached photograph, recorded in the field at the moment of
a sighting. Voice notes are typically 4-30 seconds long, in the user's
native language. The user may be Pakistani-British at a Manchester
reservoir, Kenyan at Lake Naivasha, Vietnamese-American in the Bay
Area, retired English in the Cotswolds, Polish-Brazilian in São Paulo,
Sri Lankan in Colombo, Filipino at a Manila wetland, Tamil at Vedanthangal,
Bengali at the Sundarbans, or any other birder anywhere. Languages
encountered include English, Swahili, Urdu, Hindi, Tamil, Bengali,
Punjabi, Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Khmer,
Amharic, Farsi, Arabic, Spanish, Portuguese, French, German, Polish.
The user uploads ONE voice note as inlineData (base64 audio) or via the
Gemini Files API (`files/*` resource name). Do NOT pass Firebase Storage public URLs
directly to generateContent — the API does not fetch them server-side.
Optionally, the user attaches one or more photographs of the bird.
Decide what kind of entry this is: voice_only, voice_and_photo,
photo_only, heard_only_audio, or manual_entry.
The entry includes the patch's country and rough region in the user
message (e.g. "Patch: Audenshaw Reservoir, Greater Manchester, UK").
Use this context for regional common-name disambiguation: "Maina" in
Karachi context means "Common Myna (Acridotheres tristis)"; "Maina"
in Hanoi context can mean "Crested Myna (Acridotheres cristatellus)".
Always also record the scientific binomial so the list is portable.
Output ONLY the Sighting JSON matching the provided schema.
Hard rules:
- NEVER invent a species ID. If the description is ambiguous, surface
2-3 candidates with confidence bands. Each candidate must include a
distinguishing_question the user could answer next time (e.g. "did
you see clear wing bars?"). A confirmed identification requires
confidence ≥ 0.9 across multiple cues (sight + call, or photo + at
least two field marks).
- NEVER infer a species from background bird vocalisations in the
recording alone. Gemini cannot reliably identify a bird from a brief
call in a noisy field recording. If you hear a distinctive call,
add it to heard_cues[] with source: "background_in_recording" and
describe it neutrally — never let it drive identification_basis or
confidence by itself.
- NEVER translate the user's verbatim transcript. transcript_verbatim
preserves the user's exact words and language. Provide
transcript_translation_en separately if the voice note is not in
English.
- NEVER translate regional common names provided by the user. If the
user said "Maina", record common_name_local: "Maina" alongside the
agreed regional common name (Common Myna or Crested Myna depending
on context) in common_name.
- Cap confidence: if audio_duration_seconds < 4 and no photo,
confidence cannot exceed 0.85. If the user said "I'm not sure",
"looked like", "maybe", or equivalent in any language, confidence
cannot exceed 0.75.
- Always populate identification_basis honestly. If the user said
"heard but didn't see", set identification_basis: "call_only" and
confidence_band: "heard_only".
- Behaviour is what the user said. Do NOT extrapolate. If the user
said "perched on the willows", do not annotate "defending breeding
territory". Quote the user verbatim in description_verbatim;
translate (if needed) in description_translation.
- Count: if the user said a number, use it. If the user said a group
term ("a pair", "a small flock", "two or three"), set count_min and
count_max to the bracket the term implies, and
count_inference_source: "user_stated_group_term".
- Weather: only populate if the user mentioned it in the voice note.
Do NOT infer wind direction from the recorded_at timestamp.
- Photo marks: if a photo is attached, parse visible field marks
(eye ring, wing bars, primary projection, leg colour, posture,
habitat clues, size relative to other birds in frame). Each mark
has its own confidence; do NOT inflate.
- Scientific binomial: use the IOC World Bird List 14.x taxonomy. If
uncertain, add to flagged_for_user_review.
- flagged_for_user_review names any field where confidence is below
0.7 with a one-sentence reason.
No commentary. JSON only.
```
---
### Call: Multilingual transcript cleanup (long voice note)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a verbatim transcript of a voice note in the user's native
language and your task is to clean it up MINIMALLY for the user's own
re-reading, without changing meaning, register, or vocabulary.
You are NOT translating. You are NOT smoothing. You are inserting
sentence boundaries where the user clearly paused, fixing the most
obvious phonetic-recognition errors (e.g. "fly catcher" → "flycatcher"
when the patch context implies the bird species), and preserving every
field-shorthand phrase the user used.
Hard rules:
- Preserve the user's exact vocabulary. "Tail-flicked" stays
"tail-flicked", not "wagged its tail". "Mwewe" in a Swahili voice
note stays "Mwewe", not "kite".
- Preserve the user's uncertainty markers. "Maybe a flycatcher" stays
"maybe a flycatcher", never "a flycatcher".
- Preserve every word the user said. Do NOT remove "um", "uh",
"actually", "I think" — these convey the user's confidence level.
- Do NOT correct field-jargon spelling to dictionary forms if the
jargon form is the field standard ("primaries", "PP", "GISS", "jizz",
"primary projection", "p10").
- Do NOT collapse two sentences into one. If the user paused, the
pause was meaningful.
- Insert sentence-ending punctuation where the user clearly paused;
preserve all other punctuation as recognised.
- Output the cleaned-up transcript as a single string, with line
breaks at sentence boundaries.
No commentary. Cleaned transcript only.
```
---
### Call: Patch suggestion ("you haven't seen X yet this season")
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long context over the patch's lifetime list)
```
You receive the user's full lifetime list for ONE patch, plus a
baseline regional checklist of expected species for the patch's
country/region/biome and the current season. Your task: surface up
to 8 species the user has NOT yet recorded at this patch this season,
ranked by likelihood, each with a single one-sentence fieldcraft
note (when to look, where to look, what to listen for).
The user's lifetime list arrives as an array of Sighting objects
(structured). The regional baseline arrives as a checklist of
species with expected seasonality and habitat for the patch's region.
Hard rules:
- A species qualifies as "not yet seen this season at this site" only
if the user has NEVER recorded it at this patch in this season
across any year. If they recorded it once in autumn 2019 and once
this current autumn, it does NOT qualify (they have seen it this
season).
- Likelihood bands are: very_likely (the species is regularly recorded
at this site in this season and the user has visited often enough
that the absence is conspicuous), likely (the species is regularly
recorded at this site in this season), possible (the species is
occasional at this site in this season), unlikely_but_possible
(regional vagrant or rare visitor).
- Fieldcraft notes are one sentence each, concrete and patch-aware
("listen for a fast, rich warble from inside the hawthorn after the
first cold front" not "look for warblers in scrub habitat").
- Do NOT suggest a species the user logged at this patch in this
season within the last 14 days; they have seen it.
- Do NOT confidently claim a regional vagrant — flag as
unlikely_but_possible and say so.
- Do NOT suggest more than 8 species per call. If the user's lifetime
list at this patch is fewer than 20 entries, return at most 3
suggestions and include notes_on_user_record: "few records at this
patch — suggestions are based on regional baseline only".
- Population last_seen_at_this_site_ever from the user's lifetime
list if they have ever recorded the species at this site; otherwise
null.
Output the PatchSuggestion JSON matching the provided schema. No
commentary.
```
---
### Call: Field-guide grounding (one candidate species' field marks)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding
```
You retrieve a short, accurate, sourced description of the field marks
of one bird species, to be shown alongside a candidate identification.
The user is in the field and needs the field marks fast.
Given a scientific binomial and the user's geographic region, return:
- one_line_summary (one sentence: the cleanest single field-mark
description that distinguishes this species from look-alikes in
this region)
- field_marks (3-6 short bullets, each one feature: bill shape, wing
pattern, leg colour, primary projection, eye ring, behaviour, voice)
- distinguishing_from (a list of the look-alike species in this
region and the single feature that distinguishes them)
- citation_source (which source the description comes from)
Hard rules:
- Use google_search grounding. Allowed source domains: birdsoftheworld.org,
allaboutbirds.org, ebird.org, avibase.bsc-eoc.org, birdlife.org,
iocworldbirdlist.org, africabirdatlas.org, indianbirds.in, orientalbirdimages.org,
birdsa.com.au, birdstrike.tw, and the national/regional ornithological
union sites for the user's region. Reject citations from blogspot,
pinterest, generic content farms, or wikis without primary editorial
oversight.
- If the model is uncertain or the species is not well covered in the
allowed sources, say so explicitly in one_line_summary and provide
only what is confidently sourced.
- Do NOT invent field marks. If a feature isn't in the source, don't
list it.
- Do NOT compare to North American species unless the user's region is
North America. Look-alikes are regional.
Output as JSON in the text body (NOT via responseSchema —
responseSchema and google_search cannot be combined in the same Gemini
call today). Server-side: parse the JSON, then read citation URLs from
the response's groundingMetadata.groundingChunks[].web.uri — do NOT
ask the model to include URLs in the JSON body; it will hallucinate
them.
No commentary outside the JSON.
```
---
### Call: Lifetime-list summary narration (daily / weekly TTS)
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried, the friend who comes birding with you. Pick
the Gemini 2.5 Flash TTS voice whose languageCode matches the user's
chosen narration language — pronunciation will follow that locale
automatically.
Pre-process the text before sending it to TTS:
- Read from the daily-summary string passed in the input. The summary
is structured: opening sentence, then one short sentence per
notable sighting, then a closing sentence.
- At each line break, insert a single ellipsis (…) so the TTS model
produces a natural pause. At paragraph breaks, insert a blank line
plus an em-dash (—). Gemini 2.5 TTS does not support SSML
— these textual cues are how you signal pace.
- Scientific binomials are read in classical Latin pronunciation,
slightly slowed. For the most-likely-to-be-mangled binomials,
prepend the IPA in brackets in the input text — TTS will read the
bracketed pronunciation. Phoneme overrides via SSML are
NOT supported on Gemini 2.5 TTS.
- Skip the structured metadata (counts, confidence bands). The
narration is for the user's walk back from the patch, not a list
audit.
- Target rate: ~120 words per minute — walking-pace narration, not
podcast pace.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Read warmly and unhurriedly, as a friend who
came birding with you and is recapping the day's list on the walk
back. Read scientific binomials in classical Latin pronunciation,
slowed slightly. …". There is no separate style API field on Gemini
2.5 TTS; the directive sentence inside the input is how style is
conveyed.
Mid-call voice switching is not supported. The whole narration is in
one voice; the user's chosen narration language governs the voice
locale.
```
---
### Call: Species card illustration (first-time-on-list)
Model: `gemini-3.1-flash-image` · n/a · n/a
```
You generate a small printable card illustration of one bird species
the first time the user adds it to their lifetime list. The card is
for the user's personal scrapbook list; it is NEVER used as a
field-identification reference and must be visually distinguishable
from a real field photograph.
Style: watercolour, gouache, or fine ink line — never photoreal. Soft
washes, visible brush texture, generous whitespace around the bird.
The bird occupies the centre of the frame, in a typical posture for
the species (perched if it perches, on the water if it floats, in
flight if its identification is most often by flight silhouette).
Composition:
- One bird, central, facing slightly right of three-quarters.
- A single suggestive habitat element behind it (one branch, one reed
stalk, one ripple of water).
- Soft natural light, no dramatic shadows.
- No text in the image. The species name is added by the app over the
card outside the image area.
Hard rules:
- Do NOT generate a photoreal image. The card must read as illustration.
- Do NOT add multiple birds or species in the frame.
- Do NOT include identifying field marks that the species does not
have. If unsure, render in the most generic plumage for that species
(adult breeding male if there is one canonical plumage, otherwise
female-type).
- Do NOT include people, hands, binoculars, or other props in the
frame.
Output: one image, 1024x1024, watercolour or gouache style.
```
---
### Call: Group-walk attribution (which member saw what)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a transcript of a group walk's voice notes (multiple
people taking turns recording sightings on one shared phone). Your
task: attribute each sighting to the group member who described it,
based on voice match to per-member voice samples uploaded earlier.
Group-walk mode: each member uploads a 30-second voice sample on
joining (their normal speaking voice, indoors). Per-walk voice notes
are diarised across members using the voice samples as anchors.
Hard rules:
- Speaker attribution is a probability. Each attribution carries a
confidence in [0, 1]. If confidence < 0.7, mark the sighting as
unattributed and surface to the group: "who recorded this one?"
- Two members with similar voices (siblings, parents and children)
can be flagged as ambiguous; ambiguous attributions are not
silently resolved.
- The group walk's owner decides what is shared. Members can claim or
decline any sighting attributed to them; declined attributions are
reassigned or held as group-shared.
Output: an array of attributed sightings with member_id and
attribution_confidence, plus an unattributed array for any sighting
the model couldn't confidently match. No commentary.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The Manchester reservoir, three mornings a week.** Imran walks the same loop at Audenshaw Reservoir every Tuesday, Thursday, and Saturday morning before work at the GP surgery in Levenshulme. He records 8-12 voice notes per walk on his phone, often in English with the occasional Urdu word for a bird he knew from his childhood in Lahore. The app's empty state offers a "set up your patch" flow optimised for the regular-patch birder: name the patch, pin the GPS, choose the country and biome, optionally bookmark sub-locations within the patch ("willow scrub at south end", "tower hide", "north shore").
- **Lake Naivasha, with a borrowed pair of Nikons.** Wanjiku is a retired schoolteacher in her sixties who began birding the year after her husband died. She walks the lake's edge before the tourist boats start at 9, with a 2018-model Android phone and a borrowed pair of Nikon binoculars from her son in Nairobi. Her voice notes are in Swahili, with the occasional English word for birds she remembers from her primary-school textbook. The app handles both languages in the same voice note and surfaces regional common names in Swahili and English alongside the scientific binomial.
- **The South Bay shoreline, Vietnamese voice notes.** Phước is a Vietnamese-American software engineer in his thirties who started birding during the pandemic at the Coyote Hills Regional Preserve. His voice notes are bilingual Vietnamese-English ("con chim này nhỏ, có vạch trắng trên cánh, looks like a sandpiper of some kind"). He never logs to eBird; he keeps his list to himself.
- **The candidate that sat as candidate for three weeks.** Imran spotted what he thought might be a Garden Warbler in late April but didn't get a clean look. The app filed it as `candidates: [Garden Warbler 0.55, Blackcap female 0.42]` with the distinguishing question "did you see any rufous on the cap, even faintly?" Three weeks later he saw the same patch of hawthorn again, watched the bird for a full minute, recorded a clean voice note, and the app resolved the older entry to Garden Warbler.
- **The heard-only Cetti's Warbler.** Imran has heard the explosive call from the same patch of bramble every spring for four years but has never seen the bird. Every entry is `confidence_band: heard_only`, `identification_basis: call_only`. The app does not promote any of them to a confirmed sight record. The lifetime list shows Cetti's Warbler as "heard at Audenshaw, never seen".
- **The first Brown-headed Barbet for Wanjiku.** Wanjiku records a voice note describing a green bird with a brown head and a yellow bill, calling from a yellow-barked acacia. The candidate ranking surfaces Brown-headed Barbet (Psilopogon zeylanicus) at 0.91. The app generates a small watercolour card the first time the species enters her lifetime list; the card is added to her cover page.
- **The patch comparison.** Phước compares his Coyote Hills list to his October-2025 trip list from Bottle Beach in Thailand: 41 species in common (mostly migratory shorebirds), 73 unique to Coyote Hills, 102 unique to Bottle Beach. The map view highlights the geographic gap; the species view surfaces three shared species he saw in their breeding plumage at one site and their non-breeding at the other.
- **The group walk at Cuc Phuong.** Phước joins a six-person guided walk at Cuc Phuong National Park during a trip to Vietnam. One member records all sightings on her phone; on returning, each member taps "claim my sightings". The app's voice-attribution call uses the 30-second voice samples each member uploaded on joining to attribute every sighting to a member; ambiguous ones are surfaced for the group to resolve.
- **The bird-walk guide in Costa Rica.** A guide leads weekly walks at Carara National Park and keeps a separate list per client across the year. Each Sunday's walk creates a new "walk" entry; sightings during that walk roll into both the guide's lifetime list and the client's shared list (the client receives a magic-link email after the walk and sees only their own).
- **The Sri Lankan birder rebuilding her list.** Nimali, 52, took up birding seriously after retiring from a tea-plantation administration job. She has six handwritten notebooks from 1992-2018 in Sinhala and English. The app's "rebuild from memory" mode lets her enter retrospective sightings: a single voice note ("at Sinharaja, August 1994, a Sri Lanka Blue Magpie pair, in the tall canopy near the entrance gate") records as a manual entry with `recorded_at_iso_user_overridden: true` and `identification_basis: user_asserted_no_AI_inference`.
- **The private patch.** Imran finds a Hobby nest in a private wood behind his patch. He marks the patch sub-location as `privacy_level: private`. Every export from now on shows the Hobby as recorded at "Audenshaw Reservoir, rough region", never at precise coordinates.
- **The eBird export.** Phước decides on his 40th birthday to share three years of his Coyote Hills list with eBird. The app generates a single CSV per eBird's import schema, with every sighting from the patch, his choice of which species to share at precise GPS vs rough, and a confirmation page showing him what eBird will receive before he sends it.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a hand holding a phone at the edge of a reservoir at dawn, with reeds in the foreground and a hint of a heron's silhouette in the middle distance. One paragraph: "Birder's Life List turns six-second voice notes into a lifetime list — in any patch, any continent, with every behaviour, weather note, and honest uncertainty preserved." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample lifetime list" → loads the demo archive in section 8a.
2. **Empty state — "Set up your first patch".** A short flow: name the patch ("Audenshaw Reservoir"), pin the GPS (drag a marker or use current location), choose the country and biome (drop-down), optionally bookmark sub-locations. After saving, the home screen shifts to the record-a-sighting empty state.
3. **Home / record-a-sighting screen.** A large pressable record button at the bottom of the screen, occupying the thumb-zone. Above it: a one-line greeting ("Good morning at Audenshaw Reservoir") with the current weather pulled from the device + an offline-cached forecast. Above that: the day's sightings so far, as a vertical list of cards. The record button has a press-and-hold gesture: hold to record, release to stop. A camera button next to it for an attached photo.
4. **Sighting detail view.** A three-column layout on desktop, stacked on mobile. Left column: the photograph (if any), zoomable, with photo marks annotated; the audio player below with waveform. Middle column: the verbatim transcript, with the species mentions highlighted and the user's verbatim words preserved exactly. Right column: the candidate ranking with confidence bands, the distinguishing question (if any), the field-guide grounding card (one_line_summary + field marks + citation), the behaviour and weather panel, and an "edit / override" button at the bottom. Sticky header: species name (or "candidates") · date+time · patch · confidence band chip · "(i) how the model heard this".
5. **Lifetime list view.** A vertical list of every species the user has ever recorded, chronological by date of first sighting. Each row: species common name + scientific binomial, first-seen date and patch, total sightings, confidence band. Filter by patch, season, family, confidence band, candidates-only, heard-only. Sort: by family, by date of first sighting, by total sightings, by patch. Tap a species → all sightings of that species across the user's life.
6. **Patch view.** Per-patch list. Tabs: "Species ever recorded at this patch" (chronological by first sighting), "All sightings at this patch" (chronological by date), "You haven't seen yet this season" (the patch-suggestion panel, with each row showing the species, the fieldcraft note, the likelihood band, and the last-ever sighting at this site if any). Map at the top with the patch outlined and sub-locations pinned.
7. **Map view.** A world map (or zoomed to the user's region by default) with every patch as a circle of varying density (more sightings = darker fill). Zooming in to a patch shows individual pins for each sighting. Toggle: heat-map mode vs pin mode. Private patches show only the rough region; precise coordinates are never rendered.
8. **Calendar view.** Year-at-a-glance heatmap: 365 cells, each coloured by the number of sightings that day. Hover/tap a cell → the day's sightings slide up in a panel. A second tab: a month-by-month seasonality view per species ("when have I seen Garden Warbler at this patch each year").
9. **Sharing & group walks.** Modal: "Start a group walk". The owner names the walk, sets the start time, and shares a magic link to invitees. Each invitee uploads a 30-second voice sample on joining. During the walk, one phone records; afterwards, each member taps "claim my sightings" and the voice-attribution call resolves who saw what.
10. **Exports.** "Share to eBird, BirdTrack, or the Kenya Bird Map." Pick the destination (CSV / XML / JSON), the date range, the patches included, and the privacy level (precise GPS vs rough region). A confirmation page shows exactly what will be sent. Export is explicit; nothing auto-publishes.
11. **Settings & privacy.** Patch privacy levels (per patch); voice-note retention (keep forever / delete after 12 months); auto-narration on/off; language preference for narration; account deletion (with a 30-day grace period and an explicit "delete forever" button).
12. **Footer.** "Made for the patches you keep coming back to." Privacy: "Your lifetime list is yours. We never train on it." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Birder's Life List."
- Subhead: "Turn six-second voice notes into a lifetime list — in any patch, any continent, with every behaviour, weather note, and honest uncertainty preserved."
- One paragraph (≤ 60 words) explaining who this is for: the regular-patch birder who walks the same loop three times a week, who logs sightings by voice on the move, who wants a structured lifetime list without a single false confident ID.
- Visual: a small annotated illustration of a phone at the edge of a reservoir, the record button mid-press, with a small panel showing "candidates: Spotted Flycatcher 0.78, Pied Flycatcher 0.18 — did you see clear wing bars?" — not a generic bird icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample lifetime list".
- A live demo input pre-loaded with three sample patches and twenty sightings from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the multilingual voice-note parse, the candidate ranking with distinguishing question, the patch-suggestion panel that reads the user's history before suggesting).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample lifetime list in `/data/seed-list/` for your own patches and sightings."
- "Adjust the prompts in `/server/prompts/` to fit your region's regional common names and avifauna."
- "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (audio reasoning + multimodal)** — accepts the user's voice note + an optional photograph in one call and produces a structured Sighting object. Hears whispered words and field shorthand; reads field marks from the photo; never identifies a bird from background audio alone.
- **Gemini 3.5 Flash (multilingual)** — handles English, Swahili, Urdu, Hindi, Tamil, Bengali, Punjabi, Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Khmer, Amharic, Farsi, Arabic, Spanish, Portuguese, French, German, Polish. Regional common names disambiguated by patch geography.
- **Gemini 3.5 Flash (long context)** — the patch-suggestion call reads the user's full lifetime list at the patch plus the regional baseline checklist in one call to surface what hasn't been seen this season.
- **Gemini 3.5 Flash + grounded search** — pulls field-mark descriptions from trusted ornithological sources (Cornell, Birds of the World, eBird, Avibase, BirdLife, regional atlases) with citations.
- **Gemini TTS** — narrates the day's list on the walk back, in the user's chosen language, at walking pace, with scientific binomials read in classical Latin pronunciation.
- **Nano Banana 2** — generates a small watercolour species card the first time a species enters the user's lifetime list. The card is for the user's personal scrapbook; never used as a field reference.
- **Firebase Auth** — Google and Apple sign-in, group-walk invitations via magic links.
- **Firestore** — stores your lifetime list, syncs across devices in real time.
- **Firebase Storage** — keeps the original voice notes and photographs at upload resolution, forever.
- **Cost note** — see the detailed breakdown in 6d. A typical year of patch-birding (300 sightings, 50 photographs, daily narration) costs about $7 of Gemini API spend, total.
- **Privacy note** — your lifetime list is private to you. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Private patches obscure precise coordinates on every export; nothing is published without your explicit action.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `MAPBOX_TOKEN` — optional, only if you want satellite tiles on the patch map
- `EBIRD_API_TOKEN` — optional, only if you want live eBird checklist baselines instead of the bundled GeoJSON
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: long-context patch-suggestion calls are billed per token of input — a patch with 1,500 lifetime sightings costs about $0.05 per suggestion run (default: once a week per active patch).
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this lifetime list forever" — gone in 60 seconds), what is never sent for training, what private-patch privacy means in practice (rough region only on every export, never precise GPS).
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, multilingual, audio-reasoning, long-context, TTS, Nano Banana 2 docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on eBird / BirdTrack / Atlas of Living Australia / Kenya Bird Map export schemas
**Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Parse voice note + optional photo (Gemini 3.5 Flash, medium thinking)** — typical 8-second voice note + 1 attached photo, audio input ≈ 1k tokens, image input ≈ 1k tokens, ~800 output tokens. ~$0.012/sighting.
- **Multilingual transcript cleanup (Gemini 3.5 Flash, low thinking)** — typical 30-second voice note, ~400 output tokens ≈ ~$0.002/sighting (only run on voice notes longer than 15 seconds; shorter notes use the verbatim transcript from the parse call).
- **Patch suggestion (Gemini 3.5 Flash, medium thinking, long-context)** — long-context input of the user's patch list + regional baseline, run weekly per active patch. ~$0.05 per patch with 1,500 lifetime sightings per run.
- **Field-guide grounding (Gemini 3.5 Flash + grounded search)** — ~$0.002/candidate. Cached per species per region; subsequent retrievals of the same species in the same region are free.
- **Lifetime-list summary narration (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 1-minute narration of the day's sightings ≈ $0.002 per day.
- **Species card illustration (Nano Banana 2)** — ~$0.03/image. Cached per species; subsequent first-on-list events for the same species use the existing card.
- **Group-walk attribution (Gemini 3.5 Flash, low thinking)** — ~$0.01 per group walk (regardless of size), with a one-off ~$0.005 per group member when they upload their voice sample.
- **Expected per-sighting cost on first ingest:** ~$0.014. **Typical year of patch-birding** (300 sightings, 50 photos, 50 species first-on-list, daily narration): ~$7 total. **Heavy patch-birder** (1,000 sightings/year across 3 patches, 200 photos, weekly group walks): ~$24/year.
- **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 10-second voice note at 64 kbps mono ≈ 80 KB; a year of 300 voice notes ≈ 25 MB ≈ ~$0.001/month. Photographs at typical phone resolution ≈ 2-3 MB each; a year of 50 photos ≈ 125 MB ≈ ~$0.003/month.
## 7. Design language
- **Mood:** The field. Quiet, attentive, a little damp. Not a tech product. Not a leaderboard. The reservoir at 6:47 am with the mist still on the water, the phone in a gloved hand, the binoculars on the strap, the dog patient at the heel. A serious amateur's notebook, not a gamified habit app.
- **Typography:** A clean grotesque for app chrome (Inter or Geist). A warm serif for species names and scientific binomials (Source Serif Pro). Scientific binomials always italic, with the genus capitalised and species lowercase exactly as IOC publishes them. A monospace for confidence bands and structured metadata (JetBrains Mono or IBM Plex Mono) — keeps the data-vs-prose distinction legible.
- **Palette:** A dim-dawn paper background `#F4F1EB` for the main views, deep ink `#1B1F1C` for body text, mossy green `#3D5A3D` for confirmed-sighting confidence chips, amber `#B07B2A` for candidates / heard-only, neutral grey `#6A6E6A` for review-needed. A single muted teal `#3A6E73` for the user's own annotations and overrides so they cannot be mistaken for the AI parse. Borrowed from a field notebook, not from a SaaS design system.
- **Imagery:** The user's photographs of birds are the hero. Never replace them; never crop them tighter than the user did. Nano Banana 2 species cards live in the lifetime-list cover view and are visually distinguishable from real photographs (watercolour, gouache, fine ink — never photoreal). Patch maps use restrained natural-tone fills — water, marsh, woodland — not full-saturation political colours.
- **Hand-feel touches:** A barely-visible paper grain on the sighting-detail background. The audio waveform is rendered as a thin, slightly imperfect line — like ink on paper. Hover on a candidate species reveals the distinguishing question; never aggressively glow. Confidence bands sit in small pill chips, not progress bars.
- **Spacing:** consistent 4-px base. Generous whitespace — the field needs air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Sighting cards use 6; the calendar heatmap cells use 0 (no radius, tight grid); the welcome card uses 20.
- **Shadows:** subtle, layered, slightly cool-toned. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. The map's heat-to-pin transition is the one place where motion carries meaning; respect reduced-motion by jumping rather than animating.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Set up your first patch", "Press and hold the record button to log a sighting").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible patches, sightings, voice-note content, candidates, and field-guide blurbs that fit the domain (use the seed content in section 8a as a starting point). When inventing, draw on multiple continents — a UK reservoir, an East African lake, a California shoreline, a South Asian wetland, a South American urban park — but never claim a fictional sighting is real data.
- Tone: warm, direct, free of corporate language. This template is for a person standing in the reeds at dawn, not a company.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (primary projection, GISS, jizz, p10, jizz, primary, secondary, tertial — these are the field standard).
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence candidate shows in amber with the distinguishing question right alongside; tapping the candidate reveals the alternatives the model considered).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample lifetime lists (sidebar):**
- "Imran at Audenshaw" (1,840 sightings, 142 species, owner: me) — Audenshaw Reservoir, Greater Manchester, UK. Three walks a week since March 2017. Voice notes in English with occasional Urdu.
- "Wanjiku at Naivasha" (612 sightings, 198 species, owner: me) — Lake Naivasha, Rift Valley, Kenya. Daily walks since January 2023. Voice notes bilingual Swahili-English.
- "Phước at Coyote Hills" (903 sightings, 167 species, owner: me) — Coyote Hills Regional Preserve, Alameda County, California. Weekend walks since June 2020. Voice notes bilingual Vietnamese-English.
- "Nimali — Sinharaja and home garden" (310 sightings, 88 species, owner: me) — Sinharaja Forest Reserve, Sri Lanka + home garden in Galle. Retrospective entries from 1992-2018 from handwritten notebooks plus new walks since 2021.
**Sample sighting in detail view (this is what the demo should show):**
- **Sighting id:** s_aud_2025-05-04_0712
- **Recorded at (ISO):** 2025-05-04T07:12:18+01:00
- **Patch:** Audenshaw Reservoir, sub-location "willows at south end"
- **GPS:** 53.4641, -2.1218 (±8 m)
- **Artefact type:** voice_only
- **Audio uri:** gs://birders-life-list-audio/u_imran/s_aud_2025-05-04_0712.m4a (7.4 seconds)
- **Voice note language:** en-GB
- **Transcript verbatim:** "Flycatcher, female, near the willows, low in the willows, called twice, tail-flicked, didn't get a clear look at the eye ring."
- **Candidates (2):**
- Spotted Flycatcher (Muscicapa striata), confidence 0.78, reasoning "willow scrub habitat in early May, tail-flick behaviour, female-type plumage consistent; user did not confirm eye ring", distinguishing question "did you see a faint pale eye ring or upright posture on a perch?"
- Pied Flycatcher (Ficedula hypoleuca), female, confidence 0.18, reasoning "passage migrant possible at this site in early May; female plumage could be confused; user did not mention wing patch", distinguishing question "did you see a white wing patch even briefly?"
- **Confidence band:** candidates
- **Confirmed species index:** null (user has not yet resolved)
- **Identification basis:** sight_only
- **Behaviour (3):** "perched in willow scrub" (perched), "tail-flicked twice" (perched), "called twice" (calling)
- **Heard cues (1):** "user described two short calls; no recording of the call itself" (source: user_described)
- **Photo marks:** [] (no photo attached)
- **Weather inferred:** "light cloud, no wind mentioned, no rain" (user did not describe in this note; not inferred from elsewhere)
- **Count:** 1 (user said "a flycatcher" — singular, count_inference_source: user_stated_number)
- **Ageing/sex notes:** "female-type", certainty 0.7
- **Flagged for user review (1):** field_path "candidates", reason "user did not confirm eye ring; entry sits in candidates until field marks confirmed on a return visit"
**Sample patch-suggestion output (panel under the lifetime list):**
- **Patch:** Audenshaw Reservoir
- **Season:** spring
- **Unseen candidates (4):**
- Garden Warbler (Sylvia borin), very_likely, "listen for a fast, rich, even warble from inside the hawthorn after the first cold front of mid-May; the song is often the first clue", last seen at this site ever: 2024-05-22
- Lesser Whitethroat (Curruca curruca), likely, "scan the bramble at the north shore in the first hour after sunrise; the rattling song is easier than the bird", last seen at this site ever: 2023-05-14
- Tree Pipit (Anthus trivialis), possible, "watch for parachuting song-flights over the open paddock at the south end on warm mornings", last seen at this site ever: null
- Wood Sandpiper (Tringa glareola), unlikely_but_possible, "passage migrant — check the south-end mud after a southerly weather front in late April or early May", last seen at this site ever: 2019-04-28
- **Notes on user record:** "you have recorded 142 species at this patch across eight years; this is a high-confidence patch list"
**Sample voice notes (verbatim transcript style — short, fragmentary, fieldcraft-rich):**
- "Goldcrest, single, calling from the spruce at the car park, didn't see it."
- "Mwewe wa kichwa cheupe, mmoja, juu ya mti, akilia kwa sauti kubwa, dakika saba alfajiri." (Wanjiku, Swahili; "White-headed kite, one, on top of the tree, calling loudly, seven minutes after sunrise")
- "Một con chim chích, có vạch trắng trên cánh, đang nhảy quanh bụi cây, có thể là warbler." (Phước, Vietnamese; "A small warbler with a white wing bar, hopping around the bush, could be a warbler")
- "Two redshank, south shore mud, calling, definite, the trumpet call I always confuse with greenshank but the leg colour was clearly red."
- "Yellow Wagtail, male, paddock by the entrance, breeding plumage, gorgeous."
- "Possible water rail, heard a single squeal from the reeds south of the tower hide, never saw it, putting it in as heard-only."
- "Sinharaja Blue Magpie, pair, August 1994, the tall canopy near the entrance gate, retrospective entry from notebook, I remember the day clearly." (Nimali, manual entry)
**Sample voice copy (the app's own micro-copy):**
- Onboarding: "Press and hold the record button. Say what you see — six seconds is plenty."
- Recording: "Recording… release when you're done."
- Processing: "Reading what you said…" / "Comparing with what's likely here…" / "Looking for field marks in the photo…" / "Ranking candidates…"
- Empty patch list: "This patch is waiting for its first sighting. Press the record button below to log one."
- Candidate resolved: "Resolved to Garden Warbler. Added to your lifetime list at Audenshaw — first ever recorded here in May."
- Heard-only entry: "Filed as heard-only. We'll never silently promote it to a sighting."
- Low-confidence note: "Some details were hard to make out. Tap the candidate to see what the model considered."
- Patch-suggestion panel header: "You haven't seen yet this spring at Audenshaw."
- Export confirmation: "Sharing 142 species (1,840 sightings) to eBird. Private patches are rough region only. Confirm to send."
**Sample group-walk invitation email subject + body:**
- Subject: "Phước — Sunday at Carara, 6 am, group walk"
- Body: "Hi all — I'm leading a Sunday walk at Carara, 6 am at the gate. I'll record sightings on my phone; you can claim yours after the walk. Tap to join and upload a 30-second voice sample so the app can attribute sightings back to you." [Open Walk]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a hand at the edge of a reservoir at dawn, holding a phone with the record button mid-press, reeds in the foreground, a hint of a heron's silhouette in the middle distance. Generate via Nano Banana 2 with a prompt emphasising "natural dawn light, mist on the water, real worn waterproof jacket cuff, no logo on the phone, soft focus on the distant heron".
- **App icon / wordmark:** Set in the warm serif. A small line-drawing of a feather behind the wordmark. No three-letter abbreviation.
- **Empty-state illustration:** A simple line drawing of a single bird in profile on a branch, head tilted slightly. Hand-drawn aesthetic, not a flat icon. Different bird per language locale (e.g. a Spotted Flycatcher for UK English, a Speckled Mousebird for Swahili, a Bulbul for Urdu).
- **Demo bird photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "soft natural light, slightly distant subject as in a real binocular view, no professional photography studio lighting, a hint of habitat in frame, no people in frame". Each demo photograph should look like a phone shot through binoculars, not a studio plate.
- **Species cards (per species on the lifetime list):** Watercolour, gouache, or fine ink illustrations generated via Nano Banana 2 with the prompt in the Species Card system instruction. Cached per species; the first time the user logs the species, the card is generated; thereafter the same card is used.
- **Patch maps:** Mapbox if configured, with the "muted natural" style (water, marsh, woodland, scrub); otherwise a bundled GeoJSON of the user's region with restrained natural-tone fills.
- **Stock fallbacks:** If image generation fails, fall back to a photographed sample bird image from `/public/samples/sample-bird.jpg`. Never to a "🐦" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- The record button has explicit press-and-hold states: idle, pressing, recording (with elapsed time), processing, error. The button is the largest pressable element on the screen.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Set up your first patch", "Press and hold the record button to log a sighting", "Invite the group" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving. The thinking indicator on the parse call says: "Reading what you said…" → "Comparing with what's likely at this patch this season…" → "Ranking candidates…"
- If an AI call fails, show a calm, specific error ("We couldn't make out enough from this recording — try again with a slightly louder voice, or add a quick note typed below?") and offer retry.
- Low-confidence candidates are shown in amber with the distinguishing question right alongside; tapping the candidate reveals the alternatives the model considered and the reasoning_short for each.
- The map view transition between heat-map mode and pin mode takes 600 ms with `prefers-reduced-motion` falling back to instant.
- The audio waveform animates while the recording is active (a thin line growing left to right); when playback starts, a scrubber cursor moves across the waveform with the audio.
## 11. Tech & responsive requirements
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle.
- **Model selection:** explicitly pin `gemini-3.5-flash` for parse / multilingual transcript / patch suggestion / group-walk attribution, `gemini-3.5-flash` for field-guide grounding, `gemini-3.1-flash-tts-preview` for narration, and `gemini-3.1-flash-image` for species cards. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed lifetime list on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback for group walks.
- **Storage:** Firebase Storage for original voice notes, attached photographs, and Nano Banana 2 species cards. Pre-signed URLs only. Storage must be enabled in the Firebase console before first voice note.
- **Mobile-first.** The record button must be reachable in the thumb zone on any phone screen ≥ 375 px wide. Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS (the record button must sit above the home indicator).
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the lifetime list and patch views.
- Optimistic UI on writes; reconcile on response.
- Audio recording uses the MediaRecorder API with `audio/webm; codecs=opus` where supported (Chromium, Android), falling back to `audio/mp4` (Safari, iOS). Encode at 64 kbps mono for voice notes; the original is preserved.
- **iOS Safari gotchas (graceful degradation):** mic permission does NOT persist across page reloads on iOS — re-request on every observation note; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause, persist the partial clip to IndexedDB tagged "interrupted", and prompt resume; backgrounded Safari tabs pause `getUserMedia` — pair `visibilitychange` with a screen Wake Lock during note capture so a long field-record isn't dropped; on Low Power Mode iOS may downgrade `getUserMedia` quality — surface a calm "low-light / low-power note — original audio preserved" affordance rather than failing.
- Offline mode: the record button works offline; the audio is queued in IndexedDB and uploaded + parsed when the device is back online.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring.
- The record button has an alternative keyboard-accessible interaction: `Space` to start/stop recording (with a visual indicator of which state the user is in).
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The amber confidence chip is verified against the bone-paper background at 4.5:1.
- All images have meaningful `alt` text. Attached bird photographs have `alt` describing the artefact ("photograph of a small brown bird perched on a willow twig, taken at the south end of Audenshaw Reservoir on 4 May 2025 at 7:12 am").
- Form fields have associated `