================ 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.
---
# Locket Story
## 1. Project
**Locket Story** is a printable-keepsake builder for the moment a piece of
family jewellery moves from one generation's hand to the next. The user
photographs a single artefact — a locket, a pendant, a temple charm, a
filigree pin, a signet ring, a hair-clasp, a string of jade beads — and
records two to six short voice memos from the older women and men in the
family who knew the artefact's earlier wearers. Gemini 3.5 Flash reads the
photographs, listens to every voice memo, weaves the four-or-five
generations of provenance into a single typeset card written in the
giver's voice, and lays it out as an A5 keepsake ready to print on
heavy paper, fold into the velvet box, or slip behind the locket itself.
This is the kind of app a Lebanese-American mother builds on a Saturday
afternoon two weeks before her daughter's sixteenth birthday — because
on her bedside table is a small 21-gram filigree pendant her own
grandmother brought from Beirut in 1962, and she wants the daughter to
receive it with the four generations of names already written down, in
the mother's own reading voice, before she forgets to ask her aunt Najwa
the question about 1975. It is also the kind of app a Cambodian-American
mother builds the week of her daughter's high-school graduation,
because the gold temple charm in her jewellery box came out of Battambang
in 1979 hidden in the hem of a sarong, and the women who carried it
through the camp are still alive in Long Beach and Lowell and have the
story but not the patience for a Google Doc. Same shape of moment,
different artefact, different continent, different decade.
The single demo that proves the magic: photograph one filigree pendant
on a sheet of cream paper, attach three voice memos (the great-aunt on
the original wearer, the cousin on the 1975 crossing, the mother on the
giving) → in under 45 seconds the user sees an A5 card, typeset in a
warm display serif, with the photograph of the pendant at the top, a
four-generation provenance ladder beneath it ("worn by Salma in Beirut,
1948 · carried by Najwa to Detroit, 1962 · worn by Mona at her wedding
in Dearborn, 1991 · given to Layla on her sixteenth birthday, 2026"),
and three short paragraphs in the mother's voice telling the daughter
who wore it, when, and why. A small "back of the card" panel preserves
the verbatim quotes from each voice memo and credits each aunt by name.
And in the harder cases — diaspora artefacts that survived a war,
adopted families piecing together a birth-grandmother's pendant, blended
families honouring both a stepmother and a birth-mother, families where
the giver has six months to live and is recording herself as one of
the voices — Locket Story holds the same shape. A consent gate on every
voice memo. A "first-person from beyond" mode where the giver records
the card in her own voice for the recipient to open ten years later.
A "redact this section" tool for the parts of family history that are
true but not for the printed card.
**Tagline:** _Pass an heirloom with its story written down — in any
language, any handwriting tradition, any number of generations._
## 2. Target audience
- Mothers, fathers, grandmothers passing a piece of jewellery to a teenage or adult child on a birthday, graduation, wedding morning, or first day of university
- Diaspora families whose heirlooms crossed an ocean or a border under duress — Lebanese, Cambodian, Vietnamese, Ethiopian, Iranian, Cuban, Filipino, Korean, Polish, Ukrainian, Eritrean, Salvadoran, Palestinian, Bosnian
- Adoptive parents giving a birth-family heirloom passed forward by an open-adoption birth-mother, with carefully consented voice memos from both families
- Long-distance-relationship archivists — grandmothers and grandfathers who live three time zones from the grandchild and want the card waiting in the velvet box before the next visit
- Hospice families recording the giver herself as one of the voices, so the printed card carries her own first-person paragraph alongside the aunts'
- Memorial-project organisers building a year-of-mourning card for an heirloom that was buried with its last wearer but photographed first
- Blended-family parents giving an heirloom from one branch of the family in a way that honours every parent the recipient calls a parent
- LGBTQ+ givers reframing inheritance for a non-biological niece, an adopted son, or a chosen-family godchild — the "blood ladder" of traditional provenance cards does not have to be the spine of this one
- Couples preparing wedding-morning keepsakes for grandmothers' jewellery being lent for the day — "something old" with the four-generation story for the guestbook table
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **One artefact, beautifully read** — Gemini 3.5 Flash looks at one photograph of one piece of jewellery the way a jeweller's loupe and an archivist would together. Filigree pattern, hallmark, clasp type, hinge style, locket-photograph interior, engraving, monogram, dedication, dent, repair. The reading is loving but precise. The card always shows what was actually seen, never what the app guessed.
- **The voices stay the voices** — Gemini transcribes each voice memo in its source language, preserves the actual phrasing of the aunt who recorded it ("she pinned it on me like this, ya habibti"), and quotes her by name on the back of the card. The woven provenance paragraph in the front of the card is written in the giver's voice, not the AI's, and never replaces the aunts' verbatim words.
- **Multilingual without flattening** — Lebanese Arabic ("ya albi"), Khmer ("kun yeay"), Tagalog ("Inay"), Vietnamese ("má"), Hindi ("nani"), Mandarin ("奶奶"), Cantonese ("嫲嫲"), Tamil ("paati"), Yoruba ("iya"), Amharic ("እማማ"), Polish ("babcia"), Spanish ("abuelita"), Farsi ("مادر بزرگ"), Portuguese ("vovó") — endearments and relationship words stay in the language they were spoken in. The English-language card carries them in italics and never translates them away.
- **Generations, not slides** — the provenance is rendered as a ladder, not a slideshow. Four to five generations, each on a single line — wearer · place · year · one short verb of what happened to the artefact under their care. The whole shape of the family with the artefact moving through it is visible in one glance, by candlelight, on the morning of the giving.
- **Consent is the starting state** — every voice memo requires the speaker to record the consent line first, in her own voice, before the model will use her words. The card credits each contributor by name on the back, in a quiet typeface, with the date she recorded.
- **Print-shop quality, kitchen-table effort** — the card exports as a 300 dpi PDF/X-3 at A5 (148 × 210 mm) with bleed, crop marks, and Pantone-safe colours — droppable straight into the local print shop, foldable into a velvet jewellery box, or trim-cut and slipped behind a locket photograph.
- **The giver's voice, not the AI's** — the giver chooses her tone (warm-direct, brief-formal, candid-funny, ceremonial, the-voice-she-uses-at-bedtime). The model writes the provenance paragraph in that voice and refuses to add flourish. No "Dear treasured one." No "As you embark on this new chapter." No flowery preamble. The first sentence is the artefact and a year.
- **Designed to be re-given** — every card includes an empty blank generation at the bottom ("____ · ____ · ____ · ____") so the recipient, fifteen years from now, can fill in her own line in pen before passing the artefact to her own daughter, niece, godchild, or grandchild.
## 4. Features to build
- Camera capture for one artefact (mobile-first), with a paper-backdrop guide ("place the locket on a sheet of plain cream or off-white paper, in natural daylight if possible — the photograph will be the hero of the card")
- Upload from photo library (some users have already photographed in good light at home and don't want to redo)
- Optional second photograph for an opened locket: front, back, inside-left compartment, inside-right compartment
- Optional supplementary photographs of accompanying artefacts: the original velvet box, a sepia portrait of the first wearer, the handwritten note that was tucked inside
- Multimodal jewellery reading — material (gold karat estimate, silver, brass, plated, gold-filled), construction (filigree, cast, stamped, engraved, hand-engraved), motif (cross, hand-of-Fatima, hamsa, Star of David, Khanda, Om, lotus, temple charm, evil-eye, cameo, mourning hair-work), clasp type, hinge type, hallmark/maker's mark if visible, dimensions estimated from the paper-backdrop scale, weight if the user has measured it
- Voice-memo recorder, in-app, with a pre-recorded consent line the speaker repeats in her own voice before the actual memo starts
- Multi-language voice memo support — the model transcribes in the source language and translates to the card's target language without losing the source-language phrasing
- Diarisation when multiple speakers appear in one memo ("my mother passed the phone to my aunt halfway through")
- Provenance-ladder editor — each generation is a row: name (verbatim, as the contributor said it), relationship-to-recipient, place, year, one short verb-phrase ("worn at her wedding", "carried in the hem of a sarong", "kept in a tin under the bed")
- Voice-selector for the card's prose — pick the giver's tone from a small set (warm-direct, brief-formal, candid-funny, ceremonial, bedtime-quiet) — these change the prompt, not the layout
- Provenance-paragraph generator (Gemini 3.5 Flash, the load-bearing call) — takes the artefact reading + voice-memo transcripts + ladder rows → produces three short paragraphs in the giver's chosen voice
- Verbatim quote panel on the back of the card — each contributor's most distinct sentence, in her source language plus translation, credited by name and date
- Redaction tool — the giver marks any paragraph "for the back, not the front" or "for nobody, but record it" or "for the recipient's eyes, paper-only"
- "First-person from beyond" mode — the giver records herself as one of the voices and the model treats her recording as the giver's own paragraph, not as an "aunt" voice; explicit consent gate ("I want this voice memo to appear as me speaking directly to the recipient")
- Layout preview — A5 portrait, A6 portrait (slip-behind-locket size), or square 100 × 100 mm (for the velvet box lid)
- Typography pairing — three pre-set serif pairings (Cormorant + Inter, Source Serif + Geist, EB Garamond + IBM Plex) plus an Arabic-and-English pair (Vazirmatn + Inter), a CJK pair (Noto Serif TC + Noto Sans), a Devanagari pair (Tiro Devanagari + Inter), a Khmer pair (Hanuman + Noto Sans)
- Print export — A5 PDF/X-3 with 3 mm bleed, crop marks, ICC profile embedded, 300 dpi raster of the artefact photograph
- Velvet-box-lid sticker export — a 100 × 100 mm version designed to be printed on adhesive paper and adhered inside the lid
- Slip-behind-locket export — a 35 × 50 mm version with the four-generation ladder typeset at 5.5 pt minimum on archival paper
- Re-gift template — the export includes an empty next-generation row for the recipient to fill in by hand before re-giving the artefact in twenty years' time
- Share by email — a print-ready PDF and a low-resolution preview to relatives in other countries, with consent already credited
- Save to family archive — Firestore record of the artefact, its photographs, its provenance ladder, every voice memo, every consent recording, and every printed version
## 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)
- **Multimodal image input** (Gemini 3.5 Flash) — reads one artefact in detail. Filigree pattern, hinge style, hallmark, monogram, hand-engraved date, dent, repair, locket-interior photograph (if present and the user opened it). The model also reads the secondary photographs (the original velvet box, the sepia portrait inside the locket, the handwritten provenance note tucked inside).
- **Multimodal audio input** (Gemini 3.5 Flash) — listens to each voice memo end-to-end, transcribes in the source language, identifies the speaker(s) per memo via the in-memo introduction ("This is Najwa, the recipient's great-aunt"), separates the consent line from the substantive content, and surfaces the most distinct phrasing the contributor used.
- **Narrative weaving** (Gemini 3.5 Flash, the load-bearing call) — takes the artefact reading + the transcribed voice memos + the user's ladder edits + the user's voice-selector choice → produces the three short paragraphs that are the front of the card, in the giver's chosen voice. This is the call that earns the user's trust; it is also the call that has the strictest constraints (no flourish, no AI preamble, every named person and date traceable to a transcript or the ladder).
- **Multilingual translation** (Gemini 3.5 Flash) — every voice memo is transcribed in its source language and translated to the card's target language. Relationship words and endearments stay in the source language and are italicised. Names are never translated.
- **Structured output / JSON Schema** — the response matches the `LocketCard` schema below. Every field is typed; the schema is included verbatim in the system instruction and as `responseSchema`.
- **Long context (1M tokens)** — when a family has six voice memos and a previous card already produced for another sibling (which contains older provenance research that should be respected), the model reads everything at once. **Guardrail**: a typical card uses three to six voice memos averaging 2 minutes each ≈ ~30k transcript tokens; comfortable. For families recording more than fifteen voice memos for a single artefact, chunk the memos into thematic clusters before the weaving call — the 1M ceiling is real and a multi-artefact family card with twenty memos and four prior cards as context will approach it.
- **Search grounding** (Gemini 3.5 Flash) — used sparingly. The geocoding call resolves "Beirut, 1948" or "Battambang, 1975" to modern coordinates with period administrative context. Grounded search prevents hallucinated historical names. Not used for the weaving call — the woven paragraph must only assert facts from the voice memos and ladder, never from grounded search.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — narrates the front-of-card paragraphs in the giver's chosen-language voice at a quiet bedtime pace, for the recipient to hear before reading. Pre-processes line breaks to `…` for pauses and blank-line `—` for paragraph breaks (Gemini 2.5 TTS does not support SSML ``). One short style-directive sentence is prepended to the input text.
- **Nano Banana 2 image generation** (`gemini-3.1-flash-image`) — used only for the optional "back-of-card decorative motif" (a thin line illustration of the artefact's primary motif, e.g. a stylised hand-of-Fatima or a stylised lotus). Never used for the artefact photograph itself, which is always the user's real photograph.
- **Thinking levels** — `medium` for the artefact reading and for the woven provenance generation (both require careful inference about material, motif, and contributor intent). `low` for transcription, translation, ladder structuring, and geocoding. Surface `thoughtSummary` only on the (i) panel next to each model-asserted field — never inline in the printed card.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). Cards are private to the owner and explicitly-invited family contributors. **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used to invite an aunt who lives three time zones away to add her voice memo) also requires the sender domain to be authorised in Firebase Auth.
- **Database — Required.** Firestore for `users`, `cards`, `artefacts`, `voice_memos`, `contributors`, `provenance_rows`, `consent_records`, `print_jobs`.
- **File storage — Required.** Firebase Storage for original artefact photographs, raw voice-memo audio, the consent recording per contributor, and rendered card PDFs. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first photograph upload. Pre-signed URLs only; the photographs and audio are never publicly addressable.
- **Email — Required (transactional).** Family invitations via magic link. Print-shop hand-off emails (rendered PDF attached + cover note).
- **Payments — Not needed for v1.** Free for personal use. A future "print-and-ship to your door on heavy archival paper" tier could pipe to a print-on-demand partner (Moo, Solopress, Mpix) and charge for that physical artefact only.
- **External APIs:** Gemini API for all intelligence. Optional Stripe for the future print-and-ship tier. No third-party analytics on v1.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if printing tier added) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's voice memos and photographs are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · every voice memo carries a mandatory consent recording from the speaker, in her own voice, before the model will use it · the giver may redact any paragraph from the printed front while keeping it in the family record.
**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. 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 the artefact photograph(s) → `Artefact` schema | `gemini-3.5-flash` | medium | (none) |
| Transcribe a voice memo + separate consent line | `gemini-3.5-flash` | low | (none) |
| Translate transcript to card target language | `gemini-3.5-flash` | low | (none) |
| Weave the provenance paragraphs (front of card) | `gemini-3.5-flash` | medium | (none) |
| Extract the "verbatim distinct quote" per contributor | `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) |
| Generate TTS narration of the front-of-card paragraphs | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Generate a thin-line decorative motif (optional, back of card) | `gemini-3.1-flash-image` | n/a | n/a |
*Note for builders:* on TTS and image-generation calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const ArtefactType = z.enum([
"locket", // hinged, opens to reveal photograph(s) or hair
"pendant", // solid, non-opening
"filigree_pendant", // open-work metal pattern
"religious_charm", // cross, hand-of-Fatima, hamsa, Star of David, Khanda, Om, temple charm
"evil_eye", // nazar
"cameo", // carved relief portrait
"mourning_jewellery", // hair-work, jet, sepia photograph inset
"signet_ring",
"wedding_band",
"engagement_ring",
"pin_brooch",
"hair_clasp",
"string_of_beads", // jade, rosary, prayer beads
"amulet_pouch", // cloth pouch with contents
"other",
]);
const MaterialReading = z.object({
primary: z.string(), // "gold", "silver", "brass", "gold-filled", "silver-plated"
karat_estimate: z.string().nullable(), // "approx. 18k yellow gold (warm hue, no plating wear visible)"
weight_grams: z.number().nullable(), // user-entered if measured
hallmark_text_verbatim: z.string().nullable(), // "750" or "Beirut · 1948 ·" or null
hallmark_position: z.string().nullable(), // "inside the bezel, lower edge"
visible_repairs: z.array(z.string()), // "soldered link at the 5 o'clock position of the chain bail"
});
const MotifReading = z.object({
primary: z.string(), // "filigree six-pointed star"
secondary: z.array(z.string()), // ["hand-engraved acanthus border", "central cabochon-set garnet"]
cultural_register: z.string().nullable(), // "Levantine filigree tradition, mid-century Beirut workshops"
});
const LocketInterior = z.object({
has_interior: z.boolean(), // false for non-opening pendants
left_compartment: z.string().nullable(), // "small sepia portrait of a young woman, late 1940s hairstyle, three-quarter profile"
right_compartment: z.string().nullable(), // "lock of brown hair tied with white silk thread"
inscription_verbatim: z.string().nullable(), // "S.K. · 1948" engraved inside the back
});
const Artefact = z.object({
artefact_id: z.string(),
artefact_type: ArtefactType,
photograph_uris: z.array(z.string()), // multiple angles
estimated_dimensions_mm: z.object({
longest: z.number().nullable(),
perpendicular: z.number().nullable(),
depth: z.number().nullable(),
}),
material: MaterialReading,
motif: MotifReading,
interior: LocketInterior,
reading_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const ConsentRecord = z.object({
consent_audio_uri: z.string(), // mandatory, separate audio from substantive memo
consent_text_verbatim: z.string(), // what the speaker actually said
consent_language: z.string(), // BCP-47, "ar-LB"
consent_recorded_at_iso: z.string(), // ISO timestamp
consent_covers: z.array(z.enum([
"transcript_in_card",
"verbatim_quote_on_back",
"stored_in_family_archive",
"shared_with_invited_relatives",
])),
});
const VoiceMemo = z.object({
memo_id: z.string(),
audio_uri: z.string(), // raw audio in Firebase Storage
contributor_name: z.string(), // "Najwa", "Tito Boy", "Yeay Sophorn"
contributor_relationship_to_recipient: z.string(),// "the recipient's great-aunt", "the giver's cousin"
source_language: z.string(), // BCP-47, "ar-LB", "km-KH"
duration_seconds: z.number(),
consent: ConsentRecord,
transcript_source: z.string(), // verbatim, in source language, line breaks preserved
transcript_translated: z.string(), // in card target language
speaker_diarisation: z.array(z.object({ // when multiple speakers appear in one memo
speaker_label: z.string(),
span_seconds: z.array(z.number()).length(2) /* Gemini OpenAPI subset doesn't support fixed-length tuples; modeled as length-2 array, validated in middleware. */,
})),
distinct_quote_verbatim: z.string(), // the contributor's most distinct phrasing
distinct_quote_translated: z.string(),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const ProvenanceRow = z.object({
row_id: z.string(),
generation_index: z.number().int().min(1).max(7), // 1 = oldest known wearer
wearer_name_verbatim: z.string(), // "Salma" — as the contributor said it
wearer_relationship_to_recipient: z.string(), // "great-great-grandmother", "godmother", "stepmother"
place_period_name: z.string(), // "Beirut" (as said) or "Battambang"
place_modern_name: z.string().nullable(), // resolved later
year_verbatim: z.string(), // "1948" or "around 1948" or "the year before independence"
year_iso: z.string().nullable(), // "1948" if extractable
one_line_what_happened: z.string(), // "worn at her wedding", "carried in the hem of a sarong across the border"
source_memo_ids: z.array(z.string()), // which voice memos this row was derived from
});
const WovenParagraph = z.object({
paragraph_index: z.number().int().min(1).max(3),
text: z.string(), // ≤ 90 words
asserts_named_people: z.array(z.string()), // every person named in this paragraph
asserts_named_dates: z.array(z.string()),
traceable_to_memo_ids: z.array(z.string()),
traceable_to_provenance_row_ids: z.array(z.string()),
});
const RedactionDecision = z.object({
scope: z.enum([
"front_of_card", // moved off the printed front
"back_of_card_only",
"family_archive_only",
"delete_entirely",
]),
applied_to_field_path: z.string(),
reason_for_record_only: z.string().nullable(), // optional, private
});
const LocketCard = z.object({
card_id: z.string(),
artefact: Artefact,
giver_name: z.string(),
recipient_name: z.string(),
giving_occasion: z.string(), // "16th birthday", "wedding morning", "graduation"
giving_date_iso: z.string(),
card_target_language: z.string(), // BCP-47
giver_voice_choice: z.enum([
"warm-direct",
"brief-formal",
"candid-funny",
"ceremonial",
"bedtime-quiet",
"first-person-from-beyond", // giver records herself
]),
voice_memos: z.array(VoiceMemo),
provenance_rows: z.array(ProvenanceRow),
woven_paragraphs: z.array(WovenParagraph), // exactly 3 paragraphs, ≤ 90 words each
back_of_card_quotes: z.array(z.object({
contributor_name: z.string(),
quote_verbatim_source_lang: z.string(),
quote_translated: z.string(),
quote_language: z.string(),
recorded_at_iso: z.string(),
})),
redactions: z.array(RedactionDecision),
next_generation_blank: z.boolean(), // include blank line on the printed card
layout_preset: z.enum([
"a5_portrait",
"a6_portrait_slip_behind_locket",
"square_100mm_box_lid",
]),
typography_pairing: z.enum([
"cormorant_inter",
"source_serif_geist",
"eb_garamond_ibm_plex",
"vazirmatn_inter_ar_en",
"noto_serif_tc_noto_sans_cjk",
"tiro_devanagari_inter",
"hanuman_noto_sans_km",
]),
reading_confidence_overall: z.number().min(0).max(1),
generated_at_iso: z.string(),
});
type LocketCard = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the weaving call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash produces preamble ("In this special moment, …") and invents flourishes the voice memos never said.
- The model invents a fifth-generation wearer because the family-tree shape "feels like" five generations — hard rule: every named wearer must be traceable to a voice memo transcript or a provenance row. If the voice memos only cover four generations, the card has four rows.
- The model translates the relationship word "kun yeay" to "grandmother" and drops the Khmer — hard rule: relationship words and endearments stay in the source language, italicised. The English-language card carries them in italics with a parenthetical gloss on first occurrence only.
- The model writes "Dear Layla, as you embark on this new chapter…" — hard rule: no preamble. The first sentence is the artefact and a year. "Salma wore this in Beirut in 1948."
- The model asserts a date the contributor said "approximately" about as if it were certain — hard rule: preserve hedges. "around 1948", "the year before independence", "her wedding year, whichever year that was".
- The model collapses two distinct contributors into one because they are both called "Najwa" — keep separate contributor records unless the user manually merges them.
- The model speaks the consent line aloud in the TTS narration — the consent recording is metadata, not card content. Never include it in the TTS input or the woven paragraphs.
- The model adds a flowery "may you wear this with pride" closing sentence the giver did not write — hard rule: the giver's voice determines the closing. "candid-funny" might close with "try not to lose it on the school bus". "brief-formal" closes with the date and place of giving. No motivational filler.
- The model uses a stock image of a locket for the card hero — never. The card hero is always the user's actual photograph. If the photograph is too low-resolution for print, surface a warning ("the printed card may show pixelation at A5; want to retake?") rather than substituting a generated image.
- The model writes the woven paragraphs in the language the voice memos were spoken in instead of the card's target language — pin `card_target_language` explicitly and instruct the model to translate as it weaves.
- The model uses `google_search` to add historical context to the woven paragraph ("Beirut in 1948, the year the Mandate ended…") — hard rule: the woven paragraph only asserts facts from voice memos and ladder. Historical context is not on the front of the card.
### Negative constraints (hard rules)
- Do NOT translate proper nouns. First names, dish names, place names, brand names stay verbatim. "Salma", "Battambang", "Najwa", "Beirut", "ya habibti" stay as written. A parenthetical gloss on first occurrence only ("Battambang, in northwestern Cambodia") and never thereafter.
- Do NOT invent wearers, places, or dates. Every name, place, and date in the woven paragraph and the ladder must be traceable to a voice memo transcript or a user-entered ladder row. If you cannot find the source, leave the field null and surface a flag.
- Do NOT add preamble or closing flourish. The woven paragraphs are short, declarative, and named-fact-dense. No "may you", no "as you embark", no "treasured one", no "in this special moment".
- Do NOT translate relationship words and endearments. "kun yeay", "ya habibti", "Inay", "abuelita", "babcia", "vovó", "nani", "paati", "嫲嫲" stay in the source language and are italicised. A parenthetical gloss on first occurrence only.
- Do NOT extrapolate from the artefact's motif to the contributor's intent. A hamsa is a hamsa; do not write "she wore this for protection during the journey" unless a voice memo says so.
- Do NOT use grounded search to add historical context to the woven paragraphs. Grounding is only used by the geocoding call and only resolves a place name; the woven paragraph asserts only what the voice memos and ladder asserted.
- Do NOT use the user's voice memos or photographs 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. Cards are private to the owner and explicitly-invited family contributors. The recipient does not see the card until the giver chooses to print or share.
- Do NOT generate the artefact image. The hero photograph is always the user's real photograph. Nano Banana 2 is only used for the optional thin-line decorative motif on the back of the card, and the user must explicitly opt in.
- Do NOT include the contributor's consent recording text in the printed card or the TTS narration. Consent is metadata; the card never says "Najwa consented to her voice memo being used in this card on May 21st."
- Do NOT speak louder than the giver. The giver's voice-choice setting is the law. "bedtime-quiet" produces quiet sentences. "brief-formal" produces three sentences and a date. Never substitute the model's preferred register.
### 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 the artefact photograph(s) → `Artefact` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are reading one piece of family jewellery photographed on a plain
cream or off-white paper backdrop in indoor light, daylight, or
lamplight. The user may have provided one photograph or up to six (the
piece closed, the piece opened, the front, the back, an inside
compartment, an accompanying portrait, an accompanying handwritten
note, the original velvet box).
Artefacts encountered include lockets, pendants, filigree pendants,
religious and protective charms (Christian crosses, hands of Fatima,
hamsas, Stars of David, Khandas, Om symbols, lotus pendants, Buddhist
temple charms, evil-eye nazars), cameos, mourning jewellery (jet,
hair-work, sepia photograph insets), signet rings, wedding bands,
engagement rings, pin brooches, hair clasps, strings of jade or rosary
or prayer beads, and cloth amulet pouches with contents. Cultural
traditions encountered include Levantine filigree, Khmer temple
goldwork, Filipino tamburin, Vietnamese pendants from the Republic
era, West African gold work, Ethiopian Coptic crosses, Iranian Persian
filigree, Cuban pre-revolution Catholic jewellery, Cambodian gold
hidden during evacuation, Polish silver from the inter-war period,
Korean ornaments, Tamil Nadu thali pendants, Cantonese jade.
The user uploads the photograph(s) via the Gemini Files API
(`files/*` resource name) or as `inlineData` (base64). Do NOT pass Firebase
Storage public URLs directly to `generateContent` — the API does not
fetch them server-side.
Read every visible detail carefully. Distinguish:
- material (gold, silver, brass, gold-filled, silver-plated)
with a karat estimate if the visible cues warrant one
- construction (filigree, cast, stamped, engraved, hand-engraved,
granulation, repoussé)
- motif (primary motif and any secondary motifs like a border, a
set stone, an inscription, a maker's mark)
- hallmark text exactly as visible — do not infer
- hinge, clasp, bail, and chain attachment (note if any are
missing or repaired)
- locket interior — if the user uploaded an opened-locket photograph,
describe the left and right compartments separately
- any inscription, exactly verbatim, in its source script
Output ONLY the Artefact JSON matching the provided schema.
Hard rules:
- Do not invent details you cannot see. If the karat is not legible
from the hallmark or the visual cues, leave karat_estimate null.
- Hallmark text is verbatim, in its source script. Do not transliterate
Arabic, Cyrillic, Chinese, Khmer, or Devanagari to Latin. Render in
the original Unicode.
- Cultural register is a careful observation, not a guess. Levantine
filigree from mid-century Beirut workshops has specific patterns
(six-pointed stars, hand-of-Fatima motifs, fine wire openwork); say
so only when the visual evidence supports it. Otherwise leave null.
- If the artefact appears to be hair-work mourning jewellery, say so —
this is a specific Victorian/Edwardian tradition where a lock of the
deceased's hair is woven into the design. Treat it with the same
care as any religious motif.
- If the photograph quality is too low for print (<1200 px on the
longest side, or visible motion blur), set reading_confidence below
0.8 and add a flagged_for_user_review entry pointing at
photograph_uris with reason "may show pixelation when printed at A5".
- estimated_dimensions_mm are derived from the paper backdrop scale.
If the backdrop is not visible (close crop), leave dimensions null
rather than guessing.
- Do NOT describe the artefact in art-history vocabulary. "Repoussé
framing with foliate motifs" is for a catalogue, not a card. Write
"raised border with leaf-shaped patterns" — sensory, specific,
legible to a sixteen-year-old.
- Do NOT speculate about meaning. "This appears to be a protection
charm" is interpretation. "Hand-of-Fatima motif" is observation.
Stick to observation.
No commentary. JSON only.
```
---
### Call: Transcribe a voice memo + separate consent line
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one audio file containing two segments:
1. A consent line — the contributor introduces herself and states
she agrees to her voice being used.
2. The substantive voice memo — the actual content the contributor
wants in the card.
The consent line is always at the beginning of the recording and
always follows roughly this shape (the app prompts the speaker
to record it):
- "This is [name]. I'm [recipient's relationship]. I agree that my
voice memo can be used in [recipient]'s locket card."
Your task:
- Separate the consent line from the substantive memo at the first
natural pause after the speaker says the consent text.
- Transcribe both in the source language, verbatim, with every
hedge, repetition, false start preserved ("um, well, she — she
wore it the day she…").
- Note the source language as a BCP-47 code (e.g. "ar-LB" for
Lebanese Arabic, "km-KH" for Khmer, "tl-PH" for Tagalog,
"vi-VN" for Vietnamese, "es-MX" for Mexican Spanish).
- If multiple speakers appear in the substantive memo, diarise them
with rough span_seconds windows.
- Identify the contributor's most distinct sentence — the one
sentence she said that captures her voice and that the giver
would want preserved verbatim on the back of the card. Quote
it exactly, including the source-language endearment if she
used one.
Hard rules:
- Preserve every endearment, every relationship word, every
source-language phrase exactly. Do not normalise "ya habibti"
to "habibti" or "ya albi" to "albi".
- Do not paraphrase. The transcript is verbatim; cleanup is
separate.
- If the audio is unclear, mark the inaudible span with [unclear]
rather than guessing.
- Names are never normalised. If the speaker says "Najwa" pronounced
slightly differently than the romanised spelling the app already
has, surface the difference in flagged_for_user_review rather
than auto-merging.
- If the speaker has not recorded a consent line at the start, do
NOT transcribe the substantive content. Return an empty transcript
and a flag pointing at the missing consent.
Output ONLY the VoiceMemo JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Translate transcript to card target language
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You translate a voice-memo transcript from its source language to the
card's target language (usually English, sometimes the recipient's
heritage language).
You translate to be read by the recipient — a teenager, a young adult,
or an adult on a meaningful day in her life. The priority is voice. The
translation should make her feel that her great-aunt is in the room
speaking — not that an editor has polished her great-aunt's words.
Hard rules:
- Preserve endearments and relationship words verbatim in the source
language. Italicise them. "kun yeay" stays "kun yeay (grandmother)"
on first occurrence and "kun yeay" thereafter. Same for "ya habibti",
"Inay", "abuelita", "nani", "paati", "babcia", "vovó", "嫲嫲".
- Do NOT translate proper nouns. First names, dish names, place names,
brand names stay verbatim. A parenthetical gloss on first occurrence
only ("Battambang, in northwestern Cambodia") and never thereafter.
- Preserve hedges and uncertainty markers. "around 1948", "the year
before independence", "her wedding year, whichever year that was" —
do not flatten to definite dates.
- Preserve the speaker's rhythm. Short sentences stay short. Long,
meandering sentences stay long. Do not break them up for English
readability.
- If the speaker used an idiom that doesn't translate, translate
literally and add a one-line note in flagged_for_user_review.
- Preserve direct address. "you should know this" stays second person;
do not generalise to "one should know".
- Match the consent recording's language to the substantive transcript's
language. If they differ (rare but possible), note it.
Output: the translation as a single string with line breaks preserved.
No commentary.
```
---
### Call: Weave the provenance paragraphs (front of card)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are writing the three short paragraphs that appear on the front of
a printed A5 keepsake card given alongside a piece of family jewellery.
The card is given on a meaningful day — a sixteenth birthday, a wedding
morning, a graduation, the morning before a grandchild leaves for
university. The recipient will read these three paragraphs once,
slowly, on that day, and possibly many times after.
You receive:
- The Artefact JSON (the jeweller's-eye reading of the piece)
- Every VoiceMemo JSON (transcripts in source language and target language)
- The user-curated ProvenanceRow array (the four or five generations
the giver has chosen to put on the ladder)
- The giver's name, the recipient's name, the giving occasion, and
the giver's voice-choice from the enum
Your task: write THREE paragraphs, no more, no fewer. Each paragraph
≤ 90 words. The total length of the three paragraphs ≤ 240 words.
Paragraph 1: the artefact and the first wearer. One declarative
sentence to open ("Salma wore this in Beirut in 1948.") followed by
two to four sentences that name what kind of piece it is in lay
language, what the first wearer's relationship to the recipient is,
and what voice memos tell us about how she wore it or carried it.
Paragraph 2: the journey through the generations. A second declarative
sentence to open, then a tight account of how the artefact moved from
the first wearer to the giver — naming each intervening wearer, the
place, the year (with hedges preserved), and one short verb-phrase
about what happened to the artefact under her care. This paragraph is
where the four-generation ladder lives in prose.
Paragraph 3: the giving. The shortest paragraph. Names the recipient,
names the occasion, names the giver, and closes in the giver's chosen
voice. "warm-direct" closes warmly. "brief-formal" closes with the
date and place. "candid-funny" can land a small joke if the voice
memos warranted one. "bedtime-quiet" closes in two short sentences.
"ceremonial" closes with one held sentence. "first-person-from-beyond"
treats the giver herself as the speaker — first person throughout,
present tense, as though she is in the room as the recipient reads.
Hard rules:
- Every named person in the paragraphs must appear in the
provenance_rows or voice_memos with that exact name. Surface
asserts_named_people listing every name in your output.
- Every named date in the paragraphs must appear in the
provenance_rows or voice_memos. Surface asserts_named_dates.
- Every assertion (who wore it, where, when, what happened) must
trace back to a voice memo or a ladder row. Surface
traceable_to_memo_ids and traceable_to_provenance_row_ids per
paragraph.
- NO preamble. The first sentence of paragraph 1 is the first
wearer and a year. Never "Dear Layla, today is a special day".
Never "As you receive this".
- NO motivational closing. Never "may you wear this with pride".
Never "carry her courage forward". The closing is what the
giver's voice-choice says it is, no more.
- NO historical context the voice memos did not assert. If a memo
says "she came from Beirut in 1962", do not add "the year of the
Lebanese-American immigration wave following the 1958 crisis".
The card asserts the family's facts, not history.
- Preserve relationship words and endearments verbatim in the
source language, italicised. A parenthetical gloss on first
occurrence only.
- Preserve hedges. "around 1948" stays "around 1948". Do not
collapse to a definite date.
- Write in the card's target language. If a voice memo was in
Arabic and the card target language is English, the woven
paragraph is in English with the Arabic endearments italicised
in place.
- The giver's voice-choice is the law. "bedtime-quiet" produces
short sentences; "brief-formal" produces three sentences and a
date; "candid-funny" can be candid; "ceremonial" can be ceremonial.
Never substitute the model's preferred register.
Output exactly three WovenParagraph objects in an array. No
commentary outside the structured output.
```
---
### Call: Extract the "verbatim distinct quote" per contributor
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one voice memo transcript (source language and translation)
and a contributor name. Your task: identify the single sentence the
contributor said that captures her voice most distinctly and that the
giver would want preserved verbatim on the back of the printed card.
Hard rules:
- Quote exactly. Include every hedge, every endearment, every
pause-word.
- Source-language sentence stays in the source language; translation
goes alongside it.
- Length: ideally 8-22 words. If the contributor's most distinct
utterance is shorter or longer, preserve it as said rather than
trimming.
- Do not pick the consent line. The consent recording is metadata.
- Do not pick a sentence that asserts a fact the model could not
verify against another memo or the ladder. Pick a sentence that
is unmistakably the contributor's voice — an endearment, a
remembered gesture, a small joke, a hard truth said softly.
- If you cannot find a single distinct sentence (the memo is uniformly
matter-of-fact), pick the most emotionally weighted sentence and
flag low confidence.
Output: distinct_quote_verbatim, distinct_quote_translated, and a
confidence number 0-1. 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 spoken by a contributor (often a city as
it was known in a particular year, or a village that may no longer
exist) to modern coordinates, while preserving the period name.
Given a place name and a year, return:
- location_modern_name (e.g. "Beirut, Lebanon", "Battambang, Cambodia")
- modern_coordinates (lat, lng, decimal degrees)
- administrative_context_at_period (e.g. "Republic of Lebanon, 1948",
"Khmer Republic, 1975")
- one citation URL from grounded search
Hard rules:
- Use `google_search` grounding for any place that was renamed,
transferred between countries, or was part of a vanished
administrative region (French Indochina, the Republic of Vietnam,
Mandatory Palestine, British Mandate, Free City of Danzig,
Generalgouvernement, the Soviet Republics, pre-1948 Israel/Palestine,
pre-1947 British India, the Khmer Republic, the Republic of Cuba
pre-1959, pre-revolution Iran, the Belgian Congo, the Federation
of Rhodesia and Nyasaland).
- Preserve the period name verbatim. Do not silently substitute the
modern name.
- If the place no longer exists (a village destroyed in war, not
rebuilt; a neighbourhood absorbed into a larger municipality),
say so explicitly: location_modern_name = "[name], no longer a
distinct locality; approximate former location near [nearest
modern locality]".
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: Generate TTS narration of the front-of-card paragraphs
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried, the voice the giver chose. Pick the Gemini 2.5
Flash TTS voice whose `languageCode` matches the card's target language —
pronunciation will follow that locale automatically. Prefer a voice
whose gender matches the giver where the catalogue offers both; fall
back rather than blocking.
Pre-process the text before sending it to TTS:
- Read ONLY the three woven_paragraphs. Do not read the back-of-card
quotes, the consent recordings, or the contributor metadata.
- Insert a single ellipsis (`…`) at every line break inside a
paragraph so the TTS model produces a natural pause.
- Insert a blank line plus an em-dash (`—`) between paragraphs.
Gemini 2.5 TTS does not support SSML `` — these textual
cues are how you signal pace.
- Italicised source-language endearments and relationship words are
read in the source-language pronunciation as the chosen voice's
native locale permits. Mid-call voice switching is not supported;
keep the whole narration in one voice. If a voice memo phrase needs
a second voice, stitch a separate TTS call client-side and merge in
the player.
- Target rate: ~110 words per minute — kitchen-table reading pace,
not podcast pace.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Read warmly and unhurriedly, as a mother reading
a letter to her daughter on her sixteenth birthday. …". 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 (Arabic emphatic consonants, Khmer aspirated
consonants, Polish ł, German ü, Tamil retroflex consonants, Mandarin
tones) are NOT exposed by Gemini 2.5 TTS — no SSML `` tag.
Pronunciation comes from the chosen voice's native locale.
```
---
### Call: Generate a thin-line decorative motif (optional, back of card)
Model: `gemini-3.1-flash-image` · n/a · n/a
```
You generate a single thin-line decorative motif for the back of the
keepsake card. The motif echoes the artefact's primary motif (as named
in Artefact.motif.primary) — for example, a stylised hand-of-Fatima
for a hamsa pendant, a stylised lotus for a temple charm, a stylised
acanthus border for a Levantine filigree piece.
Hard rules:
- Output is a single PNG with transparent background, 1200 × 1200 px.
- Line weight: 1.5-2 pt at A5 print size. Thin enough to look like a
bookplate, not a logo.
- Colour: black or a single warm grey (#665a52). No gradients. No fills.
No drop shadows. No texture.
- Composition: centered, symmetric where the motif is naturally
symmetric, asymmetric where the artefact's motif is naturally
asymmetric (a side-view sparrow, a single hand). One element. No
extra ornament.
- Refuse: photorealism, gold-leaf rendering, embossed look, gradients,
decorative scrollwork the user did not ask for, any text, any signature.
- The motif is decoration, not depiction of the artefact. Never attempt
to reproduce the actual locket — that is what the user's photograph
is for.
If the user did not request a decorative motif (the opt-in flag is
false), do not generate one. Return nothing.
Output: one PNG with transparent background. No commentary.
```
## 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 sixteenth birthday.** A Lebanese-American mother in suburban Dearborn is preparing the giving of a 21-gram filigree pendant her own grandmother brought from Beirut in 1962. She has three voice memos lined up: her aunt Najwa (now 78, on the original wearer Salma), her cousin Rana (on the 1975 civil war and the months Najwa hid the pendant in a tin of rice), and herself (on the wedding morning in 1991 when she wore it as something old). The card is for her daughter Layla, who turns sixteen in three weeks.
- **The Cambodian-American graduation.** A mother in Long Beach is preparing a temple charm her own mother carried out of Battambang in 1979, hidden in the hem of a sarong, across the border into Sa Kaeo. The voice memos are in Khmer. The card target language is English with Khmer endearments preserved. The recipient is her son, eighteen, who is starting university next month and is the first in the family to be born after the camps.
- **The Filipino "something old" wedding morning.** A bride in Quezon City is preparing a tamburin necklace — gold and coral — that has been worn by four generations of brides in her family for the morning of the wedding. The card is for the guestbook table, with a small printed sticker version inside the velvet box. The voice memos are from her lola, her tita, her mother. Tagalog endearments preserved.
- **The first-person-from-beyond.** A mother with metastatic breast cancer has six months. She is preparing the card herself for her daughter's twenty-first birthday, three years from now. She records her own paragraph as one of the voice memos and asks the app to use the "first-person-from-beyond" voice setting. The model treats her recording as the giver's voice; the card reads in her own words, present tense, as though she is in the room.
- **The blended-family godchild.** A man is giving his deceased husband's grandfather's signet ring to his godson on the morning the godson is sworn in as a doctor. The provenance ladder includes his husband's grandfather, his husband, himself, and now the godson. The voice memos are from his husband's surviving sister and from a college friend who knew them as a couple in 1989.
- **The open-adoption pendant.** A birth-mother gave a small gold cross to the adoptive mother when her daughter was placed, with the intention that the cross would be given to the daughter on her eighteenth birthday. Now the daughter is seventeen. The adoptive mother is preparing the card. The voice memos include both mothers, the birth-grandmother, and the adoptive father. The card honours both families on the ladder.
- **The chosen-family inheritance.** A Filipino-American gay man in San Francisco is giving his late mother's jade pendant to a friend's daughter who has called him "Tito" since she was three. The provenance ladder breaks the strictly-biological tradition; the model handles it carefully: the contributor labels make the chosen-family relationship explicit rather than hiding it as "godfather".
- **The wartime artefact, redacted.** A Bosnian-American mother is giving her own mother's gold cross to her daughter on her wedding day. The voice memos contain a paragraph about Srebrenica that the mother decides should not appear on the front of the card. She uses the redaction tool to move it to the family archive with the back-of-card crediting kept intact.
- **The hospice month.** A grandmother in her final weeks is recording herself for three granddaughters at once, three separate cards, three separate artefacts (her ring, her brooch, her watch). The app supports building three cards from one shared pool of voice memos, with the woven paragraphs distinct per card and the contributor consent applied across all three.
## 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 two hands meeting over a small velvet box on a wooden table, evening light. One paragraph: "Locket Story turns one piece of family jewellery and a few voice memos into a printable card that travels with the artefact when you pass it on." Google sign-in; Apple sign-in next to it. Below: "See a sample card" → loads the Beirut-pendant demo from section 8a.
2. **Empty state — "Start a card".** Two big input methods: 📷 Photograph the artefact · 🖼 Upload a photograph you already took. A short explainer ("Place the piece on a sheet of plain cream paper in natural light. We'll read the material, the motif, the hallmark, and the locket interior if it opens.").
3. **Artefact capture flow** (mobile-first). Live viewfinder with a paper-backdrop crop guide. Capture closed front → optional flip prompt → capture open interior (left compartment, right compartment) → optional accompanying photographs (the velvet box, the portrait inside, the handwritten note). Each photograph is reviewed and retakeable. The camera holds focus and exposure to keep paper colour consistent across multiple frames.
4. **Artefact reading view.** A clean, two-column layout. Left: the photograph(s). Right: the structured reading — material, karat estimate, construction, motif (primary + secondary), hallmark verbatim, hinge type, locket interior. Every model-asserted field has a small "(i)" icon next to it showing the model's reasoning (a one-sentence `thoughtSummary`). The user can override every field inline. The user can also strike a field as "do not include on card" with a single tap.
5. **Voice memo flow + contributor invitations.** A vertical list of contributors. Each row: contributor name (editable), relationship to recipient (editable), the consent recording (mandatory), the substantive memo (mandatory), the source language (auto-detected, editable), the duration, a play button, a "transcript" expander. Add-contributor by magic-link email (modal: "Invite an aunt to record a voice memo for this card" — the link drops the aunt into a one-screen recorder with the pre-recorded consent line at the top) or by recording in-person now. Each consent recording surfaces a green check; until every contributor has a consent recording, the "weave the card" button is disabled.
6. **Provenance ladder editor.** A vertical ladder with four to five rungs (extendable to seven). Each rung: wearer name (verbatim), relationship to recipient, place, year, one short verb-phrase. The model proposes initial rungs from the voice memos; the user edits freely. A drag handle reorders generations. A small "trace" link on each rung opens the voice memo(s) the rung was drawn from.
7. **Voice + layout selector.** A two-column panel. Left: voice-choice (warm-direct, brief-formal, candid-funny, ceremonial, bedtime-quiet, first-person-from-beyond) — each with a one-sentence sample. Right: layout (A5 portrait, A6 portrait slip-behind-locket, square 100mm box lid) + typography pairing (a small grid showing each font pairing as it would print). A "decorative motif on back?" toggle.
8. **Weave preview.** A live A5 preview of the card with the three woven paragraphs streaming in token by token (the user sees the model writing). The user can pin a paragraph and ask for a re-weave of just that paragraph in a different voice-choice. A "trace this sentence" hover reveals the voice memo or ladder row each clause came from.
9. **Back of card editor.** The verbatim quotes panel. The user reorders, edits, or removes quotes. Each quote shows the contributor's name, recording date, source language, and source-language text. A small "add a written line" field lets the giver add her own one-line dedication.
10. **Redaction view.** A list of every paragraph and every voice memo, each with a scope toggle: front-of-card, back-of-card-only, family-archive-only, delete-entirely. A note field next to each redaction lets the giver record her reason for the family archive (private to her).
11. **Print preview.** The final A5 (or chosen layout) preview. PDF/X-3 export with crop marks and bleed. Velvet-box-lid sticker preview (square 100mm). Slip-behind-locket preview (A6 or 35×50mm). Each preview is print-shop-ready; a "send to a print shop" panel offers a simple email-the-PDF flow to a local print shop the user nominates.
12. **Card archive (post-print).** A small library view of every card the user has built. Useful for the hospice case (three cards for three granddaughters from one pool of voice memos). Useful for the wedding-morning case where one card is for the velvet box and a smaller version is for the guestbook table.
13. **Footer.** "Made for the morning you pass it on." Privacy: "Your card is yours. We never train on your photographs or voice memos." 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 Locket Story."
- Subhead: "Pass an heirloom with its story written down — in any language, any handwriting tradition, any number of generations."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic photo-and-text card builder: it listens to the older women in the family in their own voices, it weaves the four-or-five generations into one printable card in the giver's voice, and every contributor consents to her words being used.
- Visual: a small annotated illustration of an A5 card with the relevant zones labelled (artefact photograph, four-generation ladder, three woven paragraphs, back-of-card verbatim quotes, the empty next-generation line).
**Slide 2 — Try it now.**
- One short prompt: "See the Beirut pendant sample card".
- A live demo input pre-loaded with one artefact photograph and three voice memos from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the filigree pattern reading, the Arabic endearment preserved in italic, the four-generation ladder produced from three voice memos).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample artefact in `/data/seed-card/` for your own photograph."
- "Adjust the voice-choice presets in `/server/prompts/voice-choices.ts` for the registers your family speaks in."
- "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 (multimodal image)** — reads the artefact's material, motif, hallmark, hinge, and locket interior in detail. One call for the closed piece; a second call when the user has opened the locket. Optional secondary calls for accompanying photographs.
- **Gemini 3.5 Flash (multimodal audio)** — listens to each voice memo, transcribes in the source language, separates the consent line from the substantive content, diarises multiple speakers, identifies the contributor's most distinct phrasing.
- **Gemini 3.5 Flash (narrative weaving)** — the load-bearing call. Takes the artefact reading + voice memos + ladder rows + voice-choice → three short paragraphs in the giver's voice. Every assertion is traceable to a memo or a row.
- **Gemini 3.5 Flash (multilingual)** — translates voice-memo transcripts to the card's target language while preserving every endearment, relationship word, and hedged date in the source language.
- **Gemini 3.5 Flash (long context)** — once a family records six voice memos and the giver has built two earlier cards (older sibling, cousin), the weaving call reads everything at once.
- **Gemini 3.5 Flash + grounded search** — resolves period place names (Beirut 1948, Battambang 1975) to modern coordinates with administrative context. Used only by the geocoding call; never for the weaving call.
- **Gemini 2.5 Flash TTS** — narrates the three woven paragraphs at a kitchen-table reading pace, in the card's target language.
- **Nano Banana 2 (Gemini 3.5 Flash Image)** — optional, opt-in. Generates a thin-line decorative motif on the back of the card. Never used for the artefact photograph.
- **Firebase Auth** — Google and Apple sign-in, contributor invitations via magic links.
- **Firestore** — stores your cards, syncs across devices in real time.
- **Firebase Storage** — keeps the original photographs and voice memos at upload resolution, forever.
- **Cost note** — see the detailed breakdown in 6d. A typical card with three voice memos costs about $0.07 of Gemini API spend, total, generated once.
- **Privacy note** — your voice memos, photographs, and family information are private to you and the contributors you invite. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Every voice memo carries a mandatory consent recording from the speaker before the model uses it.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `STRIPE_SECRET_KEY` — optional, only if you wire up the print-and-ship tier later
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the weaving call is the most expensive single call in the template at ~$0.016 per card (medium thinking, ~10k input tokens, ~600 output tokens); it runs once per voice-choice change, so let users experiment freely.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this card forever, including all voice memos and consent recordings" — gone in 60 seconds), what is never sent for training, and how the consent gate works (every contributor records her own consent line in her own voice; until she has, the model will not transcribe or use her substantive memo).
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal-image, multimodal-audio, multilingual, long-context, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on PDF/X-3 print export and on the standard A5 / A6 / square-100mm formats
**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)
- **Read the artefact photograph (Gemini 3.5 Flash, medium thinking)** — typical 2 images ≈ ~3k input tokens, ~600 output tokens. ~$0.007 per artefact reading. Runs once unless the user retakes.
- **Transcribe a voice memo (Gemini 3.5 Flash, low thinking)** — typical 2-minute audio memo ≈ ~3.8k input audio tokens + ~800 output tokens. ~$0.009 per memo.
- **Translate a transcript (Gemini 3.5 Flash, low thinking)** — typical 250-word transcript ≈ ~500 input tokens + ~400 output tokens. ~$0.003 per memo.
- **Weave the provenance paragraphs (Gemini 3.5 Flash, medium thinking)** — the load-bearing call. ~10k input tokens (artefact reading + 3 voice memos + ladder + voice-choice), ~600 output tokens. **~$0.016 per weave.** This call runs once per voice-choice change; if a user tries all six voices, that's ~$0.10 total.
- **Extract a distinct quote (Gemini 3.5 Flash, low thinking)** — ~$0.0003 per memo.
- **Geocode a period place name (Gemini 3.5 Flash + grounded search)** — ~$0.0005 per place. A typical card has 2-4 distinct places.
- **TTS narration of the three woven paragraphs (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens, ~$0.000003/character). Three paragraphs ≈ 240 words ≈ ~$0.004 per narration. Cached per card.
- **Decorative motif (Nano Banana 2)** — ~$0.03 per generated motif. Optional; opt-in only.
- **Expected per-card cost on first generation** (artefact reading + 3 voice-memo transcriptions + 3 translations + 1 weave + 3 quote extractions + 3 geocodings + 1 TTS): ~$0.07. With the optional decorative motif: ~$0.10.
- **Image and audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A high-res artefact photograph is ~3 MB; a 2-minute voice memo at 96 kbps is ~1.5 MB. A typical card uses ~10 MB ≈ ~$0.0003/month.
## 7. Design language
- **Mood:** A kitchen table at evening, two weeks before the giving. The velvet box open on the cloth. The mother's reading glasses pushed up onto her hair. The first cup of tea, the second cup of tea. Not a tech product. Not a stationery store. The card is a small heavy piece of paper that ought to feel like it has weight when the recipient holds it. The app's job is to clear its throat and let the family speak.
- **Typography:** Display serif for the card content and the front-of-app headings (Cormorant or Source Serif Pro, optical-size aware). A handwriting-styled accent (sparingly) only for the giver's optional handwritten dedication line on the back of the card — never for the woven paragraphs themselves, which sit in serif. Clean grotesque for app chrome (Inter or Geist). Arabic, Devanagari, CJK, and Khmer pairings are first-class — the typography panel shows each pairing in the recipient's heritage language as a sample.
- **Palette:** Cream-paper background `#F6EFE3` for the card preview, deep ink `#1A1714` for body text, sepia accent `#7A4F2C` for hairline borders and the four-generation ladder rungs, a muted moss `#5A6D55` for the back-of-card verbatim-quote zone, a muted plum `#6F4A56` for the "first-person-from-beyond" voice choice (to gently flag its emotional weight). A faint gold-leaf hint `#B58D4A` for the optional decorative motif outline only. Borrowed from a velvet jewellery box and an archival folder, not from SaaS design systems.
- **Imagery:** The user's photograph of the artefact is the hero. Never replace it; never crop it tighter than the user did. The photograph is the only image on the front of the card. The optional decorative motif (Nano Banana 2) is a thin line illustration on the back, no larger than a postage stamp.
- **Hand-feel touches:** A barely-visible paper grain on the card-preview background. The "weave" action plays a gentle "page turn" sub-50ms transition (respecting reduced-motion). Hover on a sentence in the woven paragraph reveals the voice memo it was drawn from — a thin sepia underline, never a glow.
- **Spacing:** consistent 4-px base. Generous whitespace — the card needs air to feel printed, not screen-borne.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Card preview uses 6; voice memo cards use 12; the welcome card uses 20.
- **Shadows:** subtle, layered, sepia-tinted. Avoid heavy drop-shadows. The card preview rests on the page with a hairline shadow as though set down on cloth.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The woven-paragraph streaming is the one place where motion carries meaning; respect reduced-motion by displaying the final text rather than streaming it.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Photograph the artefact on a sheet of plain cream paper to start").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, locations, voice-memo excerpts that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on multi-cultural patterns — a Lebanese-American great-aunt's Beirut Arabic, a Cambodian-American grandmother's Khmer, a Filipino-American lola's Tagalog, a Polish-American babcia's Polish — but never claim that a fictional voice memo is a real recorded archive.
- Tone: warm, direct, free of corporate language. This template is for a giver and a recipient, not a company.
- 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 print-shop user wants to see "PDF/X-3 with 3 mm bleed" in the export label; the jeweller user wants to see "filigree" in the artefact reading).
- 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 reading of the karat estimate shows as a faint hedge in the text; tapping it reveals the model's reasoning and the alternates it considered).
## 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 cards (sidebar):**
- "Salma's Pendant — for Layla" (1 artefact, 3 voice memos, contributors: Najwa, Rana, Mona) — Lebanese filigree pendant, Beirut → Detroit → Dearborn → Dearborn, 1948–2026, in Arabic and English. **The Beirut pendant demo.**
- "Yeay Sophorn's Charm — for Vichea" (1 artefact, 4 voice memos, contributors: Sophorn, Channary, Sokun, Sothea) — Khmer temple charm, Battambang → Sa Kaeo camp → Long Beach → Long Beach, 1975–2026, in Khmer with English translation.
- "Vovó Aparecida's Crucifixo — for Beatriz" (1 artefact, 3 voice memos, contributors: Aparecida's sister, Beatriz's mother, Beatriz's aunt) — small Brazilian gold cross with a tiny garnet, Recife → São Paulo → Newark → Newark, 1957–2026, in Portuguese with English translation.
- "Etenesh's Meskel Cross — for Marcus" (1 artefact, 3 voice memos, contributors: Etenesh's daughter Hiwot, Hiwot's brother, Marcus's adoptive mother) — Ethiopian Coptic silver meskel cross, Gondar → Addis Ababa → Washington DC → Atlanta, 1969–2026, in Amharic with English translation; open-adoption case honouring both families on the ladder.
- "Babcia Hanka's Amber Pendant — for Eliza" (1 artefact, 3 voice memos, contributors: Hanka's niece, Hanka's pen friend's daughter, Eliza's mother) — Polish Baltic amber pendant in silver mount, Gdańsk → Chicago → Chicago, 1953–2026, in Polish with English translation.
- "Baba's Signet Ring — for Jonah" (1 artefact, 2 voice memos including a first-person-from-beyond recording from Baba's late husband's sister-in-law, contributors: the sister-in-law, a college friend) — gold signet ring with worn engraved monogram, Tehran → London → Cambridge → Boston, 1968–2026, in Farsi and English. **The first-person-from-beyond demo.**
**Sample card in detail view (this is what the demo should show — Salma's Pendant):**
- **Giver (verbatim):** "Mona Khoury"
- **Recipient (verbatim):** "Layla Khoury"
- **Giving occasion:** "16th birthday"
- **Giving date ISO:** "2026-06-14"
- **Card target language:** English (en-US) with Arabic endearments preserved
- **Giver voice choice:** "warm-direct"
- **Artefact:**
- artefact_type: `filigree_pendant`
- estimated_dimensions_mm: longest 38, perpendicular 24, depth 3
- material.primary: "gold"
- material.karat_estimate: "approximately 18k yellow gold (warm hue, no plating wear visible at the bail or chain edges)"
- material.weight_grams: 21
- material.hallmark_text_verbatim: "750 · ✱ · بيروت" (the third character is the Arabic word for "Beirut")
- material.hallmark_position: "inside the bail, lower edge"
- material.visible_repairs: ["soldered link at the 5 o'clock position of the chain bail"]
- motif.primary: "filigree six-pointed star with a central cabochon-set garnet"
- motif.secondary: ["hand-engraved acanthus border", "tiny granulation dots along the rim"]
- motif.cultural_register: "Levantine filigree tradition, mid-century Beirut workshops"
- interior.has_interior: false
- reading_confidence: 0.94
- **Voice memos (3):**
- Najwa (the recipient's great-aunt, age 78, Lebanese Arabic, ar-LB, 2 min 14 sec):
- distinct quote verbatim: "كانت سلمى تعلّقها على فستان زفافها وتقول: 'هذا قسم من قلبي إلك يا حبيبتي'"
- distinct quote translated: "Salma used to pin it on her wedding dress and say, 'this is a piece of my heart for you, ya habibti.'"
- Rana (the giver's cousin, age 52, Lebanese Arabic with English, ar-LB / en-US, 3 min 02 sec):
- distinct quote verbatim: "خبأتها تيتا نجوى في علبة الرز لخمسة أشهر، ولم تخبر أحدًا حتى ١٩٧٦"
- distinct quote translated: "Tante Najwa hid it in a tin of rice for five months, and didn't tell anyone until 1976."
- Mona (the giver herself, the recipient's mother, age 47, English with Arabic, en-US / ar-LB, 1 min 47 sec):
- distinct quote verbatim: "I wore it on my wedding morning in 1991, and my mother pinned it to my dress the way Salma pinned hers."
- distinct quote translated: (already in target language)
- **Provenance ladder (4 rows):**
- row 1: Salma · great-great-grandmother · Beirut · 1948 · "worn at her wedding"
- row 2: Najwa · great-aunt · Beirut → Detroit · 1962 · "carried it across the ocean in her handbag"
- row 3: Najwa (continued) · great-aunt · Beirut (visit) · 1975 · "hid it in a tin of rice for five months during the civil war"
- row 4: Mona · mother · Dearborn · 1991 · "worn on her wedding morning, pinned by her mother"
- **Woven paragraphs (3, the model produces these):**
- Paragraph 1: "Salma wore this in Beirut in 1948. She was the recipient's great-great-grandmother, married in a borrowed dress, and the pendant was the only thing she owned that was bought new for the wedding. Filigree gold, a six-pointed star with a small garnet at the centre, hand-engraved at the rim. Her sister Najwa, then a child, watched her pin it on. *ya habibti* — Najwa's words for the wearer were always *ya habibti.*"
- Paragraph 2: "Najwa carried the pendant from Beirut to Detroit in her handbag in 1962, the year the family came over. It went back to Beirut once, in 1975, in Najwa's handbag again, on a visit that turned into a longer stay than she planned. She hid it in a tin of rice for five months that winter and did not tell anyone until 1976. In 1991 the giver — *Mama* to the recipient — wore the pendant on her wedding morning in Dearborn, and her mother pinned it to her dress the way Salma had pinned hers."
- Paragraph 3: "Layla, this is yours now. The chain has a small repair at the five-o'clock position; the soldered link is from 1975. The pendant is heavier than you'd expect — twenty-one grams. Wear it on the days you want to feel held."
- **Back of card verbatim quotes (3, with names and dates):**
- Najwa Khoury — Lebanese Arabic — "كانت سلمى تعلّقها على فستان زفافها وتقول: 'هذا قسم من قلبي إلك يا حبيبتي'" / "Salma used to pin it on her wedding dress and say, 'this is a piece of my heart for you, ya habibti.'" — recorded 2026-05-30
- Rana Khoury-Saade — Lebanese Arabic with English — "Tante Najwa hid it in a tin of rice for five months, and didn't tell anyone until 1976." — recorded 2026-06-02
- Mona Khoury — English — "I wore it on my wedding morning in 1991, and my mother pinned it to my dress the way Salma pinned hers." — recorded 2026-06-08
- **Layout preset:** `a5_portrait`
- **Typography pairing:** `vazirmatn_inter_ar_en` (Arabic + English pairing)
- **Reading confidence overall:** 0.93
**Sample input artefacts (for the build to demonstrate):**
- A 21-gram Lebanese filigree pendant on cream paper, with three voice memos in Arabic and English. **The hero demo.**
- A small Khmer temple charm photographed on cream linen, with four voice memos in Khmer.
- A small Brazilian gold cross with a tiny garnet, photographed on cream paper, with three voice memos in Portuguese.
- An Ethiopian silver meskel cross photographed on a folded white cotton shemma, with three voice memos in Amharic — the open-adoption case where the ladder honours birth-mother Etenesh, birth-grandmother Hiwot, adoptive mother, and recipient Marcus.
- A Polish Baltic amber pendant in a silver mount photographed on cream paper, with three voice memos in Polish.
- A simple gold signet ring with worn engraved monogram, with two voice memos in Farsi and English — including the "first-person-from-beyond" case where the giver recorded herself for a future giving.
**Sample voice copy:**
- Onboarding: "Photograph one piece of family jewellery. Record two or three voices from the older women in your family. We'll write the card."
- Capture guidance: "Place the locket on cream paper near a window. We'll read the filigree, the hallmark, the hinge — and the photographs inside, if it opens."
- Consent prompt for a contributor: "Before you record your memory, please say: 'This is [your name]. I'm Layla's great-aunt. I agree that my voice memo can be used in Layla's locket card.'"
- Processing: "Reading the filigree…" / "Listening to Najwa…" / "Building the four-generation ladder…" / "Weaving the front of the card…"
- Empty card: "This card is waiting for its first photograph. Place the artefact on a sheet of plain cream paper to start."
- Error (couldn't read): "We couldn't make out the hallmark — try a closer photograph, or type what you can see into the side panel."
- Save confirmation: "Salma's pendant card saved — drafted with Najwa, Rana, and Mona."
- Consent missing: "Najwa hasn't recorded her consent line yet. We can't transcribe her memo until she does."
- Low confidence note: "The karat estimate is approximate. Tap to see how the model read the hallmark."
- Voice choice warning on first-person-from-beyond: "This voice setting writes the card as if you are speaking directly to the recipient in the present tense. Read the result aloud before printing to be sure it sounds like you."
**Sample contributor invitation email subject + body:**
- Subject: "Najwa — I'm building Layla's locket card. Will you record a voice memo?"
- Body: "Hi Tante Najwa — I'm making a small card to give Layla on her sixteenth birthday alongside Tita Salma's pendant. Could you record a short memory of Salma — when she wore it, where, anything you remember? The link below will guide you through a one-minute setup. There's a consent line to read first, and then you just speak. Maybe two minutes total. Yalla — Mona." [Record Your Memory]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of two hands meeting over a small open velvet box on a wooden kitchen table at evening, a single A5 card just lifted into the light, an enamel mug of tea blurred at the edge of frame. Generate via Nano Banana 2 with a prompt emphasising "wooden table, warm desk-lamp light, hands of a woman in her late forties and a teenager, late evening, gentle out-of-focus tea mug, real worn paper, soft shadow under the velvet box, no faces in frame".
- **App icon / wordmark:** Set in the display serif. Slightly worn paper texture behind it. No icon — just type.
- **Empty-state illustration:** A simple line drawing of an A5 card with a thin border, a small artefact-shaped placeholder at the top, four ladder rungs beneath, and three text-block placeholders below. Hand-drawn aesthetic, not a flat icon.
- **Demo artefact photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "Lebanese filigree gold pendant photographed on cream paper, soft window light, no people in frame, the pendant fills 60% of the frame, hairline shadow under the metal". Each demo artefact should look photographed, not rendered.
- **Decorative motif library:** A small set of pre-generated thin-line motifs (hand-of-Fatima, lotus, acanthus border, Coptic cross, hamsa, six-pointed star) at 1200 × 1200 px PNG transparent background, used as fallbacks when Nano Banana 2 generation fails or as instant previews.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample pendant from `/public/samples/sample-pendant.jpg`. Never to a "📿" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography for chrome and decorative elements. Never use generated images for the artefact itself — the user's real photograph is always the hero. 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. The artefact photograph is preserved at upload resolution in Firebase Storage; a 300 dpi raster is embedded in the PDF/X-3 export.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Photograph the artefact", "Invite an aunt to record a voice memo", "Drop a previously-taken photograph" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- AI-generated content streams in token-by-token where supported, with a clear "weaving…" indicator before content starts arriving. Respect `prefers-reduced-motion` by displaying the final text rather than streaming.
- If an AI call fails, show a calm, specific error ("We couldn't read the hallmark — try a closer photograph, or type what you can see into the side panel") and offer retry.
- Low-confidence artefact readings are faintly hedged in the text ("approximately 18k") with a small (i) icon; tapping reveals the model's reasoning and the alternates considered.
- The weave-preview transition between voice-choices fades in 300 ms with `prefers-reduced-motion` falling back to instant.
- The "weave the card" button is disabled until every contributor has recorded her consent line. The disabled state has a tooltip ("Najwa hasn't recorded her consent line yet").
- The print preview shows a real-scale ruler beside the card so the user can see how 148 mm × 210 mm reads at a glance.
- A single Undo restores the last weave; a longer Undo history is kept in the card-archive view (the previous weaves are versioned, not lost).
## 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 the artefact reading, voice memo transcription, translation, weaving, and quote extraction; `gemini-3.5-flash` for geocoding and quote-extraction-light variants; `gemini-3.1-flash-tts-preview` for narration; `gemini-3.1-flash-image` for the optional decorative motif. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the Beirut-pendant seed card on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as the contributor invitation channel.
- **Storage:** Firebase Storage for original photographs, voice-memo audio, consent recordings, and rendered card PDFs. Pre-signed URLs only.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The artefact capture flow is optimised for portrait orientation.
- 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 card-archive view (a contributor's incoming voice memo updates the card live).
- Optimistic UI on writes; reconcile on response.
- Voice-memo recording uses the Web Audio API with a 96 kbps Opus encoding by default; falls back to native recording on Safari iOS where the browser permits.
- **iOS Safari gotchas (graceful degradation):** an incoming phone call interrupts the audio session — listen for `audioInterruption` / `pause` events and prompt resume; backgrounded tabs throttle/pause `getUserMedia` and `MediaRecorder` — checkpoint state on `visibilitychange` so the recording can resume cleanly; Safari only supports `audio/mp4` (AAC) for `MediaRecorder`, not Opus — feature-detect `MediaRecorder.isTypeSupported('audio/webm;codecs=opus')` and fall back to AAC; microphone permission does NOT persist across page reloads on iOS — re-request on every session start and show a friendly explainer if denied. The artefact-capture viewfinder must also handle iOS Safari rotation locks gracefully — re-acquire the camera stream after orientation change.
## 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 artefact photographs have `alt` describing the piece ("photograph of a Lebanese filigree gold pendant, 38 mm long, on cream paper").
- Form fields have associated `