================ 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. --- # Teach Me My Letters ## 1. Project **Teach Me My Letters** is a long-distance alphabet practice app for a grandparent and a small grandchild on opposite sides of a FaceTime call. The grandchild, somewhere between four and six, holds up a sheet of paper with a wobbly hand-drawn letter on it. The app — running on the grandparent's phone or tablet during the call — sees the letter the child has drawn, names it back warmly ("Yes, that's an M — look at those lovely lines"), and suggests the next letter for them to try. A small per-child progress book, kept in sync between the kid's parent's account and the grandparent's account, remembers what letters the child has practised, which ones come easily, and which ones the child likes to draw over and over because they enjoy the shape. This is the kind of app a lola in Daly City builds with her son one Sunday afternoon, after her four-year-old apo in Manila keeps asking her to "teach me my letters, Lola" on FaceTime and she keeps fumbling because she can't see the paper properly, can't tell whether it's a sideways E or a real M, and runs out of letters by Tuesday because she forgot which ones they had already done. It is also the kind of app a halmoni in Seoul uses to keep weekly Sunday-morning calls with her two grandkids in New Jersey going — half the call in English so the five-year-old can keep up, the alphabet pages saved so the three-year-old gets her turn next week. Same shape of moment, different alphabet, different time zone. **Tagline:** _Alphabet practice over a video call — in any letters, any language, with the celebrating done by the grandparent and the seeing done by the app._ ## 2. Target audience - Grandparents who FaceTime weekly with a grandchild in another city, country, or time zone, and who want a small structure to anchor the call beyond "how was your week?" - Diaspora grandparents whose grandkids are growing up speaking a different first language — Filipino lolas, Korean halmonis, Vietnamese ông bà, Indian dadis and nanis, Tamil paatti, Chinese yéye and nǎinai, Italian nonni, Mexican abuelos, Nigerian iya agba, Iranian madarbozorg, Greek yiayia and pappous - Parents of preschoolers who like the idea of their child practising letters with a grandparent over video calls, and who want to see, between calls, which letters their kid is getting comfortable with — without turning it into homework - Adoptive grandparents and chosen family elders ("Auntie Marisol" style relationships) who play a grandparent role and want the same warm structure - Long-distance step-grandparents who only see the grandkids once a year in person and want a recurring weekly thread that builds something - Parents of children with learning differences — dyslexia, dysgraphia, autism, fine-motor delay — where a kind, patient, never-corrective grandparent voice with no time pressure is exactly the right tutor - Foster grandparent program volunteers matched with a child they only meet by video - Speech-and-language therapists working with families who need a low-pressure pre-literacy activity to send home between sessions - Multilingual families teaching two scripts in parallel — Roman alphabet on weekdays, Hangul on weekends; English at school, Tamil at Amma's house ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **The grandparent sees what the camera sees, properly.** A five-year-old's wobbly capital M, held up at an angle, in indifferent light, on the back of an envelope — Gemini 3.5 Flash reads it correctly. The grandparent doesn't have to squint or ask "is that an N or an M, sweetie?" The app names the letter on screen the moment the child holds it up. - **Never critical of the child's hand.** The model is forbidden from ever saying "that's not quite right" or "try again". It always finds the thing that worked — the strong vertical, the brave curve, the careful corner — and names that. If a letter is genuinely unreadable, the app says "I love how you held it up — can you show Lola again, this time a bit closer?" The hand stays warm; the praise stays specific; the work continues without anyone losing face. - **A celebration voice the grandparent can borrow.** The Gemini Live API runs a warm child-friendly celebration voice the grandparent can let speak ("Yes! That's M! Look at those two tall lines!") or override with their own voice. Many grandparents prefer to celebrate themselves and let the app just whisper the letter name into their ear. Both modes work; both are first-class. - **Suggests the next letter — but never insists.** After each successful letter, the app suggests the next one with a small note ("After M, try N — they're sisters"). The grandchild can ignore it. The grandparent can ignore it. The flow is led by the child, not the curriculum. - **A shared progress book between two accounts.** The kid's parent has an account. The grandparent has an account. Both see the same per-child progress book — which letters the child has practised, which ones came easily, which ones the child likes drawing over and over because they enjoy the shape. The parent never sees a "score". The grandparent never sees a "level". Neither sees a leaderboard. Both see a kind, honest record. - **Multi-script, multi-language from day one.** Roman alphabet, Hangul, Devanagari, Tamil, Bengali, Arabic, Hebrew, Cyrillic, Hiragana and Katakana, Thai. The grandparent picks the script their grandchild is learning. Mixed-script families can switch mid-call: today Roman, next week Hangul, the same celebration tone in both. - **Capped at 12 minutes per day, parent-configurable.** The app stops being available after the daily cap — softly, with a "you both did such lovely work today" close-out. This is not gamification with a fake limit; this is the design refusing to become drudgery. Parents can lower the cap (8 minutes for a younger child), raise it slightly (15 for an enthusiastic six-year-old), or set it to "no cap, we'll just stop when we stop". Default 12. - **Works on a video call without taking the call over.** The app is a side-by-side companion to FaceTime, WhatsApp video, Google Meet — it never tries to be the call. The grandparent holds the phone for the call as they normally would, and the app picks up the camera feed via a small picture-in-picture window or a paired second-device flow. The relationship is the call; the app is the helper. ## 4. Features to build - One-tap "Start a practice session" button on the grandparent's home screen, large enough for arthritic hands - Live camera view of whatever the child is holding up — front camera on tablet, back camera on phone, either picture-in-picture beside the call or full-screen on a paired second device - Letter recognition the moment the child holds up a piece of paper steady for ~1.5 seconds (debounced, so a child waving paper doesn't fire 40 recognitions) - Multimodal handwriting parse — Gemini 3.5 Flash reads the letter, names it, identifies the script (Roman, Hangul, Devanagari, etc.), and returns a structured `LetterAttempt` record - Warm celebration in audio: either the Live API celebration voice ("Yes! That's M! Lovely lines!") or a silent on-screen text-only mode for grandparents who prefer to celebrate in their own voice - Specific praise — never "great job", always "look at those two tall lines", "the curve on top is exactly right", "you closed the loop on the O all the way round" — generated from what the model actually saw - Suggested next letter, drawn from the child's progress book and a small pedagogical map per script (Roman: M → N, L → T, C → O; Hangul: ㅁ → ㄴ; Devanagari: क → ख) - Never-corrective mode: the app is forbidden from ever saying "that's not right". If the letter is unreadable, it gently asks the child to show it again, closer or in better light, and names what it did see — "I see a beautiful tall line and a curve" - Per-child progress book: every letter the child has practised, the day they first drew it, the day they drew it most confidently, an optional photo of the best version (kept locally on the parent's device, never uploaded without explicit consent) - Parent's view: a calm timeline of recent practice sessions, the letters worked on, who was on the call, and how long the call ran. No score, no streak, no badges. The only emphasis is on the letters the child enjoys drawing over and over. - Grandparent's view: today's call ready to go. A "you and Apo last practised L, M, N — shall we try O today?" suggestion at the top. The grandparent can accept, ignore, or pick any letter from the book. - Daily session cap timer, default 12 minutes, parent-configurable between 6 and 20 minutes (or off). The app eases out with a soft "you both did such lovely work today" close-out and disables itself until tomorrow. - Multi-script switcher — the grandparent can pick a script at the start of the session (or have the child choose). The progress book records script per letter so a child learning Hangul and Roman in parallel has two books that stack into one. - Voice clone option (parent-consented) — record a 30-second sample of the grandparent's own voice once; the Live API celebration voice can fall back to TTS in the grandparent's own voice for moments when audio celebration is wanted but the grandparent's mouth is full of biscuit. Off by default. Requires explicit double-consent from both the grandparent (recording themselves) and the parent (allowing TTS playback to their child). - Letter-formation hint mode (parent toggle) — for a child who is asking "Lola, how do you make a B?", the app can draw a slow, single-stroke animation of the letter on the grandparent's screen so the grandparent can describe the strokes ("a tall line down, then a bump, then another bump"). The animation is on the grandparent's side only — the child sees the grandparent draw it on paper, not a screen - Print-at-home practice page (Lola's idea, James's evening hobby) — the parent can print a single A4 page of dotted-line tracing letters for whatever letter the child is currently enjoying, so between calls there is something on paper - "Lola is offline" mode — if the grandparent is on a flight or has no signal, the parent can record a short letter introduction by themselves; the child plays it back from a paired account, then draws, and the next day's grandparent call starts with "look what you did when I wasn't there" - End-of-session shared moment — a photo of the child holding up their favourite letter from today's session, captured on the grandparent's screen with one tap, sent only to the parent's account. The child never sees it being saved; this is a memento for the parent, not a performance metric. - Family invitations — a single archive (per grandchild) shared between the kid's parent, the kid's other parent if applicable, both sets of grandparents, and any other elder the family invites. Each adult sees the same progress book; only one adult is on the call at a time - Quiet-on-Sundays mode — parent toggle for any specific day of the week where the app does nothing, the cap does nothing, no nudges, no "you haven't practised in three days" notifications (the app has none of these by default — see the negative constraints) ## 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) - **Multimodal handwriting recognition** (Gemini 3.5 Flash) — reads a child's hand-drawn letter from a still frame of the camera feed. Handles Roman, Hangul, Devanagari, Tamil, Bengali, Arabic, Hebrew, Cyrillic, Hiragana, Katakana, and Thai scripts. Robust to: paper held at an angle (up to ~40°), low evening light, marker bleeding through to the back of the page, glare from a window, the child's own hand partially covering the letter, the letter drawn in pencil or in glitter pen or in coloured wax crayon. One API call per debounced "the child is showing me something" event. Pinned at `gemini-3.5-flash` with `thinkingLevel: medium` — Flash conflates U with V and reads sideways letters as different letters entirely. - **Live API** (`gemini-3.5-flash` Live model) — the warm celebration voice on the grandparent's side, when audio celebration is enabled. The Live API is told to be a friendly, never-corrective companion who speaks in short specific sentences ("Yes! That's M! Look at those two tall lines!"), never longer than two sentences at a time. Voice-only; no video output; latency target <500ms from letter recognised to celebration spoken. The Live API session lasts only as long as the practice session — opened on session start, closed on session end. It is NOT a persistent assistant. - **Structured output / JSON Schema** — each `LetterAttempt` record matches the `LetterAttempt` schema below. Returned by the recognition call; appended to the per-child progress book in Firestore. - **Multilingual TTS (fallback)** (`gemini-3.1-flash-tts-preview`) — when the Live API celebration voice is disabled or unavailable (poor connectivity, parent has turned audio off), the app falls back to a TTS-spoken praise line generated by a separate `gemini-3.5-flash` call. Used only as a fallback; the Live API is the primary celebration path. - **Long context (1M tokens)** — used once per week, server-side, to look at the last seven days of the child's progress book and suggest a kind, never-prescriptive "letters your grandchild enjoyed drawing this week" summary for the parent's view. The progress book for a single child for a week is small (~10–30k tokens); the long context ceiling is not stressed. **Guardrail**: if a family adds many grandchildren under one parent account (say five cousins), batch per child, not per family. The cap is per-child. - **Thinking levels** — `medium` for the recognition call (a wobbly letter held sideways needs the model to reason about orientation and intent). `low` for the praise-line generation call (short, friendly, no reasoning needed). `low` for the weekly summary call. Surface `thoughtSummary` only to the parent in a small "(i) how the app read this letter" tooltip on the progress book — never to the child. ### 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 to invite the second parent, both sets of grandparents, etc.) requires the sender domain to be authorised in Firebase Auth. Archives are private to the family group and explicitly-invited members. No public-by-default. No public-by-anything. - **Database — Required.** Firestore for `users`, `children`, `family_groups`, `progress_books`, `letter_attempts`, `practice_sessions`, `invitations`, `consents`. - **File storage — Required.** Firebase Storage for the optional "best version of this letter" photos kept by the parent + the optional grandparent voice-clone sample (encrypted at rest; never used for training; deletable at any time from the parent's settings). **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 photo or audio upload. Pre-signed URLs only; the photos and audio are never publicly addressable. - **Email — Required (transactional).** Family invitations via magic links (Firebase Auth). Weekly "letters your grandchild enjoyed this week" summary to the parent, on opt-in only (off by default). - **Payments — Not needed for v1.** Free for personal use. A future "print a bound alphabet book of every letter the kid drew across the year" 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; the FaceTime / WhatsApp / Meet call is the user's call, not the app's — the app never tries to be the video infrastructure. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if printing tier added) 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 child's progress book' inside the UI (the parent owns this; gone in 60 seconds, no soft delete) · explicit opt-in for any analytics · the child's hand-drawn letters and the grandparent's voice clone 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) · COPPA: the child is not an account holder; the parent is. The parent consents on the child's behalf, and the parent can revoke consent and delete everything at any time. **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. **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 | |------|-------|---------------|---------------| | Recognise the held-up letter → `LetterAttempt` schema | `gemini-3.5-flash` | medium | (none) | | Generate the specific warm praise line for the recognised letter | `gemini-3.5-flash` | low | (none) | | Live API celebration voice on the call | `gemini-3.5-flash` (Live) | n/a | n/a | | Suggest the next letter from the child's book | `gemini-3.5-flash` | low | (none) | | Weekly "letters your grandchild enjoyed" parent summary | `gemini-3.5-flash` | low | (none) — long-context over the week's progress book | | Generate the slow letter-formation hint animation strokes | `gemini-3.5-flash` | low | (none) | | Fallback TTS speak the praise line | `gemini-3.1-flash-tts-preview` | n/a | n/a | *Note for builders:* on TTS, Live API, 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 LetterIdentified = z.object({ script: z.enum([ "roman", "hangul", "devanagari", "tamil", "bengali", "arabic", "hebrew", "cyrillic", "hiragana", "katakana", "thai", "punjabi-gurmukhi", "amharic-ethiopic", "other", ]), letter_canonical: z.string(), // e.g. "M", "ㅁ", "क", "م" letter_canonical_name_in_script: z.string(), // "M", "mieum", "ka", "meem" case_or_form: z.string().nullable(), // "uppercase", "lowercase", "isolated form" (Arabic) confidence: z.number().min(0).max(1), }); const NoticedStrength = z.object({ // The model names ONE concrete, specific thing about how the child drew it. // Never generic. Never "good job". Always the specific stroke/curve/closure. text_for_grandparent: z.string(), // "two tall vertical lines and a brave V between them" text_for_child_voice: z.string(), // "Look at those two tall lines!" }); const LetterAttempt = z.object({ attempt_id: z.string(), child_id: z.string(), family_group_id: z.string(), practice_session_id: z.string(), captured_at_iso: z.string(), frame_uri: z.string().nullable(), // gs:// URI of the captured frame, if parent has enabled saving frame_saved_locally_only: z.boolean(), // true if parent has opted out of cloud storage identified: LetterIdentified.nullable(), // null only if the model genuinely couldn't read it noticed_strength: NoticedStrength.nullable(), // null only if unreadable child_drew_intentional_letter: z.boolean(), // false for a scribble or a drawing — used to skip recognition is_intentional_drawing_not_letter: z.boolean(), // child held up a drawing of a cat, not a letter — celebrate the cat, do not flag as failure unreadable_reason: z.enum([ "letter_too_far_from_camera", "letter_too_close_to_camera", "letter_held_at_extreme_angle", "letter_partially_off_screen", "letter_obscured_by_hand", "lighting_too_dark", "lighting_too_bright_glare", "paper_too_translucent_marker_bleed", "model_uncertain", "not_a_letter", "not_unreadable", // when it IS readable ]), next_suggested_letter: z.object({ letter_canonical: z.string(), letter_canonical_name_in_script: z.string(), short_reason_for_grandparent: z.string(), // "After M, N is its sister" short_reason_for_child_voice: z.string(), // "Want to try N next? It's M's sister!" }).nullable(), praise_already_spoken: z.boolean(), // true once Live API has voiced it on the call }); const ProgressBookEntry = z.object({ child_id: z.string(), family_group_id: z.string(), script: z.enum([ "roman", "hangul", "devanagari", "tamil", "bengali", "arabic", "hebrew", "cyrillic", "hiragana", "katakana", "thai", "punjabi-gurmukhi", "amharic-ethiopic", "other", ]), letter_canonical: z.string(), letter_canonical_name_in_script: z.string(), first_practised_iso: z.string(), last_practised_iso: z.string(), times_practised: z.number().int().min(0), times_drawn_confidently: z.number().int().min(0), // confidence >= 0.85 in identified child_appears_to_enjoy_drawing_this_one: z.boolean(), // surfaced when the child draws it 3+ times across sessions best_attempt_id: z.string().nullable(), // parent-curated, optional }); const PracticeSession = z.object({ practice_session_id: z.string(), child_id: z.string(), family_group_id: z.string(), grandparent_account_id: z.string(), started_at_iso: z.string(), ended_at_iso: z.string().nullable(), cap_minutes: z.number().int().min(6).max(20), cap_reached: z.boolean(), cap_reached_softly: z.boolean(), // did we ease out gently? attempts: z.array(z.string()), // attempt_ids letters_practised: z.array(z.string()), script: z.enum([ "roman", "hangul", "devanagari", "tamil", "bengali", "arabic", "hebrew", "cyrillic", "hiragana", "katakana", "thai", "punjabi-gurmukhi", "amharic-ethiopic", "other", ]), notes_for_parent_optional: z.string().nullable(), // grandparent can add a one-line note: "she's so proud of her N today" }); type LetterAttempt = z.infer; type ProgressBookEntry = z.infer; type PracticeSession = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` on the the recognition call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash confuses sideways E with M, reads upside-down N as Z, and silently smooths over genuinely-wobbly hands by guessing the alphabet position from context. The recognition call is the one place we spend the money. - Model is too kind and says "yes!" to anything held up — including a scribble, a drawing of a cat, and an empty page. Set `child_drew_intentional_letter` and `is_intentional_drawing_not_letter` honestly; the celebration logic handles each path with its own warm response ("oh you drew a cat! Tell Lola about your cat!" vs the letter celebration). - Praise reads as generic AI flattery — "amazing job!", "fantastic!", "you're so smart!". Hard rule: praise must reference what the model actually saw. Specific strokes. Specific curves. Specific closures. If the model can't name a specific thing, the praise line is the child's name + "I saw a beautiful tall line — can you show Lola again, a bit closer?" Never "great job". - Praise turns corrective — "almost!", "try again!", "that looks a bit like a W though!". This is forbidden in the system instruction. There is no correction path. If the letter is unreadable, the path is "show me again" plus naming what was visible. There is no "wrong". - Model translates a letter name into the grandparent's language and gets it wrong ("ㅁ" → "M" because they look similar). The letter name comes from the script's native naming convention. ㅁ is "mieum", not "M". Devanagari क is "ka", not "K". Arabic م is "meem", not "M". Pinned in the system instruction. - Live API session left open for the whole day, racking up minutes. Open on practice-session start, close on practice-session end (or on cap reached, or on app backgrounded for >60 seconds). Treat the Live session as a small expensive resource. - TTS reads "ㅁ" by trying to speak the Unicode codepoint. Fallback path: the praise line is always generated in the grandparent's preferred language by the `gemini-3.5-flash` praise call; the letter is referenced by its native name in that language. The TTS call receives a fully-formed sentence, not a raw glyph. - Recognition fires 40 times in 4 seconds because the child waves the paper. Debounce: require ~1.5 seconds of relative stillness before calling the recognition API. Cancel the in-flight call if the child moves the paper. - Multipage / multi-image confusion — the child holds up two letters at once on the same sheet. The schema returns ONE letter per attempt; if the model sees two, it picks the larger / more central one and returns that, with `unreadable_reason: not_unreadable` and a noticed-strength for that letter. The other letter is the child's job next time. - Camera frame URLs are Firebase Storage public URLs passed straight to `generateContent`. Don't. Upload via the Files API (`files/*` resource name) or send the frame as `inlineData` (base64). Firebase Storage public URLs are not fetched server-side by Gemini. - Praise call hallucinates a feature of the letter the model didn't see ("nice serifs on your T!"). System instruction: praise must reference ONLY the strokes the recognition call identified. If you didn't see it, don't praise it. - Live API celebration voice continues talking after the grandparent has already started celebrating themselves. Soft barge-in: if the grandparent's microphone is detecting their own voice celebrating, the Live API yields silently. The grandparent always has the floor. ### Negative constraints (hard rules) - Do NOT correct the child. Ever. There is no path in this app where the model says "that's not quite right", "almost", "try again", "look more closely", or any synonym. If the letter is unreadable, the model says what it DID see and asks the child to show it again. The correction-shaped move is forbidden. - Do NOT flatter the child generically. "Great job!", "Amazing!", "You're so smart!" are AI-flatter words that mean nothing to a five-year-old and even less to a four-year-old. Praise must reference a specific concrete observable thing the model actually saw — a stroke, a curve, a closure, the way the child held the paper steady. - Do NOT score, rank, or grade the child. There is no number anywhere in the child-facing view. No stars, no points, no progress bars, no levels, no streaks. The progress book in the parent's view is calm and observational; it has counts (this letter has been practised seven times across four sessions) and a "your child seems to enjoy drawing this one" surfacing — that is all. - Do NOT send daily reminders to the parent about "your kid hasn't practised in N days". The app is for the grandparent–grandchild relationship; it is not a habit tracker. The parent gets one weekly summary email if they have opted in (off by default). That is the only recurring notification. - Do NOT exceed the daily cap. The cap is a hard cap. When it is reached, the celebration voice eases out with a warm close-out and the practice mode disables itself until the next calendar day in the child's timezone. The parent can configure the cap; the parent cannot disable the cap from inside the child's session — that is a parent-settings change, deliberately friction-laden. - Do NOT use the child's hand-drawn letters or the grandparent's voice clone 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. The progress book is private to the family group. There is no public profile, no shareable link, no social anything. The parent can invite specific named adults to the family group; those adults see the same book. - Do NOT make the child an account holder. The parent owns the data. COPPA: under 13, the child is not a user in the legal sense — the parent is. The child has a name and a profile inside the parent's account. The grandparent has a separate account; they are invited by the parent into the family group. - Do NOT translate the letter name into a Roman-alphabet equivalent if the child is learning a non-Roman script. ㅁ is "mieum", not "the Korean M". क is "ka", not "the Hindi K". The child is learning the actual script, not a transliteration. - Do NOT auto-clone the grandparent's voice from arbitrary FaceTime audio. The voice clone path is explicit-consent, double-consent (grandparent consents to being recorded, parent consents to playback for their child), one-time-recorded, parent-revocable, deletable in 60 seconds from settings. The Live API celebration voice is the default; voice clone is opt-in only. - Do NOT show the parent a "your grandparent missed the call" or "your grandparent hasn't joined a session in N days" notice. The grandparent's pace is the grandparent's pace. The app respects it. - Do NOT use the camera feed for anything except the held-up letter. No face detection. No "happy face detected" celebration. No emotion recognition on a four-year-old. Ever. ### 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: Recognise the held-up letter → `LetterAttempt` schema Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You are watching a video frame from a grandparent's phone or tablet during a video call with their small grandchild. The child is between roughly three and seven years old. The grandchild is holding up a piece of paper (or a card, or an exercise book, or the back of an envelope) on which they have hand-drawn a single letter. The letter may be drawn in pencil, ballpoint, marker, glitter pen, crayon, or coloured wax. The paper may be held at an angle of up to about 40 degrees from vertical. The light may be evening lamp-light, late afternoon window light, or overhead kitchen fluorescent. The child's own hand may partially cover the lower edge of the letter. Other things may be visible in the background: a cup, a sibling, a parent's torso, an unrelated drawing. Your job is to: 1. Identify the script the letter belongs to (Roman, Hangul, Devanagari, Tamil, Bengali, Arabic, Hebrew, Cyrillic, Hiragana, Katakana, Thai, Punjabi Gurmukhi, Amharic Ethiopic). If the script is ambiguous from one letter alone, use the script hint passed in the user prompt (the grandparent selected it at the start of the session). 2. Identify the canonical letter being drawn. Use the script's native naming convention for the letter name: "M" not "the Roman M", "ㅁ" → "mieum" not "the Korean M", "क" → "ka" not "the Hindi K", "م" → "meem" not "the Arabic M". 3. Notice ONE specific concrete thing about how the child drew it. This is the load-bearing kindness of the app. Never generic. Examples of good noticed strengths: - "two tall vertical lines and a brave V between them" (for M) - "a closed loop all the way round, no gap" (for O) - "the bump on top is the same size as the bump on the bottom" (for B) - "the line is held very steady — see how straight it is" (for I) - "a brave curve that swings all the way down" (for C) - "the dot above the line is right where it should be" (for i) Bad noticed strengths (forbidden): - "great job" - "well done" - "perfect" - "good letter" - "amazing handwriting" - any praise that does not reference a specific stroke, curve, closure, proportion, or steadiness that you actually see in the frame. The noticed_strength has two forms: - text_for_grandparent: descriptive, complete sentence, the grandparent's natural language. - text_for_child_voice: short, warm, ~6-10 words, the kind of thing a kind adult would say to a four-year-old. Specific, never generic. 4. If the child is holding up a DRAWING that is not a letter (a cat, a house, a tree, a stick figure, scribbles), set is_intentional_drawing_not_letter: true and set identified to null. Do NOT pretend it is a letter. Do NOT shame the child. The app handles the drawing path separately ("oh, you drew a cat! Tell Lola about your cat!"). 5. If the letter is genuinely unreadable, set identified to null, set noticed_strength to null, and pick the most accurate unreadable_reason from the enum. The downstream app will ask the child warmly to show it again, naming whatever was visible. 6. Suggest the next letter from the script's natural pedagogical order (Roman: A → B; M → N; L → T; C → O. Hangul: ㅁ → ㄴ; ㄱ → ㄴ. Devanagari: क → ख. Arabic: ا → ب). The grandparent may ignore the suggestion entirely; this is a hint, not a plan. The reason given must be warm and short ("after M, N is its sister", "after C, O closes the curve all the way round"). Hard rules: - Never correct. There is no path where you say "that's not quite right", "almost", "try again", or any synonym. - Never grade. No confidence percentages, no scores, no stars in the child-facing fields. - Specific praise only. If you cannot name a specific concrete thing you saw, set noticed_strength to null and ask the child to show again. - One letter per attempt. If the child is holding up two letters on the same sheet, pick the larger / more central one and return that. - Never name a stroke you did not see. If the letter is sans-serif, do not praise the serifs. - Never describe the child's body, face, expression, or emotion. The camera frame is for the held-up letter only. Do not say "your apo looks happy". Do not say anything about the child except what they drew. - Never mention the background. Do not describe the room, the parent visible behind the child, the cup on the table. Output ONLY the LetterAttempt JSON matching the provided schema. No commentary. JSON only. ``` --- ### Call: Generate the specific warm praise line for the recognised letter Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive: the recognised letter, the noticed_strength (text_for_child_voice form), the child's name (first name only — the parent provided it at family-group setup), the grandparent's preferred address term for the child ("Apo", "Amada", "Sweetheart", "Baby girl", "Bub", "Apo ko", "Habibi", "Mija", "Sevgilim", etc., free-text, whatever the grandparent typed), and the child's preferred language (the parent set this). Your job: compose ONE short warm spoken-aloud sentence — what the celebration voice will say through the grandparent's phone speaker on the call. The sentence must be: - Short. 6 to 14 words. Children this age switch attention fast. - Specific. Reference the noticed_strength concretely. Do NOT paraphrase to "great job" or any generic praise. - Warm. Spoken in the rhythm a kind grandparent uses with a four-year-old. Not a teacher. Not an app. A kind adult. - In the child's preferred language. If the language is one Gemini speaks well, write in that language. If not, write in the grandparent's address language. - Includes the letter name in the script's native naming. "M", "mieum", "ka", "meem". Not "the M sound" or "the K sound". - Does NOT include the child's name in every sentence. Including the name every single time becomes performative. Include it about every third or fourth time, at most. Examples of good praise lines (English, Roman script): - "Yes! That's M! Look at those tall lines!" - "Apo, that's O — a perfect closed loop." - "C! And what a brave curve." - "Mija, that's a beautiful B." - "L! Look how steady your line is." Examples of good praise lines (Tagalog, Roman script): - "Aba, M iyan! Ang ganda ng linya!" - "Apo ko, O iyan, buong-buo!" Examples of good praise lines (Korean, Hangul): - "와! 미음이야! 네모가 예쁘게 닫혔어!" - "ㄴ이네! 정말 잘 그렸어." Examples of good praise lines (Hindi, Devanagari): - "अरे वाह, क है यह! कितनी सुंदर बनी है।" Examples of BAD praise lines (forbidden): - "Great job!" - "Amazing!" - "Perfect!" - "You're so smart!" - "What a clever girl!" - "Almost!" / "Try again!" / "That looks more like a W!" Output the sentence as a single plain-text string. No quotation marks. No commentary. No JSON wrapper. ``` --- ### Call: Live API celebration voice on the call Model: `gemini-3.5-flash` (Live) · n/a · n/a ``` You are a warm voice on a grandparent's phone, speaking softly during a video call between a grandparent and their small grandchild. You speak ONLY when the app provides you with a pre-composed praise line to read aloud, or a pre-composed "show me again" line for an unreadable attempt. You do NOT improvise. Voice: warm, unhurried, mid-pitch, a kind grandparent's voice or a kind aunt's voice. Not a teacher's voice. Not a child's voice. Not a perky assistant. Take your time. Land each sentence. You receive pre-composed lines from a separate model call. Read them as written. Do not add words. Do not add filler. Do not say "okay let's see" or "alright" or any preamble. Yield the floor instantly when the grandparent speaks. If the grandparent's microphone detects their voice within the first ~300ms of your turn, stop speaking immediately and stay silent until the next pre-composed line arrives. The grandparent is the primary celebrator; you are only the secondary celebrator who helps when their hands are full or their throat is tired. Never speak more than two sentences in a row. Never start a sentence with "I". Never refer to yourself. You are not a character; you are a kind voice that names what the child drew. Never correct, never grade, never compare ("better than last time!", "even nicer than the M!"). Comparisons are forbidden. Every letter is its own moment. Silent close-out: when the practice session ends (either the grandparent ends it, or the cap is reached), say ONE warm short line — "you both did such lovely work today" or similar — at the same pace, and then stop. No commentary outside the spoken text. ``` --- ### Call: Suggest the next letter from the child's book Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive: the script the family is practising, the recent LetterAttempts in this session (most recent first), and the progress book entries for this child for this script. Your job: pick ONE letter to suggest next. Output as a small JSON object with letter_canonical, letter_canonical_name_in_script, short_reason_for_grandparent, short_reason_for_child_voice. Rules: - Prefer letters the child has practised before, with confidence rising. A letter the child drew shakily on Sunday and is now drawing more confidently on Wednesday is the perfect next one. - Prefer "sister" letters — letters whose strokes are related to the one just practised. M → N. C → O. L → T. B → P or B → D. ㅁ → ㄴ. क → ख. - Avoid jumping to a letter the child has clearly not enjoyed. "Clearly not enjoyed" = drew once, did not return to it across three subsequent sessions. - Occasionally (about every third suggestion) return to a letter the child has marked as enjoyed-drawing-this-one. Children this age love returning to a letter they like. - Never suggest a long sequence. ONE letter. The grandparent decides next. - short_reason_for_grandparent: one short phrase, the grandparent's natural language. "After M, N is its sister." - short_reason_for_child_voice: one warm short phrase, ~6-10 words, for the celebration voice to read aloud (optional — the grandparent may choose to suggest in their own voice). Output JSON only. No commentary. ``` --- ### Call: Weekly "letters your grandchild enjoyed" parent summary Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none, long context) ``` You receive: this child's full progress book and the PracticeSession records from the last seven days. The parent has opted into a weekly summary email. Your job: compose ONE short calm paragraph (3-5 sentences) for the parent, naming concretely: - Which letters the child practised this week. - Which letters the child seemed to enjoy drawing the most (drawn 3+ times across sessions, or marked enjoyed). - Any letter the child drew with rising confidence across the week. - The grandparent (or other adult) who joined each session, by first name if available. Hard rules: - Never compare the child to other children. Other children are not in scope. - Never say "your child is behind" or "your child is ahead" of anything. - Never recommend more practice, less practice, harder letters, or different times of day. The parent decides; you observe. - Never use the words "score", "level", "progress" in the ranking-and-grading sense. "Progress" in the sense of "moved along" is fine. - Never include numbers like "12 letters practised across 4 sessions" without warmth. Numbers are fine if framed warmly ("you'll see in the book that Lola joined three calls this week"). - Warm, observational, calm. The parent is reading this on a Saturday morning over coffee. The tone is "your kid is doing their thing with their grandparent, and here's what that looked like". Output the paragraph as a single plain-text string. No JSON, no markdown. No commentary. ``` --- ### Call: Generate the slow letter-formation hint animation strokes Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive: a script and a single letter (e.g. "B" in Roman, "ㅁ" in Hangul, "क" in Devanagari, "ب" in Arabic). Your job: return the canonical stroke order for that letter as an array of stroke objects. Each stroke is an array of (x, y) points on a normalised 100×100 canvas, with stroke direction implied by point order. This is for the GRANDPARENT'S screen only. The grandparent will describe the strokes to the child verbally ("a tall line down, then a bump, then another bump"). The child draws on paper, never on a screen. Rules: - Use the canonical stroke order for the script (e.g. Hangul ㅁ: top horizontal, then left vertical, then bottom horizontal, then right vertical — or as conventionally taught in primary school in Korea today). - Devanagari letters include the horizontal top line ("shirorekha") as the final stroke, not the first. - Arabic letters use right-to-left stroke direction. - Roman letters use the conventional primary-school stroke order (B: vertical down, then top bump, then bottom bump). - Keep the canvas 100×100 with reasonable margins (10 units). - Output ONE letter per call. No multi-letter. Output JSON: { strokes: [ { points: [{x, y}, ...] }, ... ] }. No commentary. ``` --- ### Call: Fallback TTS speak the praise line Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: warm, unhurried, a kind grandparent's voice for the target language (the parent's preferred language for the child). Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches that language. Prefer the gender that matches the grandparent on the call when published for the locale; fall back to whichever is available rather than blocking. Pre-process the text before sending it to TTS: - The input is one short pre-composed praise sentence (6-14 words) generated by the praise-line call. - Insert a single ellipsis (`…`) before the letter name to give a natural beat of pause ("Yes!… That's M!… Look at those tall lines!"). - Do not add filler. Do not add any words. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read warmly and unhurriedly, as a kind grandparent celebrating a four-year-old's hand-drawn letter. …". There is no separate `style` API field on Gemini 2.5 TTS; the directive sentence inside the input is how style is conveyed. Phoneme overrides (SSML ``, ``) are NOT exposed by Gemini 2.5 TTS. Pronunciation comes from the chosen voice's native locale; pauses come from ellipses and blank-line em-dashes. The voice fallback is used only when the Live API celebration path is unavailable. The grandparent should not be able to tell which path is currently active — both should sound like a kind voice on the call. ``` ## 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 Sunday FaceTime call with Lola.** A four-year-old in Manila on Sunday morning is on FaceTime with his lola in Daly City on Saturday evening. He runs to get a piece of paper and a pencil. He draws an M (or what he hopes is an M) and holds it up to the screen. Lola has the app open on her iPad next to the FaceTime. The app sees the M, names it, and Lola — who is the one celebrating today — gets to say "Yes! That's M, apo ko! Beautiful tall lines!" without having to squint at the screen first. - **The Korean halmoni at 8am on Sunday in Seoul.** Halmoni is on a video call with her two grandkids in New Jersey, where it is 7pm Saturday. The older granddaughter (six) holds up ㅁ; the younger grandson (three) holds up a scribble. The app gracefully handles both: a warm celebration for the mieum, a warm "oh, you drew something! Tell Halmoni about your drawing!" for the scribble. Neither child knows the app is doing anything differently. - **The Italian nonno in Bologna with his grandson in Toronto.** Nonno wants to teach his grandson the Italian alphabet because the kid is growing up Anglophone and Nonno does not want B-E-L-L-O to disappear from the family. The app respects that the alphabet is Roman, and the praise lines come in Italian: "Bravo, M! Che belle linee dritte!" - **The Hindi-speaking dadi in Bengaluru with her grandkids in London.** Dadi wants to introduce Devanagari before they are big enough to learn it formally at the weekend school. She picks the Devanagari script in the session setup. The five-year-old in London holds up a wobbly क, and the app says "अरे वाह, क है यह! कितनी सुंदर बनी है।" — Dadi grins, the kid grins. - **The grandparent who wants to celebrate in their own voice.** A Vietnamese ông who prefers not to have any audio coming out of the app — he wants to be the only voice the child hears. He uses the silent-celebration mode: the app shows him the letter the child drew (and the noticed strength) on his screen, and he reads it aloud himself, in his own warm Vietnamese. - **The day the child draws a cat instead of a letter.** The child holds up a beautiful drawing of a cat. The app recognises this is not a letter (`is_intentional_drawing_not_letter: true`), and the grandparent's screen says "Apo drew something! Ask them about it." The grandparent does. The cat is celebrated. No "failure" anywhere. - **The day the camera can't see properly.** The child is holding the paper too close to the camera, or the room is too dark, or a sibling is hugging them and covering the page. The app says "I see a beautiful tall line — can you show Lola again, a bit closer?" and the grandparent gently says the same thing in their own warm voice. The child shows again. The letter is read. No "wrong" path. - **The day the cap is reached.** The four-year-old and Lola have been at it for 12 minutes. The app eases out with a warm "you both did such lovely work today" and disables practice mode until tomorrow. Lola and the four-year-old keep talking on FaceTime; the call doesn't end. They just stop practising letters. - **The mid-week parent moment.** The mum, late Wednesday evening, opens the app to look at the progress book. Her daughter has practised L, M, N, and a surprise return to C twice this week (she likes drawing C). Lola joined Sunday's call; auntie Marisol joined Tuesday's. The mum sees no scores. She sees no streaks. She sees a calm record of what her kid did with her grandparents this week, and she goes to bed feeling like the relationships are alive. - **The voice-clone moment for the parent away on a work trip.** A mum is in Singapore for a week. She has recorded a 30-second voice sample with the app's voice-clone feature (double-consent, parent and the recorded adult — herself in this case). On Tuesday night the daughter has a practice session with the dad and the app, and the celebration voice is the mum's. The daughter doesn't say anything. The dad notices and texts the mum "she just kept drawing Ms because she wanted to keep hearing you say it". - **The grandparent who is offline that week.** Lola is on a flight from SFO to MNL. The four-year-old asks to do letters anyway. The dad records a quick "let's see what you can do today, apo" intro in his own voice; the practice session runs without Lola. On Sunday when Lola lands, the app shows her the letters her grandson practised while she was airborne, and the next session opens with "look what you did when I wasn't there!" - **The child with fine-motor delay.** A six-year-old whose OT has flagged fine-motor delay practises L over and over because L is one tall line and one short line and he can make them. The app, gently, celebrates every L. The progress book shows the parent that L is the most-loved letter this month. The mum mentions this at the next OT session — the OT smiles. ## 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 grandparent's hands holding a phone propped against a teacup, the screen showing a smiling child mid-laugh on a video call (illustration, not a real child). One paragraph: "Teach Me My Letters is a quiet helper for grandparents doing alphabet practice with their grandkids over video calls — in any letters, any language, with the celebrating done by you and the seeing done by the app." Single Google sign-in button; Apple sign-in next to it. Below: "I was invited — open my invitation" → magic-link flow. Below that: "Try the demo" → loads the demo family from section 8a. 2. **Role picker — first sign-in only.** "Are you the parent of a small grandchild's parent, or are you the grandparent?" Two big cards. Parents go to the family-group setup. Grandparents go to "wait for your invitation" with a friendly note. 3. **Parent: family-group setup.** Add a child (first name, age, primary language, script(s) being practised). Set the daily session cap (default 12 minutes). Invite the grandparents and any other adults the family wants in the group — magic-link emails go out. Privacy explainer here, not buried later. 4. **Parent: progress book home.** Calm timeline of recent practice sessions, the letters worked on, who joined, how long each ran. The top of the page shows ONE warm sentence ("Lola and Auntie Marisol both joined this week"). Below, a magazine-grid of letters with a small badge for "your kid seems to enjoy drawing this one" on letters the child has returned to 3+ times. Filter by script. No score anywhere. 5. **Grandparent: home screen.** Large primary button: "Start a practice session with Apo". Above it: "You and Apo last practised L, M, N on Sunday — shall we try O today?" The grandparent can accept, ignore, or pick any letter from the book. Below: a tiny "How this works" link for first-time grandparents who didn't read the onboarding. 6. **Practice session — live view.** Picture-in-picture window pinned to the corner showing the FaceTime / WhatsApp / Meet call (the grandparent is on the call already; this app does not run the call). Main view: the camera feed cropped to a paper-shaped region where the child will hold up their letter. Above the camera: the letter just identified, big and warm, with the noticed-strength sentence below it. Below the camera: the next-letter suggestion as a quiet card. A small timer bar shows the remaining cap minutes — soft, not stressful. 7. **Practice session — close-out.** When the grandparent ends the session (or the cap is reached): a warm "you both did such lovely work today" with a small grid of the letters practised in this session. One-tap "save a photo of Apo's favourite letter today" → captures the last frame the child held up and saves it to the parent's account only (never the grandparent's). The grandparent can add a one-line note to the parent ("she's so proud of her N today"). 8. **Progress book — letter detail.** Tap any letter. A clean page showing: every attempt for this letter, with the date and the noticed-strength sentence in italics. The parent can mark one attempt as the "best" (their pick, never a score). A small note: "Lola has joined six practice sessions with this letter." 9. **Family group — members.** Names and avatars of every adult in the group. Roles (parent, grandparent, other elder). Each can be removed by the parent. A friendly "invite another adult" button. 10. **Settings — parent.** Daily cap minutes per child. Quiet-on-Sundays toggle (or any day). Weekly summary email toggle (off by default). Letter-formation hint mode toggle. Voice-clone enrollment (off by default, double-consent). Delete this child's progress book (gone in 60 seconds; the explainer text is direct and warm). 11. **Settings — grandparent.** Preferred address term for each grandchild ("Apo ko", "Mija", "Habibi"). Preferred language for the celebration voice. Audio celebration on / off (silent mode = text-only on the grandparent's screen, the grandparent celebrates in their own voice). Letter-formation hint mode toggle. 12. **Footer.** "Made for the Sunday call." Privacy: "Your child's letters are yours. We never train on them." 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 Teach Me My Letters." - Subhead: "Alphabet practice over a video call — in any letters, any language, with the celebrating done by you and the seeing done by the app." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic literacy app: it is for the call between a grandparent and a small grandchild; it never corrects the child; it caps the daily session at 12 minutes so it doesn't become drudgery. - Visual: a small annotated illustration of a phone screen split between a video call window and a child's held-up wobbly M, with the noticed-strength sentence in italics underneath — not a generic alphabet icon. **Slide 2 — Try it now.** - One short prompt: "Try with the sample family". - A live demo input pre-loaded with the demo family from section 8a (Lola Aurora and four-year-old Mateo in Manila; their script is Roman; their language pair is Tagalog and English). - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the recognition of the wobbly M held sideways, the warm specific praise, the suggestion of N). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample family in `/data/seed-family/` for your own." - "Adjust the praise voice and script options in `/server/prompts/`." - "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 (multimodal handwriting recognition)** — reads a child's hand-drawn letter from a single video frame. Roman, Hangul, Devanagari, Tamil, Bengali, Arabic, Hebrew, Cyrillic, Hiragana, Katakana, Thai, Punjabi Gurmukhi, Amharic Ethiopic. Robust to paper held at an angle, evening light, marker bleed, the child's own hand partially covering the letter. - **Gemini 3.5 Flash (Live API)** — the warm, never-corrective celebration voice on the call. Speaks only when given a pre-composed line. Yields the floor instantly when the grandparent speaks. - **Gemini 3.5 Flash (long context)** — once a week, looks at the child's progress book over the last seven days and writes one calm paragraph for the parent. - **Gemini 3.5 Flash (praise line)** — composes the specific warm sentence the celebration voice will read aloud. Pinned to reference what the recognition call actually saw — never generic flattery. - **Gemini 2.5 Flash TTS (fallback)** — used only when the Live API is unavailable. The grandparent should not be able to tell which voice path is active. - **Firebase Auth** — Google and Apple sign-in, family invitations via magic links. Magic-link email and Apple sign-in are user-configured — see env-vars. - **Firestore** — stores your child's progress book, per family, syncs in real time across the parent's and grandparent's devices. - **Firebase Storage** — keeps the optional "best version of this letter" photos and the optional grandparent voice clone. NOT auto-provisioned by AI Studio Build today — enable it in the Firebase console. - **Cost note** — see the detailed breakdown in 6d. A typical 12-minute practice session with one child costs about $0.18 of Gemini API spend. - **Privacy note** — your child's hand-drawn letters and the grandparent's voice clone are private to your family. 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. COPPA: the child is not an account holder; the parent is. The parent can delete the child's progress book at any time from settings — gone in 60 seconds, no soft delete. **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) - `SENDER_DOMAIN` — the authorised sender domain for magic-link emails (configure in Firebase Auth before first invite) **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: the Gemini 3.5 Flash handwriting recognition call is the biggest line item at ~$0.012 per letter recognised. A 12-minute session typically captures ~12-18 letter attempts. - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this child's progress book forever" — gone in 60 seconds), what is never sent for training, and COPPA-aware framing of the parent as the account holder for the child. **Documentation links:** - AI Studio Build docs - Gemini API multimodal, Live API, TTS, long-context docs - Firebase Auth, Firestore, Firebase Storage docs - A short note on COPPA-friendly design (parent as legal account holder for under-13s) **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) - **Recognise the held-up letter (Gemini 3.5 Flash, medium thinking)** — one image frame + small prompt, ~250 output tokens for the LetterAttempt JSON. ~$0.012 per recognised letter. **This is the biggest line item; every practice session bills against this.** - **Praise line (Gemini 3.5 Flash, low thinking)** — ~100 output tokens, ~$0.0001 per letter. - **Next-letter suggestion (Gemini 3.5 Flash, low thinking)** — ~80 output tokens, ~$0.00008 per letter. - **Live API celebration voice (Gemini 3.5 Flash Live)** — billed per audio second of input + output. A 12-minute session typically uses ~3 minutes of Live API audio (only during celebration moments, not the whole call). At ~$0.0003/second of audio (audio output via Live), ~$0.05/session. - **Fallback TTS praise (Gemini 2.5 Flash TTS)** — only used when Live API path is unavailable. ~$0.000003/character; ~$0.0001 per spoken line. Negligible. - **Letter-formation hint strokes (Gemini 3.5 Flash, low thinking)** — generated once per letter per script, cached forever. ~$0.0005/letter, total cost across the entire Roman + Hangul + Devanagari + Arabic + Hebrew + Tamil + Bengali + Cyrillic + Hiragana + Katakana + Thai + Punjabi + Amharic catalogue: roughly $0.30, one-time. - **Weekly parent summary (Gemini 3.5 Flash, low thinking, long-context over progress book)** — ~$0.05/child/week, only if parent has opted into the weekly email (off by default). - **Expected per-session cost (12-minute session, ~15 letter attempts):** ~$0.18, of which the recognition call is ~$0.18 and Live API is ~$0.05. **Expected per-month cost for one child practising twice a week:** ~$1.50. - **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. Optional "best version of this letter" photos are tiny JPEGs (~150 KB at typical phone-camera frame size); 200 saved photos per child per year ≈ 30 MB ≈ negligible. - **Voice-clone audio storage:** one 30-second WAV per consenting adult, ~500 KB. Stored only with explicit double consent; deletable from settings. ## 7. Design language - **Mood:** A grandparent's kitchen table on a Sunday morning. Not a children's app. Not a classroom. Not gamified. The phone propped against the teacup, the iPad on the placemat, the grandchild's voice coming out of the speaker. Warm light. Quiet UI. The screen does not compete with the child. - **Typography:** Display serif for the celebration letter on the grandparent's screen — big, warm, generous — so when the child's M is recognised the M appears on screen at the size of a coffee cup. A handwriting-styled accent (sparingly) only for the noticed-strength italic line below the letter. Clean grotesque for app chrome (Inter or Geist). - **Palette:** Warm-paper background `#FBF6EE`, deep ink `#1F1A12` for body text, soft coral `#D27A5E` for the celebration accent (the recognised-letter highlight, the next-letter suggestion card), warm yellow `#E8B848` only sparingly — for the daily-cap soft countdown, the "your child seems to enjoy this letter" badge. A muted blue `#3A5B73` for parent-only UI affordances (settings, consent toggles) so the parent-versus-grandparent contexts are calmly distinguishable. No primary red. No primary green. Nothing that reads "correct" or "wrong" — the design refuses the correct/wrong binary. - **Imagery:** The illustrations of grandparents and grandchildren are intentionally non-photographic — warm line drawings with soft fills, multi-ethnic by default. No stock photography of children. No real children's faces anywhere. The recognised letter on the screen is the hero visual, not a child's face. - **Hand-feel touches:** When the recognition call returns, the letter appears on screen with a soft fade-in (300ms) — not a bounce, not a celebration confetti, not a "ding". The noticed-strength sentence underneath types in slightly, one word at a time, at reading pace — so the grandparent has time to read it before they speak. The Live API celebration voice waits ~600ms after the letter appears before speaking, so the grandparent has the chance to speak first. Every interaction respects the grandparent's pace. - **Spacing:** consistent 4-px base. Generous whitespace — the screen should look uncluttered to a grandparent who is also trying to hold a phone and watch a video call. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Letter cards use 12; the welcome card uses 20; settings cards use 6. - **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-shadows. The recognised-letter card has the only meaningful shadow — soft, low, like a card on a wooden table. - **Motion:** purposeful — entrance fades, no bouncing, no theatrical hero animations. The recognised-letter fade-in is 300ms; the noticed-strength type-in is 60ms per word; the next-letter suggestion slides up from the bottom of the screen at 250ms. Respect `prefers-reduced-motion`: every motion above falls back to instant. The Live API celebration voice's 600ms pre-speak delay does NOT shorten under reduced-motion (it is a courtesy to the grandparent, not an animation). - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance — for the grandparent's first session ever: "Tell Apo to draw any letter and hold it up to the camera. We'll do the seeing." Never a generic "no data yet". ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible names, ages, scripts, address terms, sample noticed-strength sentences that fit the domain (use the seed content in section 8a as a starting point). When inventing, span a wide cultural range — Filipino lolas, Korean halmonis, Italian nonni, Hindi-speaking dadis, Vietnamese ông bà, Iranian madarbozorg, Tamil paatti — but never claim a fictional family is a real one. - Tone: warm, direct, free of corporate language. This template is for a person, 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. The parents are tired. The grandparents may be reading on a small phone in evening light. Every word should be earnings-its-keep. - Where the app outputs AI-generated content (the praise lines, the noticed-strength sentences, the next-letter suggestions), never label it as "AI says" — let it speak naturally in the warm voice of the app. The grandparent should not feel that an algorithm is celebrating their grandchild; they should feel that the app is helping THEM celebrate their grandchild. ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, scripts, and snippets verbatim where helpful, or generate close variants that sit in the same world. **Sample families (sidebar):** - "Lola Aurora and apo Mateo" — Lola Aurora in Daly City; Mateo, age 4, in Manila with his parents. Roman script; languages Tagalog and English. Practice mostly on Sunday mornings Manila time / Saturday evenings Daly City time. - "Halmoni Soojin and her grandkids" — Halmoni Soojin in Seoul; granddaughter Ji-Ah, age 6, and grandson Min-Jun, age 3, in Princeton, New Jersey. Two scripts: Hangul (Halmoni's lead) and Roman (school). Sunday mornings Seoul / Saturday evenings New Jersey. - "Nonno Giorgio and Lucia" — Nonno Giorgio in Bologna; granddaughter Lucia, age 5, in Toronto. Roman script; languages Italian and English. Saturday afternoons Bologna / Saturday mornings Toronto. - "Dadi Lakshmi and the cousins" — Dadi Lakshmi in Bengaluru; grandchildren Aanya (age 5) and Vikram (age 4) in London with their respective parents; the cousins are not siblings. Devanagari and Roman. Sunday late-mornings IST. - "Ông Tâm and Bảo" — Ông Tâm in Hồ Chí Minh City; grandson Bảo, age 4, in Houston. Roman script; languages Vietnamese and English. Ông prefers silent-celebration mode — he wants to be the only voice the child hears. - "Bibi Halima and the twins" — Bibi Halima in Mombasa; twin grandsons Yusuf and Idris, age 5, in Birmingham. Arabic and Roman scripts. Saturday late-afternoons EAT. - "Nana Marisol and Camila" — Nana Marisol (Camila's mother's adoptive mother, chosen family) in Oaxaca; Camila, age 6, in Los Angeles. Roman script; languages Spanish and English. Sunday early afternoons CST. **Sample LetterAttempt in detail view (this is what the demo should show):** - **child:** Mateo, age 4, Manila - **family group:** Lola Aurora and apo Mateo - **grandparent on call:** Lola Aurora - **script:** Roman - **captured at:** Sunday 9:14am Manila / Saturday 5:14pm Daly City - **identified:** letter_canonical "M", letter_canonical_name_in_script "M", case_or_form "uppercase", confidence 0.93 - **noticed strength:** - text_for_grandparent: "Two tall vertical lines and a brave V right between them — the V actually touches the bottom of the verticals, which is the hard part." - text_for_child_voice: "Yes! That's M! Look at those tall lines!" - **child_drew_intentional_letter:** true - **is_intentional_drawing_not_letter:** false - **unreadable_reason:** "not_unreadable" - **next suggested letter:** "N", "After M, N is its sister — same tall lines, with a diagonal." - **praise already spoken:** true (Live API celebration voice has voiced it once on the call) **Sample input artefacts (for the build to demonstrate):** - A phone-camera frame of a four-year-old's hand-drawn M on the back of an envelope, held up at about 15° from vertical, in evening light. - A phone-camera frame of a six-year-old's Hangul mieum (ㅁ), held up squarely to the camera, drawn in red marker on a sheet of A4. - A phone-camera frame of a five-year-old's Devanagari क, held up at an angle, drawn with the top horizontal "shirorekha" line correctly placed. - A phone-camera frame of a four-year-old holding up a scribble that is clearly a drawing, not a letter — meant to demonstrate the `is_intentional_drawing_not_letter` branch. - A phone-camera frame of a five-year-old holding up a B with the camera too close to the paper, so the letter overflows the frame — meant to demonstrate the unreadable_reason: letter_too_close_to_camera path. **Sample voice copy:** - Onboarding: "Tell Apo to draw any letter and hold it up to the camera. We'll do the seeing." - Processing (grandparent's screen, while recognition is running): "Looking at what Apo drew…" - Empty progress book: "This book is waiting for its first letter. Start a session with Apo and we'll keep the record." - Error (couldn't read, kindly): "I see a beautiful tall line — can you ask Apo to show us again, a bit closer?" - Save confirmation (grandparent → parent): "Saved — Mateo's M from today's call, with Lola's note: she's so proud of her tall lines." - Cap reached, warm close-out: "You both did such lovely work today. The book will be ready for you tomorrow." - Drawing-not-letter warm path: "Apo drew something! Ask them about it." **Sample family invitation email subject + body:** - Subject: "Mum — I made you a thing for our Sunday calls with Mateo." - Body: "Hi Mum — you know how you've been doing letters with apo on Sundays? I set up a little app that watches the camera and helps you see what he's drawing, so you don't have to squint. It also remembers which letters you've done already so you don't run out by Tuesday. Tap to join — it took me ten minutes to set up. Love you. [Open the app]" ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a grandparent's hands holding an iPad propped on a placemat, the iPad screen showing a smiling four-year-old (illustrated, not real) holding up a wobbly M to their camera. Warm Sunday-morning light. Generate via Nano Banana 2 with a prompt emphasising "warm kitchen, propped iPad, grandparent's hands at the edge of frame, hand-drawn cartoon child on the iPad screen, soft natural light, no real child's face". - **App icon / wordmark:** Set in the display serif. The letter "M" repeated three times — wobbly, increasingly confident — as the icon mark. No alphabet-grid icon. No graduation-cap. No pencil. - **Empty-state illustration:** A simple line drawing of a child's hand holding up a piece of paper with a single letter on it. Warm, soft, multi-ethnic by default in skin tone. Never a specific real-looking child. - **Demo family illustrations:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "warm line drawing of an elderly Filipino woman with a phone, a five-year-old child holding a piece of paper with the letter M, evening light, no photographic realism, multi-ethnic faces, gentle palette". Each demo family card should look hand-drawn, not rendered. - **Letter-formation hint canvas:** Pure SVG strokes generated from the hint-strokes call. Never an animation video. The grandparent sees the strokes draw on their screen at the speed of a real hand drawing a letter (about 1.5 seconds per stroke). - **Stock fallbacks:** If image generation fails, fall back to the warm line-drawing sample from `/public/samples/sample-family.svg`. 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. Never generate real-looking photographs of children's faces. - **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. - Forms validate inline and show specific error messages (not "Invalid input"). The "set daily cap" field, for example, says "the cap can be between 6 and 20 minutes — set it to whatever fits your family" rather than "value out of range". - Loading states use skeletons that match the eventual layout, not spinners. The recognition wait state shows the camera feed live (not blacked out) with a thin "looking at what Apo drew…" caption underneath. - Empty states explain the next action with a button whose label fits THIS app's domain: "Start a session with Apo", "Invite the grandparents", "Choose a script", "Try the demo family" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - All AI-generated praise lines stream in word-by-word where supported, with a clear "looking…" indicator before content starts arriving. - If a recognition call fails, show a calm specific message ("I couldn't see clearly — try holding the paper a bit closer to the camera?") and offer retry. The retry button has the same warm tone, never "try again" in the corrective sense. - Low-confidence recognitions are visually indicated only on the grandparent's screen, never on anything the child would see — a small "(I think this might be an M)" caption appears under the celebration letter, so the grandparent knows to confirm in their own voice ("yes, that IS an M!"). - The daily-cap soft countdown appears as a thin warm-yellow line along the bottom of the practice screen, shrinking gradually — never as a number, never as an alarm. The countdown is a courtesy, not a stressor. - The Live API celebration voice's 600ms pre-speak delay is a hard rule even when the grandparent has clicked "let the app celebrate" — the grandparent always has the first word if they want it. ## 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. The Live API session connects from the client to the Live API endpoint with a short-lived signed token issued by the Cloud Run backend — the API key is not in the client. - **Model selection:** explicitly pin `gemini-3.5-flash` for handwriting recognition; `gemini-3.5-flash` for praise-line, next-letter suggestion, hint-strokes, and Live API; `gemini-3.5-flash` for the weekly summary; `gemini-3.1-flash-tts-preview` for fallback TTS. Set `thinkingLevel` explicitly per call. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed family on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as the invitation channel. - **Storage:** Firebase Storage for optional photos + voice clone. Pre-signed URLs only. - **Mobile-first.** The grandparent is on a phone; the parent is on a phone in evening light. 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 picture-in-picture call window and the camera feed both honour the dynamic island. - Zero horizontal overflow at any width. Zero layout shift on load. - Persist user data in Firestore. Use real-time listeners on the progress book. - Optimistic UI on writes; reconcile on response. - The camera feed uses the Web Camera API with fixed exposure where supported, so the lighting doesn't auto-correct as the child moves the paper. Falls back to default exposure otherwise. - Live API connection lifecycle is tied to the practice session: opened on session start, closed on session end OR cap reached OR app backgrounded for >60 seconds. Reopen on foreground. - **iOS Safari gotchas (graceful degradation):** on the grandparent's iPhone the picture-in-picture window must coexist with FaceTime/WhatsApp video — getUserMedia drops when another foreground app uses the camera; show a calm "your call is using the camera — switch to back camera or pair a second device" hint. The Live API audio output must handle audio-session interruption from the call itself; pause celebration audio when `MediaStreamTrack.onmute` fires and resume on `onunmute`. Camera permission does NOT persist across page reloads on iOS — re-request on every session. PCM streaming via `AudioWorklet` (Safari `MediaRecorder` is AAC-only). ## 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. - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The warm-paper background and deep-ink body text are verified at 7.5:1. - All images have meaningful `alt` text. The recognised-letter image (the captured frame the child held up) has `alt` describing the artefact ("photograph of a child's hand-drawn M on lined paper, held up to the camera in evening light"). - Form fields have associated `