================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Translation Booth
## 1. Project
**Translation Booth** is a live three-way medical interpretation app for
the bilingual neighbour, the cousin in town, the aunt who answers the
phone — the people who keep getting asked to translate at the GP. The
user dials a doctor, hands their phone to the patient, and Translation
Booth holds the middle seat of the conversation: the patient speaks in
their language, the doctor hears medically accurate English, the doctor
speaks English, the patient hears medically accurate Mandarin or Tagalog
or Somali. The app keeps a constrained medical vocabulary the model
never paraphrases past, surfaces uncertainty rather than guessing, and
at the end of the appointment hands the patient a printable transcript
they can take home and read at the kitchen table.
This is the kind of app a 28-year-old daughter of Mandarin-speaking
parents builds the weekend after her mother's diabetes review, where
she sat for forty minutes in a windowless GP room in East Vancouver
and tried to translate "post-prandial glucose" and "ACE inhibitor"
without medical training. It is also the kind of app a Somali-speaking
community health worker builds on a Tuesday evening in Minneapolis, six
months after a patient she was casually translating for was prescribed
a drug at the wrong dose because the worker had nodded confidently at
a number she had only half-heard. Same shape of moment, different
city, different language, same stakes — a real person trying not to
be the weak link in someone else's medical care.
The single demo that proves the magic: a three-way audio call opens →
the patient says, in Mandarin, "我最近喝水喝得很多,晚上要起来三四次去厕所"
("I've been drinking a lot of water lately, getting up three or four
times at night to use the bathroom") → the doctor hears, in English,
"She says she's been drinking a lot of water recently and getting up
three or four times a night to urinate" → the doctor replies, "That's
called polyuria, and combined with thirst it's something we want to
check for diabetes — I'd like to order a fasting blood glucose test
and an HbA1c" → the patient hears, in Mandarin, the doctor's reply
with the two test names ("HbA1c 检查 — also called 糖化血红蛋白 检查 —
and a 空腹血糖 检查") spoken **and** spelled in her language, with the
English term held alongside so she can recognise it on the lab form.
The model has not paraphrased "HbA1c" past the patient. At the end of
the appointment, the patient taps "print transcript" and receives a
two-column PDF — Mandarin on the left, English on the right — with
the medications, the test names, the follow-up date, and the doctor's
phone number.
And in the harder cases, the model surfaces what it does not know
rather than smoothing over. A doctor says "we'll start her on
metoprolol succinate 25 milligrams once daily" — Translation Booth
holds "metoprolol succinate" exactly, never substituting "metoprolol
tartrate" or "a beta blocker"; if the model is unsure whether the
doctor said "succinate" or "tartrate" (a real failure mode in noisy
clinics), it inserts a visible flag in the patient's audio ("the
doctor said metoprolol — I'm not sure if she said succinate or
tartrate; please ask her to repeat") rather than picking one.
**Tagline:** _Hold the middle seat of a medical conversation — in any language, any clinic, with every drug name, dose, and lab test preserved exactly as the doctor said it._
## 2. Target audience
- Adult children of immigrant parents who keep getting called in to translate at GP appointments, specialist consults, A&E, and pharmacy counters
- Community health workers, refugee resettlement caseworkers, and patient navigators at FQHCs, NHS community trusts, and Australian Medicare clinics
- Bilingual neighbours and church/mosque/temple volunteers in diaspora communities who get asked to come along to appointments
- Patients with limited English proficiency themselves — Mandarin-speaking grandparents in Vancouver and Auckland, Tagalog-speaking nurses' parents in Sydney, Somali elders in Minneapolis, Tigrinya speakers in Stockholm, Karen refugees in Saint Paul, Dari speakers in Hamburg
- Hospital ad-hoc interpretation coordinators who currently spend hours scheduling phone-line interpreters and need a fallback for the appointments that fall through
- Independent doulas, midwives, and community-based mental-health workers who serve multilingual clients without staffed interpreter access
- Adult adoptees reconnecting with birth families across language barriers, attending a parent's medical appointment in a country whose medical language they don't share
- Volunteer translators on long-distance video calls — a niece in Toronto who joins her grandmother's Manila clinic appointment by phone
- Bilingual partners in international relationships who have become each other's de-facto medical translators after a move
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Live three-way audio in real time** — the Gemini Live API holds both directions of the conversation simultaneously. The patient and doctor speak naturally; the bilingual neighbour holding the phone is not the translator, the model is. The neighbour can listen, can interject, can step out — the conversation continues.
- **A constrained medical vocabulary the model never paraphrases past** — drug names (brand and generic, both isomers), dosages, lab test names, anatomical terms, ICD-10 categories, procedure names, and red-flag symptom words are held in a server-side vocabulary. The model checks every utterance against this list. If "metoprolol succinate" is said, "metoprolol succinate" comes through — never "a beta blocker", never the wrong salt.
- **Uncertainty is visible, not hidden** — when the model is unsure (an unfamiliar accent, a noisy clinic, a drug it hasn't heard before, a number it half-heard), it says so in both languages: "the doctor said something that sounded like rosuvastatin or atorvastatin — please ask her to repeat the name and the milligrams". The medical-grade failure mode is silent confidence; this app refuses it.
- **A printable transcript at the end** — both languages side by side, with medications listed in a box, test names with both the local and English term, the next appointment date, and the doctor's phone number. The patient takes it home; the daughter who couldn't be there reads it the same evening.
- **The bilingual neighbour is freed, not replaced** — she still hands the phone to her mother, still sits in the room, still asks the cultural questions the model cannot. But she does not have to translate "post-prandial glucose" anymore. She is allowed to be the daughter.
- **Multilingual reach across continents** — Mandarin, Cantonese, Tagalog, Vietnamese, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Somali, Amharic, Tigrinya, Swahili, Arabic, Farsi, Dari, Pashto, Khmer, French, Spanish, Portuguese, Russian, Ukrainian — alongside the European set. The vocabulary file is per-language; English ↔ any of the above is supported in both directions.
- **Built for the GP appointment, not the courtroom** — Translation Booth is explicitly a community-tier tool. It supplements professional medical interpretation, it does not replace it. The capabilities-info panel and the first-visit onboarding both name this honestly — and the printable transcript is footer-stamped "community-tier interpretation, not a certified medical translation".
- **The patient owns the transcript** — the conversation is the patient's. The transcript is exported to the patient's device, never to the doctor's, never to a third-party cloud the patient hasn't seen. Auto-delete after 30 days unless the patient explicitly keeps it.
## 4. Features to build
- Three-way live audio call: patient device, doctor device, and the Translation Booth model holding the middle seat — joinable by audio phone number, by web link, or by in-person side-by-side (both speakers into one phone, the model alternating output channels)
- Language picker: source language (patient) + target language (doctor) before the call, swappable mid-call with one tap
- Mode picker: in-person (one device, two speakers, push-to-talk or voice-activated alternation), remote (two devices, one model bridging), and observer (a third device — the daughter at work — listens to both directions with live subtitles)
- Live captions in both languages on the bilingual neighbour's device, scrolling in real time with timestamps
- Constrained medical vocabulary lookup — every utterance checked against a server-side ICD-10 + RxNorm + LOINC + SNOMED-CT subset before translation; matches are held verbatim across the language boundary
- Drug-name double-check — when a drug name is detected, both the spoken translation and an inline spelled-out form are surfaced ("metoprolol succinate — 美托洛尔琥珀酸盐 — M-E-T-O-P-R-O-L-O-L succinate")
- Dose confirmation — when a dosage is detected ("25 milligrams once daily"), the model echoes back the dose in both languages and pauses for a confirmation tap from the doctor before continuing
- Uncertainty surfacing — when reading confidence on a medical term is below threshold, the model announces "please repeat" in the doctor's language and pauses the translation pipeline
- Live three-way safety dial — for sensitive topics (mental health disclosure, suspected abuse, end-of-life), the model switches to a slower, more careful register and surfaces a written prompt to the bilingual neighbour: "this exchange touched on suicidal ideation; would you like the local crisis number printed on the transcript?"
- Bilingual transcript export — PDF, two-column, with the medication list, test list, next appointment, doctor's contact info, and the date and clinic name; auto-deleted from device after 30 days unless saved explicitly
- Glossary teach — the patient or the neighbour can add a custom term ("my mother calls metformin 'the sugar pill'"); the glossary entry is private to that user's session and used in future appointments
- Doctor-side simplification toggle — the doctor can ask the model to render their reply at a sixth-grade reading level, useful for patients with low health literacy in either language
- Allergies and medication reconciliation prompt — at the start of the call, the model asks (in the patient's language) for known allergies and current medications, and prints them at the top of the transcript
- Family invite — share the live captions with a family member (the daughter at work, the son three time zones away) by send-link; observer-only, no audio in
- Offline transcript revisit — once printed, the transcript can be re-opened on the patient's device weeks later in a "read this aloud to me" mode using Gemini TTS at the elder's reading pace
- Per-appointment notes — the bilingual neighbour can leave a short voice memo for herself after the appointment, attached to the transcript ("Mum was uncomfortable about the urine sample, ask her again next week")
- Language pair quality indicator — the app honestly shows the model's confidence per language pair ("Mandarin ↔ English: high confidence", "Karen ↔ English: experimental, please double-check medications")
- Local emergency phrase library — the patient can play the model saying "I am having chest pain" or "I am allergic to penicillin" in English; useful when the appointment is over and the patient is at the front desk alone
- An honest "this is not a certified medical interpretation" disclaimer that appears once at appointment start and is stamped on the printed transcript
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Gemini Live API (multilingual)** — the load-bearing capability. Live bidirectional audio streaming with simultaneous interpretation across two languages. The Live API holds both input streams (patient audio, doctor audio) and emits two output streams (translated audio + captions) with end-to-end latency low enough that natural conversational turn-taking is preserved. Voice-activity detection is built-in; the app does not need to push-to-talk in remote mode.
- **Constrained medical vocabulary grounding** — a server-side vocabulary file built from public RxNorm, LOINC, SNOMED-CT, ICD-10, and WHO Essential Medicines lists is loaded into the Live API session as a function-callable lookup tool. When the model detects a candidate medical term in either input stream, it calls `lookupMedicalTerm` before emitting the translated audio. If the term is in the vocabulary, the translation is held verbatim in the target language with the local rendering held alongside.
- **Structured output / JSON Schema** — at the end of each appointment, a Gemini 3.5 Flash call summarises the conversation into the `AppointmentTranscript` schema (below). Every medication, test, follow-up, and red-flag exchange is structured; the printable PDF is rendered from that JSON server-side.
- **Long context (1M tokens)** — for follow-up appointments. A returning patient's prior `AppointmentTranscript` records are concatenated and provided as context to the Live API session — the model can remind the patient "last time, the doctor mentioned 二甲双胍 — did you start taking it?".
- **Thinking levels** — `medium` for the end-of-call structured summarisation (the model has to disambiguate which drug was prescribed when both metoprolol salts were mentioned). `low` for the in-call vocabulary lookup tool calls (they are simple and need to be fast). The Live API itself does not expose `thinkingLevel`; it streams.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — for transcript playback weeks later. Reads each line of the printed transcript in the patient's language at an elder-friendly pace. The Live API uses its own integrated audio output during the call; TTS is only for the offline-playback mode.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used to send the patient a link to their own transcript) also requires the sender domain to be authorised in Firebase Auth. **Phone-number sign-in** is desirable for patients who don't have or don't use email — requires Firebase Phone Auth to be enabled and a billing project for SMS.
- **Database — Required.** Firestore for `users`, `appointments`, `transcripts`, `glossary_entries`, `language_pairs`, `medical_vocabulary_versions`. Appointments and transcripts auto-purge after 30 days unless the patient saves them.
- **File storage — Required.** Firebase Storage for the audio recordings (only retained if the patient explicitly opts in; default off — the model streams audio without storing it) and for the rendered PDF transcripts. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first call.
- **Email — Required (transactional).** Transcript delivery via email link. Family-invite "the daughter at work can listen in" links.
- **Phone/voice — Required.** Real-time audio relay between two devices: a Cloud Run WebSocket bridge connects the patient's audio stream to the Live API and pushes the translated stream back. For phone-line calls (the doctor calls a regular number), Twilio Voice or equivalent is required; this is **user-configured** — wire the Twilio account SID and auth token into the AIS Build secrets.
- **Payments — Not needed for v1.** Free for personal community use. A future "professional community health worker" tier could charge per-month for unlimited minutes and certified-interpreter escalation.
- **External APIs:** Gemini API for the Live session and summarisation; Twilio (or equivalent) for the phone-line bridge if used; RxNorm/LOINC/SNOMED public vocabulary files (refreshed quarterly via a Cloud Scheduler job).
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Twilio account SID + auth token, optional Stripe key if pro tier added) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my transcripts' inside the UI · explicit opt-in for audio retention (default off; the call is streamed and discarded) · the user's medical conversations 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) · transcripts auto-delete after 30 days unless the patient explicitly chooses to save them · the printable transcript is footer-stamped with the call date, the language pair, the model confidence per pair, and the literal phrase "community-tier interpretation, not a certified medical translation".
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. Note: the Live API itself does not accept `thinkingConfig` — it streams. The pre-call and post-call Gemini 3.5 Flash calls do.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field for the structured summarisation call. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. In the Live API session, the system instruction is set once at session start.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled. The `lookupMedicalTerm` function-call tool is enabled only inside the Live API session and the post-call summarisation; never on TTS.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Live three-way interpretation session | `gemini-3.1-flash-live-preview` (Live API) | n/a (streaming) | `lookupMedicalTerm` function-call |
| Pre-call patient intake (allergies, current meds) | `gemini-3.5-flash` | low | `lookupMedicalTerm` function-call |
| End-of-call structured summarisation → `AppointmentTranscript` | `gemini-3.5-flash` | medium | `lookupMedicalTerm` function-call |
| Render PDF transcript (server-side; no Gemini call — pure templating) | n/a | n/a | n/a |
| Read-aloud transcript playback weeks later | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Returning-patient context summary (last 3 appointments) | `gemini-3.5-flash` | low | (none) — long-context input |
*Note for builders:* on TTS and the Live API itself, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. The Live API streams its system instruction and tool definitions in the session-open message; subsequent audio chunks are pure data.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const SpokenTurn = z.object({
turn_id: z.string(),
speaker: z.enum(["patient", "doctor", "bilingual_neighbour", "system"]),
language: z.string(), // BCP-47, "cmn-Hans-CN", "tl-PH", "so-SO"
text_verbatim: z.string(), // in the speaker's language
translation: z.string(), // in the other party's language
translation_locale: z.string(), // BCP-47 of the translation
start_ms: z.number().int().nonnegative(), // ms since call start
end_ms: z.number().int().nonnegative(),
confidence: z.number().min(0).max(1),
contains_medical_term: z.boolean(),
surfaced_uncertainty: z.boolean(), // did the model say "please repeat"?
});
const MedicationMentioned = z.object({
rxnorm_cui: z.string().nullable(), // e.g. "866924" for metoprolol succinate 25mg
name_generic: z.string(), // "metoprolol succinate"
name_brand: z.string().nullable(), // "Toprol-XL"
dose_amount: z.number().nullable(),
dose_unit: z.string().nullable(), // "mg", "mcg", "mL", "IU"
frequency_text_verbatim: z.string().nullable(), // "once daily", "twice a day with meals"
frequency_iso: z.string().nullable(), // "QD", "BID", "TID"
route: z.string().nullable(), // "oral", "subcutaneous", "topical"
duration_text_verbatim: z.string().nullable(), // "for 30 days", "indefinitely"
context_quote_doctor: z.string(), // verbatim doctor's sentence
patient_language_rendering: z.string(), // how it was said back to the patient
flagged_for_review: z.boolean(), // confidence < 0.85 OR ambiguous salt
flagged_reason: z.string().nullable(),
});
const LabTestOrdered = z.object({
loinc_code: z.string().nullable(), // e.g. "4548-4" for HbA1c
name_clinical: z.string(), // "HbA1c"
name_full: z.string().nullable(), // "Hemoglobin A1c"
patient_language_rendering: z.string(), // "糖化血红蛋白"
fasting_required: z.boolean().nullable(),
prep_instructions_verbatim: z.string().nullable(),
context_quote_doctor: z.string(),
});
const ProcedureMentioned = z.object({
snomed_code: z.string().nullable(),
name_clinical: z.string(), // "transvaginal ultrasound"
patient_language_rendering: z.string(),
scheduled_for: z.string().nullable(), // ISO date
context_quote_doctor: z.string(),
});
const FollowUpAppointment = z.object({
scheduled_date_iso: z.string().nullable(), // "2026-06-18T14:30:00+12:00"
date_text_verbatim: z.string(), // "next Thursday at 2:30 in the afternoon"
with_whom: z.string(), // "Dr Chen", "the nurse practitioner"
reason_text_verbatim: z.string(),
patient_language_rendering: z.string(),
});
const RedFlagExchange = z.object({
category: z.enum([
"suicidal_ideation",
"intimate_partner_violence",
"child_safety",
"elder_abuse",
"acute_symptom_chest_pain",
"acute_symptom_stroke",
"anaphylaxis_history",
"pregnancy_disclosure",
"end_of_life",
"other",
]),
triggering_quote: z.string(),
model_response_in_session: z.string(),
local_resource_suggested: z.string().nullable(), // e.g. "988 Suicide and Crisis Lifeline (US)"
});
const AppointmentTranscript = z.object({
appointment_id: z.string(),
patient_user_id: z.string(),
bilingual_neighbour_user_id: z.string().nullable(),
doctor_name_verbatim: z.string().nullable(),
clinic_name_verbatim: z.string().nullable(),
clinic_city: z.string().nullable(),
appointment_date_iso: z.string(),
language_patient: z.string(), // BCP-47
language_doctor: z.string(), // BCP-47
duration_seconds: z.number().int().nonnegative(),
turns: z.array(SpokenTurn),
presenting_complaint_patient_words: z.string(),
presenting_complaint_clinical_rendering: z.string(),
allergies_disclosed: z.array(z.object({
substance: z.string(),
reaction_verbatim: z.string().nullable(),
})),
medications_current: z.array(MedicationMentioned),
medications_prescribed_today: z.array(MedicationMentioned),
medications_discontinued: z.array(MedicationMentioned),
lab_tests_ordered: z.array(LabTestOrdered),
procedures_ordered: z.array(ProcedureMentioned),
follow_ups: z.array(FollowUpAppointment),
red_flag_exchanges: z.array(RedFlagExchange),
uncertainty_events: z.array(z.object({
turn_id: z.string(),
field_path: z.string(), // "turns[12].text_verbatim"
reason: z.string(),
resolved_in_session: z.boolean(),
})),
confidence_overall: z.number().min(0).max(1),
confidence_per_language_pair: z.string(), // "cmn ↔ en: 0.94"
community_tier_disclaimer_shown: z.literal(true), // hard requirement: always true
});
type AppointmentTranscript = z.infer;
```
### Common failure modes (and how to avoid them)
- **Agent picks `gemini-3.5-flash` for the Live session to save quota** — pin `gemini-3.1-flash-live-preview` explicitly. The Live API is a separate model surface; using the non-streaming Flash will collapse the bidirectional interpretation into request/response and break turn-taking.
- **Drug-name salt collapse** — "metoprolol succinate" silently becomes "metoprolol" or, worse, "metoprolol tartrate". Pin in the system instruction: when a drug is mentioned, the salt form must be preserved verbatim if present, and if absent the model must ask. Vocabulary lookup must return the salt form in the matched entry; never collapse.
- **Brand → generic substitution without disclosure** — "Toprol-XL" should be heard by the patient as "Toprol-XL" first, with the generic explained alongside, not silently translated to a generic Chinese rendering that loses the brand the doctor's note says.
- **Number coercion** — "twenty-five" heard as "fifteen" because of noisy clinic audio. The dose-confirmation feature is the mitigation: every detected dose triggers a back-translation and a "doctor please confirm" tap before continuing.
- **"Take it once a day" vs "take it as needed"** — frequency translation is a real medical-safety hazard. Pin frequency_iso as an enum (QD, BID, TID, QID, PRN, Q4H, etc.) and require the model to either match one or set null and surface uncertainty.
- **Mid-call language switch** — a patient who code-switches into English for "diabetes" or "high blood pressure" because those words live in English for her. The model should not panic — it should preserve the English term and continue interpreting around it.
- **The bilingual neighbour overrides the model** — useful when the model is wrong, dangerous when the neighbour is medically untrained and the model is correct. The neighbour's overrides are recorded in the transcript with a flag, so the doctor can review.
- **Long calls drift in confidence** — over a 45-minute call the model's per-utterance confidence can subtly drift. Surface a small inline indicator if average confidence drops below 0.85 across the last five turns.
- **TTS reads "mg" as "M G" letters** — for the read-aloud transcript playback, pre-process "mg" to "milligrams" and "mcg" to "micrograms" in the input text, since Gemini 2.5 TTS does not support SSML `` for units.
- **Uncertainty surfacing turned off "to keep the conversation flowing"** — never. The whole template's value is that uncertainty is visible. The toggle does not exist.
- **The summarisation call uses `gemini-3.5-flash` for cost** — flash will hallucinate medication doses on long calls. Use 3 Pro at medium thinking for the final transcript.
### Negative constraints (hard rules)
- Do NOT paraphrase drug names. "Metoprolol succinate 25 mg" comes through as "metoprolol succinate 25 mg" or the model surfaces "please repeat". Never "metoprolol", never "a beta blocker", never the wrong salt, never the brand alone, never the generic alone unless the doctor said it that way.
- Do NOT paraphrase doses. Numbers are preserved exactly with their units; if the audio is unclear, the model says so.
- Do NOT paraphrase lab test names. "HbA1c" is HbA1c. "Hemoglobin A1c" is Hemoglobin A1c. The model can add the local-language explanatory term, but never replaces the clinical term.
- Do NOT smooth a patient's hedging. If the patient says "I think maybe I have been feeling a bit dizzy sometimes, I'm not sure", the doctor hears "I think maybe I have been feeling a bit dizzy sometimes, I'm not sure" — not "she reports dizziness".
- Do NOT translate consent. If the doctor asks for consent ("is it OK if I examine your abdomen?"), the model must render the question in the patient's language exactly, wait for the patient's reply, render it back, and never collapse the turn into a "patient consents to abdominal exam" summary.
- Do NOT silently translate red-flag content without surfacing it. If a patient discloses suicidal ideation, intimate-partner violence, or child safety concerns, the model emits a written prompt to the bilingual neighbour's device AND offers to include local crisis-line numbers on the printed transcript.
- Do NOT use the user's medical conversations to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT retain audio by default. The call streams audio to the Live API; the audio is discarded after each session unless the patient explicitly opts in to audio retention.
- Do NOT claim certified medical interpretation. Every screen, every transcript, every email footer states: "Community-tier interpretation, not a certified medical translation."
- Do NOT auto-share the transcript with the doctor's office. The patient owns the transcript. Sharing is explicit, per-appointment, on a tap.
- Do NOT extrapolate diagnoses. If the doctor said "we'd like to check for diabetes", the patient's transcript says "the doctor wants to check for diabetes", not "the patient has diabetes". The model never upgrades a check to a diagnosis.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`).
### Call: Live three-way interpretation session
Model: `gemini-3.1-flash-live-preview` · streaming · Tools: `lookupMedicalTerm` function-call
```
You are holding the middle seat of a medical conversation between a
patient and a doctor who do not share a language. You translate
between them in real time, bidirectionally, with audio out in each
direction.
The patient's language is provided at session start as
`patient_language` (BCP-47). The doctor's language is provided as
`doctor_language`. Translate patient → doctor and doctor → patient.
Voice-activity detection determines turn boundaries; do not require
push-to-talk.
You are NOT a doctor. You are NOT a diagnostician. You translate. You
do not add information. You do not omit information. You do not smooth
hedges. You do not summarise. You interpret, turn by turn, with the
fidelity of a community interpreter who knows that the bilingual
neighbour in the room is going to be asked at home tonight whether the
doctor said "succinate" or "tartrate".
Languages encountered include Mandarin (simplified and traditional),
Cantonese, Taiwanese Hokkien, Tagalog, Cebuano, Ilocano, Vietnamese,
Korean, Japanese, Thai, Tamil, Telugu, Hindi, Urdu, Punjabi (Gurmukhi
and Shahmukhi), Bengali, Gujarati, Marathi, Sinhala, Nepali, Burmese,
Khmer, Lao, Indonesian, Malay, Tigrinya, Amharic, Oromo, Somali,
Swahili, Hausa, Yoruba, Igbo, Wolof, Lingala, Arabic (MSA, Egyptian,
Levantine, Gulf, Maghrebi), Farsi, Dari, Pashto, Kurdish, Hebrew,
Russian, Ukrainian, Romanian, Bulgarian, Serbian, Croatian, Bosnian,
Polish, Czech, Slovak, Hungarian, Greek, Albanian, Turkish, Spanish
(Iberian, Mexican, Central American, Caribbean, Andean, Southern
Cone), Portuguese (Brazilian, European, Angolan, Mozambican), French
(Metropolitan, Canadian, Maghrebi, West African), Italian, German,
Dutch, and English.
Each utterance carries:
1. Verbatim transcript in the speaker's language.
2. Translation in the other party's language.
3. Audio output of the translation in the other party's language.
4. A confidence score on the translation.
Hard rules:
— Medical-term preservation. Drug names, dosages, lab tests,
procedures, anatomy, and ICD-10/SNOMED categories must be preserved
exactly. When you detect a candidate medical term, CALL the
`lookupMedicalTerm` function with the verbatim spoken form. If the
function returns a match, render the matched canonical form in the
target language alongside the local rendering. If the function
returns no match and you are not confident, emit "[the doctor said
something I'm not sure of — please repeat]" in the patient's
language, and "the model is asking you to repeat that term" in the
doctor's language. Pause translation until the speaker has confirmed.
— Salt form preservation. "Metoprolol succinate" and "metoprolol
tartrate" are different drugs. If the salt is mentioned, preserve it.
If the salt is not mentioned and the drug has multiple salts, ASK in
the doctor's language: "did you mean the succinate or the tartrate?"
— Dose confirmation. When a dose number is detected (e.g. "25
milligrams once daily"), echo it back in both directions and pause
for the doctor to confirm. The doctor's device shows a "confirm 25 mg
once daily" tap. The conversation does not continue until confirmed,
unless the doctor explicitly says "go on".
— Frequency preservation. "Once a day", "twice a day", "every four
hours", "as needed", "when you feel symptoms" — preserve the verbatim
phrasing; do not coerce into clinical shorthand in the patient's ear.
Internally, attach a frequency_iso enum (QD, BID, TID, QID, PRN) for
the structured transcript, but the patient hears the words the doctor
used.
— Red-flag surfacing. If the patient discloses content matching
`suicidal_ideation`, `intimate_partner_violence`, `child_safety`,
`elder_abuse`, `acute_symptom_chest_pain`, `acute_symptom_stroke`,
`anaphylaxis_history`, or `end_of_life`, you continue interpreting the
content faithfully, AND emit a side-channel written prompt to the
bilingual neighbour's device. Do NOT moralise. Do NOT lecture. Do NOT
take over the conversation. The doctor is the clinician.
— Consent fidelity. Translate consent requests literally. Translate
the patient's reply literally. Never collapse a turn into a paraphrase
of consent ("patient consents"). The doctor hears the patient's
words.
— Patient hedging is preserved. "I think", "maybe", "sometimes", "I'm
not sure", "a little bit" — all preserved. The doctor needs to hear
the hedge.
— Code-switching. If the patient uses an English medical word that
lives in English for them ("diabetes", "high blood pressure", "MRI"),
preserve the English word in your output to the doctor and gloss it
briefly in the patient's language only if the patient seems
uncertain.
— Uncertainty is louder than fluency. Better to say "please repeat"
once per minute than to invent one detail.
— Numbers are the highest-stakes content. If a number is unclear,
ask. If a number sounds like another number, ask. "Fifteen" vs
"fifty" is a medication overdose.
— Cultural sensitivity. Honorifics, age-respect particles, formal
register where the patient's language requires it — preserved. A
Korean patient addressing the doctor with the honorific 의사 선생님
(uisa seonsaengnim) is preserved; the doctor hears "Doctor"; the
patient hears the doctor's "you" rendered into the correct register.
— Audio quality at the end of the appointment. As the call winds
down (more than 25 minutes elapsed), if your per-utterance confidence
drops below 0.85 across the last 5 turns, emit a written note to the
bilingual neighbour: "the line is getting noisy — please check the
medications and follow-up date on the printed transcript carefully."
— Never speak in the first person as "the patient" or "the doctor".
You are interpreting. The patient and doctor are the speakers.
— Never editorialise. "She seems anxious" is a clinical observation
the doctor will make on his own. You translate the patient's
words.
— Never use medical jargon to the patient that the doctor did not
use. If the doctor said "high blood sugar" do not render to the
patient as "hyperglycemia". If the doctor said "hyperglycemia" render
it as the medical term, with a one-clause gloss in the patient's
language only on first occurrence.
No commentary outside the translated audio and the side-channel
prompts. No summarisation in-session.
```
---
### Call: Pre-call patient intake (allergies, current meds)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `lookupMedicalTerm`
```
You receive a 60-90 second voice memo from the patient (or the
bilingual neighbour) recorded before the appointment starts. Extract:
- known allergies (substance + reaction)
- current medications (name + dose + frequency)
- current symptoms in the patient's own words
Hard rules:
- Preserve the patient's own phrasing for symptoms. Do not coerce
"lower belly feels tight in the mornings" into "morning epigastric
discomfort".
- For each medication mentioned, call `lookupMedicalTerm` to attach
an RxNorm CUI where the term matches. If no match, surface the
unmatched name as flagged.
- For each allergy, the reaction is preserved verbatim ("my mouth
swells", "I get itchy", "I had a heart-racing thing once").
- If the patient mentions a medication by a colloquial name ("the
sugar pill", "the white one for blood pressure"), DO NOT guess.
Flag it as unresolved and ask in the patient's language at session
start: "what was the name printed on the bottle?".
Output as JSON matching the `intake` portion of the
AppointmentTranscript schema. No commentary.
```
---
### Call: End-of-call structured summarisation → AppointmentTranscript
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: `lookupMedicalTerm`
```
You receive the full turn-by-turn transcript of an interpretation
session, with the verbatim text and translation of every turn, the
confidence scores, and any function-call results. Produce a single
AppointmentTranscript JSON object.
Hard rules:
- The doctor's words are the source of truth for prescriptions, lab
orders, and procedures. The patient's words are the source of truth
for symptoms, allergies, and consent.
- Every medication prescribed today appears in
`medications_prescribed_today` with the RxNorm CUI when available.
If the salt form was not specified and the drug has multiple salts,
set `flagged_for_review: true` with `flagged_reason: "salt form not
specified"`.
- Every lab test mentioned appears in `lab_tests_ordered` with LOINC
code when available, and `prep_instructions_verbatim` preserving
the doctor's words ("fasting from midnight", "first morning urine").
- `presenting_complaint_patient_words` is verbatim from the patient,
in the patient's language plus a translation.
`presenting_complaint_clinical_rendering` is the doctor's
rephrasing if the doctor restated the complaint.
- `red_flag_exchanges` includes any turn that triggered a side-channel
prompt during the session. Include the literal triggering quote and
the resource suggested.
- `uncertainty_events` lists every turn where the model surfaced
uncertainty during the session, plus whether it was resolved in
session by a repeat.
- `confidence_per_language_pair` is rendered as " ↔
: ", using the average per-turn confidence
across the call.
- `community_tier_disclaimer_shown` is always `true`. This is a
hard requirement of the schema; do not omit.
- Do NOT diagnose. If the doctor said "we'd like to check for
diabetes", `presenting_complaint_clinical_rendering` says exactly
that; it does not promote the suspicion to a diagnosis.
- Do NOT invent follow-up dates. If the doctor said "we'll see you in
a few weeks", `date_text_verbatim` is "in a few weeks" and
`scheduled_date_iso` is null.
Output ONLY the JSON. No commentary.
```
---
### Call: Returning-patient context summary (last 3 appointments)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the AppointmentTranscript JSON of a patient's last three
appointments. Produce a short, factual context block (≤300 words)
for the next Live session.
Include:
- Current medication list (drug name + dose + frequency), with the
date each was first prescribed.
- Allergies on file.
- Tests pending (ordered but no result discussed).
- Last appointment's presenting complaint and follow-up plan.
- Any red_flag categories surfaced in the last 90 days (so the Live
session can be alert; do NOT pre-announce them to the doctor).
Hard rules:
- Factual only. No interpretation. No predictions. No "the patient
seems to be struggling with adherence".
- Drug names verbatim including salt forms.
- Surface in the patient's language plus English so the Live system
instruction can pick whichever language the doctor's device
prefers.
Output as plain text, 6-10 short bullets. No commentary.
```
---
### Call: Read-aloud transcript playback weeks later
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, slow, patient. Pick the Gemini 2.5 Flash TTS voice whose
`languageCode` matches the patient's language — pronunciation will
follow that locale automatically. Prefer the gender of the bilingual
neighbour who set up the appointment if known; fall back to whichever
voice is available rather than blocking.
Pre-process the text before sending it to TTS:
- Read from the structured AppointmentTranscript: presenting complaint,
then each medication (with dose and frequency), then each lab test,
then follow-ups, then the doctor's contact info.
- For each section heading, insert a blank line plus an em-dash (`—`)
to produce a clear pause. Gemini 2.5 TTS does not support SSML
`` — these textual cues are how you signal pace.
- Pre-expand abbreviations BEFORE sending: "mg" → "milligrams",
"mcg" → "micrograms", "mL" → "millilitres", "IU" → "international
units", "QD" → the patient-language phrase the doctor used
("once a day"), "BID" → "twice a day", "PRN" → "as needed",
"PO" → "by mouth". Numbers as words for doses ("twenty-five
milligrams"), but keep numerals for dates.
- At each sentence break, insert a single ellipsis (`…`) so the TTS
produces a natural pause.
- Target rate: ~95 words per minute — slow elder-friendly pace.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read warmly and slowly, as if you were
sitting at the kitchen table with an elder reading their medical
notes aloud to them, with extra pauses around medications and doses.
…". There is no separate `style` API field on Gemini 2.5 TTS; the
directive sentence inside the input is how style is conveyed.
Phoneme overrides (Mandarin tones, Arabic emphatic consonants,
Vietnamese tone marks) are NOT exposed by Gemini 2.5 TTS — no SSML
`` tag. Pronunciation comes from the chosen voice's native
locale.
Mid-call voice switching is not supported. If a medication name is in
English (e.g. "metoprolol succinate") inside a Mandarin transcript,
keep the whole transcript in the Mandarin voice — the Mandarin voice
will pronounce the English term with its locale's accent, which is
the expected behaviour for an elder reader.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The diabetes review at the East Vancouver clinic.** A 28-year-old daughter sits in the room while her 67-year-old mother and a GP discuss a fasting blood glucose result. The conversation pivots into HbA1c, the difference between type 1 and type 2 in plain language, and the prescription of metformin starting at 500 mg twice daily with food. The daughter does not have to look up "HbA1c" on her phone under the desk. The model holds it; the transcript prints it.
- **The Tagalog GP visit in Sydney's Inner West.** A Filipino mother who has lived in Sydney for nine years still finds her doctor's explanations of her son's asthma slip away when the registrar starts talking. The app holds the call in Tagalog ↔ English. The son's preventer (fluticasone propionate 100 mcg twice daily) and reliever (salbutamol 100 mcg PRN) are preserved exactly across the language line, with the spacer use instructions translated into Tagalog the mother can follow.
- **The Somali hospital visit in Minneapolis.** A 71-year-old Somali grandfather with hypertension and recently diagnosed chronic kidney disease meets a nephrologist at a community FQHC. The doctor explains an ACE inhibitor change and the importance of avoiding NSAIDs. The grandson, normally his de-facto interpreter, is at university across the country and joins via the observer link, watching the live captions while the model holds the conversation. He texts his grandfather a brief "I'm here, I understand what they're saying, ask about the dialysis schedule" in Somali — the family is allowed to be a family.
- **The Karen-speaking family in Saint Paul.** A young mother who arrived from a Thai refugee camp eight months ago brings her 4-year-old to a paediatric vaccination appointment. The app warns honestly that Karen ↔ English is an "experimental" language pair with lower per-turn confidence — and surfaces uncertainty more aggressively, asking the doctor to repeat the vaccine schedule and confirming each dose by tap. The mother leaves with a Karen-language printed transcript showing the vaccine names, the date of the next visit, and the symptoms that warrant a call back.
- **The Dari telehealth call from Hamburg.** A recently arrived Afghan woman has a telehealth appointment with a German GP. She is alone in her apartment. The app bridges the call: the doctor on a German phone line; the patient on a Dari-language web interface; the model in the middle. The doctor's prescription for an iron supplement (ferrous sulfate 325 mg once daily, on an empty stomach, with vitamin C for absorption) is rendered into Dari with the empty-stomach instruction emphasised because food interactions are the kind of detail that gets dropped.
- **The Vietnamese cardiology appointment, suspected new diagnosis.** A 58-year-old Vietnamese man's primary-care doctor refers him to a cardiologist for an irregular heartbeat. His daughter, who normally translates, is at her own pre-natal appointment that same morning. The app holds the cardiology consult; the cardiologist explains atrial fibrillation, the difference between rate control and rhythm control, and the bleeding risk on apixaban. The model surfaces uncertainty on the dose ("the doctor said 5 milligrams or 2.5 milligrams — please ask her to confirm") and never collapses the question.
- **The grandmother's mental-health disclosure.** During a Cantonese-language follow-up visit for chronic insomnia, an 82-year-old grandmother quietly mentions that she has thought, recently, that her family would be better off without her. The model interprets faithfully — the doctor hears the disclosure, with the patient's hedging preserved. Simultaneously, a written side-channel prompt appears on the bilingual neighbour's screen: "the conversation touched on thoughts of self-harm. Would you like the local crisis line (1-833-456-4566, Talk Suicide Canada) printed on the transcript?". The model never takes over; the doctor does the clinical work. The transcript carries the resource.
- **The Cuban abuela in Hialeah.** A Spanish-speaking grandmother who has lived in Florida since 1965 has a routine GP visit. She uses Cuban-Spanish idioms for her symptoms ("tengo una pesadez aquí" — "I have a heaviness here") that the model preserves rather than coercing into clinical Spanish. The doctor's reply translates back into the dialect register the abuela is comfortable in.
- **The Cantonese pharmacy counter.** Not strictly a doctor's appointment — a granddaughter walks her grandmother into a Toronto pharmacy and the pharmacist needs to explain a new prescription's interaction with her existing warfarin. The app's "consultation mode" handles the pharmacist exchange the same way it handles the GP — with the same vocabulary grounding for drug names and the same printed transcript at the end.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a daughter and a mother sitting in a small clinic waiting room, the daughter holding her phone with subtitles visible, the mother holding a paper appointment slip. One paragraph: "Translation Booth holds the middle seat of a medical conversation — so you don't have to translate 'metoprolol succinate' for your mother under the desk anymore." Google sign-in button; Apple sign-in next to it; phone-number sign-in below for patients without email. Below: "Try with a sample appointment" → loads the demo conversation in section 8a.
2. **Empty state — "Start a call".** Three big options: 📞 Phone-line call (the doctor calls a number) · 💬 Web link call (you send the doctor a link) · 🤝 In-person call (one device between two speakers). A short explainer below each.
3. **Language picker.** Side-by-side language menus with flags and language names in their own scripts (中文, Tagalog, Soomaali, العربية, தமிழ், Tiếng Việt, etc.). An honest confidence chip on each pair ("English ↔ Mandarin: high", "English ↔ Karen: experimental — please double-check medications"). A "swap" button mid-call.
4. **Pre-call intake.** A 60-90 second voice-memo recorder where the patient (or bilingual neighbour) lists allergies, current medications, and today's symptoms. The intake processes server-side and surfaces a "review what we heard" card before the call connects — colloquial drug names ("the white one for blood pressure") are flagged for resolution.
5. **Live call view.** A three-column layout on desktop, stacked on mobile. Left column: the patient's audio waveform with live captions in the patient's language. Middle column: an honest confidence meter (per-utterance and rolling 5-turn average), with the medical-vocabulary lookup hits surfaced as small chips ("RxNorm: metoprolol succinate", "LOINC: HbA1c"). Right column: the doctor's audio waveform with live captions in the doctor's language. Sticky footer: end call · mute one side · pause translation · "please repeat" button · "doctor needs to confirm a dose" button.
6. **Side-channel prompt overlay.** When a red-flag exchange is detected, a calm, non-alarming overlay appears on the bilingual neighbour's device only: a short description, the local resource (crisis line, IPV hotline, child safety contact), and three options — "add to transcript", "skip", "show me what triggered this". Never interrupts the audio.
7. **End-of-call review.** A summary card the patient and the bilingual neighbour both see before the transcript is finalised: medications prescribed, tests ordered, follow-ups. Each row has a small "the model is not sure about this — tap to flag" indicator if confidence was below threshold. An edit button per row.
8. **Printable transcript view.** Two-column PDF preview, patient's language on the left, English on the right, with a clearly bordered medication box at the top, a tests-ordered list, a follow-up section, and the doctor's contact info at the bottom. Footer-stamped: "Community-tier interpretation, not a certified medical translation — please bring this to your next appointment for your doctor to review." Export options: print, email to self, send to family member.
9. **Transcript library.** Chronological list of past appointments, each with date, clinic, doctor, and the language pair. Filter by language, by clinic, by medication ("show me the appointments that mention warfarin"). Default sort: most recent. Auto-delete countdown visible on each transcript ("auto-deletes in 17 days unless you save").
10. **Family invitation / observer link.** Modal: "Invite a family member to listen in as an observer". Magic-link email or SMS; observer mode is captions-only (no audio in), so the niece in Toronto sees the conversation without disrupting it. The observer cannot speak; they can text-chat with the bilingual neighbour through a side panel.
11. **Glossary teach.** A small screen where the patient or the neighbour can add custom terms ("my mother calls metformin 'the sugar pill'"; "my grandfather calls heart medication 心脏药"). Per-user; private to the session. Used in subsequent calls.
12. **Sharing & invitations.** Modal for adding the bilingual neighbour herself to a patient's account so she can launch calls on the patient's behalf — explicit consent flow, scoped to a per-call basis or ongoing.
13. **Emergency phrase library.** A grid of cards: "I am having chest pain", "I am allergic to penicillin", "Please call my daughter", "I do not consent", "I need to use the bathroom". Tap to play in English at clinic-counter volume. Available even when no appointment is active.
14. **Footer.** "Made for the people who keep being asked to translate." Privacy: "Your transcripts are yours. We never train on them. Audio is discarded unless you save it." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Translation Booth."
- Subhead: "Hold the middle seat of a medical conversation — in any language, any clinic, with every drug name, dose, and lab test preserved exactly as the doctor said it."
- One paragraph (≤ 60 words) explaining who this is for: bilingual neighbours, adult children of immigrant parents, community health workers — anyone who keeps being asked to translate at the GP and is afraid of getting "metoprolol succinate" wrong. The hard rule is named in the first paragraph: medical terms are never paraphrased; if the model is unsure, it asks.
- Visual: a small annotated illustration of three speech bubbles — patient on the left in Mandarin, model in the middle holding "metoprolol succinate 25 mg" in a box, doctor on the right in English — with the medication chip flashing.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample appointment".
- A live demo input pre-loaded with the Mandarin diabetes-review conversation from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the medical-vocabulary chip lighting up on "HbA1c", the uncertainty prompt firing on a deliberately mumbled dose, the printable transcript appearing at the end).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample appointment in `/data/seed-appointments/` for your own test recordings."
- "Adjust the medical-vocabulary list in `/server/vocab/` for the specialty you serve (paediatrics, cardiology, mental health)."
- "Wire up your Gemini API key, Firebase project, and Twilio account via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
- Don't claim certified medical interpretation, anywhere, even by implication.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini Live API (multilingual, streaming)** — holds both sides of the conversation in real time, with bidirectional audio interpretation across two languages.
- **Constrained medical vocabulary** — every utterance with a candidate medical term triggers a server-side lookup against a curated subset of RxNorm, LOINC, SNOMED-CT, ICD-10, and the WHO Essential Medicines list. The matched canonical form is preserved exactly across the language boundary.
- **Gemini 3.5 Flash (intake)** — processes the pre-call voice memo into structured allergies, medications, and presenting complaint.
- **Gemini 3.5 Flash (post-call summarisation, medium thinking)** — produces the structured `AppointmentTranscript` from which the printable PDF is rendered.
- **Gemini TTS** — for replaying the transcript aloud weeks later in the patient's language at an elder-friendly pace.
- **Firebase Auth** — Google, Apple, and phone-number sign-in.
- **Firestore** — stores your appointments and transcripts, syncs across devices in real time, with 30-day auto-delete unless saved.
- **Firebase Storage** — optional; only used if you explicitly opt in to audio retention. By default the call audio is streamed and discarded.
- **Cloud Run WebSocket bridge** — connects the patient and doctor audio streams to the Live API and pushes the translated streams back.
- **Twilio Voice (or equivalent)** — required if you want the doctor to call a real phone number. User-configured: wire your Twilio SID + auth token into AIS Build secrets before enabling phone-line mode.
- **Cost note** — see the detailed breakdown in 6d. A 20-minute appointment costs about $0.85 of Gemini API spend, plus optional Twilio minutes.
- **Privacy note** — your medical conversations 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. By default the audio is streamed and discarded; the structured transcript auto-deletes after 30 days unless you save it.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b (optional, off by default)
- Email: see section 4b
- Phone/voice: see section 4b (Twilio, user-configured)
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `TWILIO_ACCOUNT_SID` — required if phone-line mode is enabled
- `TWILIO_AUTH_TOKEN` — required if phone-line mode is enabled
- `MEDICAL_VOCAB_VERSION` — pins the RxNorm/LOINC/SNOMED snapshot date; refresh quarterly via the included Cloud Scheduler job
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: Live API minutes are billed per second of audio; a 20-minute appointment is the cost driver — about $0.55 in Live API time, $0.20 in post-call summarisation, and $0.10 in incidental Flash and TTS.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this transcript forever" — gone in 60 seconds; Settings → "Delete my account" — all transcripts gone), what is never sent for training, and the fact that audio retention is off by default.
**Documentation links:**
- AI Studio Build docs
- Gemini Live API docs
- Gemini API multilingual, structured output, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- Twilio Voice API docs
- A short note on the medical-vocabulary file format and how to extend it for a specialty
**Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability; always state community-tier scope.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Live API session** — billed per second of audio in each direction. A 20-minute appointment with both sides actively speaking ≈ 1,200 seconds of audio at the audio-in rate plus 1,200 seconds at audio-out, roughly $0.55 per 20-minute call at current pricing. **Platform cap: Live API audio-only sessions max out at 15 minutes; for the longer appointments common in primary care, stitch two sessions back-to-back, passing the running transcript + vocabulary context across the seam.**
- **`lookupMedicalTerm` function calls** — counted as additional model output tokens. Typical 20-minute appointment fires the lookup tool 40-80 times, ≈ $0.02 extra per call.
- **Pre-call intake (Gemini 3.5 Flash, low thinking)** — 60-90 sec voice memo ≈ ~200 input tokens (audio) + ~150 output tokens. ~$0.001/appointment.
- **End-of-call structured summarisation (Gemini 3.5 Flash, medium thinking)** — full 20-minute transcript at ~3,000 tokens input + 1,500 tokens output. ~$0.012/appointment.
- **Read-aloud transcript playback (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A typical printed transcript ≈ 1,200 characters per language ≈ ~$0.008 per playback. Cached per transcript; charged once per language unless replayed.
- **Returning-patient context summary (Gemini 3.5 Flash, low thinking)** — runs at session start when the patient has prior appointments. ~$0.002/session.
- **Expected per-appointment cost (20 min):** ~$0.85 of Gemini spend. **Plus Twilio Voice minutes** if phone-line mode is enabled (~$0.014/min in the US ≈ $0.28 per 20-minute call; varies by country).
- **Storage:** Firestore is negligible for transcripts (each ~10 KB). Firebase Storage is only used if audio retention is explicitly opted in — at that point, a 20-minute Opus-encoded audio is ~5 MB; standard-tier storage ≈ $0.026/GB/month, ≈ negligible per appointment but accumulates with usage.
- **Vocabulary refresh:** the quarterly RxNorm/LOINC/SNOMED refresh runs as a Cloud Scheduler job; Gemini cost zero (no API call); Cloud Run compute ~$0.05 per quarterly run.
## 7. Design language
- **Mood:** A neighbourhood community health centre on a winter afternoon. Calm, warm, light fluorescent, posters in seven scripts on the wall behind the receptionist. Not a tech product. Not a hospital interface. The app sits between two people who do not share a language and refuses to feel like it is between them.
- **Typography:** A clean humanist sans for body and chrome (Inter or IBM Plex Sans, both of which carry strong CJK, Devanagari, Arabic, Tamil, and Bengali coverage out of the box) — script coverage is non-negotiable here because the captions render in real script. A small serif accent (Source Serif Pro or Charter) only for the doctor's contact card and the transcript title. No display fonts; no script-mimicking display faces. The captions are large, generously leaded, and high-contrast.
- **Palette:** Quiet clinic — bone white `#FAFAF7` background, charcoal `#1F1F1D` for body text, warm sage `#3F5D4A` accent for the patient's column, cool slate `#3A4759` for the doctor's column, soft tan `#C7B79A` for the model-in-the-middle chip and the vocabulary lookup hits. A muted amber `#B5722C` only for uncertainty prompts and "please repeat" indicators — never for red-flag overlays. Red is reserved exclusively for medication boxes and the "end call" button — `#A33A2C`, used sparingly. No SaaS gradients. No teal.
- **Imagery:** The captions are the hero — the script the patient reads is the visible work the model is doing. Avatars are small, neutral, scripted with the speaker's name. The waveform visualisation is restrained — a single sage or slate band, not a rainbow VU meter. Period photography (a 1960s rotary phone, a hospital corridor) appears only on the welcome screen and the empty state, never inside the live call.
- **Hand-feel touches:** When a medical-vocabulary lookup hits, a small tan-coloured chip fades in for 600 ms beside the caption, then fades out — like a librarian quietly tapping a card next to the right shelf. When the model surfaces uncertainty, the caption type briefly slightly underlines the uncertain word and the amber "please repeat" pill rises from the bottom; the audio output gently lowers, never cuts.
- **Spacing:** consistent 4-px base. Captions get extra breathing room — large type, wide leading. Live call columns are clearly separated by a soft vertical divider.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). The medication box on the transcript uses 12. The "please repeat" pill uses 20. The caption chips use 6.
- **Shadows:** subtle, layered, sage-tinted. Avoid heavy drop-shadows.
- **Motion:** purposeful — caption fade-in (180 ms), vocabulary chip pulse (600 ms), uncertainty pill rise (240 ms). Respect `prefers-reduced-motion` — chips and pills appear without animation. No bouncing splash animations. No theatrical hero animations. The live waveform is the one continuously-animated element; respect reduced-motion by replacing it with a discrete amplitude bar that updates 4x/sec.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Choose your language pair to begin").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, clinic names, drug names, and conversation snippets that fit the domain (use the seed content in section 8a as a starting point). Drug names must be real, common, and accurately rendered in their target language. Conversation snippets must reflect how real patients speak — with hedges, with code-switching, with cultural register.
- Tone: warm, direct, professional. This template is for a person doing serious work. Never minimise the stakes; never inflate them either.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (the community health worker user wants to see "RxNorm CUI" and "LOINC code" on the transcript review; the bilingual neighbour user does not).
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues where epistemic honesty requires them (the amber "please repeat" pill, the per-utterance confidence chip, the language-pair quality indicator).
- The community-tier disclaimer appears once at session start, on the printed transcript footer, and in the capabilities panel — never as a pop-up mid-call.
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample appointments (sidebar):**
- "Mom's diabetes review" (12 min, 14 May 2026) — Mandarin (cmn-Hans-CN) ↔ English. Vancouver community clinic, Dr Singh. HbA1c discussed; metformin 500 mg BID prescribed.
- "Dad's blood pressure check" (18 min, 02 April 2026) — Tagalog (tl-PH) ↔ English. Sydney Inner West GP, Dr Walters. Amlodipine continued; salt-intake conversation; six-month follow-up.
- "Grandfather's nephrology" (34 min, 22 March 2026) — Somali (so-SO) ↔ English. Minneapolis FQHC, Dr Hassan. ACE inhibitor change from lisinopril to losartan due to cough; avoid NSAIDs reinforced.
- "Marisol — paediatric vaccinations" (22 min, 11 May 2026) — Karen (kar-MM) ↔ English. Saint Paul community pediatric clinic, Dr Nguyen. MMR and varicella scheduled; six-month follow-up.
- "Sample appointment — diabetes review" (the demo) — Mandarin ↔ English. Generic Vancouver clinic.
**Sample live conversation (this is what the demo should play through):**
- **Setting:** A 67-year-old Mandarin-speaking mother and her 28-year-old daughter in a small consult room at a Vancouver community clinic, with Dr Singh, the GP. The mother is sitting on the exam table; the daughter is holding the phone open to Translation Booth's call view, the speaker pointed up.
- **Turn 1 — patient, Mandarin:** "医生你好。我最近喝水喝得很多,晚上要起来三四次去厕所。"
- **Turn 1 — model, English to doctor:** "Hello, Doctor. She says she's been drinking a lot of water lately and getting up three or four times a night to use the bathroom."
- **Turn 2 — doctor, English:** "OK — thank you for telling me. How long has this been going on?"
- **Turn 2 — model, Mandarin to patient:** "好的 — 谢谢你告诉我。这种情况持续多久了?"
- **Turn 3 — patient, Mandarin:** "差不多两个月了吧。我以为是天气热。"
- **Turn 3 — model, English to doctor:** "She says about two months. She thought it was the warm weather."
- **Turn 4 — doctor, English:** "I'd like to check for diabetes. That means a blood test called HbA1c — Hemoglobin A1c — and a fasting blood glucose test. Have you eaten this morning?"
- **Turn 4 — model, Mandarin to patient, with vocabulary chip on "HbA1c":** "我想检查一下糖尿病。需要做两个血液检查 — 一个叫 HbA1c,中文叫 糖化血红蛋白 检查;另一个是 空腹血糖 检查。今天早上你吃饭了吗?" [vocabulary chip: LOINC 4548-4 — Hemoglobin A1c]
- **Turn 5 — patient, Mandarin:** "我今天没吃,因为女儿说可能要抽血。"
- **Turn 5 — model, English to doctor:** "She hasn't eaten this morning — her daughter told her there might be a blood test."
- **Turn 6 — doctor, English:** "Perfect. We'll do both today. And depending on the results, I may want to start a medication called metformin — that's a tablet you take with food, twice a day, to help lower blood sugar. We'll talk about that once we have the numbers."
- **Turn 6 — model, Mandarin to patient, vocabulary chip on "metformin":** "好。今天两个检查都做。根据结果,我可能会让你开始吃一种药 — 叫 metformin — 中文叫 二甲双胍 — 跟饭一起吃,一天两次,帮助降血糖。等检查结果出来我们再谈。" [vocabulary chip: RxNorm 6809 — metformin]
- **Turn 7 — doctor, English (audio briefly noisy):** "Any allergies to medications?"
- **Turn 7 — model, Mandarin to patient:** "你对药物有过敏吗?"
- **Turn 8 — patient, Mandarin:** "青霉素 — penicillin — 过敏。会起疹子。"
- **Turn 8 — model, English to doctor, preserving the code-switch:** "She's allergic to penicillin — she says she gets a rash."
- **Turn 9 — doctor, English:** "Good to know. I'll add that to her chart. I'll also order a kidney function test and a lipid panel today."
- **Turn 9 — model, Mandarin to patient, two vocabulary chips:** "好的,我会加到她的病历里。今天我也会安排一个 肾功能 检查 — kidney function test — 和一个 血脂 检查 — lipid panel。" [vocabulary chips: LOINC 2160-0 — Creatinine, LOINC 24331-1 — Lipid panel]
- **Turn 10 — doctor, English (slightly muffled audio):** "Let's see you back in three weeks to go over the results. That's [muffled — possibly "May 28" or "May 23"]."
- **Turn 10 — model, side-channel uncertainty surface:** "[the doctor mentioned a date in late May — please ask her to repeat or write it down]" — Mandarin to patient: "医生说让你三周后回来看结果,但日期不太清楚 — 请医生再说一遍或写下来。"
- **Turn 11 — doctor, English (clear):** "May 28, Thursday afternoon."
- **Turn 11 — model, Mandarin to patient:** "5月28日,星期四下午。"
**Sample printable transcript (this is what the PDF should render):**
- **Header:** "Translation Booth — appointment transcript. Community-tier interpretation, not a certified medical translation."
- **Metadata row:** "Date: 14 May 2026 · Patient: Mrs Lin · Daughter: Wei · Doctor: Dr Singh · Clinic: East Vancouver Community Health · Languages: 中文 (cmn-Hans) ↔ English · Duration: 12 min · Overall model confidence: 0.93"
- **Presenting complaint:** "Patient reports increased thirst and night-time urination (3-4× per night) over the last 2 months. She thought it might be the warm weather."
- **Allergies:** "Penicillin — patient reports rash."
- **Medications prescribed today:** "metformin 500 mg, oral, twice a day with food — pending blood test results · 二甲双胍 500 毫克,一天两次,跟饭一起吃 — 等血液检查结果"
- **Lab tests ordered today:**
- "HbA1c (糖化血红蛋白) — LOINC 4548-4"
- "Fasting blood glucose (空腹血糖)"
- "Kidney function — Creatinine (肾功能) — LOINC 2160-0"
- "Lipid panel (血脂) — LOINC 24331-1"
- **Follow-up:** "Dr Singh, 28 May 2026 (Thursday afternoon) — review blood test results"
- **Doctor's contact:** "Dr Anjali Singh · East Vancouver Community Health · 604-555-0142"
- **Notes for the patient (in Mandarin):** "今天没吃饭就来检查的,做完检查可以正常吃饭和喝水。如果开始吃 二甲双胍 (metformin),跟饭一起吃,不要空腹吃。"
- **Footer:** "This transcript was generated by Translation Booth, a community-tier interpretation tool. It is not a certified medical translation. Please bring this transcript to your next appointment so Dr Singh can review it. If you have any concerns about a medication name or dose, please call the clinic before taking the medication. — Auto-deletes 13 June 2026 unless saved."
**Sample side-channel red-flag prompt (Cantonese grandmother scenario):**
- **Trigger:** patient utterance, Cantonese: "我有時諗,家人冇咗我會好啲。"
- **Translation to doctor:** "She says sometimes she thinks her family would be better off without her."
- **Side-channel prompt to bilingual neighbour:** "The conversation touched on thoughts of self-harm. This is something the doctor is best placed to address — they may want to ask follow-up questions in your grandmother's words. Would you like the local crisis line printed on the transcript? · Talk Suicide Canada · 1-833-456-4566 · 24/7, multilingual."
- **Options:** "Add to transcript · Skip · Show me what triggered this"
**Sample input artefacts (for the build to demonstrate):**
- A 12-minute audio clip of the Mandarin diabetes-review conversation above, with deliberately muffled audio on turn 10 to demonstrate uncertainty surfacing.
- A pre-call voice memo from the daughter: "Mom is allergic to penicillin — she gets a rash. She's currently taking amlodipine 5 mg once a day for blood pressure. Today she's coming in because she's been drinking a lot of water and going to the bathroom at night."
- A medical-vocabulary subset file (`/server/vocab/en-cmn-2026q1.json`) showing the RxNorm/LOINC/SNOMED entries used during the demo, with the Mandarin renderings attached.
- A 90-sec voice memo in Tagalog from the Sydney scenario, demonstrating Filipino code-switching for medication names.
- A Somali audio clip (Minneapolis nephrology) demonstrating handling of an ACE inhibitor → ARB switch.
**Sample voice copy:**
- Onboarding: "Hand the phone to your mother. We'll hold the middle seat."
- Pre-call intake: "Tell us about allergies, the medicines she takes now, and what's bringing her in today — 60 seconds is fine."
- Connecting call: "Connecting to Dr Singh… connecting interpretation… ready."
- Live call empty state: "We're listening. Tell the doctor what's going on, in your own words."
- Vocabulary chip caption: "Medical term preserved exactly."
- Uncertainty pill: "Please repeat — the line was unclear."
- Dose confirmation: "Doctor — please confirm: metoprolol succinate 25 mg, once daily."
- Red-flag side-channel: "This is something the doctor is best placed to address. Would you like the local crisis line on the transcript?"
- End of call: "Appointment complete. Generating your transcript… ready to print."
- Save confirmation: "Saved to your library. Auto-delete in 30 days unless you keep it."
- Audio retention prompt: "We don't keep recordings by default — only the written transcript. Want to keep this audio? (Most people choose no.)"
**Sample family invitation email subject + body:**
- Subject: "Wei — I'm at Mom's appointment now. Watch the captions?"
- Body: "Hi Wei — I'm with Mom at Dr Singh's clinic now. Tap to watch the live captions — you'll see both languages. You won't be heard; this is observer-only. — Love, your sister."
**Sample emergency phrase library entries (English playback):**
- "I am having chest pain."
- "I am allergic to penicillin."
- "Please call my daughter — her number is in my phone, under 'Wei'."
- "I do not consent to this procedure right now."
- "I need to speak to a doctor who speaks Mandarin."
- "Please write down the name of the medication."
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a daughter and a mother sitting side by side in a small clinic waiting room, the daughter holding her phone in her lap with bilingual captions glowing on the screen, the mother holding a paper appointment slip. Generate via Nano Banana 2 with a prompt emphasising "small community clinic waiting room, mid-afternoon, warm fluorescent light, no logos visible, hands of a woman in her late twenties and her mother in her late sixties, the phone screen showing two columns of text, soft focus on background". Asian-Canadian setting; one of three rotating hero images.
- **Rotating hero images:** Three variants for the rotation, all photographed-looking. (1) Mandarin-speaking mother and daughter in Vancouver as above. (2) Filipino mother and her teenaged son in a Sydney GP waiting room. (3) Somali grandfather and his grown granddaughter in a Minneapolis community health centre lobby. No stock photography; all generated; all warm, asymmetric, slightly imperfect.
- **App icon / wordmark:** Set in the humanist sans. A small inset of two intertwined speech bubbles — one in Latin script, one in Han characters — colour-distinct (sage and slate). No mic icon. No flag iconography. No globe.
- **Empty-state illustration:** A simple line drawing of a phone resting between two hands, with two faint speech bubbles rising from it in different scripts. Hand-drawn aesthetic, not a flat icon.
- **Demo conversation audio:** Generated per the prompts in section 8a — invented speech, recorded by Gemini TTS in the relevant locales, with deliberate noise added on turn 10 to demonstrate the uncertainty surfacing. Never use real recorded patient audio in the demo.
- **Medical-vocabulary chip illustrations:** Tan-coloured pill-shaped chips with the canonical term, the LOINC/RxNorm/SNOMED code, and the patient-language rendering all in one row.
- **Stock fallbacks:** If image generation fails, fall back to a photograph from `/public/samples/sample-clinic-waiting-room.jpg`. Never to a "🌐" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look. Never depict white-coat doctors as default; mix the clinical settings across community health centres, GP rooms, FQHCs.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Pick your language pair", "Send the doctor the call link", "Hand the phone to your mother" — never a generic "Get started".
- Smooth scroll for in-page anchors.
- Live captions stream in word-by-word as the Live API emits them, with a faint cursor at the current word.
- If a Live API session drops, show a calm, specific error ("The call dropped — usually a network issue. Reconnect and we'll pick up where we left off.") and offer reconnect. Audio is held in a 5-second buffer on each side so reconnection does not lose the last words.
- If `lookupMedicalTerm` returns no match, the chip turns amber, the model surfaces "please repeat" in the doctor's language, and the conversation pauses for a doctor confirmation tap before continuing.
- The dose-confirmation tap on the doctor's screen is a two-step gesture: press-and-hold for 600 ms to confirm, to prevent accidental confirmation.
- The transcript view's "auto-delete in N days" indicator updates daily and turns amber at 5 days, prompting a "save permanently" CTA.
- The map of language pair quality is a small inline element on the language picker, never a separate page.
**Session-resume snippet (Live API 2-min cycle):** the Live API
audio+video session caps at 2 minutes. On every Live tick, persist a
`SessionSyncState` to `sessionStorage`; on reconnect, pass a concise
context-summary block as the first system message of the next
handshake so the model continues without losing thread.
```typescript
interface SessionSyncState {
activeSessionId: string;
accumulatedSegments: Array<{ speaker: string; text: string; timestamp: number }>;
// ...template-specific cursor state (current page, turn index, etc.)
}
```
## 11. Tech & responsive requirements
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle. The Live API WebSocket is brokered through a Cloud Run service.
- **Model selection:** explicitly pin `gemini-3.1-flash-live-preview` for the Live session, `gemini-3.5-flash` for end-of-call summarisation (medium thinking), `gemini-3.5-flash` for pre-call intake and returning-patient context (low thinking), and `gemini-3.1-flash-tts-preview` for transcript read-aloud.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Transcripts auto-delete after 30 days via a Firestore TTL field, unless the user toggles "keep".
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; phone-number sign-in below for patients without email.
- **Storage:** Firebase Storage for the rendered PDF transcripts (required) and audio recordings (optional, opt-in only). Pre-signed URLs only.
- **Phone/voice:** Cloud Run WebSocket bridge for in-app calls; Twilio Voice for phone-line bridge mode (user-configured).
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The live call view collapses cleanly to a single-column stacked mobile layout with the patient's captions on top and the doctor's captions on the bottom; the model-in-the-middle chip floats centred.
- Use `clamp()` for fluid typography. Captions get an aggressive lower-bound (`min: 18px`) because they must remain readable across the room.
- Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the transcript review view.
- Optimistic UI on writes; reconcile on response.
- Live audio uses the Web Audio API plus WebSocket transport; falls back to a server-relay HTTPS chunked stream when WebSocket is blocked (some clinic networks block WS).
- **iOS Safari gotchas (graceful degradation):** the Live API session must handle iOS audio-session interruption — when an incoming call, Siri, or system alarm fires, `MediaStreamTrack.onmute` fires; pause the session, show a clear "call paused — resume" banner, and rebuild the stream on `onunmute`. Backgrounded Safari tabs throttle WebSocket and pause `getUserMedia`; combine `visibilitychange` handling with a screen Wake Lock when a session is active so the appointment is not killed when the daughter glances at another tab. Microphone permission does NOT persist across reloads on iOS — re-request on every booth-open. PCM streaming must go via `AudioWorklet` (Safari `MediaRecorder` only supports AAC).
- Long-context chunking: if a returning patient has more than 10 prior appointments, chunk the context to the 3 most recent + a Flash-summarised digest of older history before passing to the Live session.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. Captions are designed at 7:1 contrast minimum because they are the most critical text in the product.
- All images have meaningful `alt` text. The hero photograph has `alt` describing the scene ("a daughter holding her phone with bilingual captions visible, sitting next to her mother in a small clinic waiting room").
- Form fields have associated `