Live Caption
A live-captioning operator I built to put English subtitles over Gujarati devotional performances — and drove solo at a 3,000–5,000-person event.
The problem
A large devotional program with a globally-scattered, mixed-language audience (much of it non-native Gujarati) — staged skits and live speeches would land for only part of the room.
The design bet: at a live event, auto-transcription on a venue network is a liability (latency, mangled names, no recovery), so a human operator who knows the material drives captions. I sat with the AV team, learned how their mixing pipeline consumed a feed, and built exactly the clean caption layer they overlay.
The origin leads; the generalization closes — nothing is Gujarati-specific. Any language, any live event.
What I built
- Two operating modes switched live — Step Mode advances a pre-parsed script cue-by-cue (for staged skits); Typing Mode types captions live (for unscripted speakers).
- Two surfaces — a Display View (audience-facing: captions on a green background for AV to key) and a Console (all operator controls, never on feed).
- Script import + parser — handles dialogue/transcript formats with auto sentence/clause splitting and short-line merging.
- Prep-time overflow flagging — any cue that would clip is caught before you go on air.
- Setup + Calibration Mode — font/size/color/band geometry, with a calibration overlay so AV can size their crop.
How it works
Stack — React 19 + React Router (routes split the Console from the Display View), TypeScript, Vite, Tailwind, self-hosted fonts (zero runtime network), and Vitest. No backend — a deliberate reliability choice.
Architecture — operator input → a parser produces cues → a session reducer holds show state → state crosses to the Display View via the browser (localStorage + BroadcastChannel, same machine, two windows) → the Display View renders cues on a fixed virtual canvas over a green background → the AV team keys out the green and overlays on the venue screens. (The keying is the AV team's system, not this app.)
Captions must never shrink and never clip; the operator must never be surprised on-air. Worked example:
① Measure, don't guess — measure the real pixel width of text in the exact display font via an offscreen canvas (not character-counting). ② The detail that proves it was thought through — it waits for the custom font to load before measuring (else it measures a fallback font and every width is wrong). ③ Warn at prep, not on-air — any cue that would wrap past the band is flagged red in the Console during prep ("N overflows to fix"); change the style and every cue re-checks instantly.
④ Graceful failure — if something slips through live, the extra line grows upward from a fixed baseline (never shrinks/clips). ⑤ A deliberate tradeoff — wrapping is word-boundary only, so a single token wider than the band (a URL/hashtag) wouldn't wrap; intentional, because caption content is spoken prose where words always fit and mid-word hyphenation hurts readability for a case that never occurs.
Honest altitude: the clever call is measure-in-pixels + warn-at-prep + font-load timing; greedy word-wrap and upward-spill are standard. The codebase also carries ADRs and a reducer/selector Vitest seam.