# V3 Broadcast — demo (file-backed dev/test harness)

An automated 3D broadcast of a 2-on-2 team race: a Three.js scene over a Cesium ion photoreal
world, live standings / margin / per-boat tiles + right-of-way rings, an event-driven commentary
track read aloud by the browser, and a director that plays the race live and then on replay. It is
the **file-backed harness over the shared [`../core/`](../core/) engine + renderer** and the
engine's golden-file test bed. Built as a read-through over V2 data (see
[`../../../docs/V3-broadcast.md`](../../../docs/V3-broadcast.md)).

This folder holds only the **demo-specific** pieces (data + export/synthetic scripts). The engine,
renderer, contract and commentary reference live in [`../core/`](../core/).

## Run

1. **Cesium ion token** — create `../core/web/cesium-config.js` (gitignored) exporting your ion
   access token (see `../core/web/tiles.js` for the expected shape). Without it the scene falls
   back to a stylized water plane.
2. **Export the race** (needs GCP ADC: `gcloud auth application-default login`), from `demo/`:
   ```
   ../../../.venv-demo/Scripts/python export/export_team_race.py
   ```
   Writes `data/races/real.json` + `manifest.json`. (The exporter imports the scoring engine from
   `../../scoring`.) No synthetic race needed for the single-race demo; for the multi-race
   director exercise run `export/gen_races.py` instead.
3. **(Optional) High-quality neural audio** — the page reads the commentary aloud with the browser's
   built-in TTS by default (device-dependent; weak/absent for some languages → Swedish gets read
   with English phonetics). Pre-render clips for consistent high-quality voices instead. The page
   plays a clip when one exists for the on-screen line + selected language, else falls back to
   browser TTS. Re-run after re-scoring (enrich rebuilds the lines). New feeds use three stable roles
   per language: **Lead Commentator**, **Tactical Analyst**, and **Rules Analyst**. Legacy `m/f` events
   map to Lead/Tactical. Each line is synthesized with its role's pinned voice. Output:
   `data/audio/<lang>/*` + `data/audio/index.json`
   (both gitignored).

   **Google neural TTS** (needs auth + the Text-to-Speech API) deterministically picks three voices
   per language. Pin them with `TTS_VOICE_<LANG>_LEAD`, `_TACTICAL`, and `_RULES`; legacy `_M`/`_F`
   still configure Lead/Tactical
   (e.g. `TTS_VOICE_EN_M=en-GB-Wavenet-B TTS_VOICE_SV_M=sv-SE-Wavenet-C`):
   ```
   gcloud auth application-default login        # org policy blocks SA keys; ~1 h validity
   ../../../.venv-demo/Scripts/python ../live/tts/synth.py
   ```
4. **Serve** the broadcast root over HTTP and open the renderer:
   serve **`v3/broadcast/`** (e.g. `python -m http.server` from there) and open
   `core/web/index.html`. The page reads `core/web/config.js` → `demo/data/races/manifest.json`.
   Use the **Lang** selector (English / Svenska) to switch commentary + audio + overlay live.

## What's here (demo-specific)

- `export/race_real.py` — **the only file to edit for a different race**: Firestore instance,
  window, device→boat mapping, teams, title, director params, and the **manual wind**
  (`WIND = {fromDeg, kts}`, which unlocks the upwind/downwind + gassing commentary, the
  right-of-way calls/rings, the point-of-sail tile labels, and the on-water **wind arrows** —
  a drifting dart field pointing down-wind, moving at the true speed kts→m/s).
- `export/export_team_race.py` — the generic batch exporter (real Firestore window → scored feed).
  It also builds each boat's **picture/video timeline** by **listing the GCS photos bucket**
  (`{project}-viewregatta-photos`, override with `PHOTOS_BUCKET`) windowed to the race — photos are
  *not* on the track points; the app uploads them separately as `<deviceId>/<timestamp>.jpg|.mp4`.
  It **downloads** the in-window blobs into `data/media/<dev>/` (gitignored, like the audio) so the
  static demo carries its own copies. Tiles show the latest clip at/before the cursor; boats with
  none in the window show no media (slot collapsed). Nothing is drawn over the clip itself. Watch
  the `media: N …` line for the count.
- `export/synth_race.py`, `export/gen_races.py` — a synthetic race + a 3-race staggered set for
  testing the director's overlap with no GCP.
- `export/discovery/` — one-off scripts used to find this race's ids/devices (archived).
- `data/races/real.json` + `manifest.json` — the exported, scored race the page loads;
  `data/demo_race.json` — the synthetic fallback.

## In the shared core (not here)
`../../scoring/` — `score_race.py` (engine), `geo.py`, `broadcast_reference.md` (source of
truth). `../core/web/` — the render page, scene, overlays, director, `feed.js` (`FileFeed`), boat
models, Cesium tiles, flags, and `config.js` (the file-vs-live seam). To support more nations, add
to `../core/web/flags.js` `nationFor` and the `NATION` map in `../../scoring/commentary_banks.py`.
