# 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 `