# 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. --- # Live Practice Partner ## 1. Project **Live Practice Partner** is a real-time rehearsal app for the conversation you have on Tuesday. Job interview at 10 am. Customer pitch on a Zoom you cannot reschedule. A cooking demo for fifteen guests. A wedding toast. A stand-up set at an open mic. A doctor's appointment in your second language. You open the app, pick the counterpart vibe (warm, neutral, sceptical), paste the brief — the JD, the deck outline, the toast notes — and tap **Go**. A face appears across from you, a voice greets you, you have two minutes. You rehearse the real thing, out loud, with a counterpart who listens, follows up where a real interlocutor would follow up, and never coaches you mid-flow. When the timer runs out, the debrief gives you **one** specific delivery note. Not five. One. The single moment your voice did something you did not mean. This is the kind of app you reach for at 11:47 pm on a Monday, when you have practised the answer in your head fourteen times and you know that practising it in your head is not the same as saying it out loud to someone who is looking at you. Or at 8:15 am on Saturday before the wedding, alone in a hotel bathroom, when you want to hear the words leave your mouth once with someone on the other side. The counterpart is not a friend (a friend is scheduling theatre). Not a coach ($180 an hour, you needed thirty minutes). Not the mirror (the mirror does not follow up). The single demo that proves the magic: pick **"Job interview — senior PM"**, paste the JD, choose the **warm** counterpart, tap **Go**. The camera comes on, your face in a small corner tile, a friendly counterpart appears full-frame on the other side. She says hello and opens with "Walk me through a time you had to push back on a stakeholder who outranked you." You answer. She follows up on the part of your answer that deserved a follow-up. The timer fades from 2:00 toward 0:00. At 0:10 she says "anything you want to add?" At 0:00 the session ends and the debrief loads: a single note in plain English ("at 0:48 your voice softened on 'difficult' — if you mean it firmly, say it firmly"), the transcript with that moment highlighted, and a chip you can tap to hear the 6-second window. Nothing is saved unless you tap **Keep**. By default, the session evaporates. **Tagline:** _Rehearse Tuesday's conversation out loud, tonight. Two minutes, one note, no recording kept._ ## 2. Target audience - Job seekers preparing for an interview tomorrow morning — every level from new-grad screener to executive panel - Sales people rehearsing a discovery call, a renewal conversation, a contract negotiation - Founders rehearsing the investor pitch the day before the meeting - Students preparing for an oral exam, a thesis defence, a med school MMI, a bar admission interview - Public speakers rehearsing the Q&A that comes after the talk - People practising a language for a real upcoming conversation — the immigration interview, the doctor's appointment in the new country, the call you have been avoiding - Stand-up comedians running a five-minute set in front of an honest counterpart (no fake laughs) - Couples rehearsing a financial conversation, a boundary conversation, the conversation with the in-laws - Caregivers rehearsing the hospice conversation, the school meeting about their child, the conversation with the bank - Wedding speakers, eulogy writers, toast givers — anyone with three minutes of important words on a specific date - Cooking-demo presenters, tour guides, museum docents practising the patter for the next session - Lawyers rehearsing opening statements; doctors rehearsing the bad-news conversation; teachers rehearsing parent-conference scripts - Anyone who has decided that practising in their head one more time will not move the needle ## 3. Core value propositions - **Real-time, both ways** — Live API on Gemini 3.1 Flash Live is the load-bearing intelligence. The counterpart listens to your voice as you speak, sees your face on camera, responds in natural conversational pace, follows up where a real interlocutor would. Not a text exchange dressed up with TTS — a two-minute conversation in the same shape as the real one. - **Two minutes, on purpose** — audio+video Live sessions cap at 2:00 by API design, and we do not fight it. Two minutes is long enough to practise the opening, two follow-ups, and the close — the rhythm of a real interview's hardest moment. If you want more, do another two-minute session. Discipline beats marathon. - **One specific note, not five** — the debrief gives you **exactly one** delivery note grounded in a specific moment (timestamp + transcript snippet + audio playback). Not "your energy was good but you could be more confident." The specific second your voice softened past what you meant. - **The model never coaches mid-conversation** — it plays the counterpart. It does not break frame to suggest a better answer. It does not nudge. Coaching belongs in the debrief, after the timer. Mid-flow coaching ruins the rehearsal. - **Counterpart vibe, your pick — within limits** — three presets (**warm**, **neutral**, **sceptical**) and a per-scenario context (the JD, the deck, the toast notes). The model never plays hostile archetypes by default. Sceptical means honest scepticism, not aggression. - **Ephemeral by default** — no recordings, no transcripts, no video saved unless you tap **Keep**. The session evaporates the moment you close the tab. The privacy panel explains in plain English where the data goes during the session and where it does not go. - **Honest about its limits** — the app can practise the conversation; it cannot predict the specific interviewer at Stripe or the specific aunt at your wedding. The debrief language is "this is one thing I noticed, not the only thing that matters." - **Camera optional, voice required** — voice is the practice surface. Camera adds the body-language layer. Voice-only sessions cap at 15 minutes — plenty for a longer rehearsal. - **Multilingual** — the counterpart speaks the language you pick (Live API is native multilingual). Practise the immigration interview in English when your daily language is Portuguese. - **The brief is yours** — the scenario card asks for the details that matter and passes them as context to the counterpart so the questions are anchored in your specific situation, not generic. ## 4. Features to build - Scenario picker — seven preset categories (job interview, sales pitch, cooking demo, language practice, stand-up set, wedding toast, difficult conversation), plus a **custom** option where the user describes the scenario in one sentence - Counterpart vibe selector — three presets (warm, neutral, sceptical); a one-line description of each - Scenario brief form — adapted per category (paste the JD, the deck outline, the toast notes, the audience description, the question you are dreading) - Pre-session readiness check — 10-second mic + camera test before the Live socket opens - Camera tile — your face in a small corner tile, the counterpart's portrait full-frame on the other side - Live API session — Gemini 3.1 Flash Live, audio+video, 2-min cap; sync function calling for `endSession`, `setTimerWarning`, `requestPause` - Voice-only session — Live API audio-only, 15-min cap; same counterpart, no camera - Generated counterpart portrait — a still image (Nano Banana 2) rendered before the session, paired with the Live audio voice - Subtle counterpart breathing / blink animation over the still portrait; respects `prefers-reduced-motion` - Live transcript band — your words under your tile, the counterpart's under theirs; serves as live captioning - Timer with gentle fade — counts down at the top of the screen; pulses gently at 0:10 (no flashing red); `endSession` fires before the API cap - Hard stop at 0:00 — graceful close, debrief loads - Debrief screen — one specific note in large type, anchored to a timestamp; "play this moment" replays the 6-second audio window; full transcript collapsed below - "One more round" — reset the timer and run the same scenario again; counterpart opens with a different question - Keep / discard — at the end of the debrief, **Keep** saves the 6-second window + transcript + note to the user's private Notebook; **Discard** evaporates everything (default after 30s) - Notebook of kept sessions — list grouped by scenario, deletable per-session, private to the owner; sign-in required - Pre-session jitters mode — optional 30-second breathing TTS before the Live socket opens; skippable - "Try once more with the note in mind" — runs another session with the previous debrief note quietly in context for the counterpart's follow-up questions (still no mid-flow coaching) - Language picker — sets counterpart voice locale + user's expected language; full Gemini Live API BCP-47 catalogue - Network resilience — reconnect within 3 seconds on transient WebSocket drop; end gracefully if >10 seconds; partial debrief on partial transcript - Mic-quality nudge — readiness check suggests a quieter setting if background noise is sustained (does not block) - Plain-English "what this app cannot do" disclosure — visible on welcome screen and inside the capabilities panel ## 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) - **Live API on Gemini 3.1 Flash Live** (`gemini-3.1-flash-live-preview`) — the hero capability. Bidirectional audio in (PCM 16k from mic) and audio out (PCM 24k to speakers), plus image frames at ≤1 FPS from camera, plus text for the live transcript channel. Improved acoustic nuance (pitch, pace, tonal understanding) shipped at I/O 2026. **Modality caps:** audio+video 2:00; audio-only 15:00. **Function calling is synchronous only** — `endSession`, `setTimerWarning`, `requestPause` execute synchronously inside the same WebRTC turn. **Preview status:** flagged in the capabilities panel; SLA caveats acknowledged. - **Counterpart portrait** (`gemini-3.1-flash-image`, Nano Banana 2) — generates the still portrait paired with the Live audio voice. Generated once per session before the Live stream opens. Photographic 1024×1024, head-and-shoulders, vibe inflection. Not Nano Banana Pro — no 4K typography needed for a portrait; the Flash variant keeps per-session cost low. - **Post-session debrief** (`gemini-3.5-flash`) — the new default flagship as of I/O 2026, beating Gemini 3.1 Pro on multimodal benchmarks at Flash-tier price. Receives the full transcript, the server-computed audio feature summary, the scenario brief, and the counterpart vibe. Returns the **single most useful delivery note** grounded in a specific timestamp. Structured output via `responseSchema`. `thinkingLevel: medium` because the debrief is the model's one chance to be useful and benefits from thinking. - **Counterpart persona synthesis** (`gemini-3.5-flash`, `thinkingLevel: low`) — runs once before the Live session, takes the brief + vibe + language and produces the `systemInstruction` string for the Live session. Small text-only call, ~800-token output. - **Pre-session jitters TTS** (`gemini-3.1-flash-tts-preview`) — the newer expressive TTS shipped 2026-04-15, replacing `gemini-3.1-flash-tts-preview`. Reads a 30-second breathing exercise. Style directive prepended; pauses use ellipsis and blank-line em-dash (no SSML). - **Counterpart portrait regeneration** (optional, `gemini-3.1-flash-image`) — one regenerate allowed pre-session. - **Structured output** — debrief returns the `DebriefNote` schema; persona synthesis returns `CounterpartConfig`. Seeded verbatim in their system instructions and as `responseSchema`. - **Thinking levels** — `medium` for debrief, `low` for persona synthesis. Live API, TTS, and image-generation calls do NOT take `thinkingConfig` (omit entirely). - **No grounding in this template** — no `google_search` needed. The counterpart works from the user's brief; the debrief works from the transcript. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). Anonymous mode is supported — a one-off session runs without sign-in (no Notebook). Apple sign-in is optional, requires an Apple Developer account. - **Database — Required (only for the Notebook).** Firestore for `users`, `kept_sessions`, `scenarios_custom`. **Ephemeral sessions never touch Firestore.** - **File storage — Optional, only for Keep.** Firebase Storage for the 6-second audio window around the debrief note. **Storage is NOT auto-provisioned by AI Studio Build today** — enable in the Firebase console if wiring the Keep feature. Pre-signed URLs only. **The full session audio is never uploaded.** - **Live API endpoint:** the Gemini Live API uses a WebSocket with an ephemeral session token. The browser opens the socket using a short-lived token minted server-side; the API key itself never leaves the server. **Privacy point**: audio and video travel browser-to-Google over that WebSocket and do not transit our Cloud Run servers. - **Email — Not needed for v1.** No share links, no notifications. - **Payments — Not needed for v1.** Free with a per-day quota (3 audio+video sessions, or 30 minutes audio-only) enforced via Firestore counter. - **External APIs:** Gemini API only. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, ephemeral-token signing secret) 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/WSS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's audio, video, and transcript 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) · ephemeral sessions are not stored anywhere · Kept sessions deletable from the UI in under 60 seconds. **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. See the matrix below. 2. **Pin `thinkingLevel` explicitly** per call. Live API, TTS, and image-generation calls do NOT take `thinkingConfig` — omit entirely; the matrix shows `n/a`. 3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schemas are 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.** 4. **Pin the system instruction separately** from user input. `systemInstruction` for persona + behavioural rules; `contents` for user input. Never concatenate. 5. **Pre-declare tools as an enable/disable list** per call. The Live API call enables three function tools (`endSession`, `setTimerWarning`, `requestPause`) and nothing else. 6. **State negative constraints explicitly** — listed below. Hard rules, not "be careful" suggestions. 7. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. 8. **Files API uses `files/*` resource names, not `gs://` URIs.** The AI Studio Build runtime uses the Gemini Developer API (`@google/genai` SDK). Files API `upload` returns a resource name like `files/abc123xyz`, passed via `fileData: { fileUri, mimeType }`. `gs://` URIs belong to Vertex AI / Cloud Storage. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Counterpart persona synthesis (pre-session) | `gemini-3.5-flash` | low | (none) | | Counterpart portrait | `gemini-3.1-flash-image` | n/a | n/a | | Live conversation (a/v 2 min or audio 15 min) | `gemini-3.1-flash-live-preview` | n/a | function-calling: `endSession`, `setTimerWarning`, `requestPause` (sync only) | | Pre-session breathing TTS | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Post-session debrief → `DebriefNote` | `gemini-3.5-flash` | medium | (none) | | Hero / empty-state illustration | `gemini-3.1-flash-image` | n/a | n/a | *Note for builders:* on Live API, TTS, and image-generation calls, omit `thinkingConfig` entirely. The `n/a` cells in this matrix are documentation only; do not serialise into the request body. The Live API uses synchronous function calling only; do NOT declare async tool functions on the Live session. Live API is still **preview** as of 2026-06-01 — surface that status in the capabilities panel and accept the SLA caveats. ### Primary structured-output schemas (seed verbatim in the prompt) ```typescript import { z } from "zod"; const Vibe = z.enum(["warm", "neutral", "sceptical"]); const ScenarioCategory = z.enum([ "job_interview", "sales_pitch", "cooking_demo", "language_practice", "stand_up_set", "wedding_toast", "difficult_conversation", "custom", ]); const Modality = z.enum(["audio_only", "audio_video"]); const ScenarioBrief = z.object({ category: ScenarioCategory, category_custom_label: z.string().nullable(), user_role_one_line: z.string(), counterpart_role_one_line: z.string(), context_text: z.string(), user_known_worry: z.string().nullable(), language_bcp47: z.string(), modality: Modality, duration_seconds: z.number(), }); const CounterpartConfig = z.object({ display_name: z.string(), vibe: Vibe, portrait_prompt: z.string(), voice_name: z.string(), opening_line: z.string(), follow_up_strategy_summary: z.string(), language_bcp47: z.string(), hard_rules_for_session: z.array(z.string()), }); const TranscriptTurn = z.object({ speaker: z.enum(["user", "counterpart"]), text: z.string(), start_seconds: z.number(), end_seconds: z.number(), }); const AudioFeatureSummary = z.object({ // All computed server-side from the PCM stream, NOT by Gemini user_total_speech_seconds: z.number(), user_total_silence_seconds: z.number(), user_filler_word_count_estimate: z.number(), user_loudness_variance: z.number(), user_pitch_variance_hz: z.number().nullable(), user_longest_pause_seconds: z.number(), }); const DebriefNote = z.object({ session_id: z.string(), scenario_category: ScenarioCategory, vibe: Vibe, modality: Modality, language_bcp47: z.string(), // THE ONE THING. Exactly one. one_note_one_line: z.string(), one_note_full_explanation: z.string(), one_note_anchor_timestamp_seconds: z.number(), one_note_anchor_transcript_quote: z.string(), one_note_anchor_audio_window_start_s: z.number(), one_note_anchor_audio_window_end_s: z.number(), one_note_category: z.enum([ "pacing", "filler_words", "softening_when_meaning_firmly", "running_out_of_air", "answering_a_different_question", "contradiction_within_answer", "ending_on_a_question_when_a_statement_was_better", "interrupting_yourself", "rushing_the_close", "other", ]), confidence: z.number().min(0).max(1), honest_caveat: z.string().nullable(), things_left_unsaid: z.array(z.string()), is_food_safety_or_medical_or_legal_adjacent: z.boolean(), contains_hostile_critique: z.boolean(), }); type ScenarioBrief = z.infer; type CounterpartConfig = z.infer; type DebriefNote = z.infer; ``` ### Common failure modes (and how to avoid them) - Model coaches mid-conversation — the Live `systemInstruction` must hard-rule this: "you are a counterpart, not a coach. Do not give feedback during the session. React like a real interviewer / customer / family member. Save observations for the timer." - Model picks `gemini-3.5-flash` for the Live session because Flash-tier is the new default — pin `gemini-3.1-flash-live-preview` explicitly. Live has NOT moved to 3.5 yet. - Builder wires Live audio through Cloud Run as a proxy — defeats the latency benefit. Mint the ephemeral token server-side, hand to the browser, let the WebSocket open client-to-Google. - Function calling declared as async on the Live session — Live function calling is synchronous only as of 2026-06-01. Declare `endSession`, `setTimerWarning`, `requestPause` as sync tools. - Session exceeds the 2-minute cap and WebSocket drops mid-sentence — timer logic must call `endSession` at 1:55 with a "we're nearly out of time" prompt and end cleanly by 2:00. Do not push to the API cap. - Live API drops mid-session due to network — reconnect within 3 seconds; if reconnect fails, end gracefully, run debrief on partial transcript, surface the disconnect honestly. - Debrief returns more than one note — server picks highest confidence, discards the rest, logs the violation. The `things_left_unsaid` array is the place for "we also noticed" — not the main note field. - Debrief contains hostile critique — server validates `contains_hostile_critique: false`; rejects and re-requests if the note judges the person ("you sounded weak") rather than describing the moment ("your voice softened"). - Counterpart portrait reveals brand logos / real persons — Nano Banana 2 prompt must avoid celebrities, real brands, identifiable persons. If the model returns a recognisable likeness, the server regenerates. - Transcript drift on noisy mic — readiness check measures background noise and nudges before starting; if transcript shows >30% low-confidence spans, debrief includes `honest_caveat`. - Pre-session jitters TTS uses old `gemini-3.1-flash-tts-preview` — pin `gemini-3.1-flash-tts-preview` explicitly. - Counterpart in language-practice scenario corrects grammar mid-conversation — persona is a counterpart, not a tutor. Grammar coaching belongs in the debrief; the counterpart speaks naturally and asks for clarification only when meaning is unclear. - Sceptical vibe drifts into hostile — system instruction defines sceptical as "honestly probing, not antagonistic". - Builder forgets the API requires 16 kHz PCM input — implement a downsampler client-side or use the SDK's Live helper. Output arrives at 24 kHz PCM; play via Web Audio API. ### Negative constraints (hard rules) - Do NOT coach the user during the Live session. The counterpart never breaks frame. Coaching is the debrief's job, not the conversation's. - Do NOT play hostile archetypes by default. No hostile interviewer, no aggressive customer, no contemptuous judge. Sceptical means honestly probing, not antagonistic. - Do NOT save the user's audio, video, or transcript by default. Ephemeral is the default. Saving requires an explicit Keep tap. - Do NOT promise that practising with this app will get the user the job, the deal, the laugh, the win. The app practises the conversation; the conversation itself belongs to the user and the other side of the table. - Do NOT use the user's audio, video, transcript, or scenario brief to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. - Do NOT give medical, legal, or food-safety advice. If a brief looks medical/legal/safety adjacent, the app practises the conversation but the debrief surfaces a not-a-substitute disclaimer. - Do NOT generate a counterpart portrait depicting a real, identifiable person, celebrity, or recognisable brand. - Do NOT exceed the audio+video 2-minute cap or the audio-only 15-minute cap. - Do NOT deliver more than one debrief note as the headline. Other observations go in `things_left_unsaid`. - Do NOT moralise about the user's scenario. The counterpart responds in the role they were asked to play. - Do NOT auto-publish kept sessions. No share feature in v1. - Do NOT extend a 2-minute session by chaining another Live session automatically. The user taps **One more round** explicitly. ### Per-call `systemInstruction` strings Use these as the literal `systemInstruction` field for each Gemini API call. They complement the series-wide rules in `00-series-instructions.txt`. ### Call: Counterpart persona synthesis (pre-session) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive a ScenarioBrief and produce a CounterpartConfig JSON. The CounterpartConfig is read by (1) the Live session's systemInstruction and (2) the Nano Banana 2 portrait call. Produce CounterpartConfig fields: - display_name: generic short name appropriate to the language locale (English/global: Riya, Marc, Casey, Sam, Devi, Tariq, Mei, Anouk, Jordan, Alex). Localise to BCP-47. NEVER use a real public figure's name. If the brief mentions a specific person, pick a different name. - vibe: pass through. - portrait_prompt: photographic head-and-shoulders portrait, 1024×1024, gentle natural light, neutral background, looking toward camera. Clothing matched to counterpart_role (smart-casual hiring manager; business-casual customer; wedding-guest formal). NEVER celebrities, brands, identifiable persons. Vibe inflection: warm = soft smile; neutral = composed; sceptical = thoughtful, no smile, no contempt. - voice_name: pick from available Gemini 3.1 Flash Live voices for the locale and vibe. Warm = friendly, measured. Neutral = clean, professional. Sceptical = slightly cooler, more pause discipline. - opening_line: the first thing the counterpart says, one to two sentences. Warm interview example: "Hi — thanks for making time. Want to start whenever you're ready." Sceptical pitch example: "Okay — you've got two minutes. What are you selling me, and why should I care?" Sceptical, not hostile. - follow_up_strategy_summary: one or two sentences. Example: "Ask one follow-up on the most concrete part of the answer. If they generalise, bring them back to a specific example." - language_bcp47: pass through. - hard_rules_for_session: literal array embedded into the Live systemInstruction. ALWAYS include: * "Counterpart, not coach. Never give feedback during the session. Save observations for after the timer." * "Respond like the role would. React naturally — agree, disagree, follow up, push back where the role calls for it. Not artificially encouraging." * "No medical/legal/food-safety advice. Do not editorialise on the user's life choices outside the scenario." * "Never play a hostile or contemptuous archetype. Sceptical = honestly probing, not aggressive." * "Speak in language_bcp47. If the user code-switches, respond in the dominant language of their utterance." * "When setTimerWarning fires, wind down within 10 seconds — finish your thought, give the user a chance to add anything, close gracefully." * "Tools: endSession (user explicitly asks to stop or says goodbye), requestPause (user appears to need a pause). Do not call setTimerWarning yourself." * "Never reveal you are a language model unless the user asks directly. If they ask, one honest sentence: 'I'm a practice partner running on Gemini Live. For the next [N] minutes I'm the [role] you're rehearsing with.' Then offer to continue." Hard rules for THIS call: - Output ONLY the CounterpartConfig JSON. - The hard_rules_for_session array is non-negotiable. - If the brief asks for hostile roleplay, override vibe to "sceptical" and add a hard rule: "User requested hostile. We're playing this as sceptical — honest scepticism, not antagonism." - If the brief is medical/legal/safety adjacent, add: "The debrief will surface a not-a-substitute disclaimer. Do not offer professional advice in your role." Output ONLY the CounterpartConfig JSON. No commentary. ``` --- ### Call: Counterpart portrait Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a ``` Generate a single photographic-looking head-and-shoulders portrait of a generic professional person matched to the counterpart role and vibe in the prompt. This still image pairs with the audio voice generated by Gemini 3.1 Flash Live during the session. Framing: - Head-and-shoulders, 1024×1024. - Eyes looking toward the camera with a soft, present expression — the user should feel they are looking at someone who is listening. - Gentle natural light. Neutral office or domestic background, slightly out-of-focus. - Clothing matches the role (smart-casual hiring manager, business-casual sales prospect, wedding-guest formal, apron for cooking-demo audience). Vibe inflection: - warm = small natural smile, soft eyes, slight forward lean - neutral = composed, calm, neither smiling nor frowning - sceptical = thoughtful expression, intelligent attention, NOT a frown, NOT contempt Hard rules: - Photographic, not illustration, anime, or 3D render. - No celebrities or recognisable real-world persons. - No commercial brand logos in frame. - No readable text in the image. - No children unless the scenario explicitly involves one. - Diverse representation across regenerations — do not default to a single demographic. - Slight realistic imperfection — real skin texture, real hair, real eyes. Avoid the glossy AI-render look. Output: one 1024×1024 PNG. ``` --- ### Call: Live conversation Model: `gemini-3.1-flash-live-preview` · n/a · Tools: endSession, setTimerWarning, requestPause (sync only) ``` You are a counterpart in a live rehearsal. Your persona and behavioural rules are in the CounterpartConfig you were initialised with — specifically, the hard_rules_for_session array. Your role for [duration_seconds]: - You are [counterpart_role_one_line] with [vibe] energy. - The user is [user_role_one_line]. - Speak [language_bcp47]. - React naturally. Follow up where a real person in your role would. Push back where warranted. Agree where warranted. You will not: coach mid-conversation, break frame to give advice, moralise about the scenario, play hostile or contemptuous, reveal you are a language model unless asked directly. If asked, one honest sentence: "I'm a practice partner running on Gemini Live. For the next [N] minutes I'm the [role] you're rehearsing with." Then offer to continue. Function calls (synchronous only): - endSession({reason}) — user explicitly asks to stop, says goodbye and means it, or the scenario calls for ending. Not for being polite. - setTimerWarning() — DO NOT call yourself. Client calls at 0:10. - requestPause({reason}) — user appears to need a pause (technical issue, or they ask). When the timer warning fires: finish your thought in 5s, offer one chance to add anything ("Anything else?"), close gracefully ("Thanks for practising with me — let's stop here."). Pacing: match the user's tempo. Allow 2-3 second silences — don't interrupt thinking pauses. Use natural disfluencies sparingly (occasional "mm", "right"). Language: respond in language_bcp47. If the user code-switches, respond in the dominant language of their utterance — do not draw attention to the switch. For language practice, speak at conversational native pace; slow only if asked. Do NOT correct grammar — that's the debrief's job. Scenario behaviours: - Job interview: one warm-up, then one behavioural or role-specific question. Follow up on concrete details. If they generalise, gently ask for a specific example. - Sales pitch: react as the customer would. Ask what a real customer would ask. - Cooking demo: react as an audience member. One or two natural questions about ingredients or technique. - Language practice: a normal conversation in the target language. Casual topic if none specified. - Stand-up set: honest audience member. Laugh if it lands. Stay quiet if it doesn't. No fake laughs, no heckling. One natural follow-up after the set. - Wedding toast: listen as a wedding guest. Quiet appreciation. One warm follow-up about a story. - Difficult conversation: stay in role. Honour the difficulty; do not defuse artificially. Format: audio + live transcript channel. Short turns — one to three sentences unless the scenario calls for longer. Do your job. Stay in role. ``` --- ### Call: Pre-session breathing-exercise TTS Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: calm, unhurried, present. Pick the Gemini 3.1 Flash TTS voice whose languageCode matches the session language. Use case: optional 30-second breathing exercise before the Live session opens. Reduces pre-rehearsal jitters. Pre-process the text: - At sentence boundaries, insert "…" for a natural pause. - At paragraph boundaries, insert a blank line plus "—". - Gemini 3.1 Flash TTS does NOT support SSML; these textual cues are how pace is conveyed. Style: prepend ONE short directive sentence to the input: "Read this slowly and calmly, like a friend helping someone settle their nerves before a conversation. …" Target rate: ~110 wpm. The literal script (translate to session language, preserving rhythm): "Okay. Take a moment before we start. … Sit, or stand, with your feet flat on the floor. … Drop your shoulders. … Let your jaw soften. — Breathe in through your nose for four. … Hold for two. … Breathe out through your mouth for six. — Once more. … In for four. … Hold for two. … Out for six. — You've prepared for this. … The next two minutes are practice, not the real thing. … There's nothing to win, and nothing to lose. — When you're ready, the session will begin." Phoneme overrides are NOT exposed by Gemini 3.1 Flash TTS — no SSML . Pronunciation comes from the voice's native locale. ``` --- ### Call: Post-session debrief → `DebriefNote` Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You receive the session transcript, the server-computed AudioFeatureSummary (NOT computed by you), the ScenarioBrief, and the CounterpartConfig. Produce ONE DebriefNote with the single most useful delivery observation anchored to a specific moment. How to pick the one note: 1. Scan the transcript and audio features for the moment the user's delivery did something they probably did not intend. Examples: - voice softened on a word they meant firmly ("difficult", "boundary", "leave", "deserve") - ran out of air mid-sentence and rushed the close - answered a different question than was asked - contradicted themselves twenty seconds apart - ended a strong statement on upward inflection - used a filler word ≥4 times in 20 seconds - paused unusually before answering 2. Pick the ONE moment that, if noticed before Tuesday, would change how the user shows up. Concrete over abstract. "At 0:48 your voice softened on 'difficult'" is concrete; "Be more confident" is not — we never give the latter. 3. Anchor: one_note_anchor_timestamp_seconds (nearest 0.5s), one_note_anchor_transcript_quote (verbatim user words), one_note_anchor_audio_window_start_s / end_s (3s before, 3s after). 4. one_note_one_line: short, punchy, specific. Examples: - "At 0:48 your voice softened on 'difficult' — if you mean it firmly, say it firmly." - "From 1:12 to 1:24 you said 'um' four times. The pause itself is fine; fill it with breath instead." - "Around 1:35 you answered a question you weren't asked." 5. one_note_full_explanation: 3 sentences max. Pattern, moment, one thing to try differently. 6. one_note_category: from the enum. 7. things_left_unsaid: 1-3 other observations, one sentence each. Framed as "we also noticed these — not your one thing, but here if you want them." 8. confidence: based on transcript clarity, moment specificity, pattern reliability. <0.6 → soften phrasing ("you may have softened" not "you softened"). 9. honest_caveat: one sentence if relevant ("the mic was a bit hot — loudness measurement may not reflect what the listener heard"). Null if none. Hard rules: - EXACTLY ONE one_note_one_line. - DESCRIBE a moment or pattern. No abstract life advice. No "be more confident". No "smile more". - Describe the moment, do NOT judge the person. "Your voice softened" = description. "You sounded weak" = judgement. Never judge. - contains_hostile_critique MUST be false. Server rejects notes that judge the person rather than the moment. - Anchor timestamp must be inside [0, duration_seconds_actual]. Clamp the window at boundaries. - If session was <30s or dropped early, surface in honest_caveat and lower confidence. - For medical/legal/safety adjacent scenarios: is_food_safety_or_medical_or_legal_adjacent = true. - Debrief speaks the session's language. The quoted moment stays in the original language regardless. - Do NOT recommend specific words to say instead. We give delivery observation, not script-writing. - Do NOT predict outcomes ("this would have lost you the job"). We observe the rehearsal; we do not forecast. - Do NOT moralise about the user's scenario. Output ONLY the DebriefNote JSON. No commentary. ``` --- ### Call: Hero / empty-state illustration Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a ``` Generate a single photographic-looking image for the welcome screen or empty-state. Subject anchors that work: - "a softly lit home desk at evening, a laptop open with its screen warm and indistinct, an empty notebook beside it, a single glass of water, a phone face-down — no person in frame, no readable text, the moment before a rehearsal" - "a quiet hotel-room corner at golden hour, a folded jacket over a chair, a pair of soft shoes, a window with pale curtain — no person in frame, the moment before a wedding speech" - "a kitchen counter with notes laid out in three small rows of recipe cards, a wooden spoon, a half-cut onion, no person in frame, warm overhead light — the moment before a cooking demo" Hard rules: - Photographic, not cartoon or illustration. - No people in frame unless scenario requires. - No commercial branding. - No readable text. - Warm lighting, slight imperfection, real-fabric texture, real-paper texture — avoid glossy AI-render look. - Aspect ratios: 3:2 for hero, 1:1 for empty states. - No microphone or headset prop (we don't want to telegraph "podcast aesthetic"). ``` ## 5. Use cases & content to include Build dedicated UI sections or flows for each of these. - **Tuesday-morning interview, Monday night.** Preparing for a 10 am senior PM interview. Opens the app at 11:47 pm. Picks "Job interview", pastes the JD, picks **warm**, taps Go. Counterpart (Riya) opens with "Hi — thanks for making time. Want to start whenever you're ready?" Asks the walk-me-through-a-time question. Follows up on the concrete part. Debrief: "at 0:48 your voice softened on 'difficult' — if you mean it firmly, say it firmly." - **Pitch rehearsal before Sand Hill Road.** Founder rehearsing the Series A pitch picks "Sales pitch", pastes the deck outline, picks **sceptical** (a real investor will probe). Counterpart (Marc) opens with "Okay — you've got two minutes. What are you selling me, and why should I care?" Two pointed follow-ups. Debrief: "From 1:12 to 1:24 you said 'we are going to' three times — the certainty is good but the repetition undercuts it. Say it once, hold the silence, let it land." - **Wedding toast in a hotel bathroom.** Best man, 8:15 am day of the wedding. Picks "Wedding toast", pastes bullet points (the airport story, the line about the bride's laugh, the closing). Picks **warm**. Counterpart (Casey) smiles at the airport story, asks "what was it about her laugh that did it?" Debrief: "around 2:34 you ran out of breath on the closing line — take a breath before 'and that's why I love you both'." - **Immigration interview in English (Portuguese-BR daily speaker).** Picks "Language practice", language en-US, picks **neutral** (officers are not warm or hostile). Counterpart (Jordan) asks officer-style questions in English. Debrief: "at 0:54 you switched into Portuguese for two words — that's normal under pressure, but the officer will hear it; if it happens, finish in Portuguese deliberately and say 'let me say that again in English.' Don't trail off." - **Stand-up set, open mic in two days.** Picks "Stand-up set", pastes the set list, picks **neutral**. Counterpart (Tariq) laughs naturally at jokes that land, stays quiet at ones that don't, asks one follow-up. Debrief: "the third joke didn't land at 1:43 — the setup was clear but the punchline arrived two beats late; cut the 'so anyway' before it." - **The "are you a real person?" moment.** Mid-session a user asks "wait, are you a real person?" The counterpart responds honestly with one sentence: "I'm a practice partner running on Gemini Live. For the next 90 seconds I'm the hiring manager you're rehearsing with." Offers to continue. - **The disconnect.** WiFi drops at 0:47. Reconnect tries for 3 seconds and fails. Session ends gracefully. Debrief loads on partial transcript with the honest caveat: "connection dropped at 0:47. Here's what we noticed in the first 47 seconds." - **Difficult conversation: asking for a raise.** Picks "Difficult conversation", brief: "I've been at this company two years, I want 12%, my manager is sceptical." Picks **sceptical**. Counterpart responds as the sceptical manager would. Debrief: "at 1:38 you walked back your number — you said '12% — or, you know, whatever you think.' If you mean 12%, say 12% and stop talking." ## 6. Page structure Build the following screens in this order. Adjust copy to fit the voice, but keep the structural intent. 1. **Welcome / sign-in.** A photographed-looking shot of a softly lit home desk at evening — laptop open, empty notebook beside it, glass of water, phone face-down. One paragraph: "You have a real conversation on Tuesday. Practise it out loud, tonight, with a counterpart who reacts naturally and gives you one specific note at the end. Two minutes. No recording kept." Google sign-in button; below: "Skip sign-in (no Notebook — sessions evaporate)" → scenario picker in anonymous mode. 2. **Scenario picker.** A grid of seven scenario cards plus **Custom**. Each card: category name, one-line description, a small photographic thumbnail (the wedding-toast card shows a folded jacket on a hotel chair; the cooking-demo card shows recipe cards on a counter). Tap → brief form. 3. **Scenario brief form.** Three to five short fields, adapted per category. Below: language picker (defaults to browser locale), modality (audio+video / audio-only), vibe (warm / neutral / sceptical), duration slider (60s / 90s / 120s for a/v, or up to 15 min audio-only). 4. **Counterpart preview.** Pre-session screen. The generated counterpart portrait fills the upper half with their `display_name` and `opening_line`. Below: "Ready? You can run a 30-second breathing exercise first, or skip to Go." Two buttons: **Breathing exercise** (small) and **Go** (large, primary). A small "regenerate portrait" link (one regenerate allowed pre-session). 5. **Readiness check.** 10-second mic + camera test. Mic level meter, video preview. If the room is loud, a soft nudge — not a block. 6. **Live session.** Full-screen. Counterpart portrait on the larger side; user's camera in a corner tile. 2:00 / 15:00 timer at top. Live transcript band under each tile. A single **End session early** button at the bottom (small, neutral — not red; we don't want to make stopping feel like failure). At 0:10, timer pulses gently; at 0:00, the counterpart finishes their thought and the session ends. 7. **Loading the debrief.** A short pause (3-6 seconds) with "Listening back to your rehearsal…" and a thin progress line. Not a spinner. 8. **Debrief screen.** The single most useful observation in large type at the top. Below: the 3-sentence rationale, the anchor timestamp, the verbatim transcript quote, a **Play this moment** button replaying the 6-second window. Below that: the collapsed-by-default full transcript. At the bottom: "We also noticed these — not your one thing, but here if you want them" (the `things_left_unsaid`, muted, max 3). Above: a **Keep this session** / **Discard** toggle (default Discard, 30-second auto-discard countdown). 9. **One more round.** Resets the timer. Counterpart aware this is take 2; opens with a different question. Previous debrief note quietly in context for the follow-up questions — still no mid-flow coaching. 10. **Notebook (signed-in users only).** List of Kept sessions grouped by scenario. Each row: date, one-note one-liner, play-the-moment button. Tap → detail (transcript + debrief + audio window). Delete with 60-second cool-off. 11. **Settings & privacy.** Language preference, default vibe, default duration. "Delete all my kept sessions" / "Delete this account" with 60-second cool-off. Plain- English privacy panel. 12. **Footer.** "Made for the conversation you have on Tuesday." Privacy: "Ephemeral by default." Capabilities `(i)` icon in header. A small "Live API is preview" badge. ## 6b. First-visit onboarding Show on first visit (detect via `localStorage` flag). Re-entry via a `?` icon in the header. **Slide 1 — What this is.** - Headline: "Welcome to Live Practice Partner." - Subhead: "Rehearse Tuesday's conversation out loud, tonight. Two minutes, one note, no recording kept." - ≤60-word paragraph: "You have a real conversation coming up — an interview, a pitch, a cooking demo, a toast. This app gives you a friendly counterpart on the other side of the table for two minutes, then one specific note about your delivery. By default, nothing is saved. The counterpart never coaches you mid-conversation — that's the debrief's job." - Visual: small annotated illustration of the live session screen (counterpart portrait, user tile, 2:00 timer, transcript band) — not a generic camera icon. **Slide 2 — Try it now.** - "Try the sample interview". - A pre-filled brief loads: senior PM role, warm vibe, 120s. The user taps Go and lands directly in the live session. - 1-2 sentences pointing at the specific places where the Gemini magic happens (the live conversation on Gemini 3.1 Flash Live, the one specific note in the debrief from Gemini 3.5 Flash). **Slide 3 — How to remix this.** - Three short bullets: - "Add a new scenario category to `/data/scenario-presets/`." - "Adjust the counterpart vibes in `/server/prompts/`." - "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel." - Primary CTA: "Use this template" → AI Studio Build remix. - Secondary: "Just exploring — close" (sets localStorage, never auto-shows again). **Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored on close. Respect `prefers-reduced-motion`. **Don't:** gate content behind the modal; auto-reshow on return visits; include unrelated CTAs. ## 6c. Capabilities info button (persistent in header) `(i)` icon in the top-right. Click → modal/panel titled **"What powers this app"**. **Gemini capabilities (the hero list):** - **Gemini 3.1 Flash Live (preview)** — `gemini-3.1-flash-live-preview`. The bidirectional audio + video Live API that powers the 2-minute rehearsal. Acoustic nuance improvements shipped at I/O 2026 mean the counterpart hears tone, pace, and pitch — not just words. Audio+video caps at 2 minutes; audio-only at 15 minutes. Synchronous function calling only. Live API is still **preview** as of June 2026; expect occasional drops on weak connections. - **Gemini 3.5 Flash** — `gemini-3.5-flash`. The new default flagship as of I/O 2026. Powers the pre-session counterpart persona synthesis (low thinking) and the post-session one-note debrief (medium thinking). Beats the previous Gemini 3.1 Pro on multimodal benchmarks at Flash-tier price. - **Nano Banana 2** — `gemini-3.1-flash-image`. Generates the still counterpart portrait paired with the Live audio voice. One generation per session; one regenerate pre-session. Welcome and empty-state imagery uses the same model. - **Gemini 3.1 Flash TTS (preview)** — `gemini-3.1-flash-tts-preview`. Reads the optional 30-second pre-session breathing exercise. Shipped 2026-04-15, replacing the older `gemini-3.1-flash-tts-preview`. No SSML; pauses encoded as ellipses and blank-line em-dashes. - **Firebase Auth** — Google sign-in (auto-provisioned by AI Studio Build); Apple sign-in optional. Anonymous mode supported with no Notebook. - **Firestore** — stores Kept sessions only. Ephemeral sessions never touch Firestore. - **Firebase Storage** — stores the 6-second audio window for Kept sessions only. Requires manual enable in the Firebase console (not auto-provisioned). Full session audio is never uploaded. - **Cost note** — see 6d. A 120-second audio+video session including portrait, Live conversation, and debrief costs ~$0.18-$0.28. - **Privacy note** — your audio, video, and transcript are private to you. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The Live API audio and video travel browser-to-Google over a WebSocket; they do not transit our servers. By default, **nothing is saved**. Saving requires an explicit Keep tap. - **What this app cannot do** — it cannot predict the specific interviewer, customer, or audience you will meet. It plays a generic warm / neutral / sceptical counterpart. The debrief observes your delivery; it does not guarantee outcomes. For medical/legal/safety adjacent scenarios it is not a substitute for professional advice. **Backend services:** see section 4b. Apple sign-in optional. Storage requires manual enable for Keep. **Environment variables:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `LIVE_TOKEN_SIGNING_SECRET` — for ephemeral Live session tokens **Documentation links:** AI Studio Build docs; Gemini Live API docs; Gemini 3.5 Flash docs; Nano Banana 2 docs; Gemini 3.1 Flash TTS docs; Firebase Auth / Firestore / Storage docs. **Accessibility:** focus trap, `Esc`, ARIA, restored focus. Tooltip on `(i)`: "How this app is built". Mobile: full-screen sheet. Most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model; always flag Live API as preview. ## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping) - **Counterpart persona synthesis (Gemini 3.5 Flash, low)** — ~2,000 input + ~800 output tokens at $1.50/M in, $9/M out ≈ ~$0.010 per session. - **Counterpart portrait (Nano Banana 2)** — approximate ~$0.02 per image (Google has not pinned an exact public per-image figure; verify before shipping). One regenerate allowed adds ~$0.02 worst case. - **Pre-session breathing TTS (3.1 Flash TTS)** — approximate ~$10/M output tokens (the exact TTS character-token price was not pinned at I/O 2026; treat as a working estimate and verify before shipping). 100-word script ≈ ~$0.0019 on the working estimate. - **Live API audio+video session (Gemini 3.1 Flash Live)** — billed per second of audio + per frame of video. A 120-second session averages ~$0.15-$0.20 at current Live pricing (verify before shipping). Audio-only proportionally lower per-minute. - **Post-session debrief (Gemini 3.5 Flash, medium)** — ~3,000 input + ~600 output tokens ≈ ~$0.010-$0.013 per debrief. - **Hero / empty-state imagery (Nano Banana 2)** — ~$0.02 per image. Generated once per install, cached. - **Expected per-session cost (audio+video, 120s):** ~$0.20. - **Expected per-session cost (audio-only, 600s):** ~$0.30-$0.40. - **Daily quota suggestion:** 3 audio+video sessions per user per day (or 30 minutes audio-only) caps free-tier cost to ~$0.60-$1.20 per active user per day. Enforce via per-user Firestore counter; reset at UTC midnight. - **Firebase Storage:** the 6-second WAV window is ~96 KB at 16 kHz mono. 100 Kept sessions ≈ ~10 MB ≈ ~$0.0003/month per user. ## 7. Design language - **Mood:** The room before a real conversation. Quiet. Considered. The desk lamp on at 11:47 pm. The hotel bathroom at 8:15 am. The kitchen counter on Saturday morning. Not a SaaS dashboard. Not a coaching app. Not a video-call interface imitating Zoom. The surface you reach for when practising in your head is no longer enough. - **Typography:** Clean grotesque for app chrome (Inter or Geist). A measured display serif for the counterpart's display name and the one-note headline (Source Serif Pro or Fraunces — like a printed note, not brand voice). The live transcript uses the chrome face at smaller size in a quiet grey. - **Palette:** Soft paper `#F6F3EE` background; deep ink `#1A1A1F` text. Measured indigo `#3B4A7A` as primary action colour (Go button, One more round) — not a jaunty blue. Muted amber `#B58A4B` for the timer's pulse at 0:10 — never red, urgency here is gentle. Faded sage `#8FA48E` for the ephemeral indicator dot. Whisper-grey `#E4E0D9` for the transcript band. - **Imagery:** Photographic. The desk before the rehearsal. The hotel chair. The kitchen counter. No people in hero imagery — the person is the user, about to arrive at their own conversation. The counterpart portrait is the only photographic-looking person, generated per session. - **Hand-feel touches:** The counterpart portrait blinks slowly and breathes (subtle scale animation) so the screen does not feel frozen. Respects `prefers-reduced-motion`. The 2:00 timer counts down with a soft visual tick. The debrief one-note headline fades in measured. The "play this moment" button has a small waveform built from the 6-second window. - **Spacing:** consistent 4-px base. - **Radius:** consistent token set (8 / 16 / 24 px). - **Shadows:** subtle, layered, warm-tinted. - **Motion:** purposeful. Go button has a slow press. Timer at 0:10 pulses twice. Debrief headline fades in over 400 ms. Respect `prefers-reduced-motion`. The ephemeral dot has a slow 2-second breathing fade — disabled under reduced motion. - **States:** hover, focus, active, disabled on every interactive element. Skeletons over spinners. Empty states have helpful next-action labels. ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders. - Invent plausible scenarios, briefs, counterpart names, opening lines, debrief notes (use the seed content in 8a). Lean on universal jobs — the interview, the toast, the difficult conversation — and skip the "named persona in a specific city" framing the /100 set uses. - Tone: warm, direct, free of corporate language. Friend sitting next to the user, not marketing copy. - Headlines: punchy and concrete. No 'Empower your X'. No 'Revolutionize'. No 'Seamless'. No 'AI-powered'. The Live API is named (Gemini 3.1 Flash Live) in the panel. - Body: short paragraphs (2-4 sentences). - Plain language. Avoid jargon. - AI-generated content is never labelled "AI says". The counterpart speaks naturally; the debrief is anchored to a moment. - Debrief language is descriptive, not judgemental. "Your voice softened on 'difficult'" — not "you sounded weak." ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data in the content below. **Scenario cards (the picker):** - **Job interview** — "Practise the conversation you have with the hiring manager." Thumbnail: a notebook on a home desk. - **Sales pitch** — "Practise the two-minute pitch with the customer or the investor." Thumbnail: a closed laptop beside a glass of water. - **Cooking demo** — "Practise the patter with a friendly guest." Thumbnail: recipe cards laid on a kitchen counter. - **Language practice** — "Practise a real upcoming conversation in the language you're learning." Thumbnail: a textbook open beside a phone face-down. - **Stand-up set** — "Run your five minutes in front of a natural audience member." Thumbnail: a single bar stool in soft light. - **Wedding toast** — "Practise the toast in front of one warm guest." Thumbnail: a folded jacket over a hotel chair. - **Difficult conversation** — "Practise the conversation you've been avoiding — with the right energy on the other side." Thumbnail: two empty chairs across a small table. - **Custom** — "Tell us the conversation. We'll set up the counterpart." Thumbnail: a blank notebook page. **Sample scenario brief (the demo):** - Category: Job interview - User role: "candidate for senior PM role at a Series B fintech" - Counterpart role: "hiring manager (head of product)" - Context (JD excerpt): "We're looking for a senior PM to lead our merchant-onboarding pillar. You'll partner with eng, design, and risk to reduce time-to-first-payment for new merchants. We expect strong opinions weakly held, clear written thinking, a track record of shipping with cross-functional stakeholders." - User known worry: "the behavioural ones — I always ramble on the time-you-pushed-back question" - Language: en-US - Modality: audio+video - Vibe: warm - Duration: 120 seconds **Sample counterpart config (produced for the brief above):** - display_name: Riya - vibe: warm - voice_name: (a warm en-US Gemini Live voice) - opening_line: "Hi — thanks for making time today. Want to start whenever you're ready. I'll open with one question, then we'll see where it goes." - follow_up_strategy: "Ask one warm-up question, then one behavioural question. Follow up on the most concrete detail. If the candidate generalises, bring them back to a specific example." **Sample session transcript (first ~50 seconds):** - Riya (0:03): "Hi — thanks for making time today. Want to start whenever you're ready. I'll open with one question, then we'll see where it goes." - User (0:11): "Yeah, ready, go ahead." - Riya (0:13): "Okay. Walk me through a time you had to push back on a stakeholder who outranked you." - User (0:20): "Sure, so… last quarter we were planning the Q4 roadmap and our VP of product wanted to prioritise a partnership integration that I didn't think was the right call. I, uh, I disagreed because the data showed our merchant-onboarding flow was losing 18% of new sign-ups at the KYC step, and that was a much bigger lever. So I, um, scheduled a meeting with him and walked him through the funnel data." - Riya (0:48): "Got it — and what changed for you between deciding to push back and actually pushing back? That gap is sometimes the most interesting part." - User (0:58): "Yeah, that's a good question. I think… the difficult part was just, you know, deciding the data was strong enough to go to him. I had to be willing to be wrong in front of him. And…" **Sample debrief note (produced from the transcript above):** - one_note_one_line: "At 0:48, when you said 'the difficult part', your voice softened on the word 'difficult'. If you mean that the disagreement was hard, say it firmly." - one_note_full_explanation: "Around 0:48 you described the pushback as 'difficult' but said 'difficult' on a falling tone, like you were apologising for finding it hard. A hiring manager hears that softening and mirrors it back — they'll remember that you found it hard and that you seemed embarrassed about it. Say 'difficult' the way you'd say 'important', and let it land." - one_note_anchor_timestamp_seconds: 48 - one_note_anchor_transcript_quote: "the difficult part was just, you know, deciding the data was strong enough" - one_note_anchor_audio_window_start_s: 45 - one_note_anchor_audio_window_end_s: 51 - one_note_category: softening_when_meaning_firmly - confidence: 0.74 - honest_caveat: null - things_left_unsaid: [ "You used 'um' four times in your first answer — not enough to distract, but worth noticing.", "Your opening answer was strong on the data; the transition into the meeting itself could be one sentence shorter." ] - is_food_safety_or_medical_or_legal_adjacent: false - contains_hostile_critique: false **Sample voice copy:** - Welcome hero: "You have a real conversation on Tuesday. Practise it out loud, tonight." - Scenario picker subtitle: "Pick a scenario, paste your brief, choose a vibe. The counterpart is generated fresh for you." - Counterpart opening line examples — warm: "Hi — thanks for making time today. Want to start whenever you're ready." Sceptical: "Okay — you've got two minutes. What are you selling me, and why should I care?" Neutral: "Hello. Please state your purpose for travel." - Pre-session breathing intro: "Take a moment before we start. The next two minutes are practice, not the real thing." - Readiness check: "Mic looks good. Camera looks good. Whenever you're ready, tap Go." - Live ending (counterpart): "We're nearly out of time — anything you want to add?" → "Thanks for practising with me. Let's stop here." - Debrief loading: "Listening back to your rehearsal…" - Debrief one-note format: "At [0:48] your voice [softened on 'difficult'] — [if you mean it firmly, say it firmly]." - One more round button: "One more round →" - Keep prompt: "Keep this rehearsal in your Notebook? We'll save the transcript and the 6-second moment. Nothing else." - Discard confirmation: "Discarded. The session is gone." - Notebook empty state: "Your kept rehearsals will live here. Tap Keep at the end of a debrief to save one." - Privacy panel headline: "Ephemeral by default. Saved only when you tap Keep." ## 9. Media & assets - **Hero image (landing):** photographed-looking softly lit home desk at evening — laptop open with warm indistinct screen, empty notebook, glass of water, phone face-down. Generated via Nano Banana 2 per the hero systemInstruction. - **Scenario card thumbnails:** eight photographed-looking thumbnails (one per scenario in section 8a, including Custom), 4:3 WebP at 1024×768 each. Generate once at build time via Nano Banana 2 (`gemini-3.1-flash-image`); "moment before" object scenes — never a person, never a logo. Per- thumbnail prompts (object scenes, warm soft light, no people, no commercial branding, real worn surfaces): job_interview → "open paper notebook on a home desk at morning light, pen resting across it"; sales_pitch → "closed laptop on a desk beside a glass of water, late morning light"; cooking_demo → "three recipe cards laid on a wooden kitchen counter, warm overhead light"; language → "open language-learning textbook on a desk beside a phone face-down, afternoon light"; standup → "single empty bar stool in soft amber spotlight on a small stage"; wedding → "neatly folded dark jacket draped over a hotel chair, soft evening light through a window"; difficult_conversation → "two empty wooden chairs across a small round café table, soft afternoon light"; custom → "blank paper notebook page open on a desk with a pen resting diagonally, soft morning light". Ship as seed assets at `/public/scenarios/.webp`. - **Counterpart portrait (per session):** generated at session start via Nano Banana 2 (`gemini-3.1-flash-image`), 1:1 at 1024×1024. Head-and-shoulders, photographic, vibe- and-role matched per the session brief. Prompt template: "photographic head-and-shoulders portrait of a generic person matching the role '' and vibe '', soft natural light, neutral background, no recognisable celebrity, no commercial branding, slight imperfection". Stored in Firebase Storage at `sessions/{sessionId}/counterpart.webp`. No celebrities, no brand logos. One regenerate pre-session. - **Empty-state illustrations:** simple object scenes — a folded jacket on a hotel chair (wedding-toast empty state); recipe cards on a counter (cooking-demo empty state). These reuse the same scenario-thumbnail seed files above cropped to 1:1 (`/public/scenarios/wedding.webp`, `/public/scenarios/cooking_demo.webp`). For scenarios without an empty-state variant, fall back to the matching scenario thumbnail at 1:1. - **App icon / wordmark:** display serif, with a small sage ephemeral-dot beside the wordmark. - **Generated imagery:** prefer Nano Banana 2 over stock. Prompt for warmth, asymmetry, slight imperfection. - **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height`. - **Icons:** `lucide-react`. Use sparingly. The mic, camera, and timer icons are the most important; everything else is text. ## 10. Interactivity & states - Hover, focus, active, disabled on every interactive element. - Inline form validation with specific error messages: "Your brief is empty — paste a short description (a JD, a deck outline, or a one-sentence summary) so the counterpart has context." - Skeletons over spinners. The debrief-loading state uses a thin progress line with "Listening back to your rehearsal…". - Empty states have THIS-app-specific labels: "Pick a scenario", "Tap Keep at the end of a debrief to save", "Start another rehearsal". - Smooth scroll on in-page anchors. - Live session transcript streams in real-time. Debrief streams once the call completes. - Live connect failure: calm specific error ("The session couldn't open — try once more, or check your network.") with retry. - Live mid-session drop: reconnect for 3 seconds; if it fails, end gracefully and load debrief on partial transcript with honest caveat surfaced. - 2:00 timer pulses gently at 0:10 with warm-amber accent — no flashing red. - "Play this moment" button shows a small waveform; tapping plays the 6-second window and re-highlights the transcript quote in sync. - Keep / Discard defaults to Discard with a 30-second countdown visible if the user is signed in. - Anonymous users see Notebook entry point greyed with tooltip "Sign in to save rehearsals to your Notebook." — never a forced sign-in modal. - Ephemeral-dot indicator pulses while a session is in memory; stops when the session evaporates. - "Are you a real person?" mid-session moment is handled gracefully — the counterpart's transcript shows the honest one-sentence reveal; the user continues or stops. ## 11. Tech & responsive requirements - **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Shadcn UI primitives where appropriate. The Live session view uses a custom layout (portrait + camera tile + timer + transcript band) — not a generic video-call component. - **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. The ephemeral Live session token is minted server-side; the Gemini API key never leaves the server. The browser opens the Live WebSocket directly to Google using the ephemeral token. - **Model selection:** explicitly pin `gemini-3.1-flash-live-preview` for the Live session, `gemini-3.5-flash` for persona synthesis and debrief, `gemini-3.1-flash-image` for portrait + hero, `gemini-3.1-flash-tts-preview` for the breathing exercise. Set `thinkingLevel` explicitly on the two `gemini-3.5-flash` calls (low for persona, medium for debrief). Omit `thinkingConfig` entirely on Live, TTS, and image calls. - **Audio handling:** - Capture: web Recorder API + getUserMedia. Downsample to 16 kHz mono PCM client-side before streaming. - Playback: Live returns 24 kHz mono PCM via Web Audio API. Small crossfade between turns. - 6-second window: client maintains a rolling PCM buffer during the session. When the user taps Keep, extract the window around the anchor timestamp, encode as WAV, upload to Firebase Storage — only at that point. Full session audio is never uploaded. - **Video handling:** user's camera stream to Live API at ≤1 FPS per API constraint. Client-side user preview tile shows the local camera at full FPS. The counterpart's "video" is the still portrait plus subtle breath/blink — no live-rendered counterpart video. - **Database:** Firestore (auto-provisioned). Used only for `users`, `kept_sessions`, `scenarios_custom`. Ephemeral sessions never write. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in optional; anonymous mode supported. - **Storage:** Firebase Storage — only for Kept-session 6-second windows. Pre-signed URLs only. Not auto-provisioned by AI Studio Build; enable manually if using Keep. - **Live API session token:** short-lived token minted server-side via Cloud Run, handed to the browser, browser opens the WebSocket directly to Live API. Token expires at session end and cannot be reused. - **Live API session state recovery.** The Live session caps at 2 minutes (audio+video) / 15 minutes (audio-only); on every Live tick the client persists a `SessionSyncState` to `sessionStorage`, and on reconnect it passes a concise context summary block into the first system message of the next handshake cycle so the counterpart picks up where it left off: ```typescript interface SessionSyncState { activeSessionId: string; accumulatedSegments: Array<{ speaker: string; text: string; timestamp: number }>; currentRubricIndex: number; // current beat in the scenario plan counterpartPersonaId: string; // resolved persona for this session timerElapsedMs: number; // for the rehearsal timer } ``` - **Mobile-first.** Verify at 375 / 768 / 1024 / 1440+. The Live session view must work in mobile portrait — counterpart portrait stacks above the user's small camera tile. - `clamp()` for fluid typography. Container queries over media queries for component-level responsiveness. - `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS — timer and End session button must clear the home indicator. - Zero horizontal overflow at any width. Zero layout shift on load. - Optimistic UI on Keep / Discard. - Live session view requests the screen-wake-lock API so the screen does not sleep mid-rehearsal; falls back gracefully if unsupported. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with visible focus ring. - Contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. - Meaningful `alt` text on images. The counterpart portrait has alt describing vibe and role ("a warm generated portrait of the practice counterpart in the role of hiring manager — short hair, soft smile, smart-casual shirt"). - Form fields have associated `