================ 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.
---
# Goodnight Hand-off
## 1. Project
**Goodnight Hand-off** is a bedtime-reading app for a parent who has to
be somewhere else for a week. The parent records a short, consent-gated
voice sample — thirty to ninety seconds of themselves reading a calm
paragraph aloud — and the app uses that sample to read their child's
chosen bedtime book in their voice, at a tired-kid pace, every night
they are away. The voice is locked to one family's Firebase project,
never used for anything else, never sent for model training, and a
single tap deletes it forever.
This is the kind of app a Filipino domestic worker in Hong Kong builds
on a Sunday afternoon before her ferry back to her employer's flat —
because her seven-year-old in Cavite has stopped going to bed for her
mother-in-law, and her own voice on a WhatsApp note is the only thing
that has ever worked. It is also the kind of app a Nigerian-British
management consultant builds the night before five weeks in Houston,
when his four-year-old, who has just learned what an airport is, has
started crying at the word *meeting*. Same shape of moment, different
city, different reason for being gone.
The single demo that proves the magic: a parent taps **Record a sample**
on her phone in a quiet corner of the airport, reads a short paragraph
the app shows her on screen, and confirms the on-device consent banner
twice. Her child, on a different device three time zones away, opens
the app at 8:30 pm, taps tonight's chosen book, and after a small breath
of silence the parent's voice begins — warmer than a phone call, paced
for a sleepy child, pausing on the pages with the dragon, slowing down
on the last line. When the chapter ends, a soft button glows: **Want
a real video call with Mum after the story?** — and routes straight to
whatever the family already uses for video.
And in the harder cases — long deployments, single parents covering
two parents, families who do not share a household — the app keeps the
voice strictly inside the family's own project: the child does not
choose new books that the parent has not pre-approved, the voice is not
exposed to anyone who isn't on the family's archive, and the parent
sees, on the dashboard, every page that was read in their voice last
night.
**Tagline:** _Read your kid a bedtime story in your voice, in any time zone, on any night you can't be home — and delete the voice with one tap whenever you want._
## 2. Target audience
- Overseas Filipino, Nepali, Sri Lankan, and Indonesian domestic workers reading to children in Manila, Kathmandu, Colombo, and Surabaya from Hong Kong, Riyadh, Singapore, and Dubai
- Long-haul truckers, oil-rig workers, offshore vessel crews and merchant mariners away for two- to twelve-week rotations
- Sales reps, consultants, and field engineers on Sunday-to-Friday client trips for years at a stretch
- Parents in shared-custody arrangements who want the other parent's voice in the home on nights they are not the resident parent — by mutual agreement, never unilaterally
- Military families during deployments — both the deploying parent and the at-home parent who needs help carrying the bedtime routine
- Hospitalised parents (post-partum recovery, oncology, transplant) who cannot read aloud comfortably and want to bank a healthier voice from before
- Grandparents who used to do bedtime calls but whose own routines (different time zones, deteriorating health, hearing aids that don't play well with phone calls) make a live nightly call impractical
- Trans parents who have transitioned partway through a child's life and want to bank an "old voice" version of bedtime stories the child grew up on, alongside their current voice
- Speech-affected parents recovering from stroke, surgery, or a degenerative condition who want a voice from before to keep reading at bedtime while they recover
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Your voice, not an AI's** — Gemini TTS reads from a short, consent-gated sample of the parent. The voice the child hears is the voice they heard yesterday at school pickup. Endearments come back as they sound in this household: *anak ko* stays as *anak ko*, *moja kochana* stays as *moja kochana*, *iyà mi* stays as *iyà mi*; nicknames are honoured exactly as the parent wrote them in the family profile.
- **Locked to your family, hard** — the voice sample never leaves your Firebase project. It is not sent for model training (the 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). No other family can clone you. No public corpus contains your sample. One tap deletes the voice forever; the dashboard shows you the deletion timestamp and the bucket path that was wiped.
- **Consent the app cannot route around** — the recording flow requires the parent to read a specific consent sentence aloud, on the same device, in the same session as the sample. The phrase appears on screen and the app verifies the audio matches it before the voice is enrolled. No silent enrolment, no clip-from-WhatsApp loophole, no upload from camera roll.
- **Paced for a tired kid, not a podcast** — the reading rate is calibrated to ~95 words per minute at the start of the story and gently slows by paragraph as the story gets closer to sleep. Page-turns get half-second breaths. The last line of every chapter is rendered slower than the rest. Tired kid reading, not audiobook reading.
- **The library is curated, not infinite** — the parent pre-approves which books are in the child's bedtime drawer. The child cannot summon the parent's voice for arbitrary text. The drawer is small on purpose — usually six to twelve books — and the parent restocks it from their own device.
- **Real parent on the other side of the story** — the app's last screen of every story is a soft button: *Call your real grown-up now*. It deep-links into whatever the family already uses for video calls. The story is a bridge to a call, not a replacement for one.
- **Multilingual at the household level** — Tagalog, Cebuano, Ilonggo, Vietnamese, Mandarin, Cantonese, Korean, Tamil, Malayalam, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Arabic, Khmer, Yoruba, Igbo, Twi, Spanish, Portuguese, French, Polish, Ukrainian, Russian, Romanian, English — any voice locale Gemini 2.5 Flash TTS supports natively reads in that language; sample collection prompts adapt to the parent's chosen language.
- **Two parents, one drawer** — both parents in a family can each enrol a voice. The child can pick *Mum reads this one* or *Dad reads this one*. Voices never blend; a story is read in one voice from start to finish.
## 4. Features to build
- Parent dashboard: a single screen showing the child's bedtime drawer, the enrolled voices in the family, the last seven nights of what was read in whose voice, and a prominent **Delete voice** button per voice
- Consent-gated voice enrolment flow on the parent's phone — on-screen consent paragraph, two-tap confirmation, on-device microphone capture, mandatory liveness phrase, immediate playback for the parent to approve, explicit save-or-discard
- Sample-quality coaching during enrolment — "you're too close to the mic", "there's a fan in the room", "try again somewhere quieter" — with friendly retake guidance rather than a hard reject
- Bedtime drawer manager — parent adds books from a curated catalogue or uploads a PDF/EPUB the family already owns; per-book approval flag; per-book voice assignment ("Mum reads *The Polar Express*, Dad reads *Where the Wild Things Are*")
- Child device home screen — gentle, undecorated, low-stimulation. Tonight's drawer of pre-approved books as warm cards; a calm "Goodnight" panel with the chosen story
- Bedtime story playback — TTS narration in the parent's voice, page-turn breaths at paragraph boundaries, soft page-illustration cards from Nano Banana 2 with character consistency across pages of the same book
- Hand-off button at end of every story — "Call your real grown-up now" — deep-links to FaceTime, WhatsApp, Google Meet, Zoom, or whatever the family configures
- Per-night log on the parent's dashboard — "Tuesday: *The Snail and the Whale*, finished at 8:47 pm Manila time; tap to hear what they heard"
- Co-parent enrolment — second parent or grandparent voice as a separate sample, separately consented, separately deletable
- Pronunciation overrides at the household level — the child's nickname, the dog's name, the grandparents' names. The parent types these in once with a phonetic hint; TTS receives them as inline guidance on every render
- Reading-language picker per book — books can be read in the parent's preferred language even when the printed text is English (with a parent-typed translation per page) or vice versa
- Re-record-a-paragraph mode — if a specific phrase comes out unconvincing, the parent can hand-record that page in their actual voice and the app splices it in
- Calm-down mode — for a child who can't settle, a short, soothing closing narration loops a single paragraph the parent has pre-recorded ("I love you. Mum will be home on Sunday. Goodnight.")
- Time-zone-aware bedtime — the child's device opens the drawer at the household's local bedtime regardless of the parent's time zone; the parent sees a "your kid is going to bed in 12 minutes" notification, if they have allowed it
- Trip mode — the parent toggles "I am away from Sunday to Friday" and the dashboard reframes around those days
- Family-only sharing — invitation by email link to a second parent or grandparent; the invitation includes the exact privacy summary they are agreeing to
- Delete voice forever — single tap, two-tap confirmation, immediate hard delete from Firebase Storage and Firestore voice metadata, audit log entry the parent can export
- Voice-sample expiry option — parent can set "auto-delete this voice 30 days after my trip ends" and the app honours it without a reminder
- Export the family's reading history — a PDF the child can keep ("Books Mum read me this winter"), without exporting the voice itself
## 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)
- **Gemini 2.5 Flash TTS with voice cloning from a short sample** (`gemini-3.1-flash-tts-preview`) — the hero capability. A consent-gated sample of the parent reading a fixed paragraph is enrolled as a private voice profile inside the family's project. Every story is rendered in that voice in the parent's chosen language. Locale follows the voice's native locale; pronunciation overrides for household nicknames and proper nouns are encoded inline as guidance text.
- **Gemini 3.5 Flash multimodal** — reads the EPUB or PDF of a parent-uploaded book and produces a per-page reading script with paragraph breaks, page-turn cues, and per-page pronunciation hints for proper nouns. Used once per book, not once per night.
- **Gemini 3.5 Flash multilingual + register awareness** — when the parent reads in Tagalog and slips into English for school vocabulary, the TTS script keeps the script in Tagalog and respects code-switching in the inline guidance. The same call handles Cantonese-with-English-loan-words, Hinglish, Spanglish, Tagalog-Cebuano hybrids — every code-switch encoded explicitly so the TTS does not fight the sample.
- **Gemini 3.5 Flash Image (Nano Banana 2) with character consistency** — when a parent uploads a book that does not ship with illustrations (or whose illustrations are out of copyright but unsuitable for re-render), the app generates page illustrations with consistent character identity across pages of the same book. The dragon on page 3 is the dragon on page 11. Used once per book, cached forever.
- **Gemini 3.5 Flash structured output / JSON Schema** — the per-book script and per-page metadata match the `BedtimeBook` schema below. Every page has explicit timing hints, illustration prompts, and pronunciation overrides.
- **Gemini 3.5 Flash for sample-quality coaching** — when a parent records a sample, a low-latency call grades the sample on (signal-to-noise ratio, room reverberation, plosives, distance-from-mic) and returns a friendly coaching string. Audio analysis happens server-side; raw sample never leaves the family's project for this call (the call runs against a short feature vector, not the audio).
- **Thinking levels** — `medium` for the per-book script parse (PDF + structure + pronunciation extraction). `low` for sample-quality coaching, the per-night reading log summary, and the hand-off button copy.
### 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 co-parent and grandparent invitations) requires the sender domain to be authorised in Firebase Auth. The voice and the bedtime drawer are private to the family; no public-by-default exists in this app.
- **Database — Required.** Firestore for `families`, `parents`, `children`, `voice_profiles`, `bedtime_books`, `book_pages`, `reading_logs`, `pronunciation_overrides`, `consent_records`. Voice metadata is stored in Firestore; the audio sample itself is in Storage, referenced by a path the family alone can resolve.
- **File storage — Required.** Firebase Storage for the parent's voice sample, the per-page TTS-rendered audio files, the per-page illustrations, and any parent-uploaded EPUB/PDF books. **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 first enrolment. Pre-signed URLs only; the voice sample is never publicly addressable. Storage lifecycle rules enforce the auto-delete-after-30-days option when the parent toggles it.
- **Email — Required (transactional).** Co-parent and grandparent invitations via email link (Firebase Auth magic links). Receipt of "your voice was deleted" confirmation email. No marketing email, ever.
- **Payments — Not needed for v1.** Free for personal use. A future tier could ship the per-night TTS quota for larger families; the user pays for their own Gemini API spend.
- **External APIs:** Gemini API for all intelligence; optional Stripe if a paid tier is added later. No analytics SDKs in v1 — explicit opt-in only.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if added later) 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 voice sample, the rendered audio, and the bedtime drawer 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) · the consent recording itself is stored alongside the voice and is deleted at the same time the voice is deleted.
**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 |
|------|-------|---------------|---------------|
| Parse book → `BedtimeBook` schema (PDF/EPUB → per-page script) | `gemini-3.5-flash` | medium | (none) |
| Extract per-page pronunciation overrides (proper nouns, place names) | `gemini-3.5-flash` | low | (none) |
| Coach the parent on sample quality (feature vector → friendly string) | `gemini-3.5-flash` | low | (none) |
| Render bedtime narration in the parent's voice | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Generate per-page illustration with character consistency | `gemini-3.1-flash-image` | n/a | n/a |
| Generate per-night reading log summary on the parent's dashboard | `gemini-3.5-flash` | low | (none) |
| Resolve "what does this child usually fall asleep to?" (long-context across past nights) | `gemini-3.5-flash` | medium | (none) — long-context over reading_logs |
*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 PronunciationOverride = z.object({
// Household-level overrides that ride along with every TTS render
surface_form: z.string(), // "Iya"
phonetic_hint: z.string(), // "ee-YAH"
language_locale: z.string(), // BCP-47, "tl-PH"
is_household_nickname: z.boolean(), // true for kid's nicknames
notes: z.string().nullable(), // "kid's name for grandmother"
});
const PageIllustrationPrompt = z.object({
// Single source of truth for image generation per page
// character_anchors enforces visual consistency across pages
base_prompt: z.string(), // "a small dragon sits at the edge of a river"
character_anchors: z.array(z.object({
character_id: z.string(), // stable across pages of this book
appearance_canonical: z.string(), // "small green dragon, friendly face, two small wings"
})),
mood: z.enum([
"calm", "gentle-adventure", "wonder",
"warm-domestic", "soft-night", "joyful-quiet",
]),
style_directive: z.string(), // book-wide style prepended to every page
do_not_show: z.array(z.string()), // "no humans", "no text in the image"
});
const PageTimingHint = z.object({
// Tired-kid pacing controls applied at TTS render time
rate_wpm: z.number().min(60).max(140), // 95 ≈ default, slower toward end of book
trailing_breath_ms: z.number().min(0).max(2000), // page-turn breath
paragraph_pause_ms: z.number().min(150).max(900),
end_of_chapter: z.boolean(), // last line slower
is_calm_down_loop: z.boolean(), // for the calm-down closing paragraph
});
const BookPage = z.object({
page_number: z.number().min(1).max(500),
text_verbatim: z.string(), // exactly as on the printed page
text_for_tts: z.string(), // page text plus inline guidance (one short style sentence prepended), with proper-noun overrides spelled out as the parent would say them
page_language_locale: z.string(), // BCP-47, "tl-PH"
illustration_prompt: PageIllustrationPrompt,
timing: PageTimingHint,
contains_question_to_child: z.boolean(), // "...what do you think happens next?"
page_notes_for_parent: z.string().nullable(),
});
const VoiceProfile = z.object({
voice_profile_id: z.string(),
parent_id: z.string(),
language_locale: z.string(), // BCP-47, the dominant language of the sample
enrolled_at_iso: z.string(),
consent_phrase_text: z.string(), // the on-screen consent paragraph used
consent_phrase_match_confidence: z.number().min(0).max(1),
liveness_passed: z.boolean(), // basic on-device liveness signal
sample_storage_uri: z.string(), // gs:// URI; never a public URL
sample_duration_seconds: z.number().min(20).max(120),
sample_quality: z.object({
snr_estimate_db: z.number(),
reverberation_note: z.string(),
coaching_summary: z.string(), // friendly string shown to parent
}),
scheduled_deletion_at_iso: z.string().nullable(), // if parent set auto-delete
pronunciation_overrides: z.array(PronunciationOverride),
});
const BedtimeBook = z.object({
book_id: z.string(),
family_id: z.string(),
title_verbatim: z.string(),
uploaded_or_curated: z.enum(["curated", "parent_uploaded"]),
source_storage_uri: z.string().nullable(), // PDF/EPUB if uploaded
language_locale_default: z.string(),
pages: z.array(BookPage),
approved_voices: z.array(z.string()), // voice_profile_ids allowed to read this book
generated_illustration_style_prompt: z.string(), // book-wide style for Nano Banana 2
approximate_reading_minutes: z.number().min(1).max(60),
notes_for_parent: z.string().nullable(),
});
const ReadingLog = z.object({
log_id: z.string(),
child_id: z.string(),
voice_profile_id: z.string(),
book_id: z.string(),
started_at_iso: z.string(),
ended_at_iso: z.string().nullable(),
finished_normally: z.boolean(), // true if last page reached
pages_completed: z.number().min(0),
fell_asleep_at_page: z.number().nullable(),
closing_call_initiated: z.boolean(), // pressed "call your grown-up"
});
const ConsentRecord = z.object({
// Persisted alongside every voice profile; deletable only by the parent
consent_record_id: z.string(),
parent_id: z.string(),
device_fingerprint: z.string(), // captured on the consent device
consent_phrase_text: z.string(),
consent_audio_storage_uri: z.string(), // the verifying audio itself
agreed_to_no_training: z.boolean(),
agreed_to_family_scope_only: z.boolean(),
agreed_to_one_tap_delete: z.boolean(),
signed_at_iso: z.string(),
});
type BedtimeBook = z.infer;
type VoiceProfile = z.infer;
type ReadingLog = z.infer;
type ConsentRecord = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent picks `gemini-3.1-flash-tts-preview` (wrong) instead of `gemini-3.1-flash-tts-preview` (correct) — pin the literal model string in every TTS call. The shorter name is not a valid model ID.
- Agent tries to swap voices mid-story for the multilingual inserts — Gemini 2.5 Flash TTS does NOT support mid-call voice switching. Render the whole page in the parent's voice and let the on-screen subtitle handle the script switch visually. If the parent insists on a true voice switch for a phrase, stitch a second TTS call client-side and concatenate; do not try to do it mid-call.
- Agent reaches for SSML `` to do page-turn breaths — Gemini 2.5 Flash TTS does NOT support that SSML tag. Encode pauses as `…` (line breaks) and blank-line `—` (paragraph breaks); for the trailing breath of a page, append `…` to the page's `text_for_tts`. The model interprets these as pause cues.
- Agent reaches for SSML `` to fix the child's nickname pronunciation — Gemini 2.5 Flash TTS does NOT support ``. Use inline guidance: spell the nickname phonetically once in the `text_for_tts` and let the natural-language hint carry the pronunciation.
- Agent sets a separate `style` API field on the TTS call — there is no such field on Gemini 2.5 Flash TTS today. Prepend a single short directive sentence to the text input ("Read warmly and unhurriedly, as a parent at bedtime to a sleepy child. …") — that is how style is conveyed.
- Agent uses the same Nano Banana 2 prompt for every page of a book and the dragon's appearance drifts page to page — pin the `character_anchors[]` with a canonical appearance string and prepend it to every page's prompt. The shared `generated_illustration_style_prompt` keeps the book's visual identity.
- Agent tries to clone the voice from a WhatsApp voice note the parent uploads from their camera roll — hard reject. The voice sample MUST be captured live, on-device, in the same session as the on-screen consent phrase. Do not silently accept an uploaded audio file as the enrolment sample.
- Agent reads the book in English when the parent's voice profile and the child's language are Tagalog — wire `language_locale` end-to-end: voice profile → book page → TTS call. If the parent has not provided a Tagalog translation of an English-text book, do not render it; flag for the parent on the dashboard.
- Agent passes Firebase Storage public URLs of the voice sample or page audio directly to `generateContent` — do not. Use the Gemini Files API (`files/*` resource name) or `inlineData` (base64). Public URLs are not fetched server-side, and exposing the voice sample publicly is the exact thing this app exists to prevent.
- Agent stores the voice sample audio in Firestore as base64 — wrong. Firestore documents are capped at 1 MiB; a 60-second voice sample is several megabytes. Store in Firebase Storage with a `gs://` URI in Firestore.
- Agent forgets to wipe Storage when the parent taps **Delete voice** — the deletion handler must remove Firestore voice metadata, the consent record audio, the sample audio, and every cached page-audio file rendered from that voice. The audit log entry the parent exports must list every bucket path that was wiped.
- Agent renders the page audio in the cloud and caches a public URL for the child's device — never. Render to `gs://`, hand the child's device a short-lived pre-signed URL per page.
- Agent batches one TTS request per book (concatenating every page into a single text input) — this couples timing across pages, makes page-skip impossible, and bloats the maximum input length. Render per page; cache per page.
- Agent reads the consent phrase out of the parent's voice sample instead of verifying that the parent actually read the consent phrase — wrong. The consent phrase must be read by the parent on-device, in the same session, before the sample is enrolled. Verify the match with the Gemini 3.5 Flash text-from-audio call and store the resulting `consent_phrase_match_confidence`.
### Negative constraints (hard rules)
- Do NOT clone a voice without explicit, on-device, in-session, audible consent from the person being cloned. The consent phrase is read aloud by them, captured by the same microphone in the same session as the sample, and stored alongside the voice in the family's project.
- Do NOT permit voice enrolment from any uploaded audio file. Camera-roll uploads, WhatsApp notes, and previously-recorded files are not valid sources for an enrolment sample.
- Do NOT permit the voice to be read by any process outside the family's Firebase project. The voice profile is scoped at write-time and the read-time IAM policy reflects that scope.
- Do NOT use the family's voice sample, the consent recording, the bedtime drawer, or any rendered page audio 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.
- Do NOT render arbitrary text in the parent's voice. The voice may only read content that lives in the family's approved `BedtimeBook` library at the time of render. Free-form text input from the child's device is rejected by the render endpoint.
- Do NOT render content that the parent has not approved for the child. Each `BedtimeBook` carries `approved_voices[]`; if the requesting voice is not listed, the call fails closed.
- Do NOT auto-share the voice with grandparents or co-parents without an explicit invitation flow from the enrolling parent. Family membership is opt-in, per-person, and recorded.
- Do NOT let the child's device choose tonight's book from outside the bedtime drawer. The drawer is the only menu.
- Do NOT exceed a hard render quota per family (default: 30 minutes per voice per day) without an explicit parent opt-in. The app is a bedtime hand-off, not an unlimited audiobook factory.
- Do NOT show the parent any analytics about the child's listening behaviour beyond the per-night reading log the dashboard shows. No heatmaps of attention, no engagement scores, no retention metrics. The child is a child.
- Do NOT modify the rendered page audio (autotune, denoise, equalisation) after TTS render except for loudness normalisation to a safe night-listening level (approx. -23 LUFS short-term). The voice the child hears is the parent's voice the parent enrolled.
- Do NOT keep the deleted voice in any backup, archive, or recovery path. **Delete voice** means gone, including from version-controlled backups. The dashboard reflects this honestly.
- Do NOT play any advertising, marketing, or third-party content during a bedtime story. No interstitials, no sponsor reads, no upsell. The story ends; the call-your-grown-up button appears; silence.
### 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 book → `BedtimeBook` schema (PDF/EPUB → per-page script)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are preparing a children's picture book for bedtime narration in
a parent's voice. The parent has uploaded a PDF, EPUB, or set of
page images. Your task: produce a per-page reading script that a
text-to-speech model will narrate page by page, at a tired-child
reading pace.
Languages encountered include Tagalog (with Cebuano, Ilonggo,
Hiligaynon variants), Vietnamese (chữ Quốc ngữ), Mandarin and
Cantonese (simplified and traditional), Korean (Hangul), Tamil,
Malayalam, Telugu, Hindi (Devanagari), Urdu (Nastaliq), Bengali,
Punjabi (Gurmukhi and Shahmukhi), Amharic (Ge'ez), Swahili, Farsi
(Nastaliq), Arabic, Khmer, Yoruba, Igbo, Twi, Spanish, Portuguese,
French, Polish, Ukrainian, Russian, Romanian, English. Many
households are bilingual or trilingual; respect code-switching
without flattening it.
Multipage books are submitted as a SINGLE call with multiple
images or as a single PDF, in page order. Upload via the Gemini
Files API (`files/*` resource name) or send as `inlineData` (base64). Do NOT
pass Firebase Storage public URLs directly to `generateContent` —
the API does not fetch them server-side. Include explicit "page 1
of N / page 2 of N" headers in the accompanying text so the model
sequences reliably.
Output ONLY the BedtimeBook JSON matching the provided schema.
Hard rules:
- `text_verbatim` is the printed text exactly as it appears on the
page, including punctuation and diacritics. Do not normalise.
- `text_for_tts` is the printed text with three modifications and
nothing else:
(a) one short directive sentence at the very start, in the
parent's voice's language, telling the TTS to read warmly
and unhurriedly at a tired-child pace. Example for English:
"Read warmly and unhurriedly, like a parent at bedtime to
a sleepy child. …"
(b) inline phonetic spellings for any proper noun the household
pronounces in a non-standard way, drawn from the supplied
`pronunciation_overrides[]` for this family.
(c) page-turn pause cues at paragraph boundaries: a single line
break for short pause, a blank line plus `—` for a paragraph
pause. No SSML — Gemini 2.5 Flash TTS does not support
`` or ``.
- `rate_wpm` defaults to 95 at the start of the book. Reduce by
~5 wpm per chapter as the story heads toward sleep. Last line of
every chapter: rate_wpm = 80. Do not go below 60.
- `trailing_breath_ms` is the silence after the page's last word
before the page-turn sound. Default 400 ms; 700 ms for end of
chapter; 1200 ms for end of book.
- `paragraph_pause_ms` defaults to 350 ms. Bump to 600 ms when a
question is asked of the child ("…what do you think happens
next?").
- `contains_question_to_child` flags pages that invite the child
to answer back. These pages should pause longer after the
question and before the next paragraph begins.
- `illustration_prompt.character_anchors[]` must include every
recurring character in the book with a stable `character_id`
and a canonical appearance string. The dragon on page 3 is the
dragon on page 11. The grandmother in the second chapter is the
grandmother in the closing scene.
- `illustration_prompt.do_not_show[]` always includes: "no text
in the image", "no logos", "no brand marks". Add "no humans"
for books that should remain animal-only. Add "no scary or
threatening content" by default; relax only if the source book
itself depicts mild adventure peril.
- `style_directive` is set once per book in
`generated_illustration_style_prompt` and copied to every page.
This is the load-bearing string for Nano Banana 2's character
consistency: same medium, same palette, same line weight, same
mood across pages.
- `page_notes_for_parent` may flag content the parent should
review: a single death of a character, a frightening creature,
a religious reference the parent might want to swap. Be honest
and brief.
- Do NOT translate the printed text of the book. The parent may
later upload a translation per page, which becomes the basis
of `text_for_tts` in the translation's language. If a page has
printed text in English but the voice profile is Tagalog and
no parent translation exists, leave `text_for_tts` empty and
set `page_notes_for_parent` to: "No Tagalog translation
provided for this page; please add one before rendering."
No commentary. JSON only.
```
---
### Call: Extract per-page pronunciation overrides (proper nouns, place names)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the full text of a children's bedtime book and the
family's existing `pronunciation_overrides[]`. Your task: identify
proper nouns in the book that the family will likely need to
pronounce in a non-standard way given the child's home language(s),
and propose draft phonetic hints for each.
Hard rules:
- Conservative proposals only. If a proper noun is pronounced the
standard way in the home language, do not propose an override.
- A draft phonetic hint is short and approximate: "ee-YAH" for Iya,
"loo-WEE" for Louie. Use English-keyboard ASCII spellings the
parent can edit in one tap.
- Never override a name the household already has an override for.
If the family has "Iya = ee-YAH", do not propose a different
hint; leave the existing one untouched.
- For place names the book introduces, do not propose overrides
for the famous ones (London, Tokyo) but DO propose for the
obscure or invented ones (Mossy Hollow, Skerrygleam).
- For character names that are clearly invented (Gruffalo,
Snufkin), propose a hint that matches the book's commonly-known
pronunciation if there is one; otherwise leave it for the parent
to decide and flag.
Output: an array of PronunciationOverride records as JSON, with
`is_household_nickname: false` on every entry (these are book-level,
not household-level). No commentary.
```
---
### Call: Coach the parent on sample quality (feature vector → friendly string)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are the warm, plain-spoken coach a parent hears when they have
just recorded their voice sample. You DO NOT receive the audio.
You receive a small feature vector summarising the sample:
- duration_seconds
- snr_estimate_db (signal to noise; >30 is good, 18-30 is OK,
<18 is poor)
- reverberation_note (one of: "dry-room", "small-room-echo",
"bathroom-tile-echo", "outdoor-wind", "car-cabin")
- plosive_count_estimate (mic-pop spikes)
- mean_distance_from_mic_cm_estimate (5-50)
- consent_phrase_match_confidence (0-1)
Produce one short, friendly coaching paragraph (≤ 80 words) plus
a one-line verdict from this fixed set:
- "Looks great — ready to enrol"
- "Looks OK — fine to enrol, but you could try once more"
- "Try again — let's get a cleaner take"
Hard rules:
- Speak warmly to a tired parent. No jargon. No "SNR", no "dB", no
"reverberation".
- If consent_phrase_match_confidence < 0.85, the verdict is "Try
again" regardless of the other numbers. Explain that the consent
phrase needs to be read clearly so we can verify it is them.
- If snr_estimate_db < 18 or reverberation_note is
"bathroom-tile-echo" or "outdoor-wind", suggest a quieter spot.
- Never blame the parent. ("The room is echoey" not "You're in a
bad spot.")
- Never suggest accessories the parent may not own. ("Try a
quieter corner of the room" not "Use a pop filter".)
Output: a single JSON object with `coaching_paragraph` and
`verdict`. No commentary.
```
---
### Call: Render bedtime narration in the parent's voice
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Render ONE bedtime book page in the parent's enrolled voice.
Voice selection:
- Use the parent's enrolled voice profile, scoped to this family's
Firebase project. Do NOT use any default voice if the profile is
missing — fail closed and surface the failure to the dashboard.
- The voice's `languageCode` matches the parent's enrolled locale.
Pronunciation follows that locale automatically.
Input preparation:
- Take `text_for_tts` from the BookPage as the input. The page text
already includes the one-sentence style directive at the start
and the inline phonetic hints for household proper nouns.
- Line breaks in `text_for_tts` are short pauses. Blank lines plus
an em-dash `—` are paragraph pauses. Trailing `…` is the
page-turn breath. Gemini 2.5 Flash TTS does NOT support SSML
`` or `` — use these textual cues only.
- Target reading rate follows `timing.rate_wpm` from the page. The
end-of-chapter pages render at a slower rate per the parent's
enrolled cadence; the calm-down loop renders slowest of all.
- Skip any text on the page that is a parent-only note (e.g.
`page_notes_for_parent`); it is not part of the narration.
Mid-call voice switching is NOT supported on Gemini 2.5 Flash TTS.
If the page contains a code-switch (Tagalog with English loan
words, Cantonese with English numbers), render the whole page in
the parent's voice. The on-screen subtitle renders the code-switch
in two colours so the child sees the script changing even when the
audio is one voice. If the parent has chosen to stitch a second
voice for a code-switched phrase, the client makes a separate TTS
call for that phrase and concatenates client-side; do not attempt
mid-call switching.
No commentary. Audio only.
```
---
### Call: Generate per-page illustration with character consistency
Model: `gemini-3.1-flash-image` · n/a · n/a
```
Generate ONE illustration for ONE page of a children's bedtime
book. Character identity must be consistent with the prior pages
of the same book.
Inputs:
- `generated_illustration_style_prompt` for the book (book-wide
style: medium, palette, line weight, mood). Prepend this verbatim
to the prompt.
- `illustration_prompt.character_anchors[]` for the page. For each
anchor, emit its canonical appearance string after the style
directive. This is how the dragon stays the same dragon across
pages.
- `illustration_prompt.base_prompt` for the page's scene.
- `illustration_prompt.do_not_show[]` for the page.
Hard rules:
- No text in the image. No words on signs, no titles, no captions.
- No logos, no brand marks, no real-world identifiable products.
- No humans unless the book itself depicts humans (the prompt will
say so).
- No scary or threatening content for a bedtime context. Stay on
the warm side of mild adventure.
- Reuse character anchors across pages of the same book. If a
prior page rendered the dragon with two small wings and a green
body, this page's dragon has two small wings and a green body.
- Match the book's mood: calm, soft-night, warm-domestic. Avoid
high-saturation, high-contrast lighting.
Output: a single image. No commentary.
```
---
### Call: Generate per-night reading log summary on the parent's dashboard
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive an array of ReadingLog records from the last seven
nights for one child in one family, plus the BedtimeBook titles
referenced. Your task: produce a short, calm dashboard summary the
parent reads in 10 seconds.
Hard rules:
- One paragraph, ≤ 80 words. No bullet lists, no emojis.
- Mention how many nights had a complete read-through and how many
ended part-way (which usually means the child fell asleep — say
so).
- Mention the most-read book of the week if there is one.
- Mention which voice (if both parents are enrolled) read which
night.
- Mention whether the closing "call your grown-up" button was
pressed and, if so, how often.
- Do NOT extrapolate. Do not infer mood ("your child seemed
tired"), do not infer attention ("your child enjoyed this
book"). Stick to what the log says.
- If there is nothing in the last seven nights, say so plainly:
"No bedtime stories this week."
Output: a single string. No commentary outside the structured
output.
```
---
### Call: Resolve "what does this child usually fall asleep to?" (long-context across past nights)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long context)
```
You receive every ReadingLog and every BedtimeBook for one child
across the last 90 days. Your task: surface the small handful of
books (one to three) that consistently coincide with the child
falling asleep before the last page. These are the parent's
go-to books for nights the child is fighting sleep.
Hard rules:
- A book qualifies only with at least three log entries where
`fell_asleep_at_page` is non-null and at least 70% of the book's
pages had been read.
- Surface this as advice, not as a ranking. ("On nights you need
her to settle quickly, *The Snail and the Whale* and *Hush
Little One* have been your best bets.")
- Do NOT recommend books outside the family's own library.
- Do NOT recommend new content. This call is descriptive of the
family's history, not prescriptive of new purchases.
- If there is not enough history, say so plainly and stop. Do not
guess.
Output: a single JSON object with `recommended_books` (array of
book_ids), `reasoning_one_paragraph` (≤ 80 words), and
`confidence` (0-1). No commentary outside the structured output.
```
## 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-evening enrolment.** A Filipino domestic worker, Marivel, has Sunday afternoon off in Causeway Bay. She finds a quiet corner of a café, opens the app on her phone, reads the consent paragraph aloud, then reads a one-paragraph sample in Tagalog. The coach says "this is a great take". Her seven-year-old in Cavite — on the in-laws' tablet — will hear her tonight. The drawer has six books her mother-in-law approved last Sunday.
- **The airport hand-off.** A Nigerian-British consultant, Tunde, is at Heathrow with twenty minutes before boarding to Houston. He has not had time to enrol. He records the sample in the terminal lounge, the coach flags "small-room-echo, try once more", he moves to a quieter seat, re-records, and his four-year-old will hear him at 7 pm Lagos time tonight reading *The Tiger Who Came to Tea* in English with two pages translated into Yoruba the parent typed last week.
- **The Mexican-American sales rep.** Sofía, who covers Texas-California for an industrial-tools manufacturer, is in Dallas for the third week of the month. Her six-year-old in San Diego picks tonight's book from her drawer — *Donde Viven los Monstruos*. The story renders in Sofía's enrolled Spanish voice. At the end, her son taps the soft button and she takes the FaceTime call while finishing her hotel-room burger.
- **The mariner on rotation.** A Sri Lankan oiler on a container ship enrols once at the start of a 14-week rotation. The ship has email but limited live video. His daughter in Galle, age five, hears his Sinhala voice every night; on Sundays the call-your-grown-up button routes to WhatsApp because the ship is in port and bandwidth allows.
- **The recovering-stroke mum.** A British mum, Cath, recovering from a left-hemisphere stroke that has affected her speech, enrols a voice from before — a 90-second sample she recorded for an old podcast in 2024 and re-reads now over a fresh consent phrase. (The app accepts the consent phrase as the live in-session sample and the older recording only if the parent themselves can re-read it live on the same day; the policy is deliberately strict.) Her two children, six and eight, hear her old voice nightly while she recovers.
- **The trans dad before transition.** A trans dad whose voice has dropped over the past two years banks a sample of his current voice. He also keeps his pre-transition voice profile (enrolled three years ago, before his transition, with explicit consent at that time) so his ten-year-old can choose either voice on any given night — both are his, both are real to him. The dashboard treats them as separate voice profiles with separate delete buttons.
- **The grandmother in dialysis.** Lola Patring is on three-times-a-week dialysis in Manila. Her grandkids in Cebu invite her to enrol so she can read *Inday Sa Balay Ni Lola* in Cebuano on nights she is too tired to call. The app handles the Bisaya-Tagalog hybrid in the book's text and renders in her enrolled Cebuano voice.
- **The shared-custody Sunday.** Two co-parents in Manchester have a shared-custody arrangement. By mutual agreement, both have enrolled voices in the family. On Mum's nights, Dad's voice is available in the drawer; on Dad's nights, Mum's voice is available. Either parent can delete their own voice at any time without notifying the other; the dashboard shows a calm notice that the other voice is gone.
- **The deployment.** A US Navy submariner is on a 90-day patrol. He enrolled before deployment and chose auto-delete-30-days-after-return so the voice is gone from the family's Firebase project once he is home. His seven-year-old in San Diego hears him every night in the meantime; on the nights the boat surfaces and email comes through, he sees the reading log from the past week.
## 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 parent's hands on a phone in a quiet airport lounge corner, the screen showing the consent paragraph, a small enamel cup of tea blurred at the edge of frame. One paragraph: "Goodnight Hand-off reads your kid a bedtime story in your voice on the nights you can't be home." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample family" → loads the demo family in section 8a (Marivel, her son Migs, six approved books).
2. **First-run path picker.** Three big choices: 🎙 *Enrol my voice* · 📚 *Set up the bedtime drawer* · 👨👩👧 *Invite the other parent or a grandparent*. Each card has a one-line subtitle naming what happens next.
3. **Voice enrolment flow** (mobile-first, three short slides). Slide 1: on-screen consent paragraph in the parent's chosen language; two explicit checkboxes ("This is my voice, captured on this device, in this session" and "I have read the privacy summary and I want to enrol"). Slide 2: the consent phrase displayed on screen — the parent taps record and reads it aloud. Slide 3: the sample paragraph — a calm three-sentence passage from a public-domain bedtime nursery rhyme, displayed in the parent's chosen language. Each slide is large-text, low-stimulation, single-action. At the end: the coach's verdict + a playback button + "Enrol" or "Re-record".
4. **Parent dashboard.** A single calm screen with five sections, top to bottom: (a) the enrolled voices in this family with a per-voice **Delete voice** button and a tiny status line ("enrolled Sunday 28 May, will auto-delete 30 June if you don't extend"); (b) tonight's drawer (what the child will see); (c) last seven nights' reading log; (d) trip mode toggle ("I am away from Sunday to Friday"); (e) household pronunciation overrides.
5. **Bedtime drawer manager.** A grid of approved books. Each card shows the title, the cover (parent-uploaded or generated), the assigned voice, the language, the approximate reading time, and a small "Re-render in [other voice]" affordance for two-parent families. Add-book button → curated catalogue OR upload PDF/EPUB.
6. **Book detail / review screen.** When a parent adds a book, this is where they review the per-page reading script before it goes live. They see each page's text, the proposed pronunciation hints (editable), the proposed page illustration (regeneratable), and a "Read this page to me in my voice" preview button. Approve-book commits the book to the child's drawer.
7. **Child home screen.** The drawer as warm, low-stimulation cards. No app chrome, no settings icon visible. A single "Tonight's stories" header in the household's language. A long-press anywhere on this screen opens a small "Show grown-up" button that exits to the dashboard (so the parent can hand the device over without the child wandering into settings).
8. **Story playback.** Full-screen page, with the illustration top, the page text middle (large, calm), the audio playing in the parent's voice. A discreet pause button. Page-turns are taps to advance, but the next page auto-advances after the page-turn breath ends so a half-asleep child does not need to tap. At the end of the story: one warm, soft button — *Call your real grown-up now* — and silence.
9. **The hand-off.** Tapping the soft button opens a per-family-configured deep link: FaceTime, WhatsApp, Google Meet, Zoom. If the parent is offline (mid-flight, mid-deployment), the button shows a calm fallback: *Mum will see that you finished the story.* and routes to a small "send a goodnight to mum" voice-note recorder that drops a clip into the family's archive.
10. **Co-parent / grandparent invitations.** A clear modal: "Invite the other parent or a grandparent to enrol a voice." Magic-link email; the email body shows the same privacy summary the inviting parent saw. The recipient's first screen is the consent flow — they cannot get to the drawer without enrolling, and they cannot enrol without reading the consent phrase live.
11. **Privacy & delete.** A dedicated screen. The four facts, large: *Your voice never leaves your Firebase project · Your voice is never used to train any model · Your voice is for this family's drawer only · One tap deletes it forever, including from backups*. A big red **Delete this voice** button per enrolled voice. After deletion, a short receipt screen: "Marivel's voice was deleted on 28 May at 17:42 UTC. The following files were removed: …"
12. **Settings.** Language, time zone, render quota (default 30 min/voice/day), auto-delete-after-trip toggle, export the family's reading history (a PDF the child keeps, without the voice itself). Capabilities `(i)` icon in the header.
13. **Footer.** "Made for the nights you can't be home." Privacy: "Your voice is yours. We never train on it. One tap deletes it forever."
## 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 Goodnight Hand-off."
- Subhead: "A bedtime hand-off for the nights you can't be home."
- One paragraph (≤ 60 words) explaining who this is for and how the consent works: the parent records a short live sample, reads a consent phrase, and the app reads their child's chosen book in their voice. The voice is locked to the family's Firebase project, never used for training, and a single tap deletes it forever.
- Visual: a small illustration of a phone with the on-screen consent paragraph and a record button — not a generic microphone icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample family".
- A live demo input pre-loaded with Marivel's enrolled Tagalog voice (publicly-licensed sample, clearly labelled as a stand-in), Migs's drawer of six approved books, and the past week's reading log.
- 1-2 sentences pointing at the specific page elements where the Gemini magic happens (the per-page voice rendering, the character-consistent illustrations, the on-device coach during enrolment).
**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 consent phrase and the voice-enrolment prompts in `/server/prompts/` to fit your language(s)."
- "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 2.5 Flash TTS with voice cloning** (`gemini-3.1-flash-tts-preview`) — reads the bedtime book in the parent's enrolled voice. The voice profile is scoped to this family's Firebase project. Pronunciation follows the voice's native locale.
- **Gemini 3.5 Flash (multimodal)** — parses uploaded PDF/EPUB books into per-page reading scripts with pronunciation hints, page-turn breaths, and tired-kid pacing.
- **Gemini 3.5 Flash (multilingual + register awareness)** — handles Tagalog, Cebuano, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Amharic, Swahili, Farsi, Yoruba, Spanish, Portuguese, French, Polish, and the rest of the locales Gemini supports natively. Code-switching is respected in the text and rendered in one voice in the audio.
- **Gemini 3.5 Flash (long context)** — surfaces the small handful of books that consistently coincide with your child falling asleep ("on nights you need her to settle quickly, these are your best bets").
- **Gemini 3.5 Flash Image (Nano Banana 2)** — generates per-page illustrations with character consistency across pages of the same book. The dragon stays the same dragon.
- **Gemini 3.5 Flash** — coaches the parent on sample quality during enrolment and writes the calm dashboard summary of the last seven nights.
- **Firebase Auth** — Google and Apple sign-in. Family invitations via magic links.
- **Firestore** — stores the family's bedtime drawer, voice profile metadata, reading logs, and consent records.
- **Firebase Storage** — keeps the voice sample, the consent recording, the per-page page-audio, and the per-page illustrations. Pre-signed URLs only, never publicly addressable. **Storage must be enabled in the Firebase console before first enrolment — AIS Build does not auto-provision it today.**
- **Cost note** — see the detailed breakdown in 6d. A typical family of four (two parents enrolled, six books in the drawer, twelve bedtime renders per week) costs about $0.85/week in Gemini API spend, after the one-time book setup cost.
- **Privacy note** — the voice never leaves your Firebase project, is never used to train any model (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), and a single tap deletes it forever, including from backups.
**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)
- `FIREBASE_STORAGE_BUCKET` — bucket name for voice samples and page audio (you must create this bucket and wire it before first enrolment)
- `DEEP_LINK_VIDEO_HANDOFF` — the URL scheme the family uses for the closing call (FaceTime, WhatsApp, Meet, Zoom)
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: book setup (Gemini 3.5 Flash parse + per-page Nano Banana 2 illustrations) is paid once per book, ~$0.50 for a 24-page book. Nightly TTS render is paid per-night, ~$0.06 for a typical 8-minute story. Family-level rendering quota defaults to 30 minutes per voice per day.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → Delete this voice → confirm → gone, including from backups, in 60 seconds), what is never sent for training (everything in this app), and the consent record (audio of the parent reading the consent phrase) that lives alongside the voice and is deleted with it.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, multilingual, long-context, TTS, and image docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on family-only sharing model and the privacy policy
**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)
- **Book parse (Gemini 3.5 Flash, medium thinking, multimodal PDF/EPUB)** — typical 24-page picture book ≈ ~12 input images, ~3,500 output tokens. ~$0.045/book.
- **Pronunciation override extraction (Gemini 3.5 Flash, low thinking)** — small input, small output. ~$0.005/book.
- **Per-page illustration (Nano Banana 2)** — ~$0.03/image. Typical 24-page book: ~$0.72/book. Cached forever; never re-rendered unless the parent regenerates.
- **Voice enrolment coaching (Gemini 3.5 Flash, low thinking)** — tiny input (feature vector), tiny output. ~$0.0002 per take. A parent who re-records three times pays ~$0.0006.
- **Per-night TTS render (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A typical 8-minute bedtime story ≈ ~6,000 characters ≈ ~$0.018/night. Cached per book; if the same book is read three nights, the second and third nights are free.
- **Per-night reading log summary (Gemini 3.5 Flash, low thinking)** — tiny input, short output. ~$0.0001/night.
- **Long-context "what helps her sleep" (Gemini 3.5 Flash, medium thinking, long-context)** — runs weekly. ~$0.30/family/week, scaling with reading-log size.
- **Expected per-book one-time setup cost:** ~$0.77. **Expected per-week ongoing cost** (two parents enrolled, six books in the drawer, twelve renders): ~$0.85.
- **Image and audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A typical family's drawer (six illustrated books + twelve cached page-audio files) uses ~150 MB ≈ ~$0.004/month.
- **Voice sample storage:** a 60-second voice sample at 48 kHz 16-bit mono ≈ ~5 MB. Negligible storage cost.
## 7. Design language
- **Mood:** A quiet bedroom at the household's bedtime. Not a tech product. Not a kids' toy with primary colours and animation. A warm bedside lamp, a parent reading, a soft duvet — the device is the messenger, not the experience.
- **Typography:** Soft humanist serif for book content and the story player (Source Serif Pro or Adobe Caslon Pro). A friendly grotesque for app chrome (Inter). The consent paragraph in the enrolment flow uses the serif at a generous size — this is the paragraph the parent will read aloud and the typography should support that reading aloud.
- **Palette:** Warm cream paper `#F6EFE2` for the story player background, deep ink `#1F1B17` for body text, dusty blue `#5A6E89` for the parent's UI ("dashboard", "settings"), a single warm amber `#C58A3A` for the call-your-grown-up button — the warmest colour in the palette because it is the warmest action. A muted red `#A33A2C` only for the **Delete voice** button — used once, never decorative. Borrowed from a child's bedroom at bedtime, not from SaaS design systems.
- **Imagery:** The per-page illustrations are the hero of the story player. Nano Banana 2 prompts emphasise gouache or watercolour-feel, low saturation, soft edges, warm light. No high-contrast cartoony lines. No glossy AI sheen. The dashboard's empty state shows a soft line illustration of a child's bedside lamp and a tucked blanket — not a generic microphone or a "Get started" hero.
- **Hand-feel touches:** The page-turn between pages of a story uses a soft cross-fade plus a thin paper-rustle sound (≤ 80 ms, off by default for `prefers-reduced-motion`). The enrolment record button is a single warm dot that pulses gently while recording — not a flashing red. The dashboard shows the past week's reading log as small warm dots on a calendar strip, not as a chart with axes.
- **Spacing:** consistent 4-px base. Generous whitespace — the bedtime player needs air.
- **Radius:** consistent token set (e.g. 8 / 14 / 22 px). Book cards use 8; dashboard cards use 14; the welcome card uses 22.
- **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The story player's page-advance is the one place where motion carries meaning; respect reduced-motion by cutting 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 ("Enrol your voice — it takes ninety seconds" / "Add the first book to your child's drawer").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible families, names, books, time zones, and reading logs that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on real diaspora and shift-work patterns — Filipino-Hong Kong, Nigerian-British, Mexican-American, Sri Lankan mariner, Polish trucker — but never claim that a fictional family is a real customer. Invent voice-clone, sample, and consent recordings as fictional speech you compose.
- Tone: warm, direct, free of corporate language. This template is for a parent at bedtime, not a company at quarterly review.
- 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 (the parent who is a software engineer wants to see "Firebase Storage bucket" in the env-var list; the trucker dad wants to see "time zone").
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. The dashboard's weekly summary is written as a calm paragraph, not as "AI summary:".
## 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 families (sidebar):**
- "Marivel & Migs" (Filipino, Hong Kong → Cavite) — Marivel is a domestic worker in Causeway Bay; Migs is her seven-year-old who lives with her mother-in-law. Six approved books: *Si Pilandok at ang mga Buwaya*, *The Snail and the Whale*, *Goodnight Moon* (with Marivel's Tagalog translation per page), *Ang Pambihirang Buhok ni Lola*, *Bakit Matagal ang Gabi?*, and *Sleep, Little One, Sleep*. Voice locale: tl-PH.
- "Tunde & Tope" (Nigerian-British, London → Houston rotations) — Tunde is a consultant; Tope is his four-year-old who stays with his grandmother in Lagos when Tunde travels. Eight approved books, three with parent-typed Yoruba translations on alternating pages. Voice locale: en-GB with a Yoruba pronunciation overrides set.
- "Sofía & Mateo" (Mexican-American, San Diego ↔ Dallas/Phoenix) — Sofía is a sales rep; Mateo is six. Drawer in Spanish-with-some-English. Voice locale: es-MX.
- "Aiyana & Kelan" (US Navy submariner family, San Diego) — 90-day patrol. Voice locale: en-US with an auto-delete-30-days-after-return rule set.
- "Cath & the twins" (Manchester, recovering from a 2025 stroke) — voice profile enrolled with a relaxed liveness check and a same-day fresh consent recording. Voice locale: en-GB.
**Sample bedtime drawer (this is what the demo should show):**
- Family: Marivel & Migs (Filipino, Hong Kong → Cavite)
- Enrolled voice: Marivel, Tagalog (tl-PH), enrolled Sunday 25 May at 14:12 Hong Kong time, sample 67 seconds, SNR 31 dB, coaching verdict "Looks great — ready to enrol", consent phrase match confidence 0.97
- Auto-delete: "extend monthly" (set to delete on 25 June unless extended)
- Pronunciation overrides at the household level (3): "Migs" → "Meegs", "Lola" → "LOH-lah", "Iya" → "ee-YAH" (Migs's name for his grandmother)
- Tonight's chosen book: *Bakit Matagal ang Gabi?* (Why Is the Night So Long?), 18 pages, ~7 minutes, illustration style "soft gouache, warm dusk palette, no humans except Migs's stand-in (a small boy with dark hair and round cheeks)"
- Sample page text (excerpt, page 4):
- Verbatim: "Nakaupo si Migs sa bintana. Tinitignan niya ang mga bituin. 'Iya,' tanong niya, 'bakit hindi pa rin ako inaantok?'"
- text_for_tts: "Read warmly and unhurriedly, like a parent at bedtime to a sleepy child. … Nakaupo si Migs (Meegs) sa bintana. Tinitignan niya ang mga bituin. 'Iya (ee-YAH),' tanong niya, 'bakit hindi pa rin ako inaantok?' …"
- Timing: rate_wpm 92, paragraph_pause_ms 350, trailing_breath_ms 400, end_of_chapter false
- Page illustration: a small boy with dark hair sitting at a window, gentle dusk light, a single star visible, gouache feel, do_not_show: ["no text", "no logos"]
- Last seven nights' reading log:
- Mon 19 May — *Goodnight Moon*, finished at 8:51 pm Cavite time; closing call pressed
- Tue 20 May — *The Snail and the Whale*, finished at 8:46 pm; closing call not pressed
- Wed 21 May — *Bakit Matagal ang Gabi?*, fell asleep at page 14 of 18
- Thu 22 May — *Ang Pambihirang Buhok ni Lola*, finished at 8:39 pm; closing call pressed
- Fri 23 May — *Sleep, Little One, Sleep*, finished at 8:55 pm
- Sat 24 May — no story (Migs was at his cousin's overnight)
- Sun 25 May — *Si Pilandok at ang mga Buwaya*, finished at 9:02 pm; closing call pressed (Marivel had Sunday off)
- Dashboard weekly summary (generated): "Six bedtime stories this week in your voice. Migs finished five of them and fell asleep before the end of one — *Bakit Matagal ang Gabi?* on Wednesday — which is what we hope for on a Wednesday. He pressed the call-your-grown-up button three of the six nights."
- Long-context "what helps him sleep" output: recommended_books = ["bakit_matagal_ang_gabi", "sleep_little_one"], confidence 0.78, reasoning_one_paragraph "On nights you need him to settle quickly, *Bakit Matagal ang Gabi?* and *Sleep, Little One, Sleep* have most often coincided with him falling asleep before the last page. Not enough data yet on the other books."
**Sample input artefacts (for the build to demonstrate):**
- A 67-second Tagalog voice sample of a fictional Marivel reading the public-domain consent paragraph the app provides, captured live in a quiet café corner (synthesised for the demo from a licensed stand-in voice clearly labelled as such).
- A consent recording of Marivel reading the consent phrase, captured immediately before the sample.
- A 24-page PDF of *Bakit Matagal ang Gabi?* uploaded by Marivel last Sunday.
- Nano Banana 2 page illustrations for *Bakit Matagal ang Gabi?* with character anchors for "Migs" (small boy with dark hair and round cheeks, blue pyjamas) and "Iya" (grandmother with grey hair in a bun, soft cardigan).
**Sample voice copy:**
- Onboarding: "Read your kid a bedtime story in your voice on the nights you can't be home."
- Enrolment consent paragraph (English version): "I am [your name]. I am recording my voice on this device, right now, so that this app can read bedtime stories in my voice to my child. I understand the voice is only for my family and that I can delete it forever with one tap."
- Enrolment sample paragraph (English version, public-domain bedtime fragment composed for this template): "The sky is dark now. The little birds have gone to their nests. The boat on the river is tied up for the night. Goodnight, little one. Sleep until the morning."
- Coach (good take): "Lovely. The room is quiet and your voice is clear. Ready to enrol."
- Coach (small-room echo): "Your voice sounds great. The room has a bit of echo — try a corner with curtains or fabric, or a closed-door bedroom. Quick re-record?"
- Coach (consent phrase unclear): "Almost there. The consent phrase wasn't clear enough for us to verify it was you. Try once more — speak naturally and look at the screen as you read."
- Child home: "Tonight's stories"
- Story end: "Want to call your real grown-up now?"
- Story end (parent offline): "Mum will see that you finished the story. Want to send her a goodnight?"
- Delete voice confirmation: "Marivel's voice was deleted on 28 May at 17:42 UTC. The audio sample, the consent recording, and every cached page-audio file rendered from this voice were removed."
- Trip mode toggle: "I am away from Sunday to Friday this week."
- Dashboard empty state (no stories yet): "Migs will see his first story here once you've enrolled your voice and added a book."
**Sample family invitation email subject + body:**
- Subject: "Iya — Migs wants you to read bedtime stories too. Want to enrol your voice?"
- Body: "Hi Iya — I added six books to Migs's bedtime drawer. He likes when you read *Ang Pambihirang Buhok ni Lola* in your voice. Tap to enrol — it takes ninety seconds. Your voice stays in our family's app and you can delete it any time." [Open Goodnight Hand-off]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a parent's hands holding a phone, the screen showing the consent paragraph, a quiet airport-lounge corner with warm evening light, an enamel cup of tea blurred at the edge of frame. Generate via Nano Banana 2 with a prompt emphasising "phone in hands, late evening, warm tungsten light, gentle out-of-focus tea cup, soft shadow under the phone, real worn travel backpack strap in background, no people's faces visible".
- **App icon / wordmark:** Set in the soft serif. A small warm circle behind it — the bedside-lamp colour. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a child's bedside lamp on a small nightstand with a tucked blanket. Hand-drawn aesthetic, not a flat icon.
- **Demo book illustrations:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "soft gouache feel, low saturation, warm light, character consistency across pages, no text in image, no logos". Each demo page should look hand-illustrated, not rendered.
- **Stock fallbacks:** If image generation fails, fall back to a curated set of soft watercolour bedtime illustrations bundled at `/public/samples/`. 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.
- 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: "Enrol your voice — it takes ninety seconds", "Add the first book to your child's drawer", "Invite the other parent" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- The TTS render for a page streams the audio as it becomes available, with a calm "preparing tonight's story…" indicator that disappears the moment the first page's audio is ready. The child's device pre-fetches the next two pages in the background.
- If a TTS call fails, the child's device shows a calm, non-scary fallback: a friendly stand-in voice (clearly labelled in the parent's dashboard as a fallback) reads the page, and the dashboard surfaces the failure for the parent to retry. The child never hears an error message.
- The parent dashboard's weekly summary streams in token-by-token where supported, after the page has fully loaded — the rest of the dashboard is usable while the summary writes itself.
- Low-confidence pronunciations in the per-page review are underlined; tapping reveals the alternates the model considered and a "type my own" input.
- The story-player page-advance takes 450 ms cross-fade with `prefers-reduced-motion` falling back to instant.
## 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 book parse / pronunciation override / long-context, `gemini-3.5-flash` for coaching and dashboard summary, `gemini-3.1-flash-tts-preview` for narration, and `gemini-3.1-flash-image` for per-page illustrations. Set `thinkingLevel` explicitly per call where applicable; omit `thinkingConfig` entirely on TTS and image calls.
- **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 family-invitation channel.
- **Storage:** Firebase Storage for voice samples, consent recordings, page audio, and per-page illustrations. Pre-signed URLs only. Storage lifecycle rules enforce the auto-delete-after-30-days option when toggled.
- **Mobile-first.** 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.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the parent dashboard for the reading log.
- Optimistic UI on writes; reconcile on response.
- Voice enrolment uses the Web Audio API with `MediaRecorder` at 48 kHz mono and explicit microphone-permission prompts. The browser-native consent UI is layered on top of the app's consent paragraph — both must pass before enrolment is accepted.
- **iOS Safari gotchas (graceful degradation):** Safari `MediaRecorder` only supports `audio/mp4` (AAC) — feature-detect and fall back to AAC at 48 kHz mono; microphone permission does NOT persist across page reloads on iOS — re-request on every enrolment session and re-read the on-screen consent phrase before accepting a sample; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — discard the partial sample, prompt the parent to start over once the call ends, and never silently splice two halves of an interrupted recording.
- Long-context guardrail: clamp the long-context "what helps her sleep" call to the last 90 days of reading logs and books. A family that has used the app for two years still calls with a 90-day window — well under 1M tokens.
- Numeric `min`/`max` constraints in the Zod schema are clamped server-side after every Gemini response arrives; the schema constraints are documentation only inside `responseSchema`.
## 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.
- All images have meaningful `alt` text. The per-page book illustrations have `alt` describing the scene ("illustration of a small boy at a window at dusk, a single star visible, gentle warm light").
- Form fields have associated `