"It has to come out on a timeline that truly matches what a ScreenFlow recording would have had." — Dad, 2026-10-02
The Broadcast Nobody Recorded
On the morning of 2026-10-02 Dad went on air and talked for over an hour, and ScreenFlow, the app that records the screen, was never started. His camera recorded him. Logic Pro recorded his voice. The screen, her face, her cards, her effects and her voice were not recorded at all, except faintly through his microphone. So he asked for a replay: run the stored broadcast again on the same screen, capture it this time, and lay it over the tracks he already had. Then he set the bar in the quote above. A replay that drifts by half a second against his camera is not a replay he can edit with.
The Same Screen, Driven by a Clock
The tempting build is a video player: render the talk as frames somewhere and play them back. That would be a second screen that looks almost like the first one, and every later change to the live screen would have to be made twice. So a replay is the live voice screen itself: the face, the On Air set and its frame director, the stage and its cards, the effects layer, the lamp, full screen. What changes is what feeds it. Live, a microphone and a stream drive it. In a replay, a timeline does (hooks/useOnAirReplay.ts). Nothing is forked. The live controls (Share, On Air, Cut in, Pause) are absent, and the replay's own bar fades with the rest of the chrome, so the capture shows only what the broadcast showed. The Replay button sits in the composer's voice controls when the open conversation holds On Air turns, on the main WebUI only.
Her Voice Is the Broadcast's Own
A replay that synthesized her replies again would read them differently, and his voice track answers the reading she actually gave. So each spoken unit is cut exactly as the live screen cut it, by the same function over the stored reply and the places where a tool call closed a unit. Same text, same voice, same model: the same identity in Bellows' cache, so a cache hit is the take that aired. On the first broadcast replayed, all 25 takes hit. The replay asks /api/tts/check-cache first and fetches the hits as finished files. A take the cache no longer holds stays silent, timed at her pace (7.8 characters a second that morning), until Dad presses Synthesize. That costs credits and reads differently, so it never happens on its own. And a replay never extends the cache's window: Bellows keeps conversation audio for a set number of days after it was made (30 by its code default), so a lost broadcast is captured inside that window or not at all.
The Facts, and the Clock They Build
The server hands over what the broadcast left behind, read-only, from SQLite and the JSONL together (GET /api/onair/replay/{id}, through the same guard that serves the conversation, so a private one is never on air). For each of his turns: his words, when his row landed, and his kept recordings with the loudness of every 2048-sample chunk, the numbers the live loop drew as the ring. For each of her replies: the stored text and emotion, when the row landed, how the text reached the screen, where tool calls closed units, the effects she fired, and the cut-in with its reported position.
A pure module (lib/onairReplay.ts, tested on that morning's numbers) turns those facts into one clock. Every constant in it was measured on that broadcast against the server's own records:
- A short recording went up the moment he finished, so his row dates its end: he began at the row's time, minus 0.07 seconds for building and sending the recording, minus the recording's length.
- A cut-in recording opens 1.024 seconds before the cut, the pre-roll from Track 7. On that morning the two rules agreed to seven thousandths of a second.
- A recording of 30 seconds or more was transcribed again before it went out, for a time no record keeps, so it is placed after her reading instead: she stopped, and 0.45 seconds later the socket opened.
- Her take was asked for 0.38 seconds after her reply landed (0.03 after the call, for a line before a tool) and sounded 1.5 seconds later. Where a fixed point says exactly when her voice began, a cut-in or his next recording, that wins over the average, but only if it lands between 0.5 and 3.5 seconds after the ask.
- Scribe's forced commits, every 35.8 seconds of talk, were her nods and smiles, and an effect plays once the line announcing it has been read.
Checked against the access log, the replayed screen asked for the effect, the nod frame and the smile frame within about a tenth of a second of the times the model gave them.
Timing That Survives a Busy Page
A timer on the page is not a clock you can trust: a garbage collection or a reply streaming in stalls the main thread, and a take started from a stalled timer starts late or skips its first words. So the replay decodes each take 30 seconds ahead and hands it to the audio hardware's own clock 2 seconds ahead of its first sound (Web Audio's start(when)). Any stall shorter than that cannot move her voice. Pacing has two modes: Auto runs on the broadcast's own clock, so one sync point lines the whole capture up with the Logic Pro track; Manual waits after each of his turns, at his last word by the recording's loudness, until Next. "From" starts at any of his turns; Space, → and ← drive it.
What it cannot bring back is written down: her blinks (a random draw, as live), a lightbox Dad opened on air (nothing records it), and his voice, which is the Logic Pro track's job.