================ 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.
---
# Hall of Fame
## 1. Project
**Hall of Fame** is an oral-history workbench for the people who hold a
neighbourhood's memory in their heads. The user — usually a local
historian, a community-newsletter editor, or a librarian running an
elders programme — sits with a long-time resident, records a forty-minute
conversation on a phone, and walks away with a structured biography:
a chronological life narrative, the people they mention named and
disambiguated, the streets and shops and schools they remember placed
on a small period map, the strongest verbatim quotes pulled and
attributed, and a four-page newsletter-ready piece the historian can
hand back to the interviewee for review before anything is printed.
This is the kind of app a Brixton historian opens in her tote bag on
the way to Mrs Akinyemi's flat, who has lived on Atlantic Road since
1962 and remembers what the market sounded like the morning the windows
went in. It is also the kind of app a barangay archivist in Quezon City
brings to Lolo Andoy's plastic chair on the corner of Calle Mabini, who
has watched four mayors come and go and can tell you who lived in every
house on his street between 1971 and the floods of 2009. Same shape of
a moment, different city, different decade.
The single demo that proves the magic: hit record on a forty-minute
conversation with Mr Patel from #12 on a Tuesday afternoon → in under
four minutes the historian sees a four-page structured biography in a
typeset preview. Page one is the headline and the lede ("Rajesh Patel,
who has lived on this street since 1973, opened the shop on the corner
the year the bus route changed"). Page two is a chronological life
chapter spine with the five strongest verbatim quotes pulled into
sidebars, each attributed with the exact timestamp in the recording.
Page three is the people he named — his late wife Meera, his brother
Vinod who emigrated to Toronto, his neighbour Mrs Singh who taught his
children to read — each with the contextual sentence that introduced
them. Page four is a small period map of the street with the shop, the
school, the gurudwara, and the bus stop labelled, and a sidebar listing
the streets he remembered by their older names. Underneath: a single
button — _Send to Mr Patel for review_ — because nothing prints until
he reads it.
And in the harder cases — interviewees who weep at the second question,
who slip between three languages mid-sentence, who decline to name a
specific neighbour, who change their mind about a story on the second
visit — the app holds the seams of the conversation as evidence. The
ellipsis is preserved. The redaction ("Mr Patel asked that the name not
be printed") is preserved. The retraction ("on a return visit Mr Patel
clarified that…") is preserved. The historian's note that the
interviewee paused for forty-three seconds before answering is preserved.
The historical record is not flattened into smooth newsletter prose;
the smoothness is on page one, and the seams are underneath.
**Tagline:** _Turn a forty-minute conversation into a four-page biography — in any neighbourhood, any language, with every quote exact and the interviewee's consent at the centre._
## 2. Target audience
- Local historians and community-newsletter editors running an elders-and-incomers oral-history series — Brixton, Birmingham Balti Triangle, Tower Hamlets, Manchester Moss Side, Glasgow Govan
- Barangay archivists in the Philippines, lurah / kelurahan record-keepers in Indonesia, gram panchayat clerks in India, ward councillors in Kenya — anyone running a hyper-local memory project on a phone-budget
- Small-town public librarians running "Memory Circles" for residents over 70 — Lubbock TX, Marshalltown IA, Hamilton ON, Wagga Wagga NSW
- Diaspora-community oral-history projects — Vietnamese-American in San José, Cuban-American in Hialeah, Iranian-American in Westwood, Tamil in Tooting, Eritrean in Stockholm, Filipino in Daly City, Punjabi in Surrey BC, Korean in Annandale VA, Bangladeshi in Tower Hamlets, Amharic in DC
- Memorial-project organisers in the year after a beloved resident dies — collecting recollections from neighbours, regulars at the café, the priest who buried four generations, the schoolteacher who taught three
- Hospice volunteers and palliative-care workers who collect a "life story" interview as part of pre-bereavement work, with the family's permission and the patient's pace
- Volunteers on community-archive donation projects — Densho, the Eritrean Diaspora Archive, the Philippine Migrant Workers Memory Project, the BBC Listening Project, StoryCorps affiliates, local museum oral-history collections
- Faith-community archivists — the parish priest who has christened four generations, the mosque committee documenting elder members, the synagogue project recording survivors, the gurudwara committee collecting partition stories
- Heritage-grant-funded community projects with a small budget, a deadline, and one volunteer who has never edited a transcript before but knows everyone on the street
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Every quote is exact, every time.** The biography page is generated, but every word inside a quotation mark is verbatim transcript from the recording. Edits to "smooth" a quote are forbidden. If the interviewee says "we was poor, but we wasn't hungry", the newsletter says "we was poor, but we wasn't hungry" — with a timestamp, so anyone can find the moment in the recording.
- **Consent is the first screen, not the small print.** Before the recorder arms, the app walks both parties through a plain-English consent screen, captured as a brief recorded statement and a signed checkbox. The historian cannot publish anything until the interviewee has reviewed the draft and ticked _yes, you may use this_.
- **Audio reasoning, not just speech-to-text.** Gemini 3.5 Flash reads the recording for chronology, named people, places mentioned, emotional register shifts, code-switches between languages, long pauses that carry weight, and quotes whose exact wording matters. Transcripts surface diarisation (interviewer / interviewee), language-switch markers, and the timestamp of every named entity.
- **Reads any neighbourhood's language.** The interview audio can be in English, Tagalog, Vietnamese, Mandarin or Cantonese, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Arabic, Yoruba, Igbo, Twi, French, Spanish, Portuguese, Polish, Russian, Turkish — and any mid-sentence code-switch between them. The transcript holds both the verbatim source-language line and a translation that preserves the speaker's voice. Endearments, hedges, and idioms are not flattened.
- **The structured biography is a draft, not the truth.** The historian sees the draft alongside the source recording with timestamps. Every fact in the draft is hyperlinked back to the exact second of audio that supports it. Nothing reaches the newsletter until the historian (and then the interviewee) has read every sentence.
- **The neighbourhood emerges across interviews.** When the same street name, the same shop, the same Mrs Singh appears in three interviews, the People view and the Places view consolidate them — and surface the disagreements. Two interviewees remember the corner shop closing in different years; the app shows both quotes side by side.
- **Newsletter-ready output.** The four-page typeset PDF is laid out by the app — headline, lede, two-column life-narrative spine, pulled sidebar quotes, named-people sidebar, the small period map, a credit line, and a "this draft was reviewed and approved by [interviewee] on [date]" footer. Drop it straight into the community newsletter or print it on the library's letterpress.
- **Archive-grade export.** Each interview exports as a structured bundle for donation to a community archive — high-quality audio, full diarised transcript, the structured biography in TEI / EAD XML, the signed consent record, and the period-map metadata. The bundle is ready for Densho, the BBC Listening Project, StoryCorps Archive, the local university's special collections.
## 4. Features to build
- Plain-English consent flow — recorded statement + checkbox + interviewer countersign, captured before the recorder arms, replayable later
- High-quality audio recorder with pause/resume, large pause button, automatic level monitoring, "you may be too far from the mic" warning, and "the kettle is on, would you like to pause?" background-noise notice
- Pre-interview brief — the historian types or speaks the interviewee's name, age (optional), street, date of arrival in the neighbourhood, and three things they hope to talk about; this becomes the model's context
- Offline-first recording — the recording continues if the connection drops; uploads when reconnected; never loses a session
- Multimodal pre-context — the historian can attach a photograph the interviewee shows them during the conversation ("that's me at the shop in 1974") and the app links the photo to the timestamp it was discussed
- Audio reasoning → diarised transcript with interviewer (I) and interviewee (R) labels, code-switch markers, long-pause markers, laughter / tears / sighs preserved as bracketed cues
- Verbatim transcript preserved separately from the translation — every dialect word, every "uh", every "you know what I mean" is kept exactly
- Translation that preserves voice — "Anak ko", "Tu sabes", "Beta", "Mtoto wangu" come through verbatim with a first-occurrence gloss, never flattened to "my child"
- Structured biography draft — chronological life chapters, named people, named places, named events, strongest verbatim quotes pulled, all hyperlinked back to audio timestamps
- People extraction with disambiguation — when "my brother Vinod" appears in three interviews, the People view consolidates; when two interviewees disagree on a person, both quotes are surfaced
- Places extraction with period names — "the old Co-op", "the parish hall before the fire", "Calle Mabini before they renamed it" — period name preserved, modern name resolved via a separate grounded call
- Events extraction — the school opening, the floods of 2009, the year the bus route changed — collected with the verbatim quote and a "user to confirm" interpretive flag, never auto-asserted
- Hyperlinked transcript — every name, place, event, and pulled quote in the typeset biography links back to the exact second of audio
- Multilingual TTS read-back — for an interviewee who can no longer read easily, the app reads the draft aloud in the interviewee's preferred language at an unhurried pace
- Review-by-interviewee flow — a magic-link the historian sends to the interviewee (or their family); the interviewee can listen to each pulled quote in context, edit any factual error, redact any sentence, and tick "you may publish"
- Redaction is a first-class operation — "please do not name my neighbour" → the quote stays in the transcript but is rendered as "[name withheld at interviewee's request]" in the newsletter draft
- Retraction is a first-class operation — on a return visit, "I was wrong about the year; it was 1976, not 1974" — both quotes are preserved with timestamps and the later one supersedes the earlier in the typeset draft
- Map view — neighbourhood map with every place mentioned plotted; toggle period overlay where available; click a pin to see every interview that mentions it
- Cross-interview view — the People view, the Places view, the Events view across the whole project; one tap shows all interviews where Mrs Singh is named
- Newsletter-ready PDF export — four-page typeset draft with headline, lede, life narrative, sidebar quotes, named-people list, small period map, credit line, approval footer
- Archive-grade bundle export — audio + transcript + biography + consent record + map metadata as a TEI / EAD XML bundle for donation to a community archive
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Audio reasoning** (Gemini 3.5 Flash) — reads the recording end-to-end with diarisation (interviewer vs interviewee), language identification per segment, code-switch detection, named-entity recognition, long-pause detection, emotion-register shifts, laughter / tears / sighs preserved as bracketed cues. Returns the structured `Interview` object with a transcript whose every line carries a millisecond-precise `t_start` and `t_end`.
- **Long context (1M tokens)** — a forty-minute interview transcribes to roughly 6,000-8,000 words ≈ ~12,000 tokens; the structured `Interview` object is ~25,000 tokens. The People-disambiguation and Places-consolidation calls operate over a whole project (often 20-50 interviews) ≈ ~600,000 tokens. **Guardrail**: at 80 interviews the call will exceed 1M tokens; chunk by ward / street / decade above ~60 interviews.
- **Structured output / JSON Schema** — every Gemini call in this app returns typed JSON matching the schemas below. The schemas are passed via `responseSchema` (converted from Zod via the SDK helper); negative constraints live in the `systemInstruction`. Numeric `min`/`max` are documentation only; clamp server-side after parsing.
- **Multilingual translation** (built into Gemini 3.5 Flash) — handles Tagalog, Vietnamese, Cantonese and Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Arabic, Yoruba, Igbo, Twi, French, Spanish, Portuguese, Polish, Russian, Turkish — and any mid-sentence code-switch. Translation preserves diminutives, hedges, idioms, and the speaker's voice. Endearments like "Anak ko", "Beta", "Mtoto wangu" stay verbatim with a first-occurrence gloss.
- **Search grounding** (Gemini 3.5 Flash) — for the geocoding call only. "the old Co-op on Atlantic Road" or "Calle Mabini before it was renamed" must resolve to modern coordinates with the period name preserved. Grounded search prevents hallucinated street names.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — reads the draft biography aloud to the interviewee in the interviewee's preferred language, at an unhurried pace ("the kitchen-table pace, not a podcast pace"). Pauses are encoded as `…` and blank-line `—`; SSML `` and SSML `` are not supported.
- **Thinking levels** — `medium` for the primary audio-reasoning pass (diarisation + register + named entities + chronology) and for the family / neighbourhood graph resolution. `low` for translation, single-place geocoding, and the newsletter-draft synthesis. Surface `thoughtSummary` only behind a small "(i) show how the AI read this" affordance on the historian-facing review screen.
### 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** for interviewee review also requires the sender domain to be authorised in Firebase Auth — many historians prefer the simpler "share a one-time link by SMS or WhatsApp" fallback the app also supports. Interviews are private to the historian and the named interviewee; the project organiser is granted access only when explicitly invited.
- **Database — Required.** Firestore for `users`, `projects`, `interviews`, `transcripts`, `biographies`, `people`, `places`, `events`, `consent_records`, `review_states`, `project_members`.
- **File storage — Required.** Firebase Storage for the raw audio recordings (preserved at upload bit-rate, forever), for any photographs the interviewee shows the historian during the conversation, for the rendered PDF newsletter draft, and for the archive bundle ZIPs. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before the first recording. Pre-signed URLs only; audio is never publicly addressable.
- **Email — Required (transactional).** Review-by-interviewee link emails. Project-invitation emails. Archive-donation export emails to institutional archives the project nominates. SMS fallback via a configured Twilio account for interviewees without comfortable email access.
- **Payments — Not needed for v1.** Free for personal and community use. A future "professionally-printed letterpress newsletter run" tier could pipe to a small-batch print partner and charge for that physical artefact only.
- **External APIs:** Gemini API for all intelligence; optional Mapbox or MapTiler for the neighbourhood map tile layer (the app ships with a static OpenStreetMap-derived neighbourhood layer that covers the offline case). Optional Twilio for SMS review links.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Mapbox token if used, Twilio key if SMS configured) 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 interview' inside the UI · explicit opt-in for any analytics · the interviewee owns the right to publication and may revoke at any time before the newsletter is printed · interview audio is 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) · redactions requested by the interviewee are honoured at the rendering layer, never silently propagated into the raw transcript (the underlying record is preserved for the archive; the public output respects consent).
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input (the audio file + the pre-interview brief). Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Read interview audio → diarised `Interview` schema | `gemini-3.5-flash` | medium | (none) |
| Translate transcript preserving voice + register | `gemini-3.5-flash` | low | (none) |
| Synthesise biography draft from `Interview` + brief | `gemini-3.5-flash` | low | (none) |
| Geocode a period place name to modern coordinates | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Consolidate People / Places / Events across project | `gemini-3.5-flash` | medium | (none) — long-context over the whole project |
| Generate TTS read-back of the draft biography | `gemini-3.1-flash-tts-preview` | n/a | n/a |
*Note for builders:* on TTS calls, omit `thinkingConfig` entirely — the field is not supported on that model. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. The geocoding call uses `google_search` grounding and therefore must NOT set `responseSchema` in the same call — instruct the model to emit JSON in the text body and parse server-side; read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri` rather than asking the model to include URLs in the JSON body (it will hallucinate them).
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const TranscriptSegment = z.object({
speaker: z.enum(["interviewer", "interviewee", "third_party", "unknown"]),
t_start_ms: z.number().min(0),
t_end_ms: z.number().min(0),
language_bcp47: z.string(), // "en-GB", "tl-PH", "pa-IN", "yue-HK"
text_verbatim: z.string(), // exact words, dialect preserved
translation_target_lang: z.string().nullable(),
translation: z.string().nullable(),
code_switch_within_segment: z.boolean(),
embedded_phrases: z.array(z.object({
language_bcp47: z.string(),
phrase_verbatim: z.string(),
translation: z.string(),
register_note: z.string().nullable(), // "switched to Punjabi for the word for mother"
})),
paralinguistic_cues: z.array(z.enum([
"laughter", "tears", "long_sigh", "long_pause",
"interruption", "overlap", "background_noise",
"voice_softens", "voice_lifts", "child_in_background",
])),
pause_after_ms: z.number().min(0), // gap before next segment
reading_confidence: z.number().min(0).max(1),
});
const PersonMentioned = z.object({
name_verbatim: z.string(), // "Mrs Singh", "Tito Andoy", "Beta"
inferred_relationship: z.string().nullable(), // "neighbour", "uncle", "the interviewee's son"
inferred_relationship_confidence: z.number().min(0).max(1),
first_mention_ms: z.number().min(0),
contextual_quote_verbatim: z.string(), // the sentence they appear in, verbatim
contextual_quote_t_start_ms: z.number().min(0),
});
const PlaceMentioned = z.object({
name_verbatim: z.string(), // "the old Co-op", "Calle Mabini"
modern_name: z.string().nullable(), // resolved later
contextual_quote_verbatim: z.string(),
contextual_quote_t_start_ms: z.number().min(0),
is_likely_address: z.boolean(),
period_referenced: z.string().nullable(), // "the late 70s", "before the floods"
});
const EventReferenced = z.object({
description_user_facing: z.string(), // "interviewee mentions the floods of 2009"
contextual_quote_verbatim: z.string(),
contextual_quote_t_start_ms: z.number().min(0),
period_referenced: z.string().nullable(),
interpretive_note: z.string().nullable(), // historian-confirmable, never auto-asserted
});
const PulledQuote = z.object({
quote_verbatim: z.string(), // exact words inside quotation marks
speaker: z.enum(["interviewee", "interviewer", "third_party"]),
t_start_ms: z.number().min(0),
t_end_ms: z.number().min(0),
why_pulled: z.string(), // "captures arrival on the street, 1962"
suitable_for_sidebar: z.boolean(),
});
const BiographyChapter = z.object({
chapter_title: z.string(), // "Arrival on the street, 1962"
approximate_period: z.string(), // "1962-1968"
narrative_paragraphs: z.array(z.string()), // each para grounded in audio
supporting_quote_ids: z.array(z.string()), // ids in pulled_quotes[]
supporting_segment_t_starts_ms: z.array(z.number()), // hyperlinks back to audio
flagged_for_historian_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const Interview = z.object({
interview_id: z.string(),
project_id: z.string(),
audio_uri: z.string(), // gs:// URI
duration_ms: z.number().min(0),
interviewee_name_verbatim: z.string(), // "Mr Patel", "Mrs Akinyemi", "Lolo Andoy"
interviewee_preferred_pronouns: z.string().nullable(),
interviewee_languages: z.array(z.string()), // BCP-47 list
interview_recorded_at: z.string(), // ISO
interview_location_verbatim: z.string().nullable(), // "Mr Patel's front room, #12 Atlantic Road"
consent_record_id: z.string(), // foreign key to ConsentRecord
consent_summary: z.string(), // one sentence the historian and interviewee both heard
pre_interview_brief: z.string(), // historian's notes before recording
primary_language: z.string(), // BCP-47
languages_observed: z.array(z.string()), // every language detected
diarisation_quality: z.enum(["high", "medium", "low", "uncertain"]),
segments: z.array(TranscriptSegment),
people_mentioned: z.array(PersonMentioned),
places_mentioned: z.array(PlaceMentioned),
events_referenced: z.array(EventReferenced),
pulled_quotes: z.array(PulledQuote),
biography_chapters: z.array(BiographyChapter),
biography_headline: z.string(), // "Rajesh Patel, who has lived on this street since 1973"
biography_lede_paragraph: z.string(), // grounded in the audio
emotional_register_journey: z.array(z.object({
t_start_ms: z.number().min(0),
t_end_ms: z.number().min(0),
register: z.enum([
"warm-conversational", "reflective", "joyful", "sorrowful",
"wry", "hesitant", "guarded", "matter-of-fact", "tender", "angry",
]),
})),
review_state: z.enum([
"draft",
"sent_to_interviewee",
"interviewee_revising",
"interviewee_approved",
"interviewee_redacted",
"interviewee_withdrew_consent",
"published_to_newsletter",
"donated_to_archive",
]),
overall_reading_confidence: z.number().min(0).max(1),
flagged_for_historian_review: z.array(z.object({
field_path: z.string(), // "segments[42].text_verbatim"
reason: z.string(),
})),
});
const ConsentRecord = z.object({
consent_record_id: z.string(),
interview_id: z.string().nullable(), // null if pre-arming
interviewee_name_verbatim: z.string(),
interviewer_name_verbatim: z.string(),
recorded_consent_audio_uri: z.string(), // gs:// URI to the spoken consent
consent_audio_transcript_verbatim: z.string(),
consent_audio_translation: z.string().nullable(),
scope: z.array(z.enum([
"newsletter_publication",
"community_archive_donation",
"institutional_archive_donation",
"audio_excerpts_in_public_exhibition",
"named_publication", // interviewee's name may appear
"anonymous_publication", // attributed only as "a long-time resident"
])),
redactions_requested_verbatim: z.array(z.string()), // "please don't name my neighbour"
signed_off_at: z.string(), // ISO
signed_off_by_interviewee: z.boolean(),
signed_off_by_interviewer: z.boolean(),
may_be_withdrawn_before: z.string().nullable(), // ISO, often the newsletter print date
});
type Interview = z.infer;
type ConsentRecord = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the primary audio-reasoning pass call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash drops paralinguistic cues, conflates diarisation when two voices overlap, and misses code-switches inside a sentence.
- Quotes silently "smoothed" — the model rewrites "we was poor, but we wasn't hungry" to "we were poor, but we weren't hungry". Hard rule in the system instruction: every word inside quotation marks is verbatim; dialect is preserved exactly. Add a unit test against a known dialect line.
- Endearments translated — "Anak ko" comes back as "my child", "Beta" as "son". Hard rule: endearments and first names are never translated; first-occurrence gloss only, never thereafter.
- Pre-WWII / pre-independence place names silently modernised — "Calcutta" silently rewritten as "Kolkata" in the transcript. Always preserve `name_verbatim`; resolve `modern_name` in a separate grounded call.
- The biography draft asserts facts the interviewee didn't say — the model fills in a plausible-sounding sentence about "the migration of the seventies" because it knows the era. Hard rule: every paragraph of the biography lists its supporting `t_start_ms` segments; if a paragraph has no supporting segments, it is removed before rendering.
- Quote timestamps drift by seconds — the model reports `t_start_ms: 123000` when the quote actually starts at 124200. The server-side verification step replays each pulled quote against the audio and snaps it to the nearest segment boundary.
- Diarisation flips interviewer and interviewee in the first 30 seconds — common when the interviewer asks the first long question and the model decides that voice is the "main speaker". Mitigation: the pre-interview brief carries the interviewee's name, and the system instruction specifies "the longer total speaking time across the recording is the interviewee".
- Redactions leak — the historian marks "please don't name my neighbour" as a redaction, but the newsletter PDF still includes the name because the redaction was only applied at the People view layer. Mitigation: redactions live on the underlying `ConsentRecord`, are applied at every rendering layer, and are part of the unit-test suite.
- Multipart audio recordings (pause/resume) submitted as separate calls — submit as one audio file with the pauses preserved so chronology is intact; if forced to submit separately, the server reconciles by absolute clock-time rather than per-file relative timestamps.
- TTS reads a Tagalog sentence with an American-English voice — wrong. Pick the TTS voice whose `languageCode` matches the interviewee's preferred language; pronunciation follows the chosen voice's native locale.
- Consent record stored as a checkbox only — fragile. Always store the recorded audio statement alongside the checkbox. The audio is the record; the checkbox is a convenience.
### Negative constraints (hard rules)
- Do NOT alter the words inside quotation marks. Verbatim is verbatim. Dialect ("we was", "I dunno"), filler ("uh", "you know"), and false starts ("the the the bus") stay exactly as spoken. The pulled-quote sidebar in the newsletter draft is the most strictly verbatim part of the entire app.
- Do NOT translate first names, place names, endearments, dish names, or brand names. "Mrs Akinyemi", "Lolo Andoy", "Beta", "kapeng barako", "Hovis" stay verbatim. Add a parenthetical English gloss on first occurrence only ("Lolo — grandfather") and never thereafter.
- Do NOT modernise period place names without preserving the period name. Always store both `name_verbatim` and `modern_name`.
- Do NOT invent dates, names, or events. If the interviewee says "it was the year my son started school" and the year is not stated, the biography says "around the time her son started school" — never "in 1974". Do not extrapolate.
- Do NOT extrapolate to historical events not mentioned. If the interviewee says "everything changed after they took the bus route away", do not annotate "this likely refers to [TfL service change]". Surface the quote and leave the interpretation to the historian.
- Do NOT publish anything without an `interviewee_approved` review state. The newsletter PDF export button is disabled until that state is set, on the server, by a verified click from the interviewee's review link.
- Do NOT use the recordings or transcripts to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT auto-share. Interviews are private to the historian and the named interviewee by default. Project members are explicit, per-project.
- Do NOT auto-claim a person mentioned has died, moved, or disagreed with someone. The app shows the quote ("Mrs Singh used to live at number 8 but she's not there anymore"); the historian decides whether and how to footnote it.
- Do NOT include the interviewee's address, phone number, NHS number, or any government identifier in any export unless the historian has explicitly enabled "include identifiers" for the archive bundle (and the interviewee has consented to that specific scope).
- Do NOT show a "playback" UI of the recording on any public-facing render until the interviewee has approved. The historian's draft view shows playback; the read-only public draft shows pulled quotes only.
### 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: Read interview audio → diarised `Interview` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are reading a recorded oral-history interview between a local
historian (the interviewer) and a long-time resident of a
neighbourhood (the interviewee). Recordings range from twenty
minutes to two hours; the typical session is forty minutes. The
interviewer is usually a volunteer with a phone microphone in a
front room; recordings carry kitchen-table acoustics, a kettle
boiling in another room, occasional traffic, a third party in the
background offering tea.
Languages encountered include English (British, American,
Australian, Caribbean, West African and South Asian Englishes
alongside RP), Tagalog and Filipino, Vietnamese, Mandarin and
Cantonese, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi (Gurmukhi
or Shahmukhi speakers), Sinhala, Amharic, Tigrinya, Swahili, Yoruba,
Igbo, Twi, Farsi, Arabic (Maghrebi, Levantine, Gulf, Egyptian),
Khmer, Thai, Burmese, Lao, French (Metropolitan, Maghrebi,
Québécois), Spanish (Iberian, Latin American, Caribbean),
Portuguese (Iberian, Brazilian), Italian, Polish, Russian,
Ukrainian, Greek, Turkish, Kurdish, Romanian, Bulgarian. Code-
switching mid-sentence is common — Tagalog and English, Punjabi and
English, Cantonese and English, Yoruba and English — and you must
preserve both halves.
Audio is uploaded via the Gemini Files API (`files/*` resource name). Do NOT
expect Firebase Storage public URLs to be fetched by the API —
they will not be. Multi-part recordings (pause/resume) are
submitted as a single concatenated file with the pauses
preserved; chronology is intact.
Read the recording end-to-end. Distinguish:
- the interviewer (typically asks questions, typically the shorter
total speaking time, typically the voice that introduced the
consent recording)
- the interviewee (typically the longer total speaking time, named
in the pre-interview brief)
- third parties (a family member offering tea, a child in the
background) — diarise but do not include in the biography unless
the historian explicitly opts in
- paralinguistic cues — laughter, tears, long sighs, long pauses
(≥3s), interruptions, overlap, child in background, voice
softening, voice lifting — preserved in `paralinguistic_cues[]`
Output ONLY the Interview JSON matching the provided schema. Every
segment carries `t_start_ms` and `t_end_ms` to millisecond precision.
Hard rules:
- Every word in `text_verbatim` is exactly what the speaker said.
Preserve dialect ("we was", "innit", "ano ba 'yan"), filler
("uh", "you know"), false starts ("the the the bus"), and
contractions exactly. Do NOT "fix" grammar. Do NOT silently
expand contractions. Do NOT silently translate into the
interviewer's English.
- Identify the language per segment in BCP-47. Mid-sentence code-
switches go into `embedded_phrases[]` with both the verbatim
phrase and the translation. Mark `code_switch_within_segment:
true`.
- Preserve every diacritic and tone mark exactly: Vietnamese tone
marks, Tagalog ñ, Polish ł ą ę, German ü ö ä ß, Yoruba dots and
accents, Tamil pulli, Devanagari nuktas. Render in Unicode.
- Do NOT translate proper nouns. First names ("Rajesh", "Mrs
Akinyemi", "Lolo Andoy", "Beta"), endearments, place names
(period names: "the old Co-op", "Calle Mabini before they
renamed it"), dish names ("kapeng barako", "ackee and saltfish",
"biryani"), brand names ("Hovis", "Sari-Sari") — keep verbatim.
- Do NOT translate yet at the segment level; `translation` is
null at this stage. The translation pass happens in a separate
call and writes back to each segment.
- If you cannot make out a word, set `reading_confidence` below
0.7 and use the literal phonetic approximation in `text_verbatim`
with `[unclear]` afterward. Do not invent.
- Long pauses ≥3 seconds go into `paralinguistic_cues[]` as
`long_pause` AND `pause_after_ms` on the previous segment
carries the millisecond gap. Do not erase the silence.
- Tears, laughter, sighs are recorded as cues; do not summarise
them in the transcript text.
- Diarise conservatively. If two voices overlap, mark `unknown` and
flag for historian review rather than guess.
- The pre-interview brief names the interviewee. Use it. The
interviewee is the longer-speaking voice; the interviewer is
the shorter-speaking voice. If diarisation is uncertain, set
`diarisation_quality: "low"` or `"uncertain"` and flag.
- People mentioned, places mentioned, events referenced are
extracted to the corresponding arrays, each with a verbatim
contextual quote and a `t_start_ms`.
- Pulled quotes are the strongest verbatim sentences — the ones a
newsletter editor would put into a sidebar. Pick 4-8. Every
word inside a pulled quote is verbatim. Each carries
`t_start_ms` and `t_end_ms` so the historian can click and hear
the line.
- Biography chapters are a chronological draft, in the
interviewee's narrative order. Each paragraph names its
supporting segments via `supporting_segment_t_starts_ms`. If a
paragraph has no supporting segment, do not write it.
- emotional_register_journey segments the recording into stretches
of consistent emotional register, picked from the closed enum.
- flagged_for_historian_review names any field where confidence is
below 0.7, plus every diarisation uncertainty, plus every place
that needs grounded geocoding.
No commentary. JSON only.
```
---
### Call: Translate transcript preserving voice + register
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You translate oral-history transcripts from the source language to
a target language the historian specifies (usually English, but
often the interviewee's grandchildren's language: Mandarin for
Cantonese-speaking grandmothers, English for Tagalog grandfathers,
Hindi for Punjabi elders).
You translate to be read by a grandchild or a community-newsletter
reader who never met the interviewee. The priority is voice. The
translation should make the reader feel that the interviewee is in
the room speaking — not that a polished editor has intervened.
You receive the array of `segments[]` from the Interview object.
Write back the `translation` field on each segment that requires
translation (segments already in the target language pass through).
Hard rules:
- Preserve endearments and diminutives. Tagalog "Anak ko" → "my
child" — but on first occurrence only; thereafter "Anak ko"
stays verbatim. Punjabi "Beta" → "son" first time, then "Beta".
Cantonese "阿仔" → "boy" first time, then "阿仔". Swahili
"Mtoto wangu" → "my child" first time, then verbatim. Do not
flatten to "dear child" every time.
- Do NOT translate proper nouns. First names, place names
(period and modern), dish names, brand names, organisations
("the gurudwara", "the Catholic Mothers' Union") stay verbatim.
Add a parenthetical first-occurrence gloss only.
- Do NOT translate measurement units. "1 katty", "1 sho", "1 ser",
"old money" stay verbatim with a parenthetical modern
conversion on first occurrence.
- Preserve dialect AND any false starts inside quotation marks if
the source segment contains a quote-within-a-quote. The English
translation outside the inner quotation marks may be smoothed
for readability; the words inside the inner marks are translated
literally and the original idiom is footnoted in
`register_note`.
- Preserve hedges and silences. "We weren't sure, you know"
translates to "We weren't sure, you know" — not "We were
uncertain at the time".
- Match the emotional register identified for the segment. A
"guarded" segment does not become candid in translation.
- Preserve the order of sentences. Do NOT reorder for English flow.
The original rhythm is what the historian wants.
- Preserve idioms by translating to an idiom in the target
language if one exists with the same emotional weight. If not,
translate literally and add a one-line `register_note` on the
segment.
- Preserve uncertainty markers. "Maybe she came back" stays
conditional; do not write "She came back".
- Embedded phrases (mid-sentence code-switches) already have their
own translation in `embedded_phrases[].translation`. Do not
re-translate them; flow them into the segment translation in
italics with the gloss already attached.
Output: write the `translation` field on each segment and update
`embedded_phrases[].register_note` where helpful. Return the full
updated `segments[]` array. No commentary.
```
---
### Call: Synthesise biography draft from `Interview` + brief
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are drafting a four-page newsletter biography from the
structured Interview object. The historian will read your draft
alongside the source transcript and audio, and then send it to the
interviewee for review. Nothing prints until the interviewee
approves.
You receive:
- the full Interview object (segments, people, places, events,
pulled_quotes, biography_chapters as a first pass, emotional
register journey)
- the pre-interview brief
- the project's house style notes (formal, conversational, etc.)
- the target newsletter length: 1500-2200 words across four pages
Hard rules:
- Every word inside a quotation mark is verbatim from a transcript
segment. Cite the supporting `t_start_ms` and `t_end_ms`. If a
candidate quote has been edited even by a single contraction
expansion, it is NOT a quote; rewrite as paraphrase outside
quotation marks.
- Every fact in the narrative paragraphs is supported by at least
one `t_start_ms` segment from the interview. List the supporting
timestamps on each paragraph. If you cannot list one, remove
the paragraph.
- The headline names the interviewee and one specific thing that
grounds them in the neighbourhood (an address, a job, an arrival
year, an institution they ran). "Mr Patel, who has lived on this
street since 1973" is good. "A local treasure shares his story"
is forbidden.
- The lede paragraph is one sentence the interviewee themselves
could have said. It does NOT summarise the whole interview; it
introduces one specific moment.
- Biography chapters are chronological by the interviewee's life,
not by the order they were discussed in the recording.
Approximate periods are stated as the interviewee stated them:
"around 1974, when my son started school", not "in 1974".
- Pulled quotes for the sidebar are 4-8 of the strongest verbatim
sentences. Each carries its `t_start_ms` and is replayable.
- Named people appear with the interviewee's terms for them
(first name, "Auntie", "my brother"). Surnames only if the
interviewee used them.
- Period place names are preserved alongside any modern resolution
("the old Co-op, what is now the Sainsbury's").
- Events are described as the interviewee described them, with
one sentence of plain context only if the interviewee gave the
context themselves. Do NOT add "this was during the [historical
era]" unless the interviewee said so.
- If the interviewee asked for a redaction, the relevant sentence
is rendered as "[name withheld at interviewee's request]" or
"[detail withheld]" in the draft. The supporting transcript
still carries the underlying segment for the historian's view.
- Voice: conversational, specific, never marketing. Active voice
except where the interviewee was specifically passive ("we were
told to move").
- Length: 1500-2200 words. If you cannot fill it without
speculation, the draft is shorter. Empty space is better than
an invented sentence.
- House style: pick from the project's house style notes
(formal-archival, community-newsletter, family-friendly).
Default is community-newsletter.
Output: the updated Interview object with `biography_headline`,
`biography_lede_paragraph`, `pulled_quotes[]` (each carrying its
`why_pulled` and timestamps), and `biography_chapters[]` (each
paragraph carrying its supporting timestamps). No commentary.
```
---
### Call: Geocode a period place name to modern coordinates
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding
```
You resolve a place name as the interviewee remembered it — often
a period name, a building that no longer exists, a street since
renamed — to modern coordinates, while preserving the period name.
Given a verbatim place name and the approximate period the
interviewee referenced, return:
- location_modern_name (e.g. "Atlantic Road, Brixton, London")
- modern_coordinates (lat, lng, decimal degrees)
- administrative_context_at_period (e.g. "Brixton, London Borough
of Lambeth, 1962" or "Quezon City, Metro Manila, 1971")
- one citation URL from grounded search
Hard rules:
- Use `google_search` grounding for any place that was renamed,
transferred between councils or municipalities, or was part of
a vanished administrative region. Period administrative
contexts that often arise: pre-1965 London (the LCC and the
Metropolitan Boroughs), pre-1974 English districts, pre-1995
Polish voivodeships, pre-1991 Soviet republics, pre-1986 Quezon
City barangay names, pre-1976 Saigon district names,
pre-Partition Punjab and Bengal, pre-1956 British Malaya,
pre-1949 China, pre-1948 Palestine, pre-1971 East Pakistan /
Bangladesh, pre-revolution Iran, pre-1959 Cuba.
- Preserve the period name in the response. Do not silently
substitute the modern name.
- If the place no longer exists (a corner shop demolished, a
cinema gone, a street erased by a road scheme), say so
explicitly: location_modern_name = "[name], no longer exists;
approximate former location near [nearest modern locality]".
- If multiple places share a name (there are many "Atlantic
Roads") and you cannot disambiguate from the interview's
contextual quote, return all plausible candidates and let the
historian choose.
Output the response as JSON in the text body (NOT via
`responseSchema` — `responseSchema` and `google_search` cannot be
combined in the same Gemini call today). Server-side: parse the
JSON, then read citation URLs from the response's
`groundingMetadata.groundingChunks[].web.uri` — do NOT ask the
model to include URLs in the JSON body; it will hallucinate them.
No commentary outside the JSON.
```
---
### Call: Consolidate People / Places / Events across project
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long context)
```
You receive the structured Interview objects for every interview
in an oral-history project at once (long-context). Your task:
propose unified People / Places / Events records — each named
person who appears across the interviews, each place, each event,
with the verbatim contextual quotes that ground them and the
confidence level of each cross-interview merge.
Hard rules:
- Two interviewees who both mention "Mrs Singh" refer to the same
person only with strong evidence: same street, same approximate
decade, same descriptive details ("the lady who ran the corner
shop", "my children's reading teacher"). Otherwise keep them as
separate records and surface as `needs_historian_confirmation`.
- Two interviewees who DISAGREE on a fact (the year the shop
closed; whether Mrs Singh moved away or passed away) are NOT
silently reconciled. Both quotes are surfaced side by side with
their interviewee attribution and `t_start_ms`.
- Multiple "Auntie"s are the same Auntie only with strong
evidence (same name attached, same relationship). Otherwise
separate.
- Quote, do not summarise. Every contextual_quote is verbatim
from a segment; cite the `interview_id` and `t_start_ms`.
- Do NOT extrapolate to historical events that the interviewees
did not mention. If three interviewees mention "after they took
the bus route away", store as a project-wide event with their
three quotes. Do NOT annotate "this likely refers to the [TfL
service change of 1981]".
- For places, distinguish addresses from generic references ("the
corner", "the river"). Addresses get geocoded; generic places
usually do not.
- A person who appears in one interview and is named as having
died in another interview is NOT silently marked dead in the
first interview. The reader of the first interview, in
chronological order, did not yet know. Both records are linked
but the chronology is preserved.
Output: a JSON object with `people[]`, `places[]`, `events[]`,
each carrying the verbatim contextual quotes and per-interview
attributions, plus a `needs_historian_confirmation[]` list of
cross-interview merges that fell below the confidence threshold.
No commentary.
```
---
### Call: Generate TTS read-back of the draft biography
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried, the pace of a community-newsletter editor
reading a piece aloud to the resident it's about. Pick the Gemini
2.5 Flash TTS voice whose `languageCode` matches the interviewee's
preferred reading language — pronunciation will follow that locale
automatically. Prefer the gender of the interviewee when both are
published for the locale; fall back to whichever is available
rather than blocking.
Pre-process the text before sending it to TTS:
- Read from the rendered biography draft (headline + lede + each
chapter's narrative paragraphs + the pulled-quote sidebars
spoken in order at their narrative position).
- At each paragraph break, insert a blank line plus an em-dash
(`—`) so the TTS model produces a natural pause. Inside a
paragraph, use a single ellipsis (`…`) at each sentence
boundary where the interviewee paused in the source recording
≥1.5 seconds. Gemini 2.5 TTS does not support SSML `` — these textual cues are how you signal pace.
- Pulled quotes are read in the interviewee's voice register
(warm, reflective, sorrowful, joyful, as captured in the
Interview's `emotional_register_journey`) by prepending a one-
sentence directive in front of the quote, e.g. "Reading this
quote softly, as a memory: …". This is the directive-sentence
technique; there is no separate `style` API field.
- Skip metadata like timestamps, footers, and credit lines — they
are visual context only.
- Mid-call voice switching is not supported. If the interviewee
code-switched in a pulled quote (Tagalog mid-English, Punjabi
mid-English), the TTS reads the whole quote in one voice and
the on-screen subtitle marks the code-switch in italics. The
descendant or interviewee sees the script switch even if the
audio is one voice. Optional: stitch a second TTS call client-
side for the embedded phrase if dramatic switching matters.
- Target rate: ~110 words per minute — read-aloud-in-a-front-room
pace, not podcast pace.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read this biography warmly and
unhurriedly, as a community-newsletter editor reading the draft
aloud to the resident it's about. …". 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 (Vietnamese tone marks, Tagalog ñ, Polish ł,
Punjabi tones, Arabic emphatic consonants, Yoruba dots) are NOT
exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation
comes from the chosen voice's native locale.
```
## 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 Brixton historian.** A volunteer with Lambeth Archives is interviewing Mrs Akinyemi, who arrived from Lagos in 1962 and has lived on Atlantic Road since 1965. The interview is 47 minutes; she switches between English, Nigerian English, and Yoruba for the names of relatives. The app handles the code-switches without flattening, and her grandchildren — who read English but not Yoruba — see both halves of every sentence in the draft.
- **The Quezon City barangay archivist.** A 28-year-old volunteer is recording the elder residents of Barangay Pinyahan before the next election cycle erases the institutional memory of the kapitana. Lolo Andoy, 84, sits in a plastic chair on the corner of Calle Mabini and remembers the street names before they were renamed in 1986. The geocoding call resolves the old names to modern coordinates without losing the period name.
- **The Lubbock Texas librarian.** The public library runs a "Memory Circles" programme on the second Thursday of every month. The librarian interviews Mr Hernandez, who came from Reynosa in 1958 and worked the cotton fields until 1972 and then the school district as a janitor until 2003. He speaks in a Texas-Mexican Spanglish that the app reads without flattening; the final newsletter is published in both English and Spanish columns side by side.
- **The Vietnamese-American community-archive volunteer.** In San José, a volunteer is collecting the stories of "first wave" Vietnamese arrivals — those who came between 1975 and 1980. Mrs Trần, 78, remembers the boat journey, the camp on Guam, and the church in San José that took her family in. She switches between Vietnamese and English mid-sentence; the app preserves the tone marks exactly and the translation preserves her hesitation around the camp years.
- **The Tower Hamlets mosque-committee archivist.** The committee is documenting the elder members before Ramadan. Mr Rahman, 81, came from Sylhet in 1971, worked the Brick Lane curry houses, and has been on the committee since 1989. He speaks Bengali at home, English at work, and slips into Arabic for the names of dua. The app reads all three.
- **The Glasgow Govan volunteer.** A community-archive volunteer is interviewing Mrs Maguire, 89, who has lived on the same close since 1948 and remembers the shipyards in full work. She speaks in a Glaswegian register the app preserves exactly — "ah wis seven", "ma maw" — without "correcting" it. The pulled quote sidebar in the newsletter is published in her exact dialect with a one-line guide for non-Glaswegian readers.
- **The Eritrean-diaspora archivist in Stockholm.** A second-generation volunteer is interviewing the elders of the Eritrean community church before they pass. The interview is in Tigrinya with Swedish and Amharic phrases mixed in; the app handles all three scripts (Ge'ez, Latin) and preserves the religious phrases in their original language for the church newsletter.
- **The hospice oral-history volunteer.** A trained palliative volunteer sits with Mrs Petrov, 79, in the last weeks. The interview is short — twenty-six minutes, with three long pauses — because Mrs Petrov tires quickly. The app handles the pauses without flattening them; the family receives the four-page draft, listens to the full audio, and approves the publication a week after she dies.
- **The Densho-style Japanese-American oral history.** A volunteer is interviewing Mr Tanaka, 87, who was in camp at Tule Lake. The interview touches on a topic the historian flagged in the pre-brief as sensitive; Mr Tanaka asks during the recording that two specific names not be published. The redactions are captured at the moment they are requested; the newsletter draft renders them as "[name withheld at interviewee's request]"; the underlying audio is preserved for the family but never published.
- **The Punjabi-Surrey-BC gurudwara archive.** A grandson is recording his grandfather, who came to Surrey in 1972 and worked the lumber mills. The interview is in Punjabi with English place names mixed in; the granddaughter — who reads Gurmukhi but not Shahmukhi — receives the draft in Gurmukhi with the English place names preserved exactly as he said them.
- **The cross-interview reconciliation.** After eight interviews on the same street, the Places view shows that three interviewees disagree on when the corner shop closed (1987 vs 1991 vs "the year after the riots"). The app surfaces all three quotes side by side with the speaker, the timestamp, and a "needs historian to footnote" flag. The historian publishes the newsletter with the disagreement as a sidebar called "What we don't agree on yet."
- **The donation.** Two years into the project, the historian decides to donate fifty interviews to the local university's special collections. She exports the archive bundle in TEI / EAD XML with the audio, transcripts, biographies, signed consent records, and period-map metadata. The university's archivist receives it as a single ZIP; the local council newsletter continues to use the live app for fresh interviews.
## 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 small kitchen table at golden hour with a phone propped on a small wooden stand, an empty teacup, a notebook with a pen across it, and a hand at the edge of frame inviting someone to sit down. One paragraph: "Hall of Fame turns a forty-minute conversation with a long-time resident into a four-page biography — for the community newsletter, the parish bulletin, the barangay archive." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample project" → loads the demo project in section 8a.
2. **Empty state — "Start a project".** Three big options: 📓 New project · 👥 Join a project (magic link) · 🗃 Import an existing archive. A short explainer below each. The project metadata form asks for: project name, neighbourhood, target newsletter or archive, languages expected, house style preference, project lead's contact for review-link delivery.
3. **Pre-interview brief.** Before the recorder appears. The historian types or speaks: interviewee's name (verbatim), preferred pronouns, languages they speak, address or street, approximate year they arrived in the neighbourhood, and three things they hope to talk about. This form is short and warm — "what's their full name, the way they say it?" not "Surname". Save and continue.
4. **Consent flow.** A two-screen plain-English walkthrough. Screen 1: "What we'll do today — record a conversation, write a draft, send it to you to read, change anything you want, publish only after you say yes." Screen 2: a recorded statement. The interviewee speaks for fifteen seconds: "I, [name], understand I'm being recorded for the [project] newsletter. I'll see the draft before anything is printed. I can withdraw at any time before print day." The historian countersigns by tapping a button. The recorded audio plus the checkbox is the consent record.
5. **Recording view.** A single screen. Big record button. A live audio level meter that turns gentle amber if the microphone is far from the speaker. A pause button the size of a fist for the interviewee to use if they want. The current language detected appears as a small chip below the meter ("English", "English / Tagalog"). The historian's pre-brief is collapsed at the top, expandable. The screen does NOT show the transcript live during recording — the focus is the conversation, not the screen.
6. **Processing queue.** After the recording stops, a vertical list shows the night's work. Each interview shows the interviewee's name, the duration, and a step-by-step honest progress bar: "Reading the audio…" → "Identifying the speakers…" → "Translating Tagalog passages…" → "Pulling the strongest quotes…" → "Drafting the biography…" → "Mapping the places mentioned…". Each step takes 15-60 seconds. The historian can close the app and come back.
7. **Interview detail view.** A three-column layout on desktop, stacked on mobile. Left column: the audio player (waveform with chapter markers from the biography chapters), with a "skip to next pulled quote" control. Middle column: the diarised transcript with code-switch markers, paralinguistic cues in brackets, long pauses visible as gaps, low-confidence words faintly underlined. Right column: the structured biography draft with hyperlinked timestamps on every paragraph and pulled quote. Sticky header: interviewee name → date → languages → review state chip → "(i) show how the AI read this".
8. **Project view — Interviews.** Magazine-grid of every interview in the project. Filter by interviewee, language, decade of life-events, neighbourhood, review state. Default sort: chronological by recording date.
9. **Project view — People.** The consolidated People across the project. Each card shows the name verbatim, the interviewees who mention them, the verbatim contextual quotes, and a "merge" / "split" affordance for the historian. Disagreements (one interview says Mrs Singh moved, another says she died) are surfaced as side-by-side quotes with timestamps.
10. **Project view — Places.** The neighbourhood map. Every place mentioned across all interviews is a pin, sized by how many times mentioned. Click a pin → every interview that mentions it, with the verbatim contextual quote. Toggle a period overlay where available. The map view exposes a list-mode alternative for keyboard and screen-reader users.
11. **Review-by-interviewee flow.** The link the historian sends to the interviewee. The interviewee lands on a calm, four-page preview of the biography. Each pulled quote has a small play button — they can hear themselves saying that exact line. Each paragraph has an "edit / redact / approve" affordance. A consent panel at the bottom: "Tick yes if we may publish this in the newsletter."
12. **Sharing & project invitations.** Modal: "Invite a co-historian to add their interviews to this project". Magic-link email; arrival drops the co-historian straight into the same project with their own avatar. Granular permissions per project member (record / edit / publish / archive).
13. **Newsletter PDF export.** Side-by-side typeset preview. Choose: one interview per page or two interviews per spread, with or without the period map, with or without the sidebar quotes, with the disagreements sidebar enabled. Export PDF; future tier: print-bound newsletter run.
14. **Archive-grade bundle export.** "Donate to an institutional archive." Pick from a curated list (Densho, the BBC Listening Project, StoryCorps Archive, the local university's special collections, the parish archive, the gurudwara committee archive) or "other — email me the bundle". TEI / EAD XML + audio + transcripts + biographies + signed consents + map metadata zipped.
15. **Footer.** "Made for the people who hold a street's memory." Privacy: "Your interviews are yours, and the interviewee's. We never train on them." 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 Hall of Fame."
- Subhead: "Turn a forty-minute conversation with a long-time resident into a four-page biography — in any neighbourhood, any language, with every quote exact and the interviewee's consent at the centre."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic transcript app: the consent flow is the first screen, every word inside a quotation mark is verbatim, the biography draft hyperlinks every fact back to the audio second that supports it, and nothing is published until the interviewee has read and approved the draft.
- Visual: a small annotated illustration of a phone on a kitchen-table stand with a notebook beside it and a pulled-quote sidebar floating above — not a generic microphone icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample project".
- A live demo input pre-loaded with three interviews from the seed content in section 8a (Mrs Akinyemi, Lolo Andoy, Mrs Maguire).
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the Yoruba code-switch preserved in Mrs Akinyemi's transcript, the period place name "Calle Mabini before they renamed it" in Lolo Andoy's transcript, the Glaswegian dialect preserved in Mrs Maguire's pulled quotes).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample project in `/data/seed-project/` for your own neighbourhood."
- "Adjust the prompts in `/server/prompts/` for your community's languages and house style."
- "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (audio reasoning)** — reads a forty-minute interview end-to-end with diarisation, language identification per segment, named-entity recognition, paralinguistic cues, and millisecond-precise timestamps on every line. One call per interview.
- **Gemini 3.5 Flash (multilingual)** — translates Tagalog, Vietnamese, Cantonese and Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Arabic, Yoruba, Igbo, Twi, French, Spanish, Portuguese, Polish, Russian, Turkish — and mid-sentence code-switches — without flattening the speaker's voice.
- **Gemini 3.5 Flash (long context)** — once a project has many interviews, the People view and the Places view see every interview at once to resolve "is this the same Mrs Singh?" and surface cross-interview disagreements.
- **Gemini 3.5 Flash + grounded search** — resolves period place names to modern coordinates without losing the period name.
- **Gemini TTS** — reads the draft biography aloud to the interviewee in the interviewee's preferred language at an unhurried pace.
- **Firebase Auth** — Google and Apple sign-in, project invitations via magic links.
- **Firestore** — stores your project, syncs across devices in real time.
- **Firebase Storage** — keeps the original audio recordings at upload bit-rate, forever.
- **Cost note** — see the detailed breakdown in 6d. A typical forty-minute interview costs about $0.08 of Gemini API spend, total, processed once.
- **Privacy note** — your interviews are private to you and the interviewee. 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. Redactions requested by the interviewee are honoured at every rendering layer. The interviewee may withdraw consent at any time before the newsletter is printed.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b (NOT auto-provisioned; enable in Firebase console)
- Email: see section 4b
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `MAPBOX_TOKEN` — optional, only if you want richer neighbourhood-map tiles than the included static layer
- `TWILIO_ACCOUNT_SID` / `TWILIO_AUTH_TOKEN` — optional, only if you want SMS review-link delivery as a fallback to email
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: a forty-minute interview processed end-to-end (audio reasoning + translation + biography draft + geocoding for 6-10 places + TTS read-back of the draft) costs about $0.08. The long-context cross-project consolidation (People / Places / Events) runs weekly and costs about $0.30 per project of 30 interviews per run.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this interview forever" — gone in 60 seconds), what is never sent for training, how redactions are honoured at every rendering layer, and how interviewee consent can be withdrawn before print day.
**Documentation links:**
- AI Studio Build docs
- Gemini API audio-reasoning, multilingual, long-context, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on TEI / EAD XML for the donation export
- A short note on the StoryCorps DIY consent model the app's plain-English flow draws on
**Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Audio reasoning — diarised transcript (Gemini 3.5 Flash, medium thinking)** — a 40-minute interview ≈ ~12,000 tokens of audio input, ~25,000 tokens of structured output. ~$0.14 per interview. This is the single largest line item per interview.
- **Translation pass (Gemini 3.5 Flash, low thinking)** — ~25,000 tokens of segments rewritten with translations. ~$0.08 per interview (only on interviews containing non-target-language segments; pure-English interviews skip this call).
- **Biography draft synthesis (Gemini 3.5 Flash, low thinking)** — reads the structured Interview + brief + house style, returns the typeset draft. ~$0.04 per interview.
- **Geocoding (Gemini 3.5 Flash + grounded search)** — ~$0.001 per distinct place. A typical interview has 6-10 distinct places; runs once per place per project, cached. ~$0.01 per interview.
- **Cross-project consolidation — People / Places / Events (Gemini 3.5 Flash, medium thinking, long-context, project-wide)** — runs weekly. ~$0.30 per project of 30 interviews per run. **Guardrail**: above ~60 interviews, chunk by ward / street / decade before the call.
- **TTS read-back of the draft biography (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 1,800-word draft ≈ $0.03 per read-back. Cached per draft version; charged once per draft.
- **Doodle / photograph description (Gemini 3.5 Flash, low thinking)** — for the photographs the interviewee shows the historian during the conversation. ~$0.0005 per photograph. Optional.
- **Expected per-interview total cost on first ingest:** ~$0.08-$0.10 depending on language mix. **Project of 30 interviews total:** ~$3 to ingest once, plus ~$0.30/week ongoing for cross-project consolidation.
- **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 40-minute interview at 128 kbps mono AAC ≈ 38 MB; a project of 30 interviews ≈ 1.2 GB ≈ ~$0.03/month.
## 7. Design language
- **Mood:** A community-newsletter desk on a Tuesday afternoon. Not a tech product. Not a podcast studio. The volunteer's notebook on the kitchen table at Mr Patel's flat, the kettle in the next room, the recorder propped against a hardback book, the conversation that has no deadline and the draft that nobody is going to print without permission. Quiet, attentive, considered.
- **Typography:** Display serif for biography content and chapter headings (Adobe Caslon Pro or Source Serif Pro). A handwriting-styled accent (sparingly) only for the "review notes from the interviewee" margin annotations — never for the parsed transcript itself, which sits in serif. Clean grotesque for app chrome (Inter or Geist). The pulled-quote sidebars use the display serif at a larger size, ranged left, with a small ornamental drop cap from a community-newspaper masthead tradition.
- **Palette:** Newsprint-cream background `#F5F1E8` for the biography view, deep ink `#1F1B16` for body text, warm-newsprint accent `#7A5B33` for pulled quotes and chapter rules, faded red `#A33A2C` only for the redaction marks and the "needs historian to confirm" flags. A muted green `#3F6B4F` for the consent and review-state chips, picked because it reads as "approved" without the polished gloss of SaaS-green. Borrowed from a community-paper masthead, not from a podcast-app design system.
- **Imagery:** The waveform of the recording is a quiet hero — never theatrical, never animated. Period maps use restrained pastel fills and thin black borders. Photographs the interviewee shows the historian are shown at the moment they are referenced, with a small caption noting the moment in the recording when they were discussed. No stock photographs of "elders looking thoughtful".
- **Hand-feel touches:** A barely-visible paper grain on the biography-detail background. The "play this quote" button sits next to the quote with a fine sepia-tinted icon and a hairline rule below — like a margin printer's mark. Hover on a paragraph reveals the supporting audio timestamps; never aggressively glow.
- **Spacing:** consistent 4-px base. Generous whitespace — the biography needs air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Interview cards use 6; the newsletter preview uses 12; the welcome card uses 20.
- **Shadows:** subtle, layered, warm-cream-tinted. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The audio waveform's playhead progress is the one place where motion carries meaning; under `prefers-reduced-motion` the playhead jumps in 1-second discrete steps rather than animating smoothly.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Record your first conversation, or invite a co-historian to add one.").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, neighbourhoods, streets, languages, quotes that fit the domain (use the seed content in section 8a as a starting point). When inventing, span continents — Brixton, Quezon City, Lubbock, Glasgow, Stockholm, Surrey BC — and refuse the trap of centring one neighbourhood as default. Never claim a fictional interviewee is a real person.
- Tone: warm, direct, free of corporate language. This template is for a community-newsletter editor and the resident at the kitchen table, not a podcast app.
- 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 institutional archivist user wants to see "TEI / EAD XML" in the donation export label; the librarian user wants to see "patron").
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low confidence transcript word shows as a faintly underlined word; tapping it reveals the alternates the model considered).
- Every pulled quote in any visual mockup is verbatim — never paraphrased, never "cleaned up". This is the load-bearing rule of the app and it shows in every demo.
## 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 projects (sidebar):**
- "Atlantic Road Voices" (12 interviews, lead: Lambeth Archives volunteer) — Brixton long-time residents, 1962-present, English / Yoruba / Jamaican Patois.
- "Pinyahan Memory Circle" (8 interviews, lead: barangay archivist) — Quezon City elders, 1971-2009, Tagalog / English / occasional Bisaya from in-laws.
- "Memory Circles, Lubbock Public Library" (15 interviews, lead: head librarian) — West Texas elders, 1958-2003, Texas-Mexican Spanglish / English.
- "Govan Closes" (9 interviews, lead: community-history volunteer) — Govan close residents, 1948-present, Glaswegian English with Gaelic phrases.
**Sample interview in detail view (this is what the demo should show):**
- **Project:** "Atlantic Road Voices"
- **Interviewee (verbatim):** "Mrs Akinyemi"
- **Interviewee inferred:** Mrs Olubunmi Akinyemi
- **Interviewee preferred pronouns:** "she / her"
- **Languages:** English (en-GB), Nigerian English (en-NG), Yoruba (yo-NG)
- **Interview recorded at:** "2026-04-14T15:12:00Z"
- **Interview location:** "Mrs Akinyemi's front room, Atlantic Road, Brixton"
- **Duration:** 47:18
- **Pre-interview brief (verbatim):** "Mrs Akinyemi arrived from Lagos in 1962, married Mr Akinyemi (a tailor) on Atlantic Road in 1965, raised three children at the same address. She wants to talk about: the market in the seventies, the year the shop windows went in (April 1981), her late husband's tailoring."
- **Primary language:** en-GB · **Languages observed:** [en-GB, en-NG, yo-NG]
- **Diarisation quality:** high
- **Consent record (recorded statement, verbatim):** "I, Olubunmi Akinyemi, understand I'm being recorded for the Atlantic Road Voices project. I'll see the draft before anything is printed in the newsletter. I can ask for changes or withdraw at any time before print day."
- **Sample segment (verbatim):** "Interviewer (en-GB, 14:22-14:31): So tell me about the morning the windows went in. Interviewee (en-GB→yo-NG embedded phrase, 14:33-14:58): It was a Friday. I was at the back of the shop and I heard the glass. My husband said 'ọmọ, ma se idakẹ' — that means 'child, don't make a sound' — and we sat. We sat for a long time. (long_pause 6.4s) Then there were sirens."
- **Pulled quote (verbatim, suitable_for_sidebar: true):** "It was a Friday. I was at the back of the shop and I heard the glass." (t_start 14:33, t_end 14:42, why_pulled: "anchors the April 1981 moment in one sensory sentence")
- **People mentioned (3):** "my husband" (Mr Akinyemi, the interviewee's spouse, deceased 2018, mentioned at 14:42 and 22:11), "Mrs Singh" (neighbour, ran the corner shop, mentioned at 31:04), "the priest" (Father Okafor, mentioned at 38:55)
- **Places mentioned (4):** "the shop" (her late husband's tailoring shop, Atlantic Road, mentioned at 14:33), "the market" (Brixton Market, mentioned at 6:12), "the church" (the local Anglican church, mentioned at 38:55), "back home" (Lagos, mentioned at 2:14)
- **Events referenced (2):** description "interviewee mentions the morning the shop windows went in" (quote at 14:33, period referenced "April 1981", interpretive note left blank for historian); description "interviewee mentions her husband's funeral" (quote at 22:11, period referenced "2018", interpretive note left blank)
- **Biography headline:** "Mrs Olubunmi Akinyemi, who has lived on Atlantic Road since 1962"
- **Biography lede:** "She remembers the morning the shop windows went in by the sound — first the glass, then the long silence, then the sirens."
- **Emotional register journey:** [warm-conversational 0:00-14:00, reflective 14:00-15:30, sorrowful 15:30-23:00, warm-conversational 23:00-end]
- **Review state:** "sent_to_interviewee"
- **Overall reading confidence:** 0.94
**Sample input artefacts (for the build to demonstrate):**
- A forty-seven-minute audio recording of Mrs Akinyemi, English with a Yoruba embedded phrase at 14:33, a long pause at 14:42, a quiet stretch around her late husband's funeral.
- A thirty-eight-minute audio recording of Lolo Andoy in Pinyahan, Tagalog with English place names mixed in, a long pause around the floods of 2009.
- A fifty-two-minute audio recording of Mrs Maguire in Govan, Glaswegian English throughout, mentions of the shipyards and the close she has lived on since 1948.
- A twenty-six-minute audio recording of Mrs Petrov, in a hospice setting, three long pauses, a family member offering tea in the background at 12:04.
- A photograph the interviewee shows the historian at 33:11 — a wedding photograph from 1965, paper-clipped to her copy of the local paper.
**Sample voice copy:**
- Onboarding: "Record a conversation with a long-time resident. We'll write the draft. They'll read it. Nothing prints until they say yes."
- Recording start: "Press record when you're both ready."
- Recording level warning: "You may be a little far from the microphone — would you like to move it a bit closer?"
- Processing: "Reading the audio…" / "Identifying the speakers…" / "Translating Yoruba passages…" / "Pulling the strongest quotes…" / "Drafting the biography…" / "Mapping the places mentioned…"
- Empty project: "This project is waiting for its first conversation. Press record when you're both ready."
- Error (couldn't process): "We couldn't make out a stretch of audio around 23:14. Want to upload a cleaner copy, or note what you remember in the side panel?"
- Save confirmation: "Saved to Atlantic Road Voices — Mrs Akinyemi, 14 April 2026, 47 minutes."
- Redaction logged: "We've logged that Mrs Akinyemi asked us not to name her neighbour in print. The full transcript is preserved for the archive; the newsletter draft will say '[name withheld]'."
- Review-link sent: "We've sent Mrs Akinyemi a link to read her draft. We'll let you know when she's read it."
- Approved by interviewee: "Mrs Akinyemi approved the draft on 21 April 2026. You can publish to the newsletter when ready."
- Low confidence note: "Some words were hard to make out. Tap any underlined word to hear that second of the audio."
**Sample family / co-historian invitation email subject + body:**
- Subject: "Add your interviews to Atlantic Road Voices"
- Body: "Hi Marcus — I'm starting an oral-history project for the Brixton newsletter and I'd love your help. You'd be able to record your own conversations and add them to the same project. Tap to join." [Open Project]
**Sample review-by-interviewee email subject + body:**
- Subject: "Mrs Akinyemi — your draft for the Atlantic Road Voices newsletter"
- Body: "Dear Mrs Akinyemi, thank you again for sitting down with us last week. Here is the draft of your piece for the newsletter. Please listen to any of the quotes by tapping them, and let us know what you'd like to change. Nothing will be printed until you tap 'Yes, you may use this' at the bottom. — Folake, Atlantic Road Voices." [Open Draft]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a small kitchen table at golden hour with a phone propped on a small wooden stand, an empty teacup, a notebook with a pen across it, and a hand at the edge of frame inviting someone to sit down. Generate via Nano Banana 2 with a prompt emphasising "small wooden table, soft late-afternoon light, hand of a woman in her thirties at the edge of frame, empty teacup with a teaspoon, hardback notebook with a pen, phone on a small wooden stand, soft warm shadow under the table, no people's faces, real domestic feel".
- **App icon / wordmark:** Set in the display serif. Slightly worn paper texture behind it. A small drop-cap ornament borrowed from a community-newspaper masthead.
- **Empty-state illustration:** A simple line drawing of two chairs facing each other across a small table with a phone in the middle. Hand-drawn aesthetic, not a flat icon.
- **Sample interview waveforms:** Generated per the durations in 8a, with quiet stretches and long pauses visible. Each waveform should look honest, not theatrically peaked.
- **Period maps:** A static neighbourhood map layer in `/data/period-maps/` for the four sample neighbourhoods (Brixton, Pinyahan, Lubbock, Govan). No image-based tiles needed for the offline path; if Mapbox is configured, use the "muted historical" style.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample kitchen-table photo from `/public/samples/sample-table.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.
- **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"). The pre-interview brief form treats every field as optional — the historian may have only the interviewee's first name when they start.
- Loading states use skeletons that match the eventual layout, not spinners. The processing queue's step-by-step progress is the one place where text-level honesty replaces a generic spinner: "Reading the audio…" → "Identifying the speakers…" → "Translating Yoruba passages…" → "Pulling the strongest quotes…" → "Drafting the biography…" → "Mapping the places mentioned…".
- Empty states explain the next action with a button whose label fits THIS app's domain: "Record your first conversation", "Invite a co-historian", "Send the draft to Mrs Akinyemi" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving — but the biography draft is shown only after the full draft is rendered, since partial drafts mislead reviewers.
- If an AI call fails, show a calm, specific error ("We couldn't make out a stretch around 23:14. Try a cleaner copy, or type what you remember into the side panel?") and offer retry.
- Low-confidence words in the transcript are faintly underlined; tapping reveals the alternates the model considered and seeks to that second in the audio.
- The audio player's playhead under `prefers-reduced-motion` jumps in 1-second discrete steps rather than animating smoothly.
- The "Send to Mr Patel for review" button is disabled until the historian has read at least one paragraph of the biography draft, indicated by scroll position; a tooltip explains why.
- The "Publish to newsletter" button is disabled until the server-verified `interviewee_approved` review state is set. The disabled state has its own copy: "Waiting for Mrs Akinyemi to approve."
## 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.
- **Model selection:** explicitly pin `gemini-3.5-flash` for audio-reasoning / translation / biography-synthesis / cross-project consolidation and `gemini-3.5-flash` for geocoding. Set `thinkingLevel` explicitly per call. TTS uses `gemini-3.1-flash-tts-preview`.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed project on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it (user-configured); magic-link email as fallback (user-configured sender domain).
- **Storage:** Firebase Storage for original audio recordings (NOT auto-provisioned; enable in Firebase console). Pre-signed URLs only.
- **Long-context guardrail:** at >60 interviews per project, the cross-project consolidation call chunks by ward / street / decade before exceeding the 1M-token ceiling.
- **Audio file handling:** the audio is uploaded to Firebase Storage at the historian's session end, then submitted to the Gemini Files API as a `files/*` resource name for the audio-reasoning call. Do NOT pass Firebase Storage public URLs directly to `generateContent` — the API does not fetch them. Multipart (pause/resume) recordings are concatenated server-side into a single audio file before submission so the model sees one chronology.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the project view.
- Optimistic UI on writes; reconcile on response.
- Recording uses the Web Audio API with a fixed sample rate and mono channel to keep file size manageable on a phone budget; falls back to native recorder where unsupported.
- **iOS Safari gotchas (graceful degradation):** Safari `MediaRecorder` only supports `audio/mp4` (AAC) — feature-detect and persist as AAC mono; mic permission does NOT persist across page reloads on iOS — re-request on every interview session; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause, persist the partial recording to IndexedDB tagged "interrupted", and prompt the historian to resume or restart; backgrounded Safari tabs pause `getUserMedia` — pair `visibilitychange` with a screen Wake Lock during a long oral-history interview.
## 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.
- All images have meaningful `alt` text. The photograph the interviewee shows the historian carries `alt` describing the artefact ("photograph of Mrs Akinyemi and her late husband on their wedding day, 1965, taken outside the Atlantic Road church").
- Form fields have associated `