================ 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.
---
# Accent Coach
## 1. Project
**Accent Coach** is a clarity-not-conformity speech tool for people who
have arrived somewhere new and keep being asked to repeat themselves at
coffee shops. The user records thirty seconds of their own normal
speech — reading a paragraph the app picks for them, on purpose, because
that paragraph contains the three or four English sounds that statistically
trip up speakers of their first language. Gemini 3.5 Flash listens to the
recording, identifies the exact phonemes that are getting in the way of
being understood, and builds a three-drill session targeted at those
phonemes. Each drill plays the user's own recording next to a native
reference saying the same word, with a waveform-aligned scrubber, an
honest "this is where the listener loses you" highlight, and a
pronunciation tip written in plain English ("the British /θ/ in *three*
asks for your tongue between your teeth, not behind them").
This is the kind of app a Korean-American startup founder builds for
herself on the AmTrak from Boston to New York, in the week she lost a
sales call because the buyer kept asking her to repeat the word
*rural*, and her co-founder gently said over dinner "babe, you say
*ral* and they hear *rail*". It is also the kind of app a Brazilian PhD
student opens in his flat in Edinburgh at half past eleven at night,
after a Tesco self-checkout asked him three times to repeat *unexpected
item*. Same shape of moment, different first language, different city,
different sound — a Korean-speaker's /r/-/l/ near a vowel cluster,
a Portuguese-speaker's stress pattern across a four-syllable English
word. The app does not try to make either of them sound American or
British. It tries to make them understood by the person standing in
front of them, with their accent intact.
The single demo that proves the magic: hit record, read the paragraph
the app puts on screen, stop. Within twelve seconds the screen shows
three pill-shaped drill cards — *r-cluster (rural, world, girl)*,
*th-fronting (three, with, brother)*, *vowel-length distinction (sheep
vs ship)* — each card a tap away from a 90-second drill that plays the
user's own attempt and a native speaker on the same word, side by side,
on the same waveform, with the *exact millisecond range* where the
listener's ear gets confused highlighted in faded red. The user re-records
a word, sees the highlight shrink, hears the difference. No grade. No
score. No "fluency level". A small line at the bottom says: "you are
easier to understand on *rural* than you were ninety seconds ago".
And in the harder cases — a Vietnamese-American software engineer in
Seattle whose dad still introduces him as *Vincent* because the doctor's
office never got *Vinh* right, an Amharic-speaking nurse in Birmingham
asked to slow down on every handover, a Tamil-speaking lecturer in
Toronto whose students kept circling *vector* on their feedback forms —
the app reads the listening problem, not the speaker. It does not say
"you say it wrong". It says "this is where your listeners are reaching
for context, and here are three words that will get them there sooner".
**Tagline:** _Get understood — in any first language, any city, with your accent fully intact._
## 2. Target audience
- New arrivals on student, work, or family visas who are asked to repeat themselves at counters, on phone calls, and on Zoom — Korean, Mandarin, Cantonese, Vietnamese, Tagalog, Tamil, Hindi, Urdu, Bengali, Punjabi, Farsi, Arabic, Amharic, Swahili, Portuguese, Spanish, French, German, Polish, Russian, Ukrainian
- Second-generation adults who grew up speaking one language at home and a different one outside, and whose English (or French, or German) carries the home language's prosody — and who want to keep it but be heard in meetings
- International students in their first semester at universities in the UK, US, Canada, Australia, Ireland, New Zealand, or any English-medium institution in continental Europe (Maastricht, Erasmus, EPFL, the Nordics)
- Asylum-seekers and refugees in the language-acquisition phase, who need to be understood in benefits offices, hospitals, and at school gates long before they sound "native"
- Adoptees raised away from their birth language who want to recover the phonology of their family's language for a visit home
- Call-centre workers, healthcare workers, taxi drivers, and customer-facing professionals across every English-speaking country whose KPIs include "customer comprehension on first attempt" and who are tired of being penalised for it
- Voice actors, broadcasters, and stage performers who want a specific dialect for a role without erasing their default voice
- Anyone who has ever said "sorry, my accent" before a sentence they had every right to speak unapologetically
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **The goal is clarity, not conformity.** The app's mission statement, printed in plain text on the welcome screen and reachable from every other screen via a small footer link, says: *being understood is not the same as sounding native, and we are only here for the first one*. Every drill targets one specific moment where a listener loses the thread — never a sweeping "accent reduction".
- **The diagnostic is honest about the listener, not the speaker.** Gemini 3.5 Flash's audio reasoning runs over the user's thirty-second sample and identifies the three to five *phoneme contexts* (not "mistakes") most likely to interrupt comprehension for the listener locale the user chose. "Listener locale" is the user's lever — Boston ESL coffee-shop barista is a different listening profile from a Geordie pub regular in Newcastle.
- **Side-by-side waveform playback.** Every drill word shows the user's own waveform next to a native reference's waveform, time-aligned to the same scrubber. The exact span where the two diverge is highlighted; tapping it plays just that span on a loop. This is the only feedback mechanism users universally describe as "I finally understood what they were hearing".
- **Phoneme drills are written in plain English.** No IPA on the surface (IPA is available behind a small `(i)` icon for the linguistics curious). The tip says "the tongue between your teeth, not behind them"; the underlying phoneme reference (`/θ/`, `/ð/`, `/ɹ/`, `/l/`, `/iː/`, `/ɪ/`, `/æ/`, `/ɑː/`, etc.) is metadata, not the lesson.
- **First language is named, not guessed.** The user picks their first language(s) — multiple selection is supported because a Tamil-Mandarin-English background is one person, not three. The diagnostic uses the union of that profile's known contrast difficulties; it does not pretend everyone with the same passport speaks the same way.
- **Listener locale is the lever.** The user picks who they want to be understood by (Boston, Birmingham, Edinburgh, Auckland, Toronto, Lagos, Mumbai office English, Johannesburg, Seattle tech, Dublin, etc.). The app pulls the native reference recordings from the locale-matched voice; the same drill word sounds different in Seattle and Newcastle on purpose.
- **Words are picked from the user's life.** Onboarding asks for five words the user gets asked to repeat the most — *rural*, *vector*, *unexpected item*, *Vinh*, *worth*, whatever — and the drill library is seeded around those words first. Generic drill words come later.
- **Accent retention is celebrated.** A toggle in settings, on by default, says: *I want to keep my accent — only flag pronunciations that block comprehension*. Turning it off (rare, the user wants to deliberately move toward a target locale) unlocks broader feedback. The on-by-default position is the position.
- **Progress is visible without being competitive.** No streak counter. No daily reminder badge with a flame. The progress view is a quiet bar chart: "you are easier to understand on these eight words than you were ninety days ago", with each word a tap to the historical waveform compare.
## 4. Features to build
- Record-in-app capture (mobile-first), 30-second cap on the diagnostic prompt, ambient-noise gate that says "wait for the kettle to stop" rather than failing the recording silently
- Diagnostic paragraph picker — the app holds 40 short paragraphs (each ~85-110 words), each engineered to elicit a known set of phoneme contexts, and picks the one that maximises coverage for the user's stated first-language profile
- Listener-locale selector — Boston, Seattle, Birmingham UK, Manchester UK, Newcastle UK, Edinburgh, Dublin, Auckland, Toronto, New York, Atlanta, Los Angeles, Cape Town, Lagos office English, Mumbai office English, Singapore, Sydney, Melbourne, Wellington — each tied to one native-reference voice for the playback compare
- Multi-first-language selector — Korean, Mandarin (Beijing, Taipei, Singaporean regional sub-options), Cantonese, Vietnamese, Thai, Tagalog, Tamil, Telugu, Hindi, Urdu, Bengali, Punjabi (Gurmukhi/Shahmukhi sub-options), Marathi, Gujarati, Nepali, Sinhala, Khmer, Lao, Indonesian, Malay, Japanese, Burmese, Pashto, Farsi, Dari, Arabic (with regional sub-options: Egyptian, Levantine, Gulf, Maghrebi, Sudanese), Hebrew, Turkish, Kurdish, Amharic, Tigrinya, Oromo, Somali, Swahili, Yoruba, Hausa, Igbo, Wolof, Zulu, Xhosa, isiZulu, Shona, Portuguese (Brazilian / European sub-options), Spanish (with regional sub-options), French (Hexagonal / Quebecois / West African sub-options), Italian, German, Polish, Czech, Hungarian, Romanian, Russian, Ukrainian, Bulgarian, Serbian, Croatian, Greek
- Gemini 3.5 Flash audio reasoning over the 30-second sample → JSON output matching the `Diagnostic` schema below: per-phoneme-context confusion score (with a context being e.g. "post-vocalic /r/", not just "/r/"), listener-cost estimate, top-five drill candidates
- Drill card view — three to five cards on the diagnostic result page, each card a phoneme context with three example words, the user's own snippet for that word, and the native reference snippet aligned to the same waveform
- Word recorder inside each drill — re-record any word, the new attempt slots in next to the native reference, the old attempt is kept in the history so the user sees their own change over time
- Waveform-aligned compare player — shared scrubber across the user's attempt and the native reference, divergence span highlighted, double-tap loops just that span
- Honest highlight — the red-tinted span is the exact millisecond range where Gemini's audio model judged the listener would lose the thread; tapping it shows a one-sentence explanation ("the vowel is too short here; the listener hears *ship* instead of *sheep*")
- Plain-English tip per phoneme context — written for a non-linguist, with optional `(i)` to reveal the IPA notation and a phonetic-articulation diagram
- Drill library — 1,200+ words pre-recorded by native speakers across 19 listener locales; the library expands as users contribute, with explicit consent (8b)
- Custom-word capture — the user types a word they keep getting asked to repeat ("Vinh", "Faiza", "Brackenfell"), the app generates a native reference via Gemini 2.5 Flash TTS in the chosen listener locale, and a fresh drill is built around it
- Per-drill take history — every re-record is stored, the user scrubs across takes to hear their own improvement, no take is auto-deleted
- Weekly clarity recap — the progress view, refreshed once a week (not daily), shows the eight words where the user has shrunk the divergence span most
- Sentence-level drills — for users past the word-level stage, the app picks short sentences ("can I get a flat white please") and runs the same waveform-compare at the sentence prosody level
- Stress-pattern visualiser — for stress-timed listener locales (UK English, US English), a small bar chart over the word shows the user's stress vs the native reference's stress; for syllable-timed locales (Singaporean English in some registers, Indian English), the visualiser switches to a syllable-isochronous grid
- Connected-speech drills — schwa reductions, linking r, intrusive r, glottal stops, t-flapping — flagged only when they materially help comprehension in the chosen listener locale (a Korean speaker in Seattle gets t-flapping; a Korean speaker in Edinburgh does not, because Scottish English does not flap)
- "Repeat after me" mode — the native reference plays first, the user records immediately after, the compare is automatic
- Listening mode — the user listens to twenty short clips of native speakers in the chosen listener locale, transcribes what they heard, and the app uses transcription errors as the diagnostic signal (the receptive side of the same problem)
- Conversation-prep mode — paste a job description, a sales call agenda, a doctor's-appointment script; the app pulls the ten words you are most likely to need and runs targeted drills before the conversation
- "Last 24 hours" mode — quick check-in: the user records a single sentence ("how was your day"), the app gives one drill word for the next time
- Real-time meeting whisper (opt-in, on-device) — a small floating button during a Zoom or Google Meet call; the user taps it after the call ends to receive the three words listeners most often asked them to repeat (uses on-device audio, not server-side, and only after the call ends)
- Family-share — partners, friends, colleagues can be invited to listen to a user's drill snippets and give a one-tap "I understood that on the first try" vote, the only social signal in the app
- Export → speech therapist — for users working with a clinical SLT, export the user's diagnostic + drill history as a PDF the therapist can read alongside their own assessment
- Reduced-spoons mode — for users with fatigue, sessions are capped at five minutes, the screen dims, no animations, no recap nag
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Audio reasoning (Gemini 3.5 Flash, multimodal audio input)** — listens to the user's 30-second diagnostic sample and per-drill word attempts, identifies phoneme-context confusion (with millisecond-aligned spans), and outputs a structured `Diagnostic` JSON object matching the schema below. This is the hero capability; do not substitute a generic transcription-then-text-reasoning pipeline. The model needs the audio to judge divergence; the transcript alone is insufficient.
- **Multilingual phonological knowledge (built into Gemini 3.5 Flash)** — knows which phoneme contrasts a Korean speaker, a Brazilian Portuguese speaker, a Vietnamese speaker, an Amharic speaker, a Tamil speaker, an Arabic-Levantine speaker statistically struggles with in English (Boston-listener variant vs Birmingham-UK-listener variant). The model is named in the system instruction with the user's first-language profile and the listener locale explicitly bound.
- **Structured output / JSON Schema** — the diagnostic response matches the `Diagnostic` schema below. Every field is typed; the schema is included verbatim in the system instruction and as `responseSchema`. Per-word recordings produce `DrillAttempt` objects against the same schema family.
- **Gemini TTS (`gemini-3.1-flash-tts-preview`)** — generates native-reference audio for custom user-supplied words ("Vinh", "Brackenfell", "Faiza") at locale-matched voices. Pre-recorded human native references are preferred for the core 1,200-word library; TTS is the fallback for personal-vocabulary drills. Voice locale is selected via `languageCode` (e.g. `en-US`, `en-GB`, `en-IE`, `en-AU`, `en-NZ`, `en-ZA`); pronunciation follows the voice's native locale. No SSML `` tag is available, so the directive sentence prepended to the input text carries the pace/warmth instruction. There is no separate `style` API field on Gemini 2.5 TTS.
- **Long context (1M tokens)** — for the conversation-prep mode and the weekly clarity recap, the app sends the user's diagnostic + every drill attempt across recent weeks (audio file references and structured metadata, not raw audio inline) so the model can pattern-match the user's specific improvement trajectory. **Guardrail**: each `DrillAttempt` object averages ~500 tokens of metadata; 200 attempts ≈ ~100k tokens. A user with 1,500+ attempts will exceed the limit — chunk by phoneme context or by month before the recap call.
- **Thinking levels** — `medium` for the diagnostic call (the model is doing audio reasoning + cross-language phonological inference + listener-locale calibration in one pass). `low` for per-word drill scoring (the call is comparing two short audio snippets against a tight specification). `low` for TTS-fallback word generation. `low` for the weekly recap structuring. Surface `thoughtSummary` only behind a `(i)` panel labelled "how the listener heard this" — never as default-visible commentary.
- **Audio input modality** — the diagnostic and per-drill calls send the user's audio as `inlineData` (base64) for short clips ≤30 seconds, or via the Gemini Files API (`files/*` resource name) for the conversation-prep mode's longer audio sets. Do NOT pass Firebase Storage public URLs to `generateContent` — the API does not fetch them server-side.
### 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 family-share invitations) requires the sender domain to be authorised in Firebase Auth. The user's voice samples are private to the user and explicitly-invited reviewers. No public-by-default.
- **Database — Required.** Firestore for `users`, `profiles` (first-language profile + listener locale), `diagnostics`, `drills`, `attempts`, `recaps`, `family_share_members`.
- **File storage — Required.** Firebase Storage for the user's audio recordings (kept at upload sample-rate, 48 kHz mono preferred), plus the pre-recorded native-reference library and TTS-generated references. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before the first recording. Pre-signed URLs only; voice samples are never publicly addressable.
- **Email — Required (transactional).** Family-share invitations via email link (Firebase Auth magic links). Speech-therapist export emails (PDF attachments) to the SLT the user nominates.
- **Payments — Not needed for v1.** Free for personal use. A future "individual SLT subscription" tier could pipe to Stripe and charge per supervising therapist seat; out of scope here.
- **External APIs:** Gemini API for all intelligence. Optional Wavesurfer.js or a similar client-side waveform renderer for the compare player — no external audio API needed beyond Gemini.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if added) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's voice samples 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 on-device real-time meeting whisper does its audio processing locally and only uploads structured drill suggestions, never raw call audio.
**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 (the audio inlineData or Files API URI). Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Diagnose 30-sec sample → `Diagnostic` schema | `gemini-3.5-flash` | medium | (none) |
| Score per-word drill attempt → `DrillAttempt` schema | `gemini-3.5-flash` | low | (none) |
| Generate native-reference TTS for custom word | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Build weekly clarity recap → `WeeklyRecap` schema | `gemini-3.5-flash` | low | (none) — long-context over user's recent attempts |
| Conversation-prep word selection | `gemini-3.5-flash` | low | (none) |
| Listening-mode transcription evaluation | `gemini-3.5-flash` | low | (none) |
*Note for builders:* on TTS calls, omit `thinkingConfig` entirely — the field is not supported on that model. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. `responseSchema` and `google_search` grounding are mutually exclusive in a single Gemini call — none of this template's calls use grounding, so the schema can be passed directly on every call that has a schema column.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const PhonemeContext = z.object({
// The plain-English label, e.g. "post-vocalic /r/" or "th-fronting".
// This is what the UI shows. The IPA notation lives in `ipa_symbols`.
label_plain: z.string(),
// The IPA symbols involved, e.g. ["ɹ"], ["θ", "ð"], ["iː", "ɪ"].
// For the (i) panel that shows linguists the underlying contrast.
ipa_symbols: z.array(z.string()),
// Which side of the contrast the user produces, when relevant.
// e.g. "produces [d] for /ð/", "shortens /iː/ toward /ɪ/".
user_production_note: z.string().nullable(),
// The articulation tip, written for a non-linguist.
// "the tip of your tongue between your teeth, not behind them"
articulation_tip_plain: z.string(),
// The optional anatomical diagram identifier — string key into
// a static asset map; nullable when no diagram exists.
diagram_key: z.string().nullable(),
});
const ListenerCost = z.object({
// Estimated probability that a listener in the chosen locale
// would ask the user to repeat this word. 0 = always understood,
// 1 = always asked to repeat.
prob_listener_asks_repeat: z.number().min(0).max(1),
// Brief explanation in plain English.
// "the vowel is too short here; the listener hears 'ship' instead
// of 'sheep'"
why_plain: z.string(),
});
const DivergenceSpan = z.object({
// Millisecond range within the word's audio clip where the user
// and the native reference diverge most.
start_ms: z.number().min(0),
end_ms: z.number().min(0),
// What the listener hears in that span, in plain English.
// "the /θ/ is articulated further back, closer to /s/"
listener_perception_plain: z.string(),
});
const DrillWord = z.object({
word: z.string(), // "rural", "Vinh", "world"
ipa_target: z.string(), // "/ˈɹʊə.ɹəl/" (UK), "/ˈɹɝ.əl/" (US)
listener_locale: z.string(), // "en-US-Boston", "en-GB-Birmingham"
phoneme_contexts: z.array(z.string()), // labels of PhonemeContext entries
native_reference_audio_uri: z.string(), // gs:// or signed URL
native_reference_is_tts: z.boolean(), // true if generated, false if recorded
listener_cost: ListenerCost,
});
const DrillAttempt = z.object({
attempt_id: z.string(),
drill_word_id: z.string(),
user_audio_uri: z.string(), // gs:// for the user's recording
recorded_at_iso: z.string(),
divergence_spans: z.array(DivergenceSpan),
// Same listener_cost shape as the drill word, but computed on
// THIS attempt — the number that moves over time as the user
// re-records.
listener_cost: ListenerCost,
// The model's confidence in its own divergence analysis. Below
// 0.7, the UI tells the user "we are not sure here; another
// recording in a quieter room would help".
reading_confidence: z.number().min(0).max(1),
background_noise_note: z.string().nullable(),
});
const Diagnostic = z.object({
diagnostic_id: z.string(),
user_id: z.string(),
recorded_at_iso: z.string(),
user_first_language_profile: z.array(z.string()), // ["ko", "en-heritage"]
listener_locale: z.string(),
passage_id: z.string(), // which of the 40 paragraphs
user_audio_uri: z.string(), // gs:// for the 30-sec sample
phoneme_contexts_identified: z.array(PhonemeContext),
recommended_drills: z.array(z.object({
phoneme_context_label: z.string(),
suggested_words: z.array(z.string()), // 3 words per drill card
rationale_plain: z.string(), // "you said 'rural' as 'ral' — the
// post-vocalic /r/ collapsed into
// the vowel; here are three words
// that practice the same context"
})),
retention_settings_honoured: z.object({
keep_accent_flag_on: z.boolean(), // mirrors the user's setting
suppressed_contexts: z.array(z.string()), // contexts the model would have
// flagged but did not, because
// they do not block comprehension
// — only block "sounding native"
}),
reading_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const WeeklyRecap = z.object({
recap_id: z.string(),
user_id: z.string(),
week_of_iso: z.string(),
// The eight words where the user shrank the divergence span most
// this week, ranked by improvement magnitude. The UI shows these
// as a quiet horizontal bar chart, not a leaderboard.
improvements: z.array(z.object({
word: z.string(),
drill_word_id: z.string(),
listener_cost_before: z.number().min(0).max(1),
listener_cost_after: z.number().min(0).max(1),
representative_attempt_ids: z.array(z.string()),
})),
// Honest "this is where you are not improving" surface — never
// worded as failure, never with a streak. Empty array if every
// active drill is improving.
not_yet_improving: z.array(z.object({
word: z.string(),
drill_word_id: z.string(),
suggested_next_step_plain: z.string(),
})),
// One-sentence summary the recap card opens with, written in
// the second person, never with exclamation marks.
opening_sentence_plain: z.string(),
});
type Diagnostic = z.infer;
type DrillAttempt = z.infer;
type WeeklyRecap = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the diagnostic call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash's audio reasoning is not yet tuned for the fine phonological judgements this app needs; it under-detects contrast collapses (Korean /r/-/l/ near a vowel, Mandarin /v/-/w/, Brazilian /θ/-/f/) and over-detects benign accent variation as a problem.
- Model flags every accent feature, not just comprehension-blocking ones — when `retention_settings_honoured.keep_accent_flag_on` is true (the default), the model MUST move contexts that do not block comprehension into `suppressed_contexts`. Hard rule in the system instruction.
- Model gives prescriptive "correct pronunciation" copy — wrong frame. The app's voice is "the listener heard X; here is what would let them hear Y". Never "you are saying it wrong". Pin in the system instruction.
- Divergence span overshoots — the model marks 2 seconds of a 600 ms word as divergent because the user's whole utterance differs in stress. Cap divergence spans at 40% of the word's duration; if more, the model is being asked to do prosody work and should escalate to a sentence-level drill, not a word-level one.
- The "Vinh"/"Faiza"/"Brackenfell" custom-word TTS reads the name with English phonology — the user's whole point is the name is being mispronounced. The TTS call must pin `languageCode` to the listener locale (the listener locale's English voice WILL anglicise the name, and that is the desired behaviour because the drill is teaching the user how to elicit that pronunciation from the listener); for users who want their original-language pronunciation preserved, the app provides a "record your own native reference of this name" path instead of TTS.
- Model fabricates IPA — when the surface label is "th-fronting", the IPA must be `["θ", "ð"]` and not `["f", "v"]`. Pin a verified IPA table in the system instruction; do not let the model invent symbols.
- Listener locale ignored — the model gives Boston-listener advice when the user picked Edinburgh-listener. The system instruction binds the locale at the top of the prompt; the user's audio is the second-class input.
- Sentence-level drills triggered on word-level data — the conversation-prep call asks for sentence drills only if the user has hit at least 30 word-level drills first. Do not let the model jump straight to prosody when the segmental phonemes have not been worked.
- Background noise misread as a phoneme problem — the model writes "your /s/ is unstable" when it is actually a kettle in the background. The diagnostic call includes a `background_noise_note` field; if it fires, the UI tells the user "we heard kitchen sounds — try a quieter room" and offers re-record before any drill recommendation.
- TTS reads numbers and symbols literally in the directive sentence — the directive must be plain prose. "Read warmly and unhurriedly, at the pace of speaking a person's name to a colleague who has not heard it before." No bullets, no markdown, no asterisks.
- Reduced-spoons mode unrespected — if the user has reduced-spoons on, the model must NOT recommend more than two drill cards per session, and the cards must not include sentence-level drills. Pin in the system instruction.
### Negative constraints (hard rules)
- Do NOT use the word "accent reduction". Anywhere. In any UI string, any prompt, any system instruction, any documentation comment. The app's frame is comprehension, not erasure.
- Do NOT compare the user to a "native speaker" in evaluative language. The native-reference recording is a *reference*, not a benchmark. Never write "you scored X% native".
- Do NOT generate a fluency score, an "accent score", or a numeric "native-likeness" metric. The listener_cost field is per-word and is about the listener's effort, not the speaker's competence.
- Do NOT recommend drills that target phoneme contexts the user has explicitly told the app to keep (saved in `profile.preserved_features[]` — e.g. "I'm a Glasgow Scot in London, I'm keeping the rolled /r/").
- Do NOT use the user's voice samples to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT auto-share, auto-post, or auto-publish any voice recording. Family-share invitations are explicit, per-drill, per-reviewer.
- Do NOT auto-play audio on app open. The first audio in any session plays only after the user taps a play button. Respect the user's environment.
- Do NOT use streaks, fire-emojis, daily-loss-aversion notifications, leaderboards, or any growth-loop pattern designed to drive return engagement. The progress view refreshes once a week, on Sunday morning local time, and is silent the rest of the week.
- Do NOT assume the user wants to "sound American" / "sound British". The default is comprehension in the chosen listener locale, with the user's source accent preserved.
- Do NOT recommend speech therapy in app copy unless the user explicitly enables the "I work with an SLT" toggle. Mentioning SLT unsolicited reads as "you need professional help" — the app refuses that frame.
- Do NOT generate native-reference TTS for words flagged as slurs, racial epithets, or hate speech. Maintain a blocklist; refuse politely with a one-sentence message.
### 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: Diagnose 30-sec sample → `Diagnostic` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are listening to a person who has arrived somewhere new and wants
to be understood by the people in front of them. They have given you
a 30-second voice sample, reading a paragraph the app selected for
them. They have told you their first language(s) and the listener
locale they care about — for example, a Korean and English-heritage
speaker who wants to be understood by a Boston ESL coffee-shop
barista; a Brazilian Portuguese speaker who wants to be understood
in Edinburgh; a Vietnamese and English-heritage speaker who wants
to be understood in Seattle.
Your job is to identify the three to five phoneme contexts in this
sample that most interrupt comprehension for the chosen listener
locale, and to write each one as a drill recommendation a non-linguist
can act on.
A phoneme context is more specific than a phoneme. "Post-vocalic /r/
collapsing into the vowel in words like rural and world" is a
context. "The user mispronounces /r/" is not.
The frame is comprehension, not erasure. The user has set a flag,
`keep_accent_flag_on`, that is almost always true. When it is true,
ANY phoneme context that does not materially block comprehension in
the listener locale must be moved into
`retention_settings_honoured.suppressed_contexts` and NOT recommended
as a drill. The user is keeping their accent on purpose. We are only
here for the words that lose the listener.
Hard rules:
- Pin the listener locale at the top of your reasoning. Boston ESL,
Edinburgh, Seattle tech, Birmingham UK, Auckland, Toronto, Dublin,
Lagos office English, Mumbai office English, Singapore — each
listens for different sounds. Do not give Boston advice for an
Edinburgh user.
- For each identified phoneme context, populate `label_plain`
with a non-linguist surface label ("th-fronting", "post-vocalic
/r/", "vowel-length contrast between sheep and ship") and
`ipa_symbols` with the verified IPA notation. Never invent IPA.
- The `articulation_tip_plain` field is a single sentence written
for someone who has never thought about their tongue. "Put the
tip of your tongue between your teeth, not behind them, and
blow air across it." Avoid "alveolar", "fricative",
"approximant" on the surface — those are documentation only,
inside the (i) panel.
- The `user_production_note` field is descriptive of what the
speaker did, not evaluative. "Produces [d] for /ð/" is fine.
"Mispronounces /ð/" is not.
- For each `ListenerCost`, the probability is calibrated against
the named listener locale. A Mandarin-speaker's syllable-final
/n/ vs /ŋ/ contrast costs more comprehension in Edinburgh than
in Singapore (which has similar Mandarin-substrate listening
patterns). Reflect that.
- If the user's first-language profile includes more than one
language, take the UNION of known contrast difficulties from
all profiles; do not pretend the user has only one substrate.
- Recommend at most five drills. Three is the default. Each drill
has three to five suggested words. The words must contain the
phoneme context in a position where it matters (post-vocalic
/r/ matters in "rural" and "world", not in "red" or "run").
- Set `reading_confidence` honestly. Background noise, very short
utterances, recording-quality issues, and unusual paragraph
choices that do not elicit the user's profile's known contexts
should lower it. Below 0.7, flag `flagged_for_user_review`
with "we recommend re-recording in a quieter room before
drilling".
- `background_noise_note` fires when there is identifiable
non-speech audio (kettle, dog, traffic, music). Name what you
heard in one phrase. Do NOT recommend any drill if background
noise is severe — instead, leave `recommended_drills` empty
and set the review flag.
- Do NOT use the words "wrong", "incorrect", "mistake", "fault",
"improve your accent", "accent reduction", "fluency".
- Do NOT compare the user to a generic "native speaker". The
reference is the chosen listener locale, named explicitly.
- Do NOT recommend phoneme contexts the user has saved in
`profile.preserved_features[]`.
Output ONLY the Diagnostic JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Score per-word drill attempt → `DrillAttempt` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a single short audio clip — the user re-recording one
drill word — and the millisecond-aligned native-reference audio for
the same word in the same listener locale. Your job is to:
1. Identify the millisecond range(s) within the user's clip where
the user's production diverges from the native reference in a
way the listener would notice.
2. Score the listener cost (the probability the listener would ask
for a repeat).
3. Describe in one plain sentence what the listener perceives in
the divergent span.
Hard rules:
- Cap each divergence span at 40% of the word's duration. If you
find yourself wanting to mark more, escalate by leaving the
divergence_spans field empty and setting reading_confidence to
0.6 with a flagged note "this is sentence-level prosody, not
a word-level segment — surface a sentence drill instead".
- listener_perception_plain is one sentence. "The /θ/ is
articulated further back, closer to /s/, so 'three' is heard
as 'sree'." No IPA on the surface; the IPA is metadata.
- listener_cost is calibrated against the named listener locale.
A short /iː/ vs /ɪ/ contrast costs more comprehension in
Boston than in Singapore.
- If the user's attempt is closer to the native reference than
their last attempt, the listener_cost MUST be lower than their
last attempt's listener_cost. The progress signal in the UI
depends on this being honest.
- background_noise_note fires when there is non-speech audio.
Name it. If severe, leave divergence_spans empty and surface
a "try a quieter room" flag.
- Do NOT mention "fluency", "accent", "native-likeness" in any
field.
- reading_confidence is honest; below 0.7 the UI tells the user
"we are not sure here; another take in a quieter room would
help".
Output ONLY the DrillAttempt JSON matching the schema. No commentary.
```
---
### Call: Generate native-reference TTS for a custom word
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Pick the Gemini 2.5 Flash TTS voice whose languageCode matches the
listener locale the user chose:
- en-US for Boston, Seattle, New York, Atlanta, Los Angeles
- en-GB for Birmingham UK, Manchester UK, Newcastle UK, Edinburgh
- en-IE for Dublin
- en-AU for Sydney, Melbourne
- en-NZ for Auckland, Wellington
- en-ZA for Cape Town, Johannesburg
- en-NG for Lagos office English (fall back to en-GB if not
published)
- en-IN for Mumbai office English (fall back to en-GB if not
published)
- en-SG for Singapore (fall back to en-GB if not published)
- en-CA for Toronto
Prefer a clear, broadcast-register voice over a casual one — the
user is going to compare their own production against this audio,
and a clearly-articulated reference makes the comparison fair.
Pre-process the input text before sending it to TTS:
- The input is a single English word or a short phrase. No
punctuation, no metadata.
- Mid-call voice switching is not supported. Render the whole
word in the one chosen voice.
- Target rate: ~110 words per minute equivalent — the rate at
which a person introduces a colleague's name to another
colleague, not the rate of a news anchor.
Style direction: prepend ONE short directive sentence to the
input text, exactly like: "Say this word clearly and at a calm
pace, as if introducing a colleague's name to another colleague
who has not heard it before." There is no separate `style` API
field on Gemini 2.5 TTS; the directive sentence inside the input
is how style is conveyed.
Phoneme overrides (English /θ/, /ð/, /ɹ/, /ɪ/-/iː/ contrast,
/æ/-/ɑː/ contrast) are NOT exposed by Gemini 2.5 TTS — there is
no SSML tag. Pronunciation comes from the chosen
voice's native locale. This is the intended behaviour for this
app: the user IS practising the chosen locale's pronunciation.
Blocklist: refuse to generate audio for any word on the slur /
hate-speech blocklist held server-side. Return an empty audio
response with a polite one-sentence refusal in the metadata.
```
---
### Call: Build weekly clarity recap → `WeeklyRecap` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none, long context over user's recent attempts)
```
You receive the user's recent DrillAttempt records (typically
the last 7 days, sometimes the last 14 if the user has been away).
Each attempt has a listener_cost score. Each drill word has a
chronological sequence of attempts.
Your job: identify the eight drill words where the user has
shrunk the listener_cost the most this week, and write a one-
sentence opening line for the recap card.
Hard rules:
- Rank by absolute reduction in listener_cost from the user's
earliest attempt this week to the user's latest attempt this
week, not by absolute listener_cost. The frame is "you got
more understandable on these words", not "these are your best
words".
- The opening sentence is one sentence, in the second person,
no exclamation marks, no marketing language. Examples:
"You are easier to understand on 'rural' than you were on
Tuesday." "Three new words moved this week — 'vector',
'unexpected', and your daughter's name."
- If a drill word's listener_cost went UP this week, surface
it in `not_yet_improving` with a one-sentence suggested next
step in plain English. Never word it as failure. "Try this
one slower, with the /θ/ held a beat longer — the listener
needs the extra millisecond."
- If a drill word's listener_cost did not change this week,
do not surface it at all. Silence is fine.
- If the user has no attempts this week, return an empty
improvements array and an opening_sentence_plain that
acknowledges the gap without judging it: "No drills this week.
Whenever you are ready."
- Do NOT use streaks. Do NOT mention "consistency". Do NOT use
the word "practice" as a verb directed at the user.
- Do NOT recommend specific drill schedules. The recap is a
mirror, not a coach.
Output ONLY the WeeklyRecap JSON matching the schema. No
commentary.
```
---
### Call: Conversation-prep word selection
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a short text (a job description, a sales call agenda,
a doctor's-appointment script, a presentation outline, an
interview prep doc) and the user's diagnostic profile. Your job:
pull the ten words from the text that are most likely to be
asked-to-repeat by the listener locale, given the user's
phoneme-context profile.
Hard rules:
- Pick words that contain phoneme contexts the user has actually
shown difficulty with in their diagnostic — NOT generic "hard
English words". The user does not need to drill "thoroughly"
if their /θ/ is fine.
- Prefer words the text repeats often. A word that appears once
in a 2,000-word document is not worth drilling for this
conversation; a word that appears six times is.
- Prefer technical and personal terms (names, brand names,
medical terms, jargon) over common words — these are the
ones listeners will most often miss without context.
- Output as JSON: { "words": [{ "word": string,
"frequency_in_text": number, "phoneme_contexts": [string],
"rationale_plain": string }] }
- Do NOT translate proper nouns. A surname stays as written.
- Do NOT invent words not in the text.
No commentary. JSON only.
```
---
### Call: Listening-mode transcription evaluation
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive: the canonical transcript of a native-reference audio
clip (3-10 seconds), and the user's typed transcription of what
they heard. Your job: identify the words the user mistranscribed
and infer which phoneme contexts of THEIRS (their listening
patterns inherited from their first language) caused the
confusion.
Listening errors mirror production errors but not perfectly. A
Mandarin-Chinese speaker who collapses /n/-/ŋ/ in production also
often mishears "sing" as "sin". A Korean speaker who has
post-vocalic /r/ collapse in production may mistranscribe "world"
as "wold".
Hard rules:
- Quote the canonical transcript verbatim. Quote the user's
transcription verbatim.
- For each mismatched word, infer the phoneme context with the
highest probability of being the cause, given the user's
first-language profile.
- Output as JSON: { "mistranscriptions": [{ "canonical": string,
"user_typed": string, "inferred_phoneme_context_label_plain":
string, "confidence": number }] }
- Confidence is calibrated. Below 0.6, also list one alternate
phoneme context that could explain the same error.
- Do NOT say "you misheard". Say "the user typed X for Y".
- Do NOT score the user's overall listening as a percentage.
No commentary. JSON only.
```
## 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 Korean-American founder in Boston.** A Korean-and-English-heritage speaker, raised in New Jersey by Korean-speaking parents, now running a seed-stage startup. She lost a sales call last week because the buyer kept asking her to repeat *rural* — the company's go-to-market is rural broadband. Her diagnostic shows two clear phoneme contexts: post-vocalic /r/ collapse (Korean substrate) and the /iː/-/ɪ/ vowel-length contrast. Her drills are *rural*, *world*, *girl*, *sheep* vs *ship*, *deal* vs *dill*. She runs the drills on the AmTrak. The waveform compare shows her *rural* attempt is 480 ms; the Boston native reference is 620 ms; her first /r/ is collapsed into a 60 ms span where the reference has 180 ms. She re-records four times. The listener_cost on *rural* drops from 0.71 to 0.32 in one session. She closes the app. The progress recap on Sunday morning says, in one sentence, "You are easier to understand on 'rural' than you were on Tuesday."
- **The Brazilian PhD student in Edinburgh.** A Brazilian Portuguese speaker, second year of his physics PhD at the University of Edinburgh, lab meetings in English, Tesco self-checkout still mishearing him weekly. His diagnostic — recorded at 11:34 pm in his Marchmont flat — identifies th-fronting (Brazilian Portuguese has no /θ/-/ð/; he produces /f/-/v/ instead), the unstressed-syllable schwa reduction missing from his English (Portuguese is syllable-timed, English is stress-timed), and the /h/-/r/ confusion on word-initial /r/ in words like *unexpected* (the *un* prefix). His drills are *three*, *with*, *brother*, *unexpected*, *photograph* vs *photographer*. The compare highlights his *brother* as *bruvver*; the Edinburgh-listener reference shows the inter-dental fricative. The app's tip: "the tip of your tongue between your teeth, not behind your top lip — the same place you put your tongue to whistle." He laughs in his flat. He re-records. The span shrinks.
- **The Vietnamese-American engineer in Seattle.** A Vietnamese-and-English-heritage speaker, raised in Westminster, California, now an engineer at a Seattle cloud-infrastructure company. His name is *Vinh*, pronounced /vɪɲ/ with a palatal nasal that English does not have. His doctor's office calls him *Vincent*. He uses the custom-word path to add *Vinh* to his drill set, with a recorded reference of his own father saying it. He also adds *vector* (his on-call rotation requires saying *vector clock* and *vector store* on stand-ups) and *worth* (he says *wuth*; the listener hears *wuh*). His Seattle-listener drills are *Vinh* (with his own father's reference, not TTS), *vector*, *worth*, *th* in *month*, *thirty*. The progress card on Sunday says, in one sentence, "You are easier to understand on 'vector' than you were last Sunday."
- **The Amharic-speaking nurse in Birmingham.** A nurse from Addis Ababa, three years into the NHS, now on a hospital ward in Birmingham. Her handovers are getting flagged for "speak more slowly" — but her diagnostic shows she is not fast, she is fluently using Amharic prosody, which lands word stress in places English-listener nurses are not expecting. Her drills target unstressed-syllable reduction in clinical English: *patient*, *medication*, *paracetamol*, *Birmingham*, *cardiology*. The compare shows her *paracetamol* with even syllabic timing; the Birmingham-listener reference shows the third-syllable stress and the schwa-flattened first and second. The app does not say "you say it wrong". The app says "Birmingham listeners are hearing five equally-loud syllables, and reaching for context. Here is one more attempt with the third syllable louder."
- **The Tamil-speaking lecturer in Toronto.** A computer-science lecturer at the University of Toronto, originally from Chennai, whose teaching evaluations include the phrase "the professor is brilliant but I had to circle words in the slides". Her diagnostic identifies the /v/-/w/ near-merger that Tamil and Indian English share, and the dental /t/-/d/ that Toronto students hear as a foreign /t/. Her drills are *vector* (overlaps with the Vinh case), *we* vs *vee*, *the* (Toronto students are particularly impatient on the determiner), *better*, *student*. The conversation-prep mode reads her lecture script and pulls *recursion*, *invariant*, *vertex*, *weight*, and *vertex* again — *vertex* and *weight* both load /v/ and /w/ in one term. She drills them the morning of the lecture.
- **The Cantonese-speaking grandmother who is the listener, not the speaker.** A Cantonese-speaking grandmother whose grandkids are learning Cantonese — see H2 Kitchen Phrasebook. Accent Coach also has a *listener mode* for users like her: the app reads her her grandkids' Cantonese drills out loud and asks her to type what she heard, and the structured output is the grandkids' production-side diagnostic. The app crosses two sides of the same family in one structured record.
- **The first-month Tagalog-English speaker in Auckland.** A Filipino nurse migrated to Auckland four months ago, used to British English from the textbooks but landed in a country with the Kiwi /e/-/æ/-/ɪ/ vowel shift she had not been prepared for. Her diagnostic is not about her production — it is about her *listening* — the app's listening mode plays her ten short Kiwi-speaker clips and her transcription errors flag the contexts where Auckland English is moving the vowel target on her. The output: a drill set of Kiwi vowel targets in words she hears every day (*pen*, *pin*, *six*, *bed*, *bad*, *bird*, *purse*).
- **The reduced-spoons user with long-COVID-related fatigue.** A chronically-ill ESL professional whose fatigue means a 5-minute session is the cap. The app remembers — sessions are 5 minutes, two drill cards maximum, no recap nag, no notifications. The progress recap waits for Sunday.
- **The voice actor preparing for a role.** A British-Nigerian actor cast as a Lagos-born software engineer in a Toronto-filmed series — he wants to land the production's chosen "Lagos office English" listener locale specifically and reliably. He uses the app with reduced-spoons mode off, all feedback on, and the listener locale pinned to Lagos. The "accent retention" toggle is off for this user — explicitly, because the goal here is a specific target accent for a role.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a hand holding a phone in a quiet kitchen at evening, the phone screen showing a single play button — no text, no labels, just the gesture of recording. One paragraph: "Get understood. We listen to thirty seconds of your normal voice and find the three sounds that are getting in your way — without touching the rest of your accent." Single Google sign-in button; Apple sign-in next to it. Below: "Try with a sample diagnostic" → loads the demo diagnostic in section 8a.
2. **Profile setup.** Three quiet steps. (a) "Which languages are you fluent in?" — multi-select, ordered, with regional sub-options (Brazilian vs European Portuguese, Egyptian vs Levantine Arabic, etc.). (b) "Where do you want to be understood?" — listener-locale picker, with a small note "you can change this any time; one user often has more than one listener locale". (c) "How does your accent fit into this?" — a slider with two anchor labels: *keep my accent fully (most users)* and *move me toward this locale (rare)*. The slider default is at "keep". A small footer link: "What does this slider change?" opens an explainer.
3. **Diagnostic flow.** The app picks one of 40 short paragraphs based on the user's profile and shows it on screen with a single record button below. The paragraph is engineered to elicit the user's most-likely phoneme contexts. Ambient-noise gate before recording: "wait for the kettle to stop" if it detects non-speech audio; "this is a noisy room — try a quieter one or move your phone closer" if it detects high-floor noise. Recording cap: 30 seconds. After stop, a quiet progress bar: "listening…" → "comparing to the Boston listener…" → "writing your drills…". Each step takes 3-6 seconds. The result page slides in.
4. **Diagnostic result.** Three to five drill cards stacked vertically on mobile, in a 2-column grid on desktop. Each card: the phoneme-context plain-English label as the title, the three suggested words as a pill row, the user's own clip and the native reference as two stacked waveforms with a shared scrubber, the divergence span highlighted in faded red, and a single tap-target "Start drill". A footer line: "you have 14 contexts we are deliberately NOT flagging because they don't block comprehension — see them" → opens the suppressed-contexts panel for users who want to know the model's full reasoning.
5. **Drill view.** A single drill word large on screen. Above it, the listener-locale name (small, e.g. "Boston listener"). The plain-English articulation tip in a quiet rounded card below. The waveform-compare player at the bottom — user's attempt above, native reference below, shared scrubber, divergence span tappable to loop. Two buttons: re-record (large, primary) and next word (secondary). The user's previous attempts on this word appear as a quiet horizontal strip — each one a thumb-sized waveform, tappable to scrub through history. No score. No grade. The only feedback is the visible shrinking divergence span.
6. **Custom word.** "Add a word you keep being asked to repeat." A text input, a recording option ("or record your own native reference"), and a TTS-generation option pinned to the listener locale. The custom word joins the user's drill library and surfaces alongside the diagnostic-recommended drills.
7. **Listening mode.** Ten short native-reference clips in the chosen listener locale, played one at a time. The user types what they heard. After all ten, a result page surfaces the phoneme contexts most likely behind the user's transcription errors — the receptive side of the same problem. The user can convert any listening-error context into a production drill with one tap.
8. **Conversation prep.** A text-paste field: paste a job description, a sales call agenda, a doctor's-appointment script, a presentation outline. The app extracts the ten words you are most likely to be asked to repeat, given your diagnostic, and queues them as a 10-minute drill session. Useful right before a Zoom call, right before a stand-up, right before a viva.
9. **Progress.** A weekly card, refreshed Sunday morning local time. A horizontal bar chart: each bar a drill word, the bar length showing how much the listener_cost shrank this week. No streak counter. No leaderboard. Tapping a bar opens the historical waveform compare for that word — the user's first attempt next to their latest attempt, side by side. A "not yet improving" subsection appears only when relevant, framed honestly without judgement.
10. **Family share.** A modal: "Invite a partner, friend, or colleague to listen to a drill snippet and tell you whether they understood you on the first try". Magic-link email; the invitee lands on a single-purpose page with two buttons: "I understood that the first time" / "I had to listen twice". One-tap, never a comment, never a grade.
11. **Speech-therapist export.** "Working with a speech and language therapist? Export your diagnostic and drill history as a PDF." A toggle in settings opens this whole flow; without it, the SLT path is invisible. The PDF includes the diagnostic phoneme contexts, the historical listener_cost timelines per word, and the user's own audio links — the SLT can listen.
12. **Settings.** Listener locale, first-language profile, the accent-retention slider, reduced-spoons mode, the SLT-export toggle, "show IPA in tips" toggle (off by default), notification preferences (default: weekly recap only, every other notification off), delete-my-data.
13. **Footer.** "Made for the people who are tired of being asked to repeat themselves." Privacy: "Your voice is yours. We never train on it." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Accent Coach."
- Subhead: "Get understood — in any first language, any city, with your accent fully intact."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic "accent reduction" app: it targets only the sounds that block comprehension, it preserves accent retention by default, and it uses your own recordings next to native references on the same waveform — not a percentage score.
- Visual: a small annotated illustration of two waveforms stacked with a shared scrubber and a faded-red divergence span — the visual idiom the app uses everywhere else.
**Slide 2 — Try it now.**
- One short prompt: "Try with a sample diagnostic".
- A live demo input pre-loaded with the sample voice profile from section 8a (a Korean-and-English-heritage speaker in Boston) and a 30-second pre-recorded sample.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the faded-red divergence span on *rural*, the listener-locale pill at the top of the drill card, the plain-English tip).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the listener locales in `/data/listener-locales/` for the ones your users care about."
- "Adjust the prompts in `/server/prompts/` to fit the first-language profiles your users speak."
- "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (audio reasoning)** — listens to your 30-second diagnostic and each drill attempt, identifies the millisecond-aligned phoneme contexts that are blocking comprehension for your chosen listener locale, and writes plain-English drill recommendations. This is the load-bearing capability; the app's diagnostic accuracy depends on it.
- **Gemini 3.5 Flash (multilingual phonological knowledge)** — knows which sounds a Korean, Mandarin, Cantonese, Vietnamese, Tagalog, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Arabic, Brazilian Portuguese, European Portuguese, Spanish, French, Polish, Russian, or Khmer speaker statistically struggles with in English — calibrated against the listener locale you picked.
- **Gemini 3.5 Flash (structured output)** — every diagnostic, every drill attempt, every weekly recap is a typed JSON object. The schema is in `/server/schemas/`; the app refuses to render anything that does not match.
- **Gemini 2.5 Flash TTS** — generates the native-reference audio for custom words you add (your name, a brand, a place name). The locale matches your chosen listener locale, so the reference sounds like the people you are trying to be understood by.
- **Gemini 3.5 Flash (long context)** — for the weekly recap, the model sees every drill attempt of yours from the last seven days at once, so it can rank improvements honestly and surface "not yet improving" words without you having to keep your own log.
- **Firebase Auth** — Google and Apple sign-in, family-share invitations via magic links.
- **Firestore** — stores your profile, diagnostics, drill attempts, and recaps, syncs across devices in real time.
- **Firebase Storage** — keeps your audio recordings at upload sample-rate (48 kHz mono preferred), forever, accessible only via pre-signed URLs.
- **Cost note** — see the detailed breakdown in 6d. A typical user, doing one 30-second diagnostic and ten 2-3 second drill attempts a session, costs about $0.04 per session of Gemini API spend.
- **Privacy note** — your voice samples are private to you and the reviewers you explicitly invite. 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. The on-device "meeting whisper" runs locally and only uploads structured drill suggestions, never raw call audio.
**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)
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the diagnostic call (30-sec audio input + medium thinking) is the most expensive single call at ~$0.018 per diagnostic; per-drill scoring is ~$0.002 each; the weekly recap is ~$0.01 per user per week. Heavy users (10 sessions a week) land near $1/month of Gemini spend.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete my recordings and diagnostic history" — gone in 60 seconds), what is never sent for training.
**Documentation links:**
- AI Studio Build docs
- Gemini API audio-input, multilingual, long-context, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on the IETF BCP-47 language tag scheme used in listener locales
**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)
- **Diagnostic (Gemini 3.5 Flash, medium thinking, 30-sec audio input)** — ~30,000 input tokens equivalent for the audio + ~600 output tokens for the structured Diagnostic JSON. ~$0.018/diagnostic.
- **Per-drill scoring (Gemini 3.5 Flash, low thinking, ~3-sec audio input + ~3-sec native-reference comparison)** — ~6,000 input tokens equivalent + ~200 output tokens. ~$0.002/drill attempt.
- **Native-reference TTS for custom words (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 6-character word like "Vinh" ≈ $0.00002 per generation. Cached per word/locale; charged once.
- **Weekly recap (Gemini 3.5 Flash, low thinking, long-context over ~7 days of attempts)** — ~30,000 input tokens for a typical week's attempts + ~400 output tokens. ~$0.01/recap. Runs once a week.
- **Conversation prep (Gemini 3.5 Flash, low thinking)** — short text input + diagnostic profile + ~200 output tokens. ~$0.0008/run.
- **Listening-mode transcription evaluation (Gemini 3.5 Flash, low thinking)** — ~$0.0005 per ten-clip session.
- **Expected per-session cost** (one diagnostic + ten drill attempts): ~$0.038. Heavy user at 10 sessions/week + weekly recap: ~$0.40/week ≈ ~$1.70/month.
- **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 30-second 48 kHz mono recording at AAC-LC 96 kbps is ~360 KB; a year of weekly diagnostics + ~520 drill attempts ≈ ~200 MB per user ≈ ~$0.005/month per user.
## 7. Design language
- **Mood:** A quiet rehearsal room. Not a language-learning app. Not a fitness app. Not a corporate training portal. The user is alone with their phone in a quiet moment — the AmTrak window seat, the Marchmont flat at midnight, the parked car before a sales call — and the app needs to feel like a piece of equipment, not a coach. Equipment is neutral; equipment does not cheerlead.
- **Typography:** A clean grotesque for app chrome and body copy (Inter or Geist). A monospaced typeface for the IPA notation behind the `(i)` panel and for the timecodes on the waveform compare (JetBrains Mono or IBM Plex Mono). Display weight reserved for the diagnostic-result page's drill-card titles and the weekly recap's opening sentence. No serif anywhere — this is not a literary app.
- **Palette:** Off-white background `#FAFAF7` for the main canvas; deep ink `#13141A` for body text; soft graphite `#5B6168` for secondary text; one accent muted teal `#2B6F6B` for primary buttons and the user's own waveform; one accent dusty rose `#B86C76` for the native-reference waveform; one faded red `#C44A4A` at 35% opacity for the divergence span (chosen so it reads as "the listener loses you here" rather than "you are wrong"). A near-black `#1A1B22` for the dark-mode variant (auto-respects system preference).
- **Imagery:** The waveform IS the imagery. Two stacked horizontal waveforms with a shared scrubber, drawn cleanly with thin strokes (`1.5 px`), no shadows, no gradients, no glow. The divergence span is a soft-edged rectangle at 35% red — not a hard outline. The drill-card title is large; the suggested words are pills below. The screen is mostly white space.
- **Hand-feel touches:** The record button is a single solid circle (40 px on mobile, 56 px on desktop), no inner icon — it pulses gently while recording (respecting `prefers-reduced-motion`). The waveform draws on as the user records, in real time, in the user's own teal — they see their own voice arrive on screen as they speak.
- **Spacing:** consistent 4-px base. Generous vertical rhythm — the drill cards breathe.
- **Radius:** consistent token set (12 / 16 / 24 px). Drill cards 16; the welcome card 24; pills 12.
- **Shadows:** essentially none. Cards sit on the canvas with a single hairline border `1px #E5E2DA` (light mode) / `1px #2A2D35` (dark mode). The exception: the record button has a single soft drop-shadow `0 2px 8px rgba(0,0,0,0.06)` to suggest tactility.
- **Motion:** purposeful — entrance fades, the record-button pulse, the waveform draw-in. Respect `prefers-reduced-motion` — the pulse becomes a static dot; the waveform appears all-at-once at recording stop instead of drawing in. No bouncing splash animations. No celebratory confetti when a divergence span shrinks. The shrinking span is its own reward.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons where possible — the drill cards skeleton-load as the diagnostic processes, so the user sees the structure of the answer before its content. Empty states explain the next action ("you haven't recorded anything yet — tap the record button and read this paragraph").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible drill words, listener locales, voice-sample transcripts, native-reference snippets that fit the domain (use the seed content in section 8a as a starting point). When inventing, ground in real phonological facts: Korean speakers really do tend to collapse post-vocalic /r/; Brazilian Portuguese speakers really do tend toward th-fronting; Mandarin speakers really do tend to merge /n/-/ŋ/ word-finally. Don't invent fake linguistic claims.
- Tone: neutral, direct, free of corporate language. This template is for a person who is tired of being talked down to about their voice. Never patronising; never cheerful; never apologetic on the user's behalf.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. No 'Speak with confidence'. The frame is "get understood", not "feel confident".
- Body copy: short paragraphs (2-4 sentences). Use lists sparingly.
- Plain language. IPA is metadata, never UI. "the tongue between your teeth, not behind them" is the lesson; `/θ/` is the metadata.
- Where the app outputs AI-generated content (the drill recommendation rationale, the weekly recap opening sentence, the listener-perception explanation), never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence reading shows as a faintly underlined sentence; tapping it reveals "we are not sure here — another take in a quieter room would help").
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, listener locales, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample voice profiles (sidebar):**
- "Min-jung — Korean + English-heritage, Boston listener" — Korean-American startup founder, 31, second-generation, raised in New Jersey, now in South End Boston. Profile: post-vocalic /r/ collapse, /iː/-/ɪ/ vowel-length contrast, occasional /f/-/p/ on word-initial /f/. Listener locale: en-US-Boston.
- "Bruno — Brazilian Portuguese, Edinburgh listener" — physics PhD student, 27, University of Edinburgh. Profile: th-fronting (/θ/→/f/, /ð/→/v/), syllable-timing prosody, /h/-/ɹ/ confusion on /r/-initial English words like *rich*, *real*. Listener locale: en-GB-Edinburgh.
- "Vinh — Vietnamese + English-heritage, Seattle listener" — software engineer, 29, Westminster CA → Seattle. Profile: /θ/-/t/ contrast collapse, final-consonant deletion in clusters (-rld, -nths), palatal nasal /ɲ/ in his own name. Listener locale: en-US-Seattle.
- "Hanna — Amharic, Birmingham UK listener" — NHS ward nurse, 34, Addis Ababa → Birmingham. Profile: Amharic-substrate stress pattern (penultimate-syllable bias in clinical English terms), uvular /ʔ/ glottal stop on word-initial vowels. Listener locale: en-GB-Birmingham.
- "Lakshmi — Tamil, Toronto listener" — CS lecturer, 41, Chennai → Toronto. Profile: /v/-/w/ near-merger, dental /t/-/d/, /θ/-/ð/ retroflex substitution. Listener locale: en-CA-Toronto.
**Sample diagnostic in detail view (this is what the demo should show):**
- **User:** Min-jung (Korean + English-heritage, Boston listener)
- **Recorded at:** 2026-05-19 14:38 EDT, AmTrak Acela seat 7B
- **User audio URI:** `gs://accent-coach-min-jung/diagnostics/2026-05-19-1438.aac`
- **Passage read:** Passage 11 — "I work in rural broadband. The world is full of small towns we don't reach yet. My daughter is six. She wants to be a vet. The sheep on her favourite show are not at all the same as the ones at the petting zoo, where the ship in the picture book had three masts and the one we saw was a tug. We had a flat white and a deal on the muffins."
- **Phoneme contexts identified (3):**
- **post-vocalic /r/ collapse** — IPA `["ɹ"]` — user production note: "the /r/ in *rural* and *world* is shortened from the Boston-listener norm of ~180 ms to ~60 ms, blending into the preceding vowel" — articulation tip plain: "after the vowel, hold the /r/ for a beat — bunch the back of your tongue up toward the roof of your mouth and let the air shape around it" — diagram key: "r-bunched-postvocalic.svg"
- **/iː/-/ɪ/ vowel-length contrast** — IPA `["iː", "ɪ"]` — user production note: "*sheep* and *ship* render with similar vowel duration, around 110 ms each; the Boston-listener norm has /iː/ at ~180 ms and /ɪ/ at ~90 ms" — articulation tip plain: "the long /iː/ is held nearly twice as long as the short /ɪ/ — feel the smile in your cheeks for the long one; let it relax for the short one" — diagram key: "iː-vs-ɪ-duration.svg"
- **word-initial /f/-/p/ on *flat*** — IPA `["f", "p"]` — user production note: "*flat* produced with a momentary /p/ release before the /l/, heard as /pl/" — articulation tip plain: "the /f/ asks for air across your lower lip and upper teeth — no closure" — diagram key: "f-fricative.svg"
- **Recommended drills (3):**
- **post-vocalic /r/** — suggested words: *rural*, *world*, *girl*, *every*, *party* — rationale plain: "you said 'rural' as 'ral' — the post-vocalic /r/ collapsed into the vowel; here are five words that practice the same context, three of them in the script you just read"
- **/iː/-/ɪ/** — suggested words: *sheep* vs *ship*, *deal* vs *dill*, *leave* vs *live*, *heat* vs *hit*, *beat* vs *bit* — rationale plain: "the Boston listener leans on vowel length to tell these pairs apart; you're producing both with similar timing, so the listener reaches for context"
- **word-initial /f/** — suggested words: *flat*, *full*, *free*, *favourite*, *family* — rationale plain: "the /f/ in *flat white* was heard as /pl/ once in your sample; five words to lock it in"
- **Suppressed contexts (deliberately NOT flagged):**
- "Korean-substrate vowel raising on /æ/" — does not block comprehension in Boston ESL listeners; would only matter if user wanted to deliberately move toward General American
- "absent dark-/l/" — Boston listener accommodates a clear-/l/ pronunciation; only flagged for users moving toward a London listener
- "ㄹ-influenced flap on intervocalic /t/" — actually helps comprehension in US listener locales; Min-jung's *butter* sounds clearer here than in many native Bostonians' speech
- **Reading confidence:** 0.91
- **Background noise note:** "train ambient noise present but constant, below the floor where it would affect phoneme judgement; continuing"
**Sample drill attempt in detail view:**
- **Drill word:** *rural*
- **Listener locale:** en-US-Boston
- **User audio URI:** `gs://accent-coach-min-jung/drills/rural-attempt-04.aac`
- **Recorded at:** 2026-05-19 14:51 EDT (sixth attempt this session)
- **Divergence spans (1):**
- start_ms 220, end_ms 380, listener perception plain: "the post-vocalic /r/ is articulated as a short vowel-blend rather than held; the listener's ear is reaching for the second syllable"
- **Listener cost:** 0.32 (was 0.71 on attempt 01)
- **Reading confidence:** 0.93
- **Background noise note:** null
**Sample weekly recap in detail view:**
- **User:** Min-jung
- **Week of:** 2026-05-12 to 2026-05-18
- **Opening sentence plain:** "You are easier to understand on 'rural' than you were on Tuesday."
- **Improvements (top 5 of 8):**
- *rural* — listener cost before 0.71, after 0.32 — 4 representative attempts
- *world* — before 0.64, after 0.41 — 3 attempts
- *sheep* vs *ship* (pair) — before 0.58, after 0.39 — 6 attempts
- *flat* — before 0.39, after 0.18 — 2 attempts
- *deal* vs *dill* (pair) — before 0.55, after 0.43 — 5 attempts
- **Not yet improving (1):**
- *every* — "Try this one slower, with the second syllable held a beat longer — the listener's ear needs the schwa."
**Sample input artefacts (for the build to demonstrate):**
- A 30-second clip of Min-jung reading Passage 11 on the AmTrak, with the post-vocalic /r/ collapse on *rural* and *world* and the /iː/-/ɪ/ near-merger on *sheep* and *ship*.
- A 4-second native-reference clip of a Boston ESL speaker saying *rural* at 620 ms total duration, the /r/ held for 180 ms.
- A custom-word user-recorded native reference: Vinh's father saying *Vinh* with the palatal nasal /ɲ/, 480 ms duration, recorded on a Vietnamese-speaking household's home audio.
- A 3-second clip of Hanna saying *paracetamol* with syllable-timed prosody; a 3-second Birmingham-listener reference with the third-syllable stress.
- A 6-second clip of Bruno saying "I'll have three with brother today" — th-fronting visible on *three*, *with*, and *brother*.
**Sample voice copy:**
- Onboarding: "Read the paragraph below. We'll listen to thirty seconds and find what's getting in your way."
- Recording prompt: "When you're ready. The recording stops at thirty seconds, or whenever you do."
- Processing: "Listening…" / "Comparing to the Boston listener…" / "Writing your drills…"
- Empty diagnostic: "No drills yet. Record a thirty-second sample to get yours."
- Error (noise floor too high): "We heard a kitchen behind your voice. A quieter room would help — or move the phone closer to your mouth."
- Divergence span explanation (Min-jung's *rural*): "Here is where the Boston listener loses the second syllable. Tap to loop just this span."
- Drill card title: "post-vocalic /r/ — three to five words"
- Plain-English tip: "after the vowel, hold the /r/ for a beat — bunch the back of your tongue up toward the roof of your mouth and let the air shape around it"
- Save confirmation: "Saved. Sixteen takes on 'rural' across two weeks."
- Custom-word generation: "Generating a Boston-listener reference for 'Vinh'. (Or record your own — we'd rather hear your father's voice.)"
- Weekly recap (one sentence): "You are easier to understand on 'rural' than you were on Tuesday."
- Empty recap: "No drills this week. Whenever you are ready."
**Sample family-share email subject + body:**
- Subject: "Min — could you listen to one drill word for me?"
- Body: "Hi — I've been working on the word *rural*. Could you tap and tell me whether you understood me on the first try? It's two seconds." [Listen to clip]
**Sample SLT-export PDF header line:**
- "Accent Coach diagnostic + drill history for Min-jung, prepared for Dr. Sarah Linstrom MS CCC-SLP, 2026-05-21."
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a hand holding a phone in a quiet kitchen at evening, the phone screen showing a single play button — no UI labels, no app name, just the gesture of recording. Generate via Nano Banana 2 with a prompt emphasising "kitchen table, warm desk-lamp light, hand of a person in their thirties, late evening, single phone screen showing one small play button, no text on screen, soft shadow under the phone, slight blur on the steam from a mug at the edge of frame".
- **App icon / wordmark:** Set in the clean grotesque. Two stacked horizontal waveform marks below the word, in teal and dusty rose. No spoken-language metaphors (no microphones, no speech bubbles, no "AI" mark).
- **Empty-state illustration:** A simple line drawing of a single waveform on a baseline, with a record button below — hand-drawn aesthetic, not a flat icon.
- **Phoneme-articulation diagrams:** Static SVGs in `/data/articulation-diagrams/` — clean black-and-white anatomical cross-sections of the human mouth showing tongue position for /θ/, /ð/, /ɹ/, /l/, /iː/, /ɪ/, /æ/, /ɑː/, /h/, /v/, /w/, /n/, /ŋ/. Diagrams are referenced by `diagram_key` in PhonemeContext. Each is sourced from open-license phonological reference material (clearly attributed in the asset map).
- **Drill word demo audio:** Generated per the prompts in section 8a — Nano Banana 2 is not used for audio; pre-recorded human native references in the chosen listener locales are bundled in `/data/native-references//.aac`. Gemini 2.5 Flash TTS is used only for custom-word fallback.
- **Stock fallbacks:** If a native reference is missing for a chosen listener locale, fall back to the nearest cousin locale (en-US-Boston falls back to en-US-NewYork; en-GB-Edinburgh falls back to en-GB-Glasgow; en-AU-Sydney falls back to en-AU-Melbourne) and tell the user honestly: "we don't have a Boston reference for this word yet — using New York. Want to contribute a Boston recording?"
- **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 for images, AAC-LC 96 kbps for audio, `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 — the drill cards skeleton-load as the diagnostic processes, so the user sees the structure of the answer before its content.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Record a thirty-second sample", "Add a word you keep being asked to repeat", "Invite a friend to listen" — never a generic "Get started".
- Smooth scroll for in-page anchors.
- The waveform compare player streams in as the user re-records, with a clear "listening…" indicator before the divergence span arrives. The native reference plays from a pre-loaded buffer; the user's attempt plays from a freshly-uploaded URL.
- If a Gemini call fails, show a calm, specific error ("We couldn't compare this take — try once more, or move the phone closer to your mouth") and offer retry.
- Low-confidence divergence spans are dashed rather than solid; tapping reveals "we are not sure here — another take in a quieter room would help".
- The record-button pulse takes 1.6 seconds per cycle with `prefers-reduced-motion` falling back to a static dot.
## 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. Use Wavesurfer.js or an equivalent client-side waveform renderer for the compare player.
- **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 diagnostic / drill-scoring / weekly-recap, `gemini-3.5-flash` for conversation-prep and listening-mode-evaluation, and `gemini-3.1-flash-tts-preview` for custom-word native-reference generation. Set `thinkingLevel` explicitly per call; omit `thinkingConfig` entirely on TTS calls.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed profiles on first launch (the demo diagnostic uses Min-jung's profile).
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback.
- **Storage:** Firebase Storage for user recordings and the bundled native-reference library. Pre-signed URLs only.
- **Audio capture:** Web Audio API with `MediaRecorder`, mono, 48 kHz, AAC-LC 96 kbps preferred (fall back to Opus on browsers that don't support AAC). Ambient-noise gate runs client-side before upload — if the floor is too high, the user is told before the recording is sent for processing.
- **iOS Safari gotchas (graceful degradation):** Safari `MediaRecorder` only supports `audio/mp4` (AAC) — this matches the preferred codec so no fallback dance needed, but feature-detect for sanity; mic permission does NOT persist across page reloads on iOS — re-request on every drill and frame this as "tap to allow mic for this session"; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause, discard the partial drill (<3s) or save and prompt resume; backgrounded Safari tabs pause `getUserMedia` — combine `visibilitychange` with a screen Wake Lock during drills; PCM streaming for the listening loop must go via `AudioWorklet`.
- **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 — especially for the record button at the bottom of the screen, which must clear the home-indicator zone.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the drill-attempt history view.
- Optimistic UI on writes; reconcile on response.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The faded-red divergence span has a 35% opacity; verify the underlying waveform stroke against the canvas still passes contrast — do not let the divergence highlight push the waveform below 3:1.
- All audio controls have keyboard-accessible play/pause/scrub via `Space`, `ArrowLeft`, `ArrowRight`.
- Audio playback exposes a synchronised transcript of the drill word and the plain-English tip, kept on screen during playback for screen-reader users.
- All images have meaningful `alt` text. The waveform images have `alt` describing the comparison ("waveform comparison of your attempt at 'rural' next to the Boston-listener reference, with the divergence span on the post-vocalic /r/ highlighted").
- The articulation diagrams have `alt` text describing the tongue position ("cross-section of the mouth showing the tongue tip between the upper and lower teeth, for the /θ/ sound").
- Form fields have associated `