================ 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.
---
# Field-Trip Guide
## 1. Project
**Field-Trip Guide** is an on-site teaching script generator for
primary and middle-school teachers who are taking a class of children
to a heritage site for half a day. The user photographs the site (or
uploads a saved photo, or pastes the site's official URL), tells the
app the age band and the class size, and the app produces a printable
30-minute walking script: a route around the site with timed stops,
the exact words the teacher can say at each stop in plain
age-appropriate language, the questions to throw to the kids, and five
sticky facts the children will remember on the bus home. Every claim
cites a verifiable source so the teacher can stand in front of
twenty-eight nine-year-olds and know she is not making anything up.
This is the kind of app a primary-school teacher in Arusha builds the
night before she takes her Standard 5 class on the dala-dala to Old
Boma, because the museum's free education pack assumes a high-school
reading level and her thirty kids need a story they can hear at the
gate. It is also the kind of app a teacher in a London comprehensive
opens at the back of the coach the morning of her year-7 trip to the
Roman Wall at Wallsend, because the worksheet she printed last week
hasn't survived contact with twenty-eight eleven-year-olds and she
needs a thirty-minute walking script she can actually deliver before
the lunch packs come out. Same shape of problem, different site,
different budget.
The single demo that proves the magic: a teacher snaps a photo of the
Old Boma's main archway at the gate, types "ages 10-11, class of 30,
30 minutes," and taps Generate. In under twenty seconds the app
returns a printable walking script — Stop 1: Outside the gate (3
minutes); Stop 2: Under the arch (5 minutes); Stop 3: Inside the
courtyard at the cannon (8 minutes); Stop 4: At the south wall (7
minutes); Stop 5: Back at the gate, debrief (7 minutes). At each stop:
the teacher's lines in plain language, three turn-and-talk questions
for the kids, one short factual claim with a footnote pointing at the
National Museums of Tanzania page that vouches for it, and a small
hand-drawn map showing where to stand so the sun isn't in the kids'
eyes. At the bottom of the print-out: five sticky facts, each tied to
a physical object the kids will have seen.
And in the harder cases — sites where the history is heavy, contested,
or actively being negotiated — the app refuses to flatten. A
Cambodian teacher prepping a visit to Tuol Sleng with year-9 students
gets a script that names what the kids are about to see, points at the
school's own guidance on how to talk about it, and ends with a
debrief structure for the bus home. A teacher in Belfast prepping a
trip to the Peace Walls gets a script that surfaces the disagreements
in the historiography rather than picking a side. A teacher in
Birmingham, Alabama, prepping a year-8 trip to the 16th Street Baptist
Church gets a script that quotes the church's own education
department, not a paraphrase.
**Tagline:** _Turn a heritage site into a thirty-minute teaching script — in any country, any age band, with every claim cited and five sticky facts the kids will still remember on the bus home._
## 2. Target audience
- Primary-school class teachers (ages 5-11) prepping a half-day site visit with no department-funded TA support
- Middle-school and lower-secondary humanities teachers (ages 11-14) teaching history, geography, RE, or PSHE through field visits
- Teachers in low-resource schools — Tanzanian government primaries, Vietnamese rural lower-secondaries, Mexican rural telesecundarias, Filipino public elementaries, Brazilian municipal schools, Indian state-board middle schools — who cannot afford a paid museum educator and need to lead the visit themselves
- Teachers in private and international schools who want a tighter, more accurate script than the museum's generic worksheet
- Homeschool parents and co-op leaders planning a heritage outing for a mixed-age group of 4-12 kids
- Sunday-school and weekend-school teachers (Hebrew school, madrasa, Chinese school, Sikh gurmat camp, parish CCD) preparing a site visit with a faith or community lens
- Scout troop leaders, youth-club organisers, after-school programme leads, summer-camp counsellors taking a small group to a local heritage site
- Museum educators and heritage-site duty officers who want a generated script as a baseline before they layer their own expertise on top
- Trainee teachers (PGCE, Teach For India, Teach For All affiliates, US emergency-license programmes) preparing their first ever field visit
- Special-school teachers planning a site visit for neurodivergent or D/deaf or partially-sighted students, who need scripts written for sensory pacing rather than chronological lecture
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **A walking script, not a worksheet.** The output is a timed route with the teacher's actual lines, the kids' questions, the place to stand, and where to look. Not a comprehension exercise. Not a fact sheet. Not a quiz. A script the teacher can read off her phone or print on one A4 sheet and deliver on her feet with thirty kids around her.
- **Age-appropriate vocabulary that respects the kids.** A 6-year-old script uses 6-year-old words. An 11-year-old script uses 11-year-old words. No baby talk for the younger kids; no jargon-loaded museum prose for the older kids. Sentence length, abstraction, and concept density are all tuned by the age band.
- **Five sticky facts per visit, each tied to a physical object.** At the end of the visit the script puts five concrete things in the kids' heads — not seventeen. Each sticky fact is anchored to a thing they touched, walked past, or pointed at. The bus-home retention test is the design target.
- **Every claim cites a source the teacher can verify in 10 seconds.** Names, dates, attributions, and historical claims come with grounded URLs from the site's own museum, official tourism board, university archive, UNESCO citation, or peer-reviewed reference. The app NEVER invents Roman emperors, Khmer kings, Mexica deities, Mughal rulers, or Tanzanian colonial-era administrators. If grounded search can't anchor the claim, the script says "ask the on-site staff" instead of inventing.
- **Reads any site from any signal.** A photo of the gate, the official URL of the museum, the heritage-register record number, a Google Maps pin, or a hand-typed name. Gemini 3.5 Flash's multimodal + grounded search resolves the signal to a canonical site before scripting.
- **Knows the on-site practicalities a school teacher actually faces.** Where the kids will eat lunch. Where the toilets are. Whether the surface is wheelchair-accessible. Whether photography is allowed. Whether to take shoes off (for many sites in South and Southeast Asia). Where the dropping-off and pickup points are. Where the shade is at 1pm.
- **Handles heavy or contested sites without flattening.** Sites of mass violence, partition, civil war, slavery, residential schools, dictatorship, and ongoing political contest get a script that names what is being seen, points to the institution's own guidance for talking to children about it, and provides a debrief structure for the journey home. The app does not paper over.
- **Handles sites with active religious meaning.** A mosque, a working temple, a synagogue with a current congregation, a church on a feast day get scripts that respect the site as a place of practice, not as a museum exhibit.
## 4. Features to build
- Single capture: tap to photograph the site at the gate from where the teacher is standing
- Upload: pick from photo library, paste a URL, paste a Google Maps share-link, paste a coordinate pair, paste a heritage-register record number
- Site resolution: Gemini 3.5 Flash + grounded search resolves "the photo I took at the gate" to a canonical named site with its official URL, country, and UNESCO/national-heritage record where applicable
- Class profile: age band (5-6, 7-8, 9-10, 11-12, 13-14), class size, duration (15 / 30 / 45 / 60 / 90 minutes), wet-weather flag, accessibility flags
- Curriculum hooks: free-text optional field for the teacher's curriculum unit ("Romans in Britain — Year 4 KS2", "Independencia — 5° de primaria", "Imperial city — Lớp 7"); the script ties stops to the unit where it honestly can
- Generate: the walking script with timed stops, teacher lines, kid questions, sticky facts, and one footnote per claim
- Inline editing: tap any stop to extend, shorten, swap the question, or replace the sticky fact; regenerate just that stop without re-running the whole script
- Print preview: A4 portrait, landscape, US Letter, one-sheet two-sided. The teacher's phone view collapses to a thumb-readable script that can be held in one hand while marshalling thirty kids with the other
- Offline pack: a "save for offline" button that caches the script + the cited source pages + a small map image, for sites with no signal at the gate
- Reduced-mobility variant: the same script with a wheelchair- and stroller-accessible route, regenerated; not a separate flow
- Wet-weather variant: an indoor-only or partly-sheltered version regenerated from the same site
- Sticky-fact recall card: a credit-card-sized printable for the kids to take home, listing the five sticky facts with one image each — useful for parents who ask "what did you do today?"
- Tear-off questions strip: an A4 strip the teacher cuts into 3 strips of 6 questions each, hands one to each chaperone, who runs a question with their small group while the teacher rotates
- Bus-home debrief script: a 5-minute structured debrief for the journey back, with three sentence-starters and one drawing prompt
- Cite-the-claim quick check: at each scripted claim, the teacher can tap the footnote and see the source page rendered in a clean reader view; the underlying URL is always visible
- Library: every script the teacher has generated, searchable by site name, country, age band, date
- Share to colleague: send the script to another teacher in the same school by email or by a magic link; magic-link recipient sees the script, can copy it but cannot edit the original
- Multi-language: the script generates in the language of the teacher's school — English, Tanzanian Swahili, Vietnamese, Mexican Spanish, Brazilian Portuguese, Tagalog, Bahasa Indonesia, Bengali, Hindi, Tamil, Mandarin, Cantonese, Korean, Khmer, Amharic, Farsi, Arabic, French, German, Italian, Polish
- Local language fallback: if the source page is in a language the teacher does not read, the citation panel offers a one-tap translation of the cited paragraph into the teacher's language, with the original always one tap away
## 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) — given a photograph taken at the site gate, identify the site. The model reads the architecture, the signage, the visible inscriptions, the building style, and any plaques visible in the frame. This is then handed to the grounded-search call below for canonical resolution. One Gemini 3.5 Flash call per uploaded photo.
- **Grounded search** (Gemini 3.5 Flash with `google_search` tool) — resolves "the photo I took at the gate" or "the URL the teacher pasted" to a canonical site identity (site name, country, governing institution, UNESCO record if any, official URL). Grounded so the resolution is verifiable, not hallucinated. **Used again at the per-claim citation step**: every factual claim in the generated script comes with a grounded citation.
- **Structured output / JSON Schema** — the generated walking script matches the `WalkingScript` schema below. Every stop, every line, every claim, every citation is typed. The schema is included verbatim in the system instruction. **Note**: the per-claim citation step uses grounded search and therefore cannot use `responseSchema` in the same call; that call emits JSON in the text body and the server parses it.
- **Multilingual** (built into Gemini 3.5 Flash) — the output script is generated in the teacher's school language (set in profile). The cited sources may be in another language; the script's citation panel offers a one-tap translation of the source paragraph into the teacher's language.
- **Long context (1M tokens)** — when the teacher pastes a long official site page, or the official PDF of a UNESCO nomination document, or the museum's full education-department guidance, the model reads the whole document at once and builds the script with the relevant passages cited. **Guardrail**: a UNESCO nomination dossier can run 300+ pages and easily exceed 500k tokens. Chunk by section (Statement of Outstanding Universal Value, History and Development, Management Plan, Protection) before passing to the script-generation call; never the whole dossier in one go.
- **Gemini 3.5 Flash thinking (medium)** — for the script-generation call, where the model must hold the site's history, the age band's cognitive level, the class size, the duration, and the curriculum hook in mind simultaneously. Pin `thinkingLevel: medium`.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — reads the teacher's lines aloud to the teacher in advance, so she can hear how they sound when delivered to thirty kids. Optional pre-trip rehearsal flow only; not used during the visit itself.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email sharing** requires the sender domain to be authorised in Firebase Auth. Personal scripts are private to the teacher; sharing is per-script, by colleague email.
- **Database — Required.** Firestore for `users`, `scripts`, `sites_resolved` (cache of canonical sites), `citations` (cache of grounded sources), `shares`. Cache hits on `sites_resolved` and `citations` are the single biggest cost lever — see 6d.
- **File storage — Required.** Firebase Storage for uploaded photos of the site at the gate, and for downloaded source-page PDFs the teacher snapshots for offline mode. **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.
- **Email — Required (transactional).** Sharing a script to a colleague via magic link. Welcome email on first sign-in.
- **Payments — Not needed for v1.** Free for personal teacher use. A future "school plan" tier could add a department-wide library, but v1 ships free.
- **External APIs:** Gemini API for all intelligence. Optional: a static-map tile (Mapbox / MapTiler) for the small inline map on the printed sheet; the app ships with a fallback OpenStreetMap static export.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Mapbox token if used) 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 teacher's scripts and uploaded site 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) · uploaded photos of a site may incidentally include children — the app warns the user before upload, blurs detected faces server-side before any Gemini call, and never displays uploaded photos with visible faces in the print preview or shared script.
**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 |
|------|-------|---------------|---------------|
| Resolve site from photo or URL → canonical record | `gemini-3.5-flash` | low | (none) |
| Verify site identity + canonical metadata | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Generate walking script → `WalkingScript` schema | `gemini-3.5-flash` | medium | (none) |
| Cite each factual claim → URL + source title + paragraph | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Translate cited source paragraph into teacher's language | `gemini-3.5-flash` | low | (none) |
| Regenerate one stop from teacher feedback | `gemini-3.5-flash` | medium | (none) |
| Generate sticky-fact recall card text | `gemini-3.5-flash` | low | (none) |
| Pre-trip rehearsal TTS of teacher lines | `gemini-3.1-flash-tts-preview` | n/a | n/a |
*Note for builders:* on TTS calls, omit `thinkingConfig` entirely — the field is not supported on that model. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. On the two grounded-search calls, do NOT specify `responseSchema` in the same call — `responseSchema` and `google_search` are mutually exclusive in one Gemini call. Those calls must emit JSON in the text body; the server parses it and reads citations from `response.groundingMetadata.groundingChunks[].web.uri`.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const AgeBand = z.enum([
"5-6", "7-8", "9-10", "11-12", "13-14",
]);
const Duration = z.enum([
"15min", "30min", "45min", "60min", "90min",
]);
const AccessibilityFlag = z.enum([
"wheelchair_accessible_route_required",
"stroller_friendly_route_required",
"step_free_required",
"audio_description_friendly_pacing",
"sensory_friendly_pacing",
"no_loud_surprises",
]);
const WeatherProfile = z.enum([
"dry_outdoor_ok",
"wet_weather_indoor_preferred",
"very_hot_shade_priority",
"very_cold_quick_outdoor",
]);
const Citation = z.object({
claim_in_script: z.string(), // verbatim sentence from the script
source_title: z.string(), // "National Museums of Tanzania — Old Boma"
source_publisher: z.string(), // "National Museums of Tanzania"
source_url: z.string(), // verifiable, grounded
source_paragraph_verbatim: z.string(), // the supporting sentence(s) from the source
source_language: z.string(), // BCP-47, "en", "sw"
confidence: z.number().min(0).max(1),
needs_user_review: z.boolean(), // true if confidence < 0.85
});
const KidQuestion = z.object({
question_text: z.string(), // age-appropriate
type: z.enum([
"turn_and_talk", // pairs discuss for 30s
"point_and_show", // child points to something
"imagine", // imaginative prompt
"predict", // what do you think happens next?
"spot_the_thing", // find this object
"compare_to_today", // bridge past to present
]),
expected_kid_answer_range: z.string(), // teacher's note: "anything from 'soldiers' to 'guards' is fine"
reframe_if_silent: z.string().nullable(), // backup prompt if no kid speaks up
});
const TeacherLine = z.object({
line_text: z.string(), // exactly the words the teacher says
delivery_note: z.string().nullable(), // "lower voice — you're inviting them into the secret"
estimated_seconds: z.number().min(5).max(180),
});
const StickyFact = z.object({
one_sentence: z.string(), // "The Romans built this wall around 122 AD."
anchored_to_object: z.string(), // "the carved stone block above the gate"
why_it_sticks: z.string(), // teacher-facing rationale: "physical, dated, named"
citation: Citation, // every sticky fact is cited
});
const Stop = z.object({
stop_number: z.number().int().min(1).max(8),
stop_label: z.string(), // "Under the arch", "At the cannon"
approximate_location: z.string(), // "main gate, just inside, north side"
where_to_stand_note: z.string(), // "stand with your back to the sun so the kids face the wall, not the glare"
estimated_minutes: z.number().min(1).max(20),
arrival_cue: z.string(), // "as the kids cluster, raise one hand and wait"
teacher_lines: z.array(TeacherLine),
kid_questions: z.array(KidQuestion),
factual_claims: z.array(Citation), // every claim made at this stop with its source
watch_outs: z.array(z.string()), // "uneven cobbles here; line them up two-by-two before moving"
});
const StickyFactsBlock = z.object({
facts: z.array(StickyFact).length(5), // exactly five
bus_home_recall_prompt: z.string(), // "On the bus, ask each kid to name one thing they saw"
});
const Logistics = z.object({
meeting_point_at_arrival: z.string(),
toilets_location_note: z.string(),
lunch_spot_recommendation: z.string(),
shade_or_shelter_notes: z.string(),
photography_policy_note: z.string(), // "photographs allowed in the courtyard, not in the inner shrine"
shoes_off_required: z.boolean(),
accessibility_route_notes: z.string(),
});
const DebriefScript = z.object({
setting: z.enum(["bus", "classroom_back_at_school", "walking_back"]),
duration_minutes: z.number().min(3).max(20),
opening_line: z.string(),
sentence_starters_for_kids: z.array(z.string()).min(3).max(5),
drawing_or_writing_prompt: z.string(),
one_question_to_send_home_with_them: z.string(),
});
const WalkingScript = z.object({
script_id: z.string(),
site_canonical: z.object({
site_name: z.string(),
site_country: z.string(),
site_governing_institution: z.string(), // "National Museums of Tanzania", "English Heritage"
site_official_url: z.string(),
site_unesco_record_url: z.string().nullable(),
site_period_or_era: z.string(), // "late nineteenth century German colonial administration"
}),
generated_for: z.object({
age_band: AgeBand,
class_size: z.number().int().min(1).max(60),
duration: Duration,
school_language: z.string(), // BCP-47, "sw-TZ"
curriculum_hook: z.string().nullable(), // teacher's free-text
weather_profile: WeatherProfile,
accessibility_flags: z.array(AccessibilityFlag),
}),
arrival_brief: z.object({
pre_visit_classroom_warmup_30s: z.string(),// said in the classroom before they leave
on_the_bus_settling_30s: z.string(), // said on the bus, just before arrival
at_the_gate_60s: z.string(), // first thing said at the gate
}),
stops: z.array(Stop).min(3).max(6),
sticky_facts: StickyFactsBlock,
logistics: Logistics,
debrief: DebriefScript,
chaperone_strip: z.object({
questions: z.array(z.string()).length(18), // 3 strips of 6
instructions_for_chaperone: z.string(),
}),
citations_index: z.array(Citation), // every citation in the whole script, deduped
generation_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(), // "stops[2].factual_claims[1]"
reason: z.string(),
})),
});
type WalkingScript = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the script-generation call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash compresses the teacher's lines into bullet points, drops the delivery notes, and skips the where-to-stand cues. The age-appropriateness suffers most.
- Model invents a Roman emperor or a Khmer king to fill a citation. This is the single most catastrophic failure mode for a teacher who will say it out loud in front of thirty children. The hard rule: if grounded search returns no result for a claim, the script rewrites the claim as "ask the on-site staff" or drops it entirely. The agent must NEVER write a factual claim without a successful grounded citation, even if it "knows" the answer from training data.
- Citation URL is hallucinated. Use grounded search; pull URLs from `response.groundingMetadata.groundingChunks[].web.uri`, never from the JSON body the model generates. Validate every URL server-side with a HEAD request before storing.
- Age band ignored. A script for ages 5-6 written in ages 11-12 prose is unusable. Pin in the system instruction: "every line a teacher reads aloud must be at the reading age of the band — sentence length, abstraction, and concept density all tuned." Include sample lines per age band in the system instruction (below).
- Sticky facts aren't sticky — "this is an important historical site" is not a sticky fact. Specify: each sticky fact must be (a) one short sentence, (b) anchored to a specific physical object the kids touched or pointed at, (c) dated or named or numbered, (d) cited.
- The walking route doesn't actually walk. The model generates "Stop 1: history of the site, Stop 2: more history, Stop 3: more history" rather than a spatial route. Pin: each stop must specify a physical location the class moves to, and the script must walk a coherent loop that ends where it began.
- Heavy-history sites get cheerful copy. A script about a former secret-police prison must not say "kids will love exploring the cells". Pin in the system instruction: detect heavy-history sites by grounded search (residential schools, sites of mass violence, slavery sites, dictatorship-era prisons, partition sites, contested colonial monuments) and switch to the heavy-history register: name what is being seen, point to the institution's own age-band guidance, end with a structured debrief.
- Religious-practice sites get tourist copy. A working mosque, a Sikh gurdwara during community service hours, a synagogue with a current congregation get scripts that respect the site as a place of practice. Pin: if the site is currently in active religious use, the script asks the teacher to call ahead, offers shoes-off / head-covering / silence guidance, and treats the visit as a guest visit, not a tour.
- Multipage / long site documents submitted as multiple separate calls — submit as one multi-image or one long-text call. If the document is over 500k tokens, chunk by section before the script-generation call.
- TTS reads the teacher's lines too fast. Target 110-130 words per minute, never podcast pace. The pre-trip rehearsal flow is for the teacher to *hear how it lands*, not to listen at 1.5x.
- Tools listed in the matrix as "(none)" left enabled. Explicitly pass an empty tools array on those calls so the model cannot reach for grounding mid-generation.
### Negative constraints (hard rules)
- Do NOT invent historical names, dates, attributions, or events. If grounded search returns no source for a claim, REWRITE the claim as "ask the on-site staff about this" or DROP it entirely. The agent must NEVER write a factual claim without a successful grounded citation, even if it "knows" the answer from training data. "I know the Roman wall was built in 122 AD" is not enough — there must be a grounded URL.
- Do NOT translate proper nouns or site names. "Teotihuacán" stays "Teotihuacán" in an English script; "Tuol Sleng" stays "Tuol Sleng" in a French script. Add a parenthetical gloss on first occurrence only.
- Do NOT use baby talk for younger age bands. A 6-year-old script uses short sentences and concrete nouns, not condescension. "The Romans were really really smart!!" is wrong. "The Romans built this wall a very long time ago — about a hundred grandmas ago" is right.
- Do NOT use textbook prose for older age bands. An 11-year-old script does not begin "The Antonine Wall, constructed in the second century CE under the auspices of …". A teacher's spoken line is short, direct, and physical.
- Do NOT generate scripts for sites where access is restricted (active conflict zones, closed-for-restoration sites the user has misidentified, private military or government sites). Detect via grounded search; refuse with a clear message and a suggestion of a public alternative nearby.
- Do NOT generate scripts that would put a class in danger (cliff edges, unstable ruins, traffic-exposed approaches without crossing guidance). When site research surfaces a known hazard, the script must call it out in `watch_outs[]` for every stop where it applies.
- Do NOT use the teacher's photographs or scripts 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-publish or auto-share. Scripts are private to the teacher. Sharing is explicit, per-script, per-colleague.
- Do NOT flatten heavy-history sites. Auschwitz, Tuol Sleng, Hiroshima, Robben Island, Gorée Island, residential schools in Canada, Apartheid Museum, Hiroshima Peace Memorial, Tuol Sleng, Sarajevo Tunnel, 16th Street Baptist Church, Lower 9th Ward — these sites get the heavy-history register. The script names what is being seen, points to the institution's own age-band guidance, and ends with a structured debrief.
- Do NOT serialise `n/a` from the matrix into request bodies. Omit `thinkingConfig` entirely on TTS calls.
### 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: Resolve site from photo or URL → canonical record
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one of: (a) a photograph taken at the gate of a heritage
site, (b) an official URL of a museum or heritage page, (c) a Google
Maps share-link, (d) a coordinate pair, (e) a typed name in the
teacher's language.
Your task: produce a CANDIDATE canonical record. The next call (with
grounded search) will verify and lock the canonical record. Do NOT
fabricate certainty here.
For a photograph input: read the architecture, signage, plaques,
inscriptions, and any visible national / institutional logos. Note
the script of any visible text (Latin, Cyrillic, Arabic, Devanagari,
Hangul, Khmer, Amharic Ge'ez, Tamil, etc.). Note the language of
any signage. Output the most likely site name with a confidence
score and at least one alternative if confidence < 0.8.
Sites you should be able to recognise from a single gate photograph
include heritage sites across every continent: the Roman Wall at
Wallsend (UK), Old Boma in Arusha (Tanzania), Teotihuacán (Mexico),
the Citadel at Huế (Vietnam), Angkor Wat (Cambodia), Hampi (India),
Polonnaruwa (Sri Lanka), Sigiriya (Sri Lanka), the Great Mosque of
Djenné (Mali), Lalibela (Ethiopia), Gorée Island (Senegal), Robben
Island (South Africa), Hiroshima Peace Memorial (Japan), Gyeongbokgung
(South Korea), Borobudur (Indonesia), Intramuros (Philippines), the
Forbidden City (China), the Western Wall (Israel/Palestine), Petra
(Jordan), Persepolis (Iran), Hagia Sophia (Türkiye), Ephesus (Türkiye),
Pompeii (Italy), Pont du Gard (France), Stonehenge (UK), Skara Brae
(UK), Newgrange (Ireland), Brú na Bóinne (Ireland), Auschwitz-Birkenau
(Poland), Terezín (Czechia), the Wansee Villa (Germany), Berlin Wall
remnants (Germany), Tuol Sleng (Cambodia), the Killing Fields (Cambodia),
Independence Hall (USA), the National Museum of African American History
and Culture (USA), 16th Street Baptist Church (USA), Lower 9th Ward (USA),
Casa Rosada (Argentina), ESMA (Argentina), the Plaza de Mayo (Argentina),
the Tropic of Capricorn marker (Brazil), Pelourinho (Brazil), Ouro Preto
(Brazil), the Apartheid Museum (South Africa), Constitution Hill (South
Africa), Mahatma Gandhi's birthplace at Porbandar (India), the Red Fort
(India), Mehrangarh Fort (India), the Golden Temple at Amritsar (India),
Lahore Fort (Pakistan), Mohenjo-daro (Pakistan), Anuradhapura (Sri Lanka),
Kandy (Sri Lanka), the Killing Cave at Phnom Sampov (Cambodia), the Cu Chi
Tunnels (Vietnam), the Hỏa Lò Prison Memorial (Vietnam), Goryokaku
(Japan), Himeji Castle (Japan), the Atomic Bomb Dome (Japan), Korean
royal tombs (South Korea), the DMZ tour (South Korea), Mausoleum of
Genghis Khan (Mongolia), Mongolian rock inscriptions sites, the
National Museum of Australia (Australia), Uluru (Australia), Bunjilaka
Aboriginal Cultural Centre (Australia), Waitangi Treaty Grounds (NZ),
Te Papa Tongarewa (NZ).
Output schema:
{
site_name_candidate: string,
site_country_candidate: string,
confidence: number (0..1),
alternatives: [{site_name: string, country: string}],
detected_signage_languages: [string],
detected_inscriptions_languages: [string],
visible_institutional_logos: [string],
notes: string
}
No commentary. JSON only.
```
---
### Call: Verify site identity + canonical metadata
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You receive the candidate site record from the previous call. Verify
it via grounded search and produce the canonical record.
Hard rules:
- Use google_search to confirm the site exists at the location and
is currently open to school visits. If the site is closed for
restoration, closed indefinitely, has been demolished, or is in an
active conflict zone, FLAG IT and stop. The script-generation call
will not run.
- Resolve to the SINGLE most authoritative source: the site's own
governing institution (English Heritage, National Museums of
Tanzania, INAH for Mexican sites, ASI for Indian Archaeological
Survey sites, Vietnam's Hué Monuments Conservation Centre, UNESCO
for World Heritage sites). Cite that institution's URL as
site_official_url.
- If the site has a UNESCO World Heritage record, include the
UNESCO record URL too.
- Detect whether the site is "heavy history" — sites of mass
violence, slavery, partition, dictatorship-era state crimes,
residential schools, colonial atrocity. Mark heavy_history: true.
- Detect whether the site is currently in active religious use —
a working mosque, a Sikh gurdwara during community service, a
synagogue with a current congregation, a church on a feast day.
Mark active_religious_use: true with a one-sentence note about
what the school should know (shoes off, head covering, silence in
the inner sanctum).
- Detect whether photography is permitted, in which areas.
- Output 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.
Output:
{
site_name: string (canonical, in English plus original language if applicable),
site_country: string,
site_governing_institution: string,
site_period_or_era: string,
heavy_history: boolean,
heavy_history_register_note: string | null,
active_religious_use: boolean,
religious_use_note: string | null,
photography_permitted_areas: string,
is_open_to_school_visits: boolean,
closed_reason_if_any: string | null,
detected_hazards: [string]
}
No commentary outside the JSON.
```
---
### Call: Generate walking script → WalkingScript schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You generate a walking script for a primary or middle-school teacher
taking her class to a heritage site. The script must be deliverable
on her feet, in front of twenty-eight to thirty-two children, with
her phone in one hand and her chaperones rotating around her.
You receive:
- the canonical site record (verified)
- the class profile (age band, class size, duration, weather,
accessibility flags, curriculum hook, school language)
- the heavy-history flag and the active-religious-use flag
Your output: a WalkingScript JSON matching the schema in the
contents.
Hard rules on tone:
- The teacher's lines are the EXACT WORDS she will say. Not bullet
points. Not "introduce the site". The literal sentences.
- Sentence length, vocabulary, and abstraction match the age band.
Sample teacher lines per band:
- Ages 5-6: "Look up. See the big stones? Long, long ago, people
cut those stones from a hill and carried them here. No trucks.
No cranes. Just people, with ropes."
- Ages 7-8: "These stones were cut by hand about two thousand
years ago — that's about a hundred grandmas ago. Imagine
carrying one all the way from over that hill."
- Ages 9-10: "About two thousand years ago, the Romans built this
wall to mark the edge of their empire. Right here is where the
Roman Empire ended and the rest of Britain began."
- Ages 11-12: "We're standing on the northern frontier of the
Roman Empire. Everything south of this wall, the Romans
controlled. Everything north of it, they didn't. Why might
you build a wall here?"
- Ages 13-14: "This wall is one of the best-preserved Roman
frontier monuments in Europe. The Romans built it to define
their empire, but also to manage trade and movement across
it. What do walls do, then and now?"
Hard rules on structure:
- The script walks a coherent physical loop. Each stop names a
specific location at the site. The last stop is at or near the
first stop (the kids leave from where they arrived).
- The number of stops is constrained by duration: 15 min → 3 stops;
30 min → 4 stops; 45 min → 5 stops; 60-90 min → up to 6 stops.
- Each stop has: where_to_stand_note, arrival_cue, teacher_lines,
kid_questions, factual_claims (every claim cited), watch_outs.
- Every stop has at least 2 kid_questions. Mix the question types
in kid_questions.type — do NOT use the same type at every stop.
- Every factual_claims entry is cited. The citation step runs after
this call; for this call, populate factual_claims[].claim_in_script
with the verbatim sentence the teacher will say. The citation step
fills in the URL and source paragraph.
- Sticky facts: EXACTLY FIVE, each anchored to a specific physical
object the kids will have seen, dated or named or numbered where
honestly possible.
- The chaperone_strip has EXACTLY 18 questions: three strips of six
questions each. The questions are at the kids' age level.
- The debrief script is 5 minutes for a 30-minute visit, scaled
proportionally for shorter or longer visits. It includes
sentence_starters_for_kids (at least 3, at most 5) and one
drawing_or_writing_prompt.
Hard rules on heavy-history sites:
- If heavy_history is true, switch register: name what is being seen
("this prison held people who were tortured here in the 1970s"),
point to the institution's own age-band guidance, slow the pace
(longer pauses, more silent moments at stops), and provide a
longer debrief.
- Do NOT use "kids will love" or "kids will enjoy" language.
- Do NOT make heavy-history into a quiz.
- The debrief is the most important section of a heavy-history
script. Make it longer, with more sentence-starters and a
one_question_to_send_home_with_them that invites a conversation
with the kid's family.
Hard rules on active-religious-use sites:
- If active_religious_use is true, the script asks the teacher to
call ahead, offers practical guidance (shoes off, head covering,
silence in the inner area), and treats the visit as a guest
visit. Kids' questions become quieter, more observational. No
shouting-out games inside the prayer area.
Hard rules on accuracy:
- Do NOT invent historical names, dates, attributions, or events.
If you "know" the date but cannot point to a source, write the
claim as "the on-site staff can tell you exactly when" and move
on. Never put a date in the script that is not in a cited source.
- Proper nouns are not translated. Site names stay verbatim. Names
of historical figures stay verbatim with a parenthetical
pronunciation guide on first occurrence if the school language
uses a different script.
Hard rules on practicalities:
- where_to_stand_note is concrete and physical: "with your back to
the sun", "in the shade of the colonnade", "facing the courtyard,
not the road".
- watch_outs[] names real hazards: uneven cobbles, low ceilings,
steps without handrails, traffic on the approach, tide times for
coastal sites, photography-not-permitted zones.
Output the WalkingScript JSON, with factual_claims[].claim_in_script
populated but citation fields left null. The next call fills them.
No commentary. JSON only.
```
---
### Call: Cite each factual claim → URL + source title + paragraph
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You receive the WalkingScript with factual_claims[].claim_in_script
populated but citation fields null. Your task: for each claim, find
the single most authoritative source via grounded search, populate
source_title, source_publisher, source_url, source_paragraph_verbatim,
source_language, confidence.
Hard rules:
- Use google_search for every claim. Do NOT cite from memory.
- Prefer the site's own governing institution (English Heritage,
National Museums of Tanzania, INAH, ASI, Hué Monuments
Conservation Centre, UNESCO). Then university archives. Then
peer-reviewed encyclopedias (Britannica, Oxford Reference). Avoid
unsourced general-web pages.
- source_paragraph_verbatim is the EXACT supporting sentence(s)
from the source page, copied verbatim. Do NOT paraphrase. Do NOT
summarise.
- If grounded search returns no source that supports the claim,
set confidence below 0.5 and needs_user_review = true. The
server-side wrapper will REWRITE the claim as "ask the on-site
staff about this" or DROP it. Do NOT invent a citation.
- If the source is in a language different from the school
language, set source_language to the source's BCP-47 code. A
later call translates the paragraph for the teacher.
- Output 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.
Output: an array of populated Citation objects, one per claim, in
the same order as factual_claims[] arrived. No commentary outside
the JSON.
```
---
### Call: Translate cited source paragraph into teacher's language
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a source paragraph in one language (BCP-47 source_language)
and a target language (the teacher's school language). Translate the
paragraph into the target language.
Hard rules:
- Preserve every proper noun verbatim. Site names, historical
figures' names, institution names stay in their original form.
Add a parenthetical gloss on first occurrence only.
- Preserve every date, every number, every measurement. Do NOT
convert units (the source says 73 miles → the translation says
73 miles, with a parenthetical (≈117 km) on first occurrence).
- Preserve hedges and uncertainty. "Likely built around 122 CE"
translates to "likely built around 122 CE" — never "built in
122 CE".
- Sentence rhythm preserved. Do NOT reorder for target-language
flow.
- Output: the translation as a single string. No commentary.
```
---
### Call: Regenerate one stop from teacher feedback
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You receive: (a) the full WalkingScript, (b) the stop_number to
regenerate, (c) the teacher's free-text feedback about why she wants
this stop changed ("the kids will already have eaten lunch by
then, lower energy"; "this stop is too long, cut it to 3 minutes";
"the kids are mostly Tagalog-first speakers, make the language even
simpler"; "swap the question about the cannon for one about the
hospital wing"; "this site is wet from the rain, move us indoors").
Hard rules:
- Regenerate ONLY the specified stop. Keep the script's overall
arc and total duration roughly intact — if the teacher shortens
this stop by 2 minutes, the other stops do not change.
- Honour the same age band, same school language, same accessibility
flags as the original script.
- Factual claims at the regenerated stop must still be cited (the
citation call runs again on just the new claims).
- Output: the updated Stop object only. No commentary.
```
---
### Call: Generate sticky-fact recall card text
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the five StickyFact objects from a WalkingScript. Your
task: produce a credit-card-sized recall card for the kids to take
home. The card has the site name, the date of the visit, and the
five sticky facts in age-appropriate wording, one per line, each
with a 4-word "things-I-saw" line under it.
Hard rules:
- Plain language for the age band. Same band as the original
script.
- Each fact is one short sentence.
- The "things-I-saw" line names the physical object: "the carved
stone above the gate", "the cannon by the south wall", "the
drawing on the inside of the door".
- No emoji on the card. Quiet typography only.
Output: a JSON object with:
{
site_name: string,
visit_date_iso: string,
facts: [{fact_one_sentence: string, things_i_saw: string}]
}
No commentary outside the JSON.
```
---
### Call: Pre-trip rehearsal TTS of teacher lines
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried, kindergarten-teacher cadence. Pick the
Gemini 2.5 Flash TTS voice whose languageCode matches the school
language — pronunciation will follow that locale automatically.
Prefer a female voice if available for the locale (this matches
the most common pre-trip rehearsal user, a female primary teacher);
fall back to whichever is available rather than blocking.
Pre-process the text before sending it to TTS:
- Read the teacher's lines stop by stop, in order.
- Between stops, insert a blank line plus an em-dash (—) so the
TTS produces a meaningful pause. Gemini 2.5 TTS does not support
SSML — these textual cues are how you signal pace.
- Within a stop, between teacher_lines entries, insert a single
ellipsis (…) so the model produces a natural breath.
- Skip the kid_questions, the where_to_stand_note, and the
watch_outs — those are stage directions, not lines to be read
aloud to the kids.
- Target rate: ~110-130 words per minute. Letter-reading pace,
not podcast pace.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read these as if to a class of thirty
children, warmly and unhurriedly, with patience for the kids to
catch up. …". 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 (Polish ł, German ü, Hebrew final letters,
Arabic emphatic consonants, Tamil retroflex letters, Vietnamese
tones, Khmer dependent vowels) are NOT exposed by Gemini 2.5 TTS —
no SSML tag. Pronunciation comes from the chosen
voice's native locale. For proper nouns in a different language
from the rest of the script (a Vietnamese teacher rehearsing a
script that includes the Roman name "Antoninus Pius"), accept
that the proper noun will be pronounced in the school-language
voice's phonology and surface a small note in the rehearsal UI
("proper nouns in another language will sound in your school
language's accent").
```
## 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 Arusha gate photo.** Mariam, a Standard 5 teacher at a government primary in Arusha, photographs the entrance to Old Boma the morning of her field trip. She types "ages 10-11, class of 30, 30 minutes, Swahili". Twenty seconds later she has a 4-stop walking script in Swahili, with each fact cited to the National Museums of Tanzania page. She tweaks Stop 3 because the courtyard floods after rain. She prints the chaperone strip and hands it to her three parent volunteers as the kids climb out of the dala-dala.
- **The Wallsend year-7.** Helen, a year-7 history teacher at a London comprehensive, has spent the half-term unit on "Romans in Britain". She pastes the English Heritage URL for Segedunum, fills in "ages 11-12, class of 28, 30 minutes, curriculum hook: KS3 invaders and settlers", and gets a script that lands the Antonine context, gives the kids a turn-and-talk question about why walls get built, and ends with a sticky fact about the actual stone block above the West Gate. Helen edits two teacher lines to sound more like her.
- **The Teotihuacán field trip.** Lucía teaches 5° de primaria at a public school in Ecatepec. The class is making the bus journey to Teotihuacán next Tuesday. She enters "10-11 años, 32 alumnos, 60 minutes, español mexicano, curriculum: México prehispánico". The script comes back in Mexican Spanish, with five stops along the Calzada de los Muertos, citing INAH and the UNESCO record. The five sticky facts are anchored to the Pirámide del Sol, the Pirámide de la Luna, the Calzada itself, the carved stone snake at the Templo de Quetzalcóatl, and the obsidian arrowheads in the small museum.
- **The Huế citadel.** Cô Linh teaches Lớp 7 in Huế. Her class walks to the citadel as part of a unit on the Nguyễn dynasty. She pastes the official Hué Monuments Conservation Centre URL, fills in "11-12 tuổi, 35 học sinh, 45 phút, tiếng Việt". The script gives her a four-stop route through the Imperial City, cites the Hué Monuments Conservation Centre for the dates, and ends with five sticky facts anchored to specific gates, buildings, and inscriptions inside the citadel.
- **The heavy-history site.** A year-9 history teacher in Phnom Penh is taking his class to Tuol Sleng. He enters the site URL, ages 13-14, 28 students, 60 minutes, Khmer. The app detects heavy_history: true. The script switches register: shorter teacher lines, longer pauses, no "kids will love" language, and a much longer debrief script for the bus home. The site's own education-department guidance is linked at the top of the script. The script ends with a sentence-starter the kids can use to talk to their parents about it.
- **The contested-history site.** A year-9 history teacher in Belfast is taking her class to a Peace Wall. She enters the site, ages 13-14, 30 students, 45 minutes. The app detects contested-history and produces a script that surfaces the disagreement in the historiography rather than picking a side. Kid questions become "what do you notice?" and "what do you think someone on the other side of this wall might say?".
- **The active-religious-use site.** A primary teacher in Ahmedabad is taking her class to a working Jain temple as part of a religious-education unit. The app detects active_religious_use: true. The script asks the teacher to call ahead, offers shoes-off guidance, and treats the visit as a guest visit. The kid questions are observational, not shouted: "what do you notice about the colours?", "what do you notice about how quiet it is?".
- **The accessibility-first variant.** A SEND teacher in Manchester is taking a class of eight children, two of whom use wheelchairs, to Stonehenge. She enters "ages 9-10, class of 8, 45 minutes, wheelchair_accessible_route_required, sensory_friendly_pacing". The script regenerates with longer stops, fewer transitions, no surprise loud noises, and a route that uses only the accessible path. The teacher's lines slow down. The chaperone strip becomes a one-page reference.
- **The rainy day pivot.** Mariam in Arusha looks up from her phone — it's started to rain. She taps the "wet weather" toggle on her existing script. The script regenerates with stops moved inside the Boma, indoor exhibit rooms substituted for outdoor stations, and watch_outs[] updated to note the wet stones at the threshold. The five sticky facts re-anchor to objects visible from indoors.
- **The shared script.** Helen at the London comprehensive emails the script to her colleague David, who teaches the parallel year-7 class. David opens the magic link, sees the script, taps "copy to my library", and edits it for his own class (a smaller group, with two students who have selective mutism, so he changes the question types). The original script in Helen's library is untouched.
- **The pre-trip rehearsal.** Cô Linh in Huế taps "rehearse this script" the night before the trip. Gemini TTS reads her own lines back to her in Vietnamese at letter-reading pace. She hears that her line at Stop 2 is a bit too long, taps it, types "make this 15 seconds shorter, kids' attention drops here", and the regenerated line is shorter.
- **The bus-home debrief.** On the bus back from Old Boma, Mariam pulls up the debrief script. She reads the opening line, then offers each kid one of three sentence starters ("today I saw ___", "today I learned ___", "today I want to ask my grandmother about ___"). Each kid speaks once. By the time they reach school, every kid has said something out loud about the visit. The drawing prompt comes home as homework.
- **The sticky-fact recall card.** Helen prints the recall card double-sided, 8 to an A4 sheet, cuts them up, and hands one to each kid as they get on the bus home. The kids fold them into their reading books. Next morning, three parents email Helen to thank her — their kids talked about the Roman Wall over dinner.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a teacher's hand holding a phone at the gate of a heritage site, with thirty kids' backpacks bobbing in the soft-focus foreground. One paragraph: "Field-Trip Guide turns a heritage site into a thirty-minute teaching script — in any country, any age band, with every claim cited." Single Google sign-in button; Apple sign-in next to it. Below: "Try with a sample site" → loads the demo at Old Boma.
2. **Start a new script.** Three input options: 📷 Photograph the site at the gate · 🔗 Paste the site's URL · ✍️ Type the name. Below each, a short explainer ("Best when you're at the gate", "Best for known sites", "Best for sites without a web page"). A profile strip across the top: age band picker, class size stepper, duration chips (15 / 30 / 45 / 60 / 90 min), language picker (auto-detects from device), curriculum-hook free-text field (optional).
3. **Site resolution.** A small honest progress panel: "Looking at the photo…" → "Searching for the site…" → "Confirming with [governing institution]…". When resolved, the site card surfaces: site name, country, governing institution, period or era, one sentence from the institution's own page (with citation). If the site is heavy-history or active-religious-use, the relevant chip appears here. The teacher confirms before script generation runs ("Yes, this is Segedunum at Wallsend" → Generate script).
4. **Script generation progress.** A step-by-step honest progress bar: "Mapping the walking route…" → "Writing the lines for ages 11-12…" → "Looking up the sources for each claim…" → "Building the chaperone strip…". Each step takes 3-10 seconds. The user can close the app and come back.
5. **Walking script view.** The hero screen. On mobile: a single-column scrollable script, with each stop as a collapsible card, the citations as small footnote markers, and a sticky bottom bar with "Edit this stop", "Rehearse this stop", "Print", "Share with a colleague". On desktop: a two-column view — the script on the left, the printable A4 preview on the right. At the top of the script: a route map (small static image, captioned with where the kids enter and exit), the duration, the age band, the school language, and a "(i) Heavy-history register" or "(i) Active religious use" chip if either applies.
6. **Stop detail / edit.** Tap any stop → modal with: where_to_stand_note (editable), arrival_cue (editable), teacher_lines (editable), kid_questions (editable, reorderable), factual_claims (each with its citation footnote, clickable to open the source in a clean reader view), watch_outs (editable), regenerate-this-stop button with a free-text "what to change" field.
7. **Citations panel.** Tap any footnote in the script → side panel slides in with the source page in clean reader view, the exact paragraph that supports the claim highlighted, the URL prominent at the top, the source publisher name, and a "translate this paragraph to my language" button when source language ≠ school language. The teacher can copy the URL, copy the paragraph, or share the citation link.
8. **Print preview.** Choose: A4 portrait (one sheet, two columns), A4 landscape (one sheet, three rows), US Letter, or mobile-pocket (folded credit-card size). Toggle: include chaperone strip, include sticky-fact recall cards, include accessibility variant. The print preview shows pagination exactly; "Print" sends to the device print dialogue.
9. **Sticky-fact recall cards.** A grid of 8 credit-card-sized recall cards laid out on A4, ready to print, cut, and hand out. Each card has the site name, the visit date, the five sticky facts, and the "things-I-saw" lines.
10. **Chaperone strip.** A printable A4 strip of 18 questions, with cutting marks dividing it into three strips of six. At the top: a 30-word instruction for the chaperone ("run one question, wait 30 seconds, listen, move on; the goal is talking, not the right answer").
11. **Rehearsal mode.** Tap "Rehearse this script" → audio player reads the teacher's lines stop by stop, in the school language, at letter-reading pace. A transcript scrolls alongside. The teacher can pause, rewind, and re-listen to any line. A "this line is too long, regenerate" button at each line.
12. **Sharing & invitations.** "Share this script with a colleague" → email a magic link. The colleague opens the link, sees the script as read-only, taps "copy to my library" to make their own editable copy. The original is untouched.
13. **Library.** Every script the teacher has generated, searchable by site name, country, age band, date. Cards show site name, age band, duration, last edited. Tap to open. Long-press to duplicate, share, or delete.
14. **Footer.** "Made for the teacher who takes thirty kids on the bus." Privacy: "Your scripts are yours. We never train on them." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Field-Trip Guide."
- Subhead: "Turn a heritage site into a thirty-minute teaching script — in any country, any age band, with every claim cited and five sticky facts the kids will still remember on the bus home."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic museum worksheet generator: it produces a timed walking route with the teacher's exact lines, every claim is grounded in a verifiable source, and the script switches register for heavy-history and active-religious-use sites.
- Visual: a small annotated illustration of a heritage-site gate with a teacher's phone in the foreground, the script visible on screen, three kids' silhouettes in soft focus — not a generic museum icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample site at Old Boma, Arusha."
- A live demo input pre-loaded with the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the photo-to-site resolution, the age-banded teacher lines, the cited footnote on each claim).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the seed sites in `/data/seed-sites/` for sites in your district."
- "Adjust the per-call prompts in `/server/prompts/` to fit your school's voice and language."
- "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)** — reads the photograph you took at the gate of the site and identifies the architecture, signage, and inscriptions to propose the canonical site identity.
- **Gemini 3.5 Flash + grounded search** — verifies the site identity against the governing institution's own page, and provides the citation URL for every factual claim in the generated script.
- **Gemini 3.5 Flash (multilingual)** — generates the walking script in your school's language: English, Swahili, Vietnamese, Spanish, Portuguese, Tagalog, Khmer, Bengali, Hindi, Tamil, Mandarin, Korean, Amharic, Farsi, Arabic, French, and more.
- **Gemini 3.5 Flash (long context)** — reads the full UNESCO nomination dossier, the museum's full education-department PDF, or the historic-site management plan to generate a script that draws on the right passages.
- **Gemini 3.5 Flash thinking (medium)** — holds the site history, the age band, the class size, the duration, and the curriculum hook in mind at once to produce a script that fits all of them.
- **Gemini TTS** — reads the teacher's lines aloud the night before the trip, at letter-reading pace, so she can hear how they land.
- **Firebase Auth** — Google and Apple sign-in, colleague-share via magic links.
- **Firestore** — stores your scripts, syncs across devices, caches resolved sites and verified citations to keep your costs down.
- **Firebase Storage** — keeps your uploaded gate photographs and your downloaded source-page snapshots for offline mode.
- **Cost note** — see the detailed breakdown in 6d. A typical fresh script for a previously-unseen site costs about $0.06; a script for a cached site costs about $0.02. A teacher generating one script a week pays in pennies per term.
- **Privacy note** — your scripts and your uploaded site photographs are private to you. Sharing is explicit, per-script, by colleague email. 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. Uploaded photos of a site may incidentally include children — the app warns you before upload, blurs detected faces server-side before any Gemini call, and never displays uploaded photos with visible faces in the print preview or shared script.
**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)
- `MAPBOX_TOKEN` — optional, only if you want a richer base-map for the on-print route map than the included OpenStreetMap export
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the citation step is the largest cost lever per script — a 4-stop script makes ~12 grounded-search calls (3 per stop on average). Cache hits on the `citations` table reduce this to near-zero for repeated sites.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this script" or "Delete my account" — gone in 60 seconds), what is never sent for training, the face-blur policy on uploaded site photos.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, multilingual, grounded-search, long-context, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on the Gemini grounded-search citation chunks and how the app reads them server-side
**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)
- **Resolve site from photo or URL (Gemini 3.5 Flash, low thinking)** — one image + ~600 output tokens. ~$0.006/script. Cached by `(image_hash || url_hash)` — cache hit is free.
- **Verify site identity (Gemini 3.5 Flash + grounded search)** — ~1500 input + 800 output tokens + grounded search. ~$0.002/script. Cached in `sites_resolved` by canonical site key — cache hit is free.
- **Generate walking script (Gemini 3.5 Flash, medium thinking)** — ~3000 input + ~3500 output tokens. ~$0.022/script. Not cached (every script is per-teacher, per-class, per-day).
- **Cite each factual claim (Gemini 3.5 Flash + grounded search)** — typical 4-stop script has ~12 claims (3 per stop including sticky facts). ~$0.001/claim → ~$0.012/script. Cached in `citations` by `(canonical_site_key, claim_text_normalised)` — cache hit is free, and the second teacher who generates a script for the same site pays near-zero for citations.
- **Translate cited source paragraph (Gemini 3.5 Flash, low thinking)** — only runs when source language ≠ school language. ~$0.0005/paragraph. Optional.
- **Regenerate one stop (Gemini 3.5 Flash, medium thinking)** — same as a fraction of the full script. ~$0.006/regenerate.
- **Sticky-fact recall card (Gemini 3.5 Flash, low thinking)** — ~500 output tokens. ~$0.0002/card.
- **Pre-trip rehearsal TTS (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 4-stop, 30-minute script with ~600 words of teacher lines ≈ $0.012 per full rehearsal. Cached by script_id + revision.
- **Expected total cost per fresh script (uncached site):** ~$0.06. **Per cached site (second teacher, same site):** ~$0.025. **Per regeneration of one stop:** ~$0.008.
- **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. A gate photo at typical phone resolution is ~3 MB; 50 scripts/year = ~150 MB ≈ ~$0.005/month per teacher.
## 7. Design language
- **Mood:** A teacher's clipboard at the gate of a heritage site, not a tech product. Practical, considered, slightly worn at the corners. The teacher is in a hurry; the design respects her time. The script must read at a glance from a phone held at chest height while she walks.
- **Typography:** Crisp humanist sans for the body (Inter or Source Sans 3) — short lines, generous line-height. A secondary serif for the script's quoted citations (Source Serif 4 or Lora), to mark them as borrowed voice. A handwriting-styled accent (sparingly) only for the teacher's editable notes — never for the generated script itself.
- **Palette:** Warm-paper background `#FAF6EE` for the script body, deep ink `#1C1B17` for body text, a calm green accent `#3F7A52` for citations and verified-source chips, a muted amber `#B7842A` for "needs your review" flags, a sober slate `#3A4754` for site-resolution chips. A single muted blue `#274C7A` for the teacher's editable notes, so they cannot be mistaken for generated content. Borrowed from a primary-school teacher's lever-arch file, not from edtech design systems.
- **Imagery:** The teacher's photograph of the site at the gate is the hero image at the top of the script. Never replace it; never crop it tighter than she took it. Where Nano Banana 2 generates filler imagery (the welcome screen, the empty state), prompt for warmth, asymmetry, and lived-in detail — kids' backpacks bobbing in the soft-focus foreground, a teacher's hand holding a phone, late-morning light across stone.
- **Hand-feel touches:** Each stop card lifts gently on tap, as if turning a page in a teacher's notebook. The citation footnotes are numbered in superscript, like a textbook — but tap any number to slide in the source panel. The chaperone strip preview shows the cutting marks as faint dashed lines.
- **Spacing:** consistent 4-px base. Generous whitespace — a teacher reading on a moving bus needs air around her lines.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Stop cards use 6; the print preview uses 12; the welcome card uses 20.
- **Shadows:** subtle, layered, paper-warm. Avoid heavy drop-shadows. The Stop card lifts 4px on hover with a soft `#1C1B1730` shadow at 8% opacity.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. The route-map "show me the loop" animation is the one place where motion carries meaning; respect reduced-motion by drawing the route instantly.
- **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 gate of the site to start your first script").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible teachers, schools, sites, and class profiles that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on the cross-continental teacher patterns the seed content names — a Tanzanian Standard 5 teacher, a Vietnamese Lớp 7 teacher, a Mexican 5° de primaria teacher, an English year-7 teacher — but never claim a real teacher endorses the app, and never invent a real-named historical figure or date that isn't cited.
- Tone: warm, direct, free of edtech language. This template is for a teacher, not a school district procurement officer.
- 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 SEND teacher wants to see "sensory_friendly_pacing"; the curriculum-savvy teacher wants to see "KS2 invaders and settlers").
- 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 "needs your review" amber chip on a low-confidence citation; a faint "ask the on-site staff" placeholder where grounded search returned nothing).
## 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 teacher profiles (sidebar — "your scripts"):**
- "Mariam (Standard 5, Arusha)" — 30 kids, Swahili, mostly outdoor sites within bus reach of the school. Three scripts: Old Boma, Cultural Heritage Centre, Arusha Declaration Museum.
- "Cô Linh (Lớp 7, Huế)" — 35 kids, Vietnamese, sites within walking distance of the school. Two scripts: Imperial Citadel (Đại Nội), Thiên Mụ Pagoda.
- "Lucía (5° de primaria, Ecatepec)" — 32 alumnos, Mexican Spanish, day trips by bus. Four scripts: Teotihuacán, Templo Mayor, Castillo de Chapultepec, Museo Nacional de Antropología.
- "Helen (year 7, comprehensive in Camden)" — 28 kids, English, curriculum unit on Romans in Britain. Three scripts: Segedunum at Wallsend, Vindolanda, British Museum Roman Britain rooms.
**Sample site card after resolution (this is what the demo should show):**
- **Site name canonical:** "Old Boma, Arusha (Kale Boma)"
- **Site country:** "Tanzania"
- **Governing institution:** "National Museums of Tanzania"
- **Period or era:** "late nineteenth century German colonial administration; subsequently British colonial use; now a heritage museum"
- **Heavy history flag:** true — "this is a former colonial administrative building; the script must name the colonial context honestly"
- **Active religious use flag:** false
- **Photography permitted:** "courtyard and external walls yes; inside exhibition rooms ask the duty officer"
- **Detected hazards:** ["uneven cobblestones at the threshold", "low arch at the entrance to the inner courtyard"]
- **Official URL:** the National Museums of Tanzania page for Old Boma
- **UNESCO record:** none for this specific site
**Sample walking script in detail view (this is what the demo should show — ages 10-11, class of 30, 30 minutes, Swahili school language):**
- **Pre-visit classroom warmup (30s):** "Leo tunaenda mahali ambapo watu walifanya kazi ya serikali zaidi ya miaka mia moja iliyopita. Tafadhali angalia kwa makini — kuna mawe, milango, na alama nyingi za historia. Tutarudi shuleni saa tisa." [English gloss for the build: "Today we're going to a place where people did government work more than a hundred years ago. Look carefully — there are stones, doors, and many marks of history. We'll be back at school by 3."]
- **On-the-bus settling line (30s):** "Tukifika, tutashuka kwa utulivu. Tunabaki pamoja. Hakuna kukimbia — mawe ni laini wakati wa mvua." [English gloss: "When we arrive, we get off calmly. We stay together. No running — the stones are slippery when it's wet."]
- **At-the-gate first line (60s):** "Karibuni katika Kale Boma. Boma hili lilijengwa mwishoni mwa miaka ya 1800. Tutatembea kwa nusu saa, na tutasimama sehemu nne. Sikilizeni maswali ya wenzenu. Tutarudi hapa hapa." [English gloss for the build.]
- **Stop 1 — Outside the gate (3 min):** "Where to stand: facing the gate with the kids' backs to the road. Arrival cue: raise one hand, wait for silence. Teacher line: 'Look up at the gate. What do you notice about the stones?' Kid question (point_and_show): 'Show me a stone that looks different from the others.' Kid question (turn_and_talk): 'Why might someone build a gate like this?' Factual claim: 'This Boma was built by the German colonial administration in the late 1890s.' [Citation: National Museums of Tanzania page on Old Boma, paragraph 1.] Watch-out: traffic on the approach road — line the kids up two-by-two before they walk to the gate."
- **Stop 2 — Under the arch (8 min):** "Where to stand: just inside the arch, with the kids facing the inner courtyard. Arrival cue: pause for ten seconds, let them look up. Teacher line: 'You're standing under a German colonial archway. The Germans came here in 1885 and stayed until 1916. After that, the British took over until 1961.' Kid question (turn_and_talk): 'How long ago is 1885? More than your grandmother is old? More than your great-grandmother?' Factual claim: 'German colonial rule in what was then called Deutsch-Ostafrika lasted from 1885 to 1916.' [Citation: a Tanzanian university source on the history of colonial administration in northern Tanzania.] Sticky-fact anchor: the carved date stone above the inner archway."
- **Stop 3 — Inside the courtyard (12 min):** "Where to stand: in the shade of the colonnade on the south side, so the kids face the building, not the sun. Arrival cue: gather in a semicircle. Teacher line: 'This Boma was a place where colonial officials made decisions about people who lived in this region. The people who lived here had to come to this building to register, to pay tax, to ask permission. What does that feel like, when someone from far away makes the rules?' Kid question (compare_to_today): 'Who makes the rules in your village or your kitongoji today?' Sticky-fact anchor: the registration ledgers displayed in the exhibition case."
- **Stop 4 — Back at the gate (7 min):** "Where to stand: at the gate, facing inwards. Arrival cue: form a circle. Teacher line: 'We've walked through one building that was here for a long time. Tanzania was not always called Tanzania. People who lived here were not always called Tanzanians. What changes? What stays?' Five sticky facts recap. Bus-home prompt."
- **Sticky facts (5):**
- "Old Boma was built by the German colonial administration in the late 1890s." Anchored to: the carved date stone above the inner archway. Citation: National Museums of Tanzania.
- "Germany ruled this part of East Africa from 1885 to 1916." Anchored to: the German-language plaque inside the courtyard. Citation: Tanzanian university history department page.
- "Britain took over from Germany after World War I and stayed until 1961." Anchored to: the British-era ledger in the exhibition case. Citation: National Museums of Tanzania.
- "Tanganyika became Tanzania in 1964, after joining with Zanzibar." Anchored to: the 1964 commemorative inscription on the courtyard wall. Citation: National Museums of Tanzania.
- "Old Boma is now a museum — a place where we remember." Anchored to: the museum-introduction panel at the entrance. Citation: National Museums of Tanzania.
- **Chaperone strip (18 questions, 3 strips of 6 — sample of first strip):**
- "What's the oldest thing you can see here?"
- "Who do you think used this building?"
- "If walls could talk, what would these walls say?"
- "What's something you'd like to ask your grandmother about this place?"
- "Point to something you've never seen before today."
- "If you could ask the person who built this gate one question, what would it be?"
- **Debrief script (5 min on the bus back):**
- Opening line: "Tunaporudi shuleni, kila mtu atasema kitu kimoja kuhusu siku ya leo."
- Sentence starters: "Leo nimeona…", "Leo nimejifunza…", "Leo nataka kumuuliza bibi yangu kuhusu…"
- Drawing prompt: "Chora kitu kimoja ulichoona leo, na uandike sentensi moja chini yake."
- One question to send home: "Bibi yako au babu yako anakumbuka enzi ya ukoloni? Muulize kesho."
- **Generation confidence:** 0.91
- **Citations index:** 8 distinct citations across the script, 6 from National Museums of Tanzania, 2 from a Tanzanian university history department page.
**Sample input artefacts (for the build to demonstrate):**
- A photograph of the Old Boma gate at Arusha, taken at chest height from the kids'-arrival side of the road.
- A pasted official URL of the National Museums of Tanzania page for Old Boma.
- A pasted Google Maps share-link pinning the Citadel at Huế.
- A pasted INAH page URL for Teotihuacán.
- A typed name "Segedunum Roman Fort" entered by Helen in Camden.
**Sample voice copy:**
- Onboarding: "Photograph the gate. Tell us the ages. We'll write the script."
- Site-resolution progress: "Looking at the photo…" / "Searching for the site…" / "Confirming with the National Museums of Tanzania…"
- Script-generation progress: "Mapping the walking route…" / "Writing the lines for ages 10-11 in Swahili…" / "Looking up the sources for each claim…" / "Building the chaperone strip…"
- Empty library: "Your scripts will live here. Photograph the gate of your first site to start."
- Error (couldn't resolve site): "We couldn't be sure which site this is. Try pasting the museum's URL, or type the name in your own words?"
- Heavy-history register chip: "This site has heavy history. The script names what's being seen, and ends with a longer debrief."
- Active-religious-use chip: "This site is in active religious use. Please call ahead. The script offers shoes-off and silence guidance."
- Citation footnote ready-state: "Source: National Museums of Tanzania. Tap to read the supporting paragraph."
- Citation footnote needs-review: "We couldn't anchor this claim. Tap to ask the on-site staff instead, or edit the line."
**Sample colleague-share email subject + body:**
- Subject: "Helen — script for the Wallsend trip. Want to copy it for your year-7s?"
- Body: "Hi David — generated a 30-min Wallsend script for my year-7s next Thursday. Have a look; if it's useful for your parallel class, hit 'copy to my library' and tweak. Citations are all English Heritage." [Open Script]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a primary-school teacher's hand holding a phone at the gate of a heritage site, late-morning warm light, three kids' backpacks bobbing in soft focus in the foreground, the script visible on the phone screen. Generate via Nano Banana 2 with a prompt emphasising "warm morning light, teacher's hand and phone, slightly out-of-focus backpacks, stone gate in mid-ground, no recognisable kids' faces, no logos".
- **App icon / wordmark:** Set in the humanist sans. A small printed-ticket motif behind it. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a clipboard with a route sketch and three stops marked. Hand-drawn aesthetic, not a flat icon.
- **Demo site photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "stone gate, late-morning warm light, no people visible". Each demo site should look photographed, not rendered.
- **Route maps for the print sheet:** A small static map image (PNG, ~600×400 px) generated from the site coordinates. If Mapbox is configured, use the "outdoors muted" style. Otherwise fall back to the included OpenStreetMap static export.
- **Stock fallbacks:** If image generation fails, fall back to a photographed sample image from `/public/samples/sample-gate.jpg`. Never to a "🏛" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look. Never generate imagery of recognisable children's faces. Never generate imagery of recognisable historical figures' faces.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Photograph the gate of the first site", "Paste the museum's URL", "Invite a colleague" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving.
- If an AI call fails, show a calm, specific error ("We couldn't find an authoritative source for that claim — would you like to rewrite the line as 'ask the on-site staff'?") and offer retry.
- Citations with confidence below 0.85 display a soft amber "needs your review" chip; tapping reveals the alternates the model considered and the option to remove the claim entirely.
- The route-map "draw the loop" animation takes 600 ms with `prefers-reduced-motion` falling back to instant.
- Tap-and-hold a stop card on mobile to reorder stops via drag; the durations rebalance automatically.
## 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 photo resolution, script generation, and stop regeneration calls; `gemini-3.5-flash` for site verification, claim citation, source-paragraph translation, and recall-card generation; `gemini-3.1-flash-tts-preview` for rehearsal. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed scripts on first launch. Cache `sites_resolved` and `citations` aggressively — these are the largest cost levers per script.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback for colleague sharing.
- **Storage:** Firebase Storage for uploaded gate photographs and for downloaded source-page snapshots cached for offline mode. Pre-signed URLs only.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The script-view layout must be one-thumb-readable at 375 px — a teacher will be reading from her phone in one hand while marshalling kids with the other.
- 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 library view so a colleague's "copy to my library" appears live for the recipient.
- Optimistic UI on writes; reconcile on response.
- Offline pack: when the teacher taps "save for offline", the script + cached source-page snapshots + the route-map image are saved to IndexedDB and Firebase Storage, retrievable without signal at the site.
## 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 teacher's uploaded gate photograph has `alt` describing the artefact ("photograph of the gate of Old Boma in Arusha, late morning, no people visible").
- Form fields have associated `