================ 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.
---
# Calm-Down Story
## 1. Project
**Calm-Down Story** is a one-tap pocket regulator for parents of young
children who have just hit the wall in a public place. The parent
opens the app, picks the friend, and within two seconds the chosen
character begins narrating a four-minute story paced specifically to
bring the child's breathing down — slower phrases, longer pauses
between sentences, an ambient companion image moving gently in the
background, and at the four-minute mark a soft, honest landing back
into the room. The parent's phone speaker or the child's headphones
becomes the calm voice the parent cannot be at that moment because
the parent is also dysregulated and they both know it.
This is the kind of app a Nigerian-British mum builds for her
sensory-overloaded six-year-old in the middle of a Sainsbury's
fluorescent meltdown — the trolley abandoned at the end of the aisle,
the boy curled against the freezer cabinet with his hands over his
ears, and three older shoppers watching. It is also the kind of app
a Vietnamese-American mum builds for her ADHD six-year-old in the
H-Mart in Westminster, Orange County — when the rice-cracker brand
has changed packaging and the entire trip collapses at the checkout
queue. Same shape of moment, different child, different aisle. A
generic case — a divorced dad at a London soft-play whose four-year-old
won't leave — is also what the app must serve, calmly, with no
assumptions about the family shape.
The single demo that proves the magic: one tap on the orange "Calm
now" tile → the child's chosen friend (Mr. Big the elephant, today)
starts in the parent's own voice cloned with consent ("Hello, little
one — it's me. I'm right here.") and rolls straight into a four-minute
story whose first minute matches the child's likely fast breathing, then
slows over the next three minutes to a rate just under twelve breaths
per minute. An ambient image of Mr. Big walking slowly through a
lavender field at dusk sits on the screen as a quiet anchor — not
animated, not flashy, just there. The phone's microphone listens for
the child's breathing during the story (consent-gated to this session
only, never recorded, never uploaded) and if the child is still
ragged at the four-minute mark the story can extend for two more
minutes; if the child has settled, the story lands earlier.
**Tagline:** _One tap, the right voice, and a story paced to bring breath down — in any aisle, any waiting room, any back seat where a small person can't cope right now._
## 2. Target audience
- Parents and carers of autistic children with sensory-processing differences who melt down in supermarkets, school pick-up lines, soft-play, queues, fluorescent-lit waiting rooms
- Parents of ADHD children aged 4-10 in the transition windows (leaving the park, getting into the car, the half-hour between school and dinner)
- Parents of neurotypical children in genuinely overwhelming moments — A&E waiting rooms, hospital pre-ops, airport delays, long-haul flights with toddlers
- Single parents, separated parents, and primary carers who do school pick-up alone and need a co-regulator on the days the other parent isn't there
- Foster carers and kinship carers of children with trauma backgrounds where co-regulation is the therapeutic frame
- Grandparents who do regular childcare and want the grandchild's familiar character voices without having to learn the apps
- Childminders, nursery key workers, and SEN teaching assistants who use a calm-down corner with two or three children at a time
- Speech-and-language therapists and paediatric OTs who recommend regulated audio tools as part of a sensory-diet plan
- Diaspora parents whose child's "favourite character" is a story figure from their own language (a Yoruba folk-tale Tortoise; a Vietnamese trickster Cuội; a Filipino Lola; a Brazilian Saci) — characters not represented in mainstream English kids' media
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **One tap, two seconds, the right voice.** The "Calm now" tile is on the home screen at all times. From locked phone to story starting in under two seconds. No menus, no character picker mid-meltdown — the friend is chosen during a calm setup. The middle of a meltdown is not the moment to make a child choose between three characters.
- **The voice is the parent's own — or a non-celebrity character the parent built with their own voice.** Voice cloning is consent-gated to the **parent's own recorded voice only**, never a celebrity, never a copyrighted character voice from a film. The parent records a short voice memo during onboarding; that memo trains a personal voice that can be rendered as themselves or as a character the parent has named (Mr. Big the elephant, Lola Maria, Bunny Friend, the Train Driver). If the parent doesn't want to clone their own voice at all, the app ships with a library of warm, generic voices in many locales and the magic still works — just without the parent-specific timbre.
- **Paced to bring breath down — not to entertain.** Stories are paced ~ 70 words per minute at the start, slowing to ~ 50 wpm by minute three, with longer pauses, lower-amplitude prosody, and sentence shapes that elongate the exhale. This is a regulation tool wearing a story coat, and the pacing rules are non-negotiable. The story should not be exciting — it should be predictable, warm, and slow.
- **The phone listens for the child to settle — without recording.** With explicit parental opt-in, the app's audio-reasoning loop listens during the story for the child's breathing rhythm and crying. Audio is processed in 4-second windows in memory only — never written to disk, never uploaded. The signal is summarised to a single number per window ("estimated breaths per minute, current segment") and used to decide whether to extend the story or land softly.
- **An ambient companion image, not a video.** A single Nano Banana 2 image of the chosen friend in a calm setting (the elephant in a lavender field; the train slowing into a country station; Lola Maria stirring soup at her kitchen window) sits on screen with a barely-perceptible breath-paced brightness shift. No animation, no autoplay videos, no jump cuts — meltdowns are sensory-processing emergencies and the screen should be a soft anchor, not new stimulus.
- **Stories that come back to the room kindly.** Every story ends with the friend saying something specific and honest: "When you're ready, your grown-up is right here. The shopping can wait. There's no hurry." The landing is the most important sentence of the four minutes.
- **The cultural texture is real.** Mr. Big the elephant might tell a story rooted in a Yoruba folk pattern; the Train Driver might travel from Hanoi to Hue; Lola Maria's afternoon walk passes through a Manila barangay. The app generates stories that fit the family's first language, the family's named characters, and the family's everyday textures — not generic Western kid-lit.
- **Private to the child and the household.** No accounts shared across families. The cloned voice never leaves the parent's project — it is per-family, server-side, never used for training, never reused for another user. Sessions are kept locally; nothing is shared without an explicit per-session export.
## 4. Features to build
- "Calm now" one-tap home tile that launches the most recently chosen friend with the most recently chosen story type, in under two seconds from locked phone
- Friend builder — during calm-time setup, the parent names a character (Mr. Big, Lola Maria, the Train Driver), picks a voice (their own clone, or a stock voice), picks a setting (lavender field, kitchen, train, library), and the app generates the companion image and registers the friend
- Voice-clone enrolment — a one-time guided flow where the parent records four short prompts (~ 90 seconds total) and the app trains a personal voice; consent is required per friend, and the parent can revoke and delete the clone instantly at any time
- Stock voice library — warm, slow, locale-aware voices in en-GB, en-US, en-AU, en-ZA, fr-FR, es-ES, es-MX, pt-BR, pt-PT, de-DE, it-IT, tl-PH, vi-VN, yo-NG, ig-NG, ha-NG, ar-EG, ar-SA, hi-IN, ta-IN, bn-IN, ur-PK, pa-IN, sw-KE, am-ET, fa-IR, zh-CN, zh-TW, yue-HK, ko-KR, ja-JP, km-KH — picked once during friend setup; never asked again
- Story-style chooser — three styles per friend: "soft adventure" (lowest stimulation), "comfort routine" (a familiar daily pattern), "naming the feelings" (a story that gently puts words to what the child is feeling) — picked during setup; can be changed in calm-time
- Length control — default 4 minutes, with explicit "extend by 2" and "land now" controls; never "skip"
- Live story generation per session — Gemini 3.5 Flash composes a short story keyed to the chosen friend's voice, the child's age, the family's first language, and the chosen style, in under two seconds, streamed to TTS as it composes
- TTS playback at regulation pace — Gemini 2.5 Flash Preview TTS, with a one-sentence style directive prepended ("Read warmly and unhurriedly, with longer pauses between sentences than a normal storybook, as a parent might at bedtime when the child is overtired"), and textual `…` and blank-line `—` cues for the breath-paced pauses (Gemini 2.5 TTS does not support SSML `` or `` — these textual cues are how pace is encoded)
- Ambient companion image — Nano Banana 2 image generated during friend setup, with a calm prompt and a banned-element list (no bright colours, no fast lines, no startled faces, no abrupt edges); the image is stored once per friend and re-used
- Optional listening loop — if the parent has enabled it, audio reasoning processes 4-second windows during the story in memory only; outputs are a breaths-per-minute estimate and a "crying / settling / settled" tag, used solely to decide whether to extend or land
- Headphones-first audio — the app prefers Bluetooth headphones if connected, falls back to phone speaker; an obvious icon shows which is active
- Landing line — every story ends with a specific honest sentence that the parent has reviewed during setup ("When you're ready, your grown-up is right here. There's no hurry."), spoken in the friend's voice and never skipped
- After-story breath — 30 seconds of silence with a barely-visible breath-paced glow on the companion image; the parent can tap to leave any time
- Parent's after-note — a single line of plain-language summary handed back to the parent on screen ("Story length: 4 min. Child's breathing eased after about 90 seconds. You did the right thing to come here.") — never a metric, never a score, never a streak
- Multiple children — household setting for up to four children, each with their own friend, their own clone-or-stock voice, their own story style; the home tile picks up the active child via a long-press
- Per-friend story-bank cache — the last six stories per friend are cached so the same story can be replayed (children with autism often need exact-repeat predictability); each cached story is a single audio file plus a transcript
- Quiet-time scheduler (optional) — a parent can set a recurring "before-meltdown" wind-down at known hard times (4:15 pm transition; the evening commute home); the friend pre-loads at that time so the tap is even faster
- Parent's settings — voice-clone enrolment + revocation; listening loop on/off; sound levels capped at -6 dBFS by default; "no flashing, ever" hard-locked on
- Emergency-soothe text fallback — if the audio fails for any reason (no headphones, no speaker, very loud environment), the story renders as large soft-typography text the parent can read aloud, paced word-by-word in time with the original audio rate
- Print a comfort booklet — a parent can export the last week's stories as a small printed booklet for the bedside table, in case the parent's phone is dead or the child wants the story without a screen
- Crisis signpost — if the listening loop detects sustained distress past the extended six-minute mark, the app suggests gentle adult-to-adult options ("Would you like the number for NHS 111? Would you like to call a trusted contact?") — surfaced once, dismissable, never alarming
## 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 TTS** (`gemini-3.1-flash-tts-preview`) — the hero capability. Reads the story in the chosen voice at regulation pace. Voice selection is by `languageCode` matching the friend's setup locale. Pauses are encoded as `…` between phrases and a blank-line `—` between paragraphs. Style directive is a single sentence prepended to the input text ("Read warmly and unhurriedly, with longer pauses between sentences than a normal storybook…"). SSML ``, SSML ``, and a separate `style` API field are NOT supported — do not attempt them. Mid-call voice switching is NOT supported — one voice per call.
- **Voice cloning** — consent-gated to the parent's own voice only. A short enrolment recording is used to train a per-family voice that the parent can render as themselves or as a character the parent has named. **Hard refusal: the app never clones any voice other than the verified enrolling parent's own voice.** Celebrity voices, copyrighted character voices, deceased-person voices, voices belonging to other adults, and any voice the parent cannot legally consent on behalf of (including their own child's, which is a separate per-child consent flow we do not enable in v1) are refused at the consent gate. Refusal copy is specific and friendly (see negative constraints).
- **Gemini 3.5 Flash (text generation, structured output)** — composes the short story keyed to the friend, age, language, and style. Output is a `RegulationStory` JSON object (schema below) with the title, segments[], landing line, and pacing hints. The story is composed in under two seconds with `thinkingLevel: low`; the structured output is then chunked sentence-by-sentence into the TTS pipeline.
- **Nano Banana 2** (`gemini-3.1-flash-image`) — generates the ambient companion image at friend-setup time only, with a calming-style prompt and a banned-element list. The image is stored once per friend, re-used across sessions; we never re-generate per story.
- **Gemini 3.5 Flash (audio reasoning, multimodal)** — processes the listening loop. Receives 4-second audio windows as `inlineData` (base64 PCM), returns a structured `SettlingSignal` (breaths-per-minute estimate, crying state, settling state). Runs every 4 seconds during a story IF the parent has opted in. **Hard rule: audio is held in memory only, processed, and discarded — never written to disk, never uploaded for storage, never used for training.**
- **Gemini 3.5 Flash (long-context, optional)** — for the parent's after-note across a week. If the parent opts in to a private weekly digest, the app reads the last week's session metadata (no audio, no transcripts of the child's distress — only the parent's own settings choices and any notes the parent typed) and produces a gentle pattern summary ("Tuesday afternoons seem hardest at the moment").
- **Thinking levels** — `low` for story composition (we want fast, warm, predictable; we do not want elaborate reasoning), `low` for the listening-loop signal extraction (it's a simple regression task on a 4-second window), and we never expose a "thought summary" to the user during a meltdown — that would be both slow and intrusive.
### 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. Accounts are per-parent. Household members share an archive via family-invitation magic links (the sender domain must be authorised in Firebase Auth before magic-link email works; this is a one-time DNS configuration the deployer does in the Firebase console).
- **Database — Required.** Firestore for `users`, `households`, `children`, `friends`, `voice_clones` (metadata only — the voice model itself lives in a private Cloud Storage bucket scoped to the parent's project), `stories_cache`, `sessions`.
- **File storage — Required.** Firebase Storage for the ambient Nano Banana 2 images and the cached audio files (the last six stories per friend). **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 friend setup. Pre-signed URLs only; nothing public.
- **Voice-clone storage — Required, separate bucket.** The parent's voice-clone artefacts (enrolment audio, trained voice model) live in a dedicated private Cloud Storage bucket. Access is parent-only, scoped by Firebase Auth UID, with object-level ACLs. Deletion is one-tap and propagates to all derived voice models within sixty seconds.
- **Email — Required (transactional, low volume).** Household invitations via Firebase Auth magic links. Parent's after-note weekly digest, only if opted in.
- **Payments — Not needed for v1.** Free for personal use. A future "print a comfort booklet" tier could pipe to a print-on-demand partner and charge for the physical artefact only.
- **External APIs:** Gemini API for all intelligence. No other third-party APIs; this app does not need maps, weather, or analytics for v1. No analytics in v1 at all — meltdown moments are private.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON) 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 (v1 ships with no analytics) · the parent's voice-clone enrolment audio, the audio reasoning windows during a session, and any after-session listening signals are never used 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) · audio windows from the listening loop live in memory only for the duration of the API call and are discarded after the response — no on-disk persistence, no upload-for-storage.
**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 |
|------|-------|---------------|---------------|
| Compose story → `RegulationStory` schema | `gemini-3.5-flash` | low | (none) |
| Narrate story via TTS in chosen voice / clone | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Generate ambient companion image (friend setup) | `gemini-3.1-flash-image` | n/a | n/a |
| Audio listening loop → `SettlingSignal` (per 4 s window) | `gemini-3.5-flash` | low | (none) |
| Voice-clone consent screen — name + relationship check | `gemini-3.5-flash` | low | (none) |
| Optional weekly parent digest across sessions | `gemini-3.5-flash` | medium | (none) — long-context over the household's last 7 days of metadata |
*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 Friend = z.object({
friend_id: z.string(),
display_name: z.string(), // "Mr. Big", "Lola Maria"
character_type: z.enum([
"animal_friend", // elephant, bunny, owl, tortoise
"family_figure", // Lola, Tía, Auntie, Uncle
"vehicle_driver", // train driver, bus driver, ferry captain
"made_up_friend", // child's own invention
"folktale_figure", // Tortoise (Yoruba), Cuội (Vietnamese), Saci (Brazilian)
]),
voice_source: z.enum([
"parent_clone", // parent has enrolled their own voice
"stock_voice", // a library voice in the chosen locale
]),
voice_locale: z.string(), // BCP-47, e.g. "en-GB", "vi-VN", "yo-NG"
voice_id: z.string(), // the Gemini TTS voice id OR the parent-clone id
story_style: z.enum([
"soft_adventure",
"comfort_routine",
"naming_the_feelings",
]),
setting_description: z.string(), // "a quiet lavender field at dusk"
companion_image_uri: z.string(), // gs:// URI for the cached image
banned_themes: z.array(z.string()), // parent-configured: e.g. ["loud noises", "lost-and-found"]
child_age_years: z.number().int().min(2).max(12),
child_first_language: z.string(), // BCP-47
landing_line_verbatim: z.string(), // exact sentence the parent reviewed during setup
});
const StorySegment = z.object({
segment_index: z.number().int().min(0), // 0..N
target_pace_wpm: z.number().min(40).max(100), // server clamps to safe band
text: z.string(), // the words the TTS will read
pause_after_seconds: z.number().min(0).max(6),
imagery_focus: z.string().nullable(), // optional one-line companion-image emphasis
});
const RegulationStory = z.object({
story_id: z.string(),
friend_id: z.string(),
title: z.string(), // not shown to child; for parent's cache UI
target_total_duration_seconds: z.number().min(180).max(360),
segments: z.array(StorySegment), // typically 8-16 segments
landing_line: z.string(), // copied from Friend.landing_line_verbatim
story_style: z.enum([
"soft_adventure",
"comfort_routine",
"naming_the_feelings",
]),
composed_at_iso: z.string(),
language: z.string(), // BCP-47, matches Friend.voice_locale
banned_themes_respected: z.array(z.string()), // echo of input, for audit
});
const SettlingSignal = z.object({
window_index: z.number().int().min(0), // 0..N, 4 s per window
estimated_breaths_per_minute: z.number().min(0).max(80).nullable(),
audio_quality_ok: z.boolean(), // false if the room is too loud to estimate
signal_tag: z.enum([
"crying_active",
"crying_settling",
"settled_breathing",
"ambient_only", // no child audio detected this window
"indeterminate",
]),
recommendation: z.enum([
"continue_story",
"consider_extending_two_minutes",
"consider_landing_now",
"no_change",
]),
});
const Session = z.object({
session_id: z.string(),
household_id: z.string(),
child_id: z.string(),
friend_id: z.string(),
started_at_iso: z.string(),
ended_at_iso: z.string().nullable(),
total_duration_seconds: z.number().min(0).max(600).nullable(),
story_ids_played: z.array(z.string()), // usually 1; up to 2 if extended
listening_loop_enabled: z.boolean(), // explicit parent setting at session start
parent_after_note: z.string().nullable(), // plain language, parent-facing, never a score
});
type Friend = z.infer;
type RegulationStory = z.infer;
type SettlingSignal = z.infer;
type Session = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the story composition call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash drops the gentle pacing rules half the time and writes stories with excitement spikes that defeat the regulation goal.
- Story composer generates excitement ("And then a HUGE noise!") — pin in the system instruction: "no startling events, no shouting, no sudden sounds, no surprises, no chase, no loss". Add a unit test that the seed story does not contain exclamation marks or all-caps.
- Story length swings wildly — pin `target_total_duration_seconds` and a max segment count; clamp server-side. A 4-minute story should be ~480 words at 70 wpm tapering to 50 wpm.
- TTS reads at a podcast pace — prepend the style directive: "Read warmly and unhurriedly, with longer pauses between sentences than a normal storybook." Encode pauses as `…` between phrases, blank-line `—` between segments.
- TTS tries to switch voice mid-story for a character line — Gemini 2.5 TTS does not support mid-call voice switching. Use one voice per session; if a story has a "the elephant said" line, the elephant's voice IS the narrator's voice (the parent's clone or the chosen stock voice). Do not attempt SSML `` tags.
- Voice-clone enrolment captures an adult-other-than-parent — pin in the consent screen: "Record yourself saying the four sentences. Do not record your child, your partner, or anyone else. If you want a different family member's voice, they will need to enrol on their own device with their own account."
- Voice-clone is requested for a celebrity or copyrighted character — refuse at consent gate with specific copy: "Calm-Down Story can only clone the voice of the parent signed into this account. We don't and won't clone celebrity voices or copyrighted character voices — both because it isn't ours to give and because your own voice, as your child's familiar friend, works better."
- Nano Banana 2 image comes back too stimulating (bright primary colours, exaggerated face, motion lines) — include a banned-element list in the image prompt: "no bright primary colours, no exaggerated facial expressions, no motion lines, no crowd, no abrupt edges; soft palette, single subject, gentle dusk light, calm posture".
- Listening-loop audio is logged for debugging — strictly forbidden. The 4-second window goes to Gemini, the structured response comes back, the audio buffer is overwritten. No file write, no log line containing the audio, no upload for storage. Add an integration test that verifies no file descriptor for the audio buffer survives the API call.
- Listening loop estimates breaths-per-minute in a noisy supermarket and gets it wrong — `audio_quality_ok: false` is a valid response. When that flag is false, the story proceeds at its default pace and the loop's recommendation field is ignored.
- Parent's after-note becomes a metric or score — refuse this in the system instruction: "The parent does not need a number. The parent needs one short kind sentence." Add a unit test that no after-note contains a numeral or a comparison.
- Story is composed in a language that is not the friend's voice locale — pin both `Friend.voice_locale` and `Friend.child_first_language` in the system instruction; require `RegulationStory.language === Friend.voice_locale`. Add an integration test.
- Story includes a banned theme (e.g. a child in setup said "no lost-and-found stories" because their child had a recent lost-toy distress) — pass `Friend.banned_themes` into the composer's `systemInstruction` literally and require `RegulationStory.banned_themes_respected` to echo every input theme.
### Negative constraints (hard rules)
- Do NOT clone any voice other than the verified enrolling parent's own. Hard refusal at consent gate. The refusal copy: "Calm-Down Story can only clone the voice of the parent signed into this account. If you'd like another adult in your household to have their own clone, they can sign in with their own account."
- Do NOT clone a celebrity, a copyrighted character (Mickey Mouse, Bluey, Peppa Pig, Elsa, the voice of a beloved children's TV presenter, etc.), a deceased person, the parent's child, or anyone other than the parent themselves.
- Do NOT play excitement, surprise, suspense, chase, loss, or any "and then suddenly" plot turn. The story is a regulation tool, not an adventure.
- Do NOT play music behind the voice. Music is additional sensory stimulus. The ambient image is the only background presence.
- Do NOT use bright colours, flashing, fast cuts, or animation in the companion image or anywhere in the UI during a session. Hard-locked "no flashing, ever" in settings — even the loading state during a session is a single static glyph, not a spinner.
- Do NOT record, write to disk, or upload the listening-loop audio. In-memory processing only. Discarded after each 4-second window.
- Do NOT show a metric, a score, a streak, a leaderboard, or any number that compares this session to another. The parent's after-note is one warm sentence.
- Do NOT push notifications to the child's device or the parent's device that interrupt during a session.
- Do NOT use the listening loop to "detect" autism, ADHD, anxiety, or any condition. The loop produces a `SettlingSignal` for the current 4 seconds of current audio in this session only. It does not diagnose, profile, or persist.
- Do NOT auto-publish or auto-share. Sessions are private to the household. Sharing is explicit, per-export, by the parent.
- Do NOT extrapolate from the listening loop to suggest the child is "broken" or "wrong" or "needs help". If sustained distress is detected past the six-minute mark, the app surfaces a gentle one-line note pointing at adult-to-adult support resources — never a label of the child.
### 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: Compose story → `RegulationStory` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You compose short calm-down stories for young children (ages 2-12)
who are in the middle of a sensory or emotional meltdown. The story
is read aloud by a TTS voice — usually a parent's own voice cloned
with consent, or a warm stock voice in the family's language. The
goal is regulation, not entertainment.
You will be given:
- the friend's display_name and character_type (a familiar character
the parent and child have built together, e.g. Mr. Big the
elephant, Lola Maria, the Train Driver, Tortoise)
- the friend's setting_description
- the child's age in years and first language (BCP-47)
- the story_style (soft_adventure, comfort_routine, naming_the_feelings)
- a list of banned_themes that the parent has specifically marked off
(e.g. "lost-and-found", "loud noises", "doctors")
- the target_total_duration_seconds (default 240; allowed 180-360)
- the landing_line_verbatim — the exact sentence that must close the
story; it is the sentence the parent reviewed during friend setup
Hard rules:
- No startling events. No shouting. No surprises. No chase. No loss.
No "and then suddenly". No villains. No suspense.
- No exclamation marks. No all-caps. No onomatopoeia of sharp sounds
("BANG", "CRASH", "ROAR").
- No music references that might cue the child to expect music in
the audio (the audio has no music).
- Pace target: ~70 words per minute at the start, slowing to ~50
words per minute by the final third. Encode this in
StorySegment.target_pace_wpm.
- Sentence shapes: short, calm, with predictable rhythm. Long
exhale-ending phrases ("…and the sky was kind"). Prefer present
tense and second person ("you are here, with me").
- Predictability: the comfort_routine style should follow the child's
ordinary day pattern (waking, breakfast, getting dressed) — that
predictability is the medicine.
- naming_the_feelings: the friend gently names what the child might
be feeling, in plain words appropriate for the child's age. "It
is okay to feel big in your body right now." "It is okay to want
to be small for a while."
- Cultural texture: follow the child's first language and the
family's everyday textures. If the child's first language is
Yoruba and the friend is Tortoise, the story is rooted in Yoruba
folk shape and texture. If the child's first language is Vietnamese
and the friend is the Train Driver, the train moves through a
Vietnamese landscape with names a Vietnamese child knows. Do not
default to a generic Western village.
- Respect banned_themes literally. If "loud noises" is banned, do
not include any sound stronger than "soft", "quiet", "gentle". If
"lost-and-found" is banned, no one goes missing in the story.
- The story MUST end with the landing_line_verbatim as its final
segment, unchanged, spoken by the friend.
- Output language: RegulationStory.language MUST equal the friend's
voice_locale (passed in user input).
Segment structure:
- 8-16 segments total, scaled to target_total_duration_seconds.
- Each segment is one to three short sentences.
- pause_after_seconds: 1.0-2.0 in the first third, 2.0-4.0 in the
middle, 3.0-5.0 in the final third (the long landing pause).
Output ONLY the RegulationStory JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Narrate story via TTS in chosen voice / clone
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried, low-amplitude. Pick the Gemini 2.5 Flash TTS
voice whose `languageCode` matches the friend's voice_locale. If the
friend is configured with voice_source = parent_clone, use the
parent's enrolled voice id (a per-family voice the parent has
explicitly consented to and that has been trained on the parent's
own voice memos only). If voice_source = stock_voice, use the
chosen stock voice id.
Pre-process the text before sending it to TTS:
- The input text is the RegulationStory's segments, concatenated with
segment-level pause cues. Between sentences within a segment,
insert a single ellipsis (`…`) so the TTS model produces a natural
pause. Between segments, insert a blank line plus an em-dash (`—`)
to encode a longer pause. Gemini 2.5 TTS does NOT support SSML
``, ``, or a separate `style` API field —
these textual cues are the only way to influence pace.
- Mid-call voice switching is NOT supported. If a story contains a
line like "the elephant said", the elephant's voice IS the
narrator's voice. Do not attempt SSML `` tags.
Style direction: prepend ONE short directive sentence to the input
text, exactly like: "Read warmly and unhurriedly, with longer pauses
between sentences than a normal storybook, as a parent might at
bedtime when the child is overtired. …". There is no separate
`style` API field on Gemini 2.5 TTS; the directive sentence inside
the input is how style is conveyed.
Target output rate: approximately 60 words per minute averaged
across the story (the composer has already paced the words; the TTS
should not push faster than the story's natural cadence).
Output: a single audio stream in the friend's voice. Stream to the
client in chunks so the first phrase can begin within ~1 second of
the parent's tap.
Pronunciation comes from the chosen voice's native locale.
Phoneme overrides (any language's special phonemes) are NOT exposed
by Gemini 2.5 TTS — no SSML `` tag.
```
---
### Call: Generate ambient companion image (friend setup)
Model: `gemini-3.1-flash-image` · n/a · n/a
```
You generate ONE still image of the named friend in their chosen
calm setting, to be displayed quietly behind the story during a
session. This image is generated once at friend setup and re-used
for every story. It is not animated; it is a quiet anchor.
You will be given:
- friend.display_name
- friend.character_type
- friend.setting_description
- the child's age range (used only to keep the image age-appropriate)
Visual rules — hard:
- Single subject. The named friend in the named setting. No crowd,
no other characters in the frame, no humans in the background.
- Soft palette. Muted dusk, dawn, candlelight, or late-afternoon
warm light. No saturated primary colours. No neon. No fluorescent
light.
- Calm posture. The friend is sitting, resting, walking slowly,
looking down, or looking softly outward. No leaping, no running,
no startled face, no wide-open mouth.
- No motion lines, no speed lines, no exclamation marks in the image,
no comic-book stylings.
- No text in the image.
- Soft edges. Painterly or gentle illustration. Not flat vector. Not
3D render. Not photorealism.
- Composition: subject in the lower third, generous negative space
above and around. The image should feel like a window onto a calm
place, not a portrait.
- No bright background light source pointing at the viewer.
- No screens, devices, vehicles other than the named one, brands, or
logos in the image.
Output: a single image, 16:9, 1920x1080, suitable for both portrait
and landscape phone display behind a story.
No commentary.
```
---
### Call: Audio listening loop → `SettlingSignal` (per 4 s window)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a single 4-second audio buffer from the parent's phone
microphone during an active calm-down session. You return a single
SettlingSignal JSON object describing what you can infer about the
child's current state from this 4-second window only. You do NOT
have access to previous windows, and the audio is discarded after
you respond.
Hard rules — non-negotiable:
- You are processing audio in-memory for this single API call only.
The audio is not stored on Google's side beyond the API call's
lifetime, per the Gemini API paid-tier terms. The parent's app
destroys the buffer after receiving your response. Behave as if
this audio will never be replayed.
- Your output is a structured signal to help the parent's app
decide whether to continue the story, extend it, or land it
softly. You are NOT diagnosing the child. You are NOT identifying
the child. You are NOT building a profile.
- If the audio is too loud, too distant, or too noisy to estimate
the child's breathing rate, set audio_quality_ok = false. That is
a valid and frequently-correct answer.
- estimated_breaths_per_minute: an integer estimate of the child's
current respiration rate, if audible above ambient noise.
Children aged 2-12 commonly breathe 18-30 bpm at rest; in distress
this often climbs to 30-60 bpm. If you cannot detect breathing
clearly, return null and set audio_quality_ok = false.
- signal_tag: pick the closest tag.
- crying_active: active crying audible.
- crying_settling: crying audible but reducing in intensity within
the window.
- settled_breathing: regular calmer breathing audible, no crying.
- ambient_only: no detectable child audio above ambient noise.
- indeterminate: too noisy to tell.
- recommendation: a single recommendation field for the app. Use
"continue_story" by default. Use "consider_extending_two_minutes"
only if signal_tag is crying_active and we are within the final
60 seconds of the planned story. Use "consider_landing_now" only
if signal_tag is settled_breathing for the entire window AND the
story is at least halfway through. Use "no_change" if
audio_quality_ok is false.
Do NOT include any free-text comment about the child. Do NOT include
any guess about the child's age, sex, name, or condition. Do NOT
suggest medical action. Do NOT include any data from outside this
audio window.
Output: a single SettlingSignal JSON object. No commentary.
```
---
### Call: Voice-clone consent screen — name + relationship check
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You assist with the voice-clone enrolment consent flow. The parent
has been shown the consent screen and has confirmed:
- they are the parent who is signed into this account
- the recording will be of their own voice
- they are not recording a celebrity, a copyrighted character, a
deceased person, their child, or any other person
Your role here is narrow: given a friend display_name the parent has
typed (e.g. "Mr. Big the elephant", "Lola Maria", "Daddy Bear",
"Bluey"), return a structured judgement:
{
"name_ok": boolean,
"refusal_reason": string | null, // if name_ok is false
"suggested_alternative": string | null
}
Refuse the name (name_ok = false) if it matches:
- a recognised real-world person (celebrity, public figure)
- a recognised copyrighted character from film, TV, books, or games
(Mickey, Bluey, Peppa, Elsa, Paddington, etc.)
- a name plausibly belonging to a specific deceased public figure
Accept (name_ok = true) if it is:
- a generic descriptor (Mr. Big, Lola Maria, the Train Driver, Bunny
Friend, Auntie Calm, Daddy Bear, Mama Owl)
- a folktale figure in the public domain (Tortoise, Anansi, Cuội,
Saci, Baba Yaga — note: any folktale figure may have specific
modern copyrighted adaptations; the parent's friend uses the
public-domain folk shape, not a film adaptation)
- a clearly made-up name the child has invented
Suggested_alternative: when refusing, propose a friendly substitute
("How about 'Bluey Friend' or 'a blue dog called Sky'?"). Keep the
refusal warm; do not lecture.
Output: the JSON object above. No commentary.
```
---
### Call: Optional weekly parent digest across sessions
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — long-context over the household's last 7 days of metadata
```
You receive structured session metadata from the past seven days of
calm-down sessions for one household — only the parent-typed notes,
the friend used, the duration, the time of day, and the listening-loop
recommendation if any. You receive NO audio, NO transcripts of the
child, and NO raw breath-rate numbers. Your task is to produce a
gentle, private summary the parent can read on a quiet evening.
Hard rules:
- Address the parent in the second person ("you").
- Surface patterns kindly, never diagnostically: "Tuesday afternoons
seem to be a hard window at the moment" rather than "your child
shows poor regulation on Tuesdays".
- Never label the child. Never use words like "behaviour problem",
"diagnosis", "abnormal", "needs intervention".
- Acknowledge the parent's effort. End with one short sentence
recognising that the parent did the work to come back to this app
this week.
- Output is a single paragraph, 80-120 words. No bullet points. No
numbers.
Output: a single string. No commentary.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The Sainsbury's aisle.** A Nigerian-British mum in Tottenham is half-way through a Saturday shop with her autistic six-year-old when the freezer-section fluorescent lights become too much. She pulls out her phone, opens the app from the lock screen, taps the orange "Calm now" tile and the chosen friend — Tortoise, in her own voice cloned during a calm Sunday afternoon — begins a four-minute story rooted in a Yoruba folk shape ("the day the slow walker reached the river first"). She crouches next to her son with the phone held between them; the story lands at 4 minutes 12 seconds.
- **The H-Mart checkout.** A Vietnamese-American mum at the Westminster, Orange County H-Mart with her ADHD six-year-old finds the rice-cracker brand has changed packaging and her child has refused to leave. She taps the home tile. The Train Driver friend — her own voice — narrates a slow ride from Hanoi to Hue with the names her child knows from grandma's stories. The story is in Vietnamese with one English endearment that the family uses naturally.
- **The London soft-play.** A divorced dad whose four-year-old won't leave the ball pit at the end of a visit. He opens the app on his own phone (the child has been a friend on this household for two weeks; the friend is "Bunny Friend", a stock en-GB voice because the dad has not chosen to clone his own voice yet). The story is a comfort-routine: the bunny's afternoon walk home, with predictable rhythm and a gentle "and now it's time to go home, slowly".
- **The hospital pre-op.** A mum in an A&E waiting room with a seven-year-old waiting for stitches. She has the listening loop on; the loop detects the child still ragged at minute four, and the app gently extends to six minutes. The friend's landing line is the same: "When you're ready, your grown-up is right here. There's no hurry."
- **The long-haul flight.** A grandparent flying with a four-year-old grandchild has pre-loaded six stories per friend into the cache before take-off. Mid-flight, the child needs the friend without WiFi. The cached audio plays from the device, complete with the still companion image, exactly as designed.
- **The school car-park transition.** A parent whose five-year-old struggles with the 3 pm pick-up uses the scheduled wind-down: at 2:55 pm the chosen friend pre-loads on the parent's phone with the headphones already paired, so when the child gets into the car at 3:10 the story is one tap away.
- **The bedtime co-regulation.** A foster carer of a seven-year-old with a trauma background uses the "naming-the-feelings" style at night. The story names: "It is okay to feel big in your body right now. It is okay to want to be small. You are here, in your room. Your duvet is here. The carer who said goodnight is right behind the door."
- **The grandmother on FaceTime.** A grandmother in Ibadan uses the app on her own phone to record a friend ("Iya Tortoise") for her grandchild in Manchester. She enrols her own voice on her own phone, the household invitation links her phone to the household, and the grandchild now hears Iya Tortoise in his grandmother's actual voice when his mum opens the app in Manchester. (Voice clones are per-account; the grandmother's clone never leaves her account and is used to render audio server-side, scoped to the household's authenticated session.)
- **The SEN classroom.** A primary-school SEN teaching assistant uses the app in the school's calm-down corner with two specific children whose parents have consented to install the app on the school's iPad. Each child has their own friend; the TA long-presses the home tile to switch between them.
## 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 soft photographed-looking image of a parent's hand holding a phone in a queue, the phone screen dark, the parent crouched at child height. One paragraph: "Calm-Down Story is a one-tap pocket regulator for the moments your kid hits the wall in public. One tap → the friend you built together starts a four-minute calming story, in your voice." Single Google sign-in; Apple sign-in next to it. Below: "Try with a sample friend" → loads the demo friend in section 8a.
2. **Household setup.** Add the children in this household. Each child: name (private, server-side only), age, first language, any banned themes (a parent-marked list — "no lost-and-found stories", "no loud-noise stories", "no doctors"). The list of banned themes is a free-text input with chip-style additions; the parent's list passes literally into every story composer call for that child.
3. **Friend builder** (calm time, never mid-meltdown). A four-step gentle flow:
- **Step 1 — name and shape.** "What does your child call this friend?" → display_name. "What kind of friend is this?" → character_type radio. The consent screen runs here for the name: if the name fails the celebrity / copyrighted-character check, a friendly refusal with one or two suggested alternatives.
- **Step 2 — voice.** "Whose voice should this friend speak with?" Two options: "Use my voice (recommended)" → opens the voice-clone enrolment, or "Pick from the voice library" → opens the locale-aware stock voice list. **The voice-clone enrolment is a separate consent gate with explicit copy** (see 6c).
- **Step 3 — setting and image.** "Where does this friend live?" → setting_description short input with three suggested seeds based on character_type (for an elephant: "a lavender field at dusk", "a quiet riverbank", "a slow kitchen with a kettle steaming"). The Nano Banana 2 ambient image generates once and is shown for the parent's approval; the parent can regenerate once if they're not happy.
- **Step 4 — landing line.** The parent reviews and edits the exact sentence the friend will say at the end of every story. Default in en-GB: "When you're ready, your grown-up is right here. The shopping can wait. There's no hurry." The parent can edit it word-by-word. This is the only piece of text in the whole flow that the parent must explicitly approve.
4. **Home — one tap.** The whole screen is the orange "Calm now" tile, full-bleed. Friend's name in soft serif at the top. A small "(i)" icon top-right opens settings (see 6c). A small avatar of the chosen friend's companion image in the corner. If multiple children are in the household, the avatar reflects the active child; long-press to switch.
5. **Session screen — during a story.** The companion image fills most of the screen at low brightness. The friend's name in small soft type at the top. A single dim "Land softly" control at the bottom — never "Skip", never "Stop". The headphone / speaker indicator is shown. No progress bar. No time remaining. No "thinking…" indicator. The screen is intentionally information-sparse: a meltdown is the wrong moment for chrome.
6. **After-story screen.** The companion image at full warmth, the parent's after-note (one sentence, computed server-side from the session metadata) below it, and three calm controls: "Replay this story", "Start a new one", "Leave the app". No metric, no rating, no score. The parent's after-note for the Sainsbury's aisle session might be: "Story length: 4 min 12 sec. Your child's breathing eased after about ninety seconds. You did the right thing to come here."
7. **Friends archive.** A list of the household's friends — name, character type, voice source ("Mum's voice" / "Bunny stock voice"), language. Tap a friend to edit setting, regenerate the ambient image, or revoke the voice clone.
8. **Story cache.** The last six stories per friend, replayable offline. Each cached story shows its title (parent-facing only) and the date it was first played. Tap to replay; long-press to print as a text comfort booklet.
9. **Calm-time scheduler.** Optional. The parent can set recurring soft-load times ("Mondays through Fridays at 2:55 pm — load Mr. Big") so the tap is even faster at the known hard windows. No notification fires; only a quiet pre-load.
10. **Voice settings.** The parent's enrolled voice details. "Your voice was enrolled on [date]. It is used only by the friends you have built and is never shared." A prominent "Delete my voice clone" button — one tap, with a clear final-confirmation copy ("This will delete your voice across every friend. They will fall back to stock voices. You can re-enrol any time. Continue?"), and once confirmed the clone is gone within sixty seconds.
11. **Listening loop settings.** "Should the app listen during a story?" The default is OFF. Explanation in plain language: "If you turn this on, the app listens for your child's breathing during a story so it can decide whether to extend or land softly. The audio is processed every 4 seconds in memory only. Nothing is recorded. Nothing is uploaded. We never use any of it for training." Toggle on/off, plus a "Test the loop" tile that plays back a synthetic example so the parent can see what the signal looks like.
12. **Household sharing.** Add another parent or carer to this household. Magic-link email. Each member signs in with their own account and can build their own friends with their own voice clones. Voice clones are NEVER shared across accounts — each adult enrols on their own device.
13. **Weekly parent digest.** Optional, opt-in. One quiet email or in-app card on a chosen evening, with the digest paragraph (no metrics, no graphs, no scores) generated by the long-context call described in 4b.
14. **Footer.** "Made for the aisles where you can't carry your kid out of." Privacy: "Your voice is yours. The audio we listen for is never recorded. We do not use your sessions for model training." 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 Calm-Down Story."
- Subhead: "One tap, the right voice, and a story paced to bring breath down."
- One short paragraph (≤ 60 words): "Build a friend during a calm afternoon. The friend speaks in your voice, or a warm voice in your family's language. When you next find yourself in a fluorescent-lit aisle with a kid who can't cope, one tap starts a four-minute story paced to slow breathing — not to entertain. The story ends kindly. You're not alone in the queue."
- Visual: a calm illustration of a phone in a queue, no faces in frame.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample friend."
- A live demo: a pre-loaded friend (Mr. Big the elephant, a warm en-GB stock voice, a lavender-field ambient image) plays a 90-second sample story in soft-adventure style so the visitor can hear the pacing and see the session screen.
- 1-2 sentences pointing at the magic: "The pacing slows as the story goes on. The screen stays the same calm image — no animation. The story ends with a specific sentence: 'When you're ready, your grown-up is right here.'"
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Build your child's friend during a calm afternoon — name, voice, setting, landing line."
- "Adjust the story styles and pacing in `/server/prompts/story-composer.ts` to fit your family's first language."
- "Wire up your Gemini API key, your Firebase project, and your private voice-clone bucket 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`. The sample-story audio has a transcript scroller for d/Deaf or hard-of-hearing parents auditioning the app.
**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 Preview TTS** — narrates each story in the friend's voice (your clone or a stock voice in your language) at regulation pace. SSML pause tags and a separate style API field are not supported on Gemini 2.5 TTS; pauses are encoded as `…` and blank-line `—` in the input, and the style directive is one sentence prepended to the text.
- **Voice cloning, parent-only** — the parent's voice is enrolled during a short guided flow on the parent's own device, while signed into their own account. The clone is used only by the friends in that parent's household, scoped server-side. We never clone any voice other than the verified enrolling parent's own — not celebrities, not film or TV characters, not deceased people, not your child, not anyone else.
- **Gemini 3.5 Flash (text generation, structured output)** — composes the story keyed to the friend, age, language, and chosen style. Output is the `RegulationStory` JSON object so the pacing is enforced segment-by-segment, not left to the model's whim.
- **Nano Banana 2 (`gemini-3.1-flash-image`)** — generates the single ambient companion image during friend setup. Not animated. Not regenerated per story. Calm palette, single subject, generous negative space.
- **Gemini 3.5 Flash (audio reasoning, multimodal)** — processes the listening loop if enabled, in 4-second windows in memory only. Returns a `SettlingSignal` per window. **No audio is recorded. No audio is uploaded for storage. No audio is used for training.**
- **Gemini 3.5 Flash (long-context, optional)** — for the weekly parent digest. Reads only the metadata of the past week's sessions and the parent's own typed notes — never the child's audio, never the listening-loop windows.
- **Firebase Auth** — Google and Apple sign-in. Household invitations via magic-link email; the sender domain must be authorised in Firebase Auth before magic-link email works (this is a one-time DNS configuration the deployer does in the Firebase console).
- **Firestore** — stores your households, children, friends, story cache metadata, and session metadata. Syncs across the devices in your household.
- **Firebase Storage** — keeps the ambient companion images and the cached audio files. **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 friend setup.
- **A separate private bucket for voice-clone artefacts** — your enrolment audio and trained voice model live in a dedicated bucket scoped to your account only. Deletion is one-tap and propagates within sixty seconds.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b (Firebase Storage + a separate voice-clone bucket)
- Email: see section 4b (transactional only — household invitations + optional weekly digest)
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b (Gemini only)
**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)
- `VOICE_CLONE_BUCKET` — the dedicated bucket for parent voice-clone artefacts (created during deployment)
- `STORY_CACHE_BUCKET` — the bucket for ambient images and cached story audio
**Cost + privacy notes:**
- TTS is the hot path. Per session ≈ 480 words ≈ 2,400 characters. At ~$10/M output tokens, a 4-minute story costs ~$0.007. Most stories are played from cache after the first time.
- Voice-clone enrolment is a one-time per parent cost, billed under the Gemini API's voice-clone pricing.
- The listening loop, if enabled, calls Gemini 3.5 Flash once per 4-second window — ~60 calls per 4-minute story. Audio reasoning on a 4-second window is small input, structured small output; expect a fraction of a cent per session.
- Story composition is ~800 input tokens, ~800 output tokens at Gemini 3.5 Flash pricing — under a cent per session.
- A typical household running one session a day costs under $5 per month including cloud storage and the optional weekly digest.
- **Privacy.** Your voice clone is yours. The listening-loop audio is never recorded. We never use your sessions, your voice, or your listening windows for training (we use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms). One tap in Settings → "Delete my voice clone forever" deletes the clone within sixty seconds.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, multilingual, audio-reasoning, image-generation, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short page on the consent model and the per-parent voice-clone scope
**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)
- **Story composition (Gemini 3.5 Flash, low thinking)** — ~800 input tokens (system instruction + friend + style + banned themes) + ~800 output tokens. ~$0.005/story. Most stories are cached and replayed for free.
- **TTS narration (Gemini 2.5 Flash Preview TTS)** — billed per output token (~$10/M), effectively ~$0.000003/character. A 4-minute story ≈ 2,400 characters ≈ $0.007 per first narration. Cached per story; charged once.
- **Ambient companion image (Nano Banana 2)** — ~$0.03/image, generated once per friend. A four-friend household ≈ $0.12 total, ever.
- **Audio listening loop (Gemini 3.5 Flash, low thinking)** — 4-second windows, ~150 input tokens (audio reasoning bills audio more efficiently than equivalent character counts of text; estimate ~5-10 tokens per audio-second under current pricing) + small structured output. With listening enabled, a 4-minute story = 60 windows ≈ $0.006 per session.
- **Weekly parent digest (Gemini 3.5 Flash, medium thinking, long-context over a week of metadata)** — ~$0.01 per weekly digest per household.
- **Voice-clone enrolment** — billed under Gemini API voice-clone pricing as a one-time cost per parent. (Document the current price at deploy time; this template assumes ~$0.20 per enrolment as an order of magnitude.)
- **Expected per-session cost on first play:** ~$0.012 (composition + TTS + listening loop if enabled). **Cached replay:** ~$0.000 (just bandwidth from your Firebase Storage). **Per-household per-month total:** under $5 even at three sessions a day.
- **Image storage:** Firebase Storage standard tier. A few ambient images + a few minutes of cached audio per friend is ~50 MB; ~$0.001/month per household.
## 7. Design language
- **Mood:** A pocket co-regulator that the parent reaches for under fluorescent lights. Not a children's-app aesthetic. Not a wellness-app aesthetic. The crouched parent's-eye view: warm, dim, kind, and immediately usable with shaking hands. The opposite of stimulating.
- **Typography:** Display serif for the friend's name and the landing line (Source Serif Pro or a similar soft serif with generous x-height). Clean grotesque for chrome (Inter or Geist). The body of the parent's after-note is set in the same serif as the friend's name — to read it as a kind sentence rather than a system message.
- **Palette:** Warm bone-paper background `#F4EFE6` in the calm time UI; deep ink `#1B1714` for body text; a single warm-orange accent `#D9722B` reserved exclusively for the "Calm now" tile so the eye finds it without thought. Soft sage `#9FB28F` for the headphone indicator. No bright primary colours anywhere. No neon. No fluorescent magenta. The session-screen background is dimmed to ~30% brightness regardless of system brightness so the parent's eye is not stung in a dark car park.
- **Imagery:** The Nano Banana 2 ambient image is the hero. It is generated with a calm-palette prompt and a banned-element list (see 4b system instruction). The companion image sits in the lower third of the session screen with the friend's name in soft serif above it, generous negative space around. No icons in the session UI. No emoji. No badges. The home tile shows a small avatar version of the active child's friend in the corner — that's the only chrome.
- **Hand-feel touches:** The "Calm now" tile lifts almost imperceptibly when tapped — a 100 ms warm shadow expansion, no bounce. The session screen's brightness breathes at the story's pacing rate (cycling between 28% and 32% across each segment) so the screen itself is a quiet co-regulator. The transition into the session is a slow fade, not a slide, because slides are perceived as motion in peripheral vision.
- **Spacing:** consistent 4-px base. Very generous whitespace — the home screen is mostly empty.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). The "Calm now" tile uses 20 because it's the only tile; the companion image card uses 12; friend list items use 6.
- **Shadows:** subtle, warm. Avoid heavy drop-shadows.
- **Motion:** purposeful, slow, and respectful of `prefers-reduced-motion`. The brightness-breathing on the session screen is the canonical reduced-motion check — under reduced-motion the brightness sits at a constant 30% instead of cycling.
- **States:** every interactive element has hover (web), focus (keyboard), active (tap), and disabled. Loading uses a single static glyph in the friend's colour — never a spinner, never a skeleton, never any rotating element during a session.
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, settings, story openings, landing lines that fit the domain (use the seed content in section 8a as a starting point). Stories must be your own composition — never quote real children's-book text, real character dialogue from copyrighted media, or real song lyrics. Common first names, place names, and short generic phrases are fine; full sentences must be your own.
- Tone: warm, direct, free of corporate language. This template is for a parent, not a brand.
- Headlines: short 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 — including therapy-speak ("co-regulation", "dysregulation") in user-facing copy; use plain words ("calm together", "having a hard moment").
- Where the app outputs AI-generated content (the story, the parent's after-note), never label it as "AI says" — the friend speaks; the parent's after-note speaks. The intelligence is in the experience.
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below.
**Sample households (sidebar):**
- "Adebayo household" — Tottenham, London. Children: Tomi (6, en-GB + Yoruba). Friend: Tortoise (Iya Tortoise), parent-clone voice in Yoruba, soft-adventure style, banned theme "loud noises". Companion image: a slow tortoise on a wide quiet riverbank at dusk.
- "Pham household" — Westminster, CA. Children: Mai (6, vi-VN + en-US). Friend: the Train Driver, parent-clone voice in Vietnamese, comfort-routine style, banned theme "lost-and-found". Companion image: an old wooden train at a small countryside station, golden hour.
- "Coleman household" — Manchester, UK. Children: Theo (4, en-GB). Friend: Bunny Friend, stock en-GB voice (dad has not enrolled), soft-adventure style, no banned themes. Companion image: a brown bunny sitting beside a hedgerow, late afternoon.
- "Bayrak household" — Berlin. Children: Eren (5, tr-TR + de-DE). Friend: Dede Owl, parent-clone voice in Turkish (the mum's voice), naming-the-feelings style, banned theme "doctors". Companion image: a small spotted owl on a quiet windowsill at dusk.
- "Almeida household" — São Paulo. Children: Sofia (7, pt-BR). Friend: Saci-Friend (public-domain folk shape), parent-clone voice in Brazilian Portuguese, soft-adventure style, banned theme "fire" (sensible given the folktale). Companion image: a small one-legged figure sitting peacefully on a tree stump at evening.
**Sample story in detail (this is what the demo should compose for Tomi, the Tottenham six-year-old):**
- **Title (parent-facing):** "Tortoise on the Riverbank"
- **Friend:** Iya Tortoise · parent-clone voice · yo-NG locale (the household's first language)
- **Style:** soft-adventure
- **Banned themes:** loud noises, doctors
- **Target duration:** 240 seconds
- **Segments (excerpt — first three):**
- Segment 0, target_pace 70 wpm, pause_after 1.5 s: "Hello, my small one. It's me. I am here with you. Today we are going to walk slowly together, you and me, to the wide quiet river."
- Segment 1, target_pace 68 wpm, pause_after 2.0 s: "The river is the kind that moves slowly. Your feet, my feet… we don't need to hurry. The sky over the river is wide and the colour of warm honey. Nobody is waiting for us. Nobody is calling us."
- Segment 2, target_pace 62 wpm, pause_after 3.0 s: "Tortoise walks with you, and Tortoise knows about slow. Tortoise has never been late for anything that matters. The grass is soft under your feet. The river is making the quietest sound."
- **Segments (last):** target_pace 50 wpm, pause_after 5.0 s, then the **landing line**: "When you're ready, your grown-up is right here. The shopping can wait. There's no hurry."
- **Language:** yo-NG (the friend's voice_locale; matches the child's first language tag in the household)
- **Banned themes respected:** ["loud noises", "doctors"]
**Sample parent's after-note (this is what the parent sees after the Tortoise session):**
- "Story length: 4 min 12 sec. Your child's breathing eased after about ninety seconds. You did the right thing to come here."
(Note that the after-note contains no numbers other than the duration and the time-to-settle, which are factual; it contains no score, no comparison, no judgement.)
**Sample voice copy:**
- Home tile: "Calm now"
- Friend builder Step 1 refusal (Bluey): "Bluey is a copyrighted character from a TV show, so this app can't clone a Bluey voice. Two friendly alternatives that often work as well: 'Bluey Friend' in your own voice, or 'a blue dog called Sky'. Which would you like?"
- Friend builder Step 2 voice-clone consent: "We'll record you saying four short sentences. The recording trains a voice that only your friends can use. We never share it across families. We never use it to train other models. Delete it any time in one tap."
- Friend builder Step 4 landing-line default (en-GB): "When you're ready, your grown-up is right here. The shopping can wait. There's no hurry."
- Friend builder Step 4 landing-line default (vi-VN): "Khi nào con sẵn sàng, người lớn của con đang ở ngay đây. Không có gì phải vội."
- Session screen Land softly control: "Land softly"
- After-story screen lead: "You came back."
- Listening loop settings explainer: "If you turn this on, the app listens for your child's breathing during a story so it can decide whether to extend or land softly. The audio is processed every 4 seconds in memory only. Nothing is recorded. Nothing is uploaded. We never use any of it for training."
- Empty friends archive: "No friends yet. Build the first one during a calm afternoon — never during a meltdown."
- Error (no headphones, very loud environment, audio fail): "The room is too loud for the speaker to be heard. Here's the story as large soft text. You can read it slowly to your child."
**Sample household invitation email subject + body:**
- Subject: "I'm setting up Calm-Down Story for our kids — want to join?"
- Body: "Hi — I've built our household in Calm-Down Story. The app is a one-tap pocket regulator for the moments the kids hit the wall in public. Each adult enrols their own voice on their own device (you'll do it in a calm afternoon, not now). Tap to join." [Open Household]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a parent's hand holding a phone at adult-eye level above a child crouched at their feet in a public space, the parent's other hand resting gently on the child's back, no faces in frame, soft natural light. Generate via Nano Banana 2 with a prompt emphasising "indoor public space at adult-eye level, soft natural light, parent's hand and phone in upper third, child's small body in lower third, no faces visible, calm warm palette, no crowd, no brand visible".
- **App icon / wordmark:** Set in the display serif. A single warm-orange dot beside the wordmark — the same orange as the "Calm now" tile. No icon character.
- **Empty-state illustration (friends archive):** A simple line drawing of an unopened envelope on a windowsill at dusk. Hand-drawn aesthetic.
- **Ambient companion images (per friend):** Generated per the prompts in section 4b — Nano Banana 2 prompts that specifically request the calm palette, single subject, soft edges, no motion lines, no text, no devices, calm posture. Each friend's image is generated once during friend setup and re-used across every story.
- **Session-screen image policy:** No new image is generated per session. The friend's image is the friend's image. This is intentional — predictability is medicine.
- **Stock fallbacks:** If image generation fails during friend setup, fall back to one of the pre-shipped calm images from `/public/samples/` matched by character_type. 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. Avoid faces of children entirely; if a child appears in any imagery in the app chrome, they are seen from behind or at low angle without face visible.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI chrome. Use very sparingly. The session screen has zero icons.
### 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 friend-builder name-check error is a friendly sentence, not a red border.
- Loading states use a single static glyph in the friend's colour — never a spinner, never a skeleton, never a rotating element. The session screen specifically has no loading indicator visible to the child.
- Empty states explain the next action in this app's domain: "Build a friend during a calm afternoon", "Enrol your voice during a calm afternoon", "No cached stories yet — the next four-minute story will become a cached story" — never a generic "Add your first item".
- The session screen has exactly one interactive control: "Land softly". Long-press shows nothing — the screen is deliberately information-sparse.
- The TTS audio streams as it composes — the first phrase begins within ~1 second of the parent's tap.
- If a Gemini API call fails during a session, the app falls back to the most recently cached story for the same friend and the same style. If no cached story exists yet, the app falls back to the text-only large-soft-typography reading mode so the parent can read aloud.
- The voice-clone enrolment fails gracefully if the parent's environment is too noisy — "Your room is a bit noisy. Want to try again in a quieter spot?" — and never charges the parent for a failed enrolment.
- The brightness-breathing on the session screen runs at the story's pacing rate; under `prefers-reduced-motion` the brightness sits at a constant 30%.
- The listening-loop indicator (a small dot near the headphone icon) pulses very slowly when listening is active so the parent can see at a glance whether the loop is on. Tapping the dot opens the listening-loop settings.
## 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 keys live in Secrets Manager, never in client bundle. Audio buffers from the listening loop are uploaded via signed-URL one-shot POST to the Cloud Run function, processed in memory by the function, and discarded — the function's container has no persistent disk write enabled for the audio path.
- **Model selection:** explicitly pin `gemini-3.5-flash` for story composition and the optional weekly digest; `gemini-3.5-flash` for the listening-loop signal and the friend-name consent check; `gemini-3.1-flash-image` (Nano Banana 2) for the ambient companion image; `gemini-3.1-flash-tts-preview` for narration. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed household and friends on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it (requires Apple Developer account + Service ID); magic-link email for household invitations only.
- **Storage:** Firebase Storage for ambient images and cached audio. A separate, dedicated private bucket for the parent's voice-clone artefacts.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 390 px (iPhone 12+), 768 px (iPad), 1024 px, 1440 px+. The session screen must work in landscape (parent holding the phone sideways with the child) at 844 px width.
- 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 household and friends views; do not use real-time listeners on the session screen (no need for cross-device sync during an active session).
- Optimistic UI on writes; reconcile on response.
- Audio playback uses the Web Audio API with explicit gain capping at -6 dBFS. Headphones detection via the Media Session API.
- **iOS Safari gotchas (graceful degradation):** the listening loop microphone capture must handle iOS audio-session interruption (incoming call, Siri, alarms) — listen for `MediaStreamTrack.onmute` / `onunmute` and pause/resume the loop; background tabs throttle or kill `getUserMedia` — capture `visibilitychange` and either pause the session or use a Wake Lock to keep it alive when the parent locks the screen with the child still calming; iOS does NOT persist mic permission across reloads — re-request on session start; `MediaRecorder` on Safari only supports `audio/mp4` (AAC), so PCM streaming for the listening loop must go via `AudioWorklet` not `MediaRecorder`.
- All Gemini calls are server-side. Client never holds an API key.
- The listening loop sends 4-second 16-bit PCM windows as `inlineData` base64 to the Cloud Run function; the function passes them to Gemini, returns the structured response, and lets the buffer go out of scope. No `await fs.writeFile` anywhere on the audio path; verified by a linter rule.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring. The "Calm now" tile is reachable via Tab from anywhere on the home screen.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The dimmed session-screen palette is verified against contrast minimums.
- All images have meaningful `alt` text. The ambient companion image has alt text describing the calm scene ("a slow tortoise on a wide quiet riverbank at dusk"). The hero landing image has alt text describing the moment without identifying any person ("a parent's hand holding a phone above a child crouched at their feet in a public space").
- Form fields have associated `