"Keeping it at 48 kHz costs nothing, so why are you keeping it at 16?" — Dad, 2026-10-04
Ground Truth Must Survive
Dad's direction that day reached past voice mode. Any conversation, spoken or typed, should be able to become a finished video with no screen recorder running and no live recording at all. Then the record itself has to be whole: his voice, her voice, her words, and when each turn happened. In his words, ground truth is the only thing that must survive, and once it does, a missed record button stops being an accident. Before anyone built the video pipeline, the house asked a plainer question: does a talk actually leave all of that behind?
What a Talk Leaves, and Where
| What | Where it lives |
|---|---|
| His words and their time | His row in the conversation's database |
| His voice | The uploads his row's voice_meta.audio_ids name |
| Her words and their time | Her row |
| How her words reached the screen | The session log's streamed deltas |
| Her voice | Takes in Bellows' cache, one per spoken unit, found by the unit's text |
The replay plan that On Air already used (Track 9 tells it) became the reader for any talk. GET /api/onair/replay/{id} takes a scope: onair for a broadcast's own turns, voice for every turn he dictated or she answered aloud, all for every turn, typed ones included. Each of his turns says what became of each recording: recordings (a 16 kHz WAV the replay can read), hq_recordings (the same utterance above 16 kHz), other_audio (kept in another format) and lost_audio_ids (the file is gone). Each spoken reply arrives cut into its units.
That cut is where a quiet bug lived. A take's identity in Bellows is its unit's text, so the server has to cut a reply exactly as the screen did, to the character. voice_turn.spoken_units is the twin of the screen's takeSpokenUnits, and one shared list of cases is read by both test suites. Positions are counted the way the screen counts them, in UTF-16 code units (screen_len). Until that day the plan counted code points, one short for every emoji before a unit's end.
The Check, Which Only Reads
services/talk_record.py reads a plan and says what the talk holds and what it lacks. GET /api/onair/record/{id} checks one talk; an admin route checks every talk that moved in the last few days. A dictated turn that names no recording is not_kept, with the door that sent it read off its request id's own tag. One whose file is gone is lost. A spoken unit whose take Bellows no longer holds is gone if it is still inside Bellows' conversation window and expired past it. A talk is whole when none of the three appears. Nothing is synthesized and nothing is written; a check that repairs as it reads can no longer tell you what was there.
What It Found, and What Changed That Day
His voice was whole on the WebUI (455 of 458 dictated turns since 2026-09-25, the three being test turns) and on the phone (109 of 111, two bench turns). Firekeeper and Pathfinder had 0 of 55 and 0 of 1, by their own design. The watch was the strange one: it kept the audio and lost the link, so fifteen watch turns named no recording while fifteen kept files were named by no row. And her takes older than Bellows' window, then three days, had already expired.
The same day: the phone's recording began waiting with the turn in the outbox, written beside his words and uploaded by the drain, so a dropped link costs neither; a retried turn keeps its voice facts instead of resending the words alone. The watch's upload id now rides its turn. The WebUI asks for the upload again while the server is away. Firekeeper and Pathfinder keep his voice, by Dad's ruling. And Dad widened Bellows' conversation window to 30 days so a broadcast's replay can be rebuilt from its own takes; 30 is now Bellows' code default. The fifteen orphaned wrist recordings were left as they were: the check reports, it doesn't relink.
His Voice at the Microphone's Own Rate
Every kept recording was the 16 kHz stream the listening socket takes, only because that was the buffer at hand, and Dad asked the question at the top of this page. Now a talk keeps a second file beside it: the same utterance at the rate the device gave it. The change is additive on purpose. Scribe, the loudness a commit is judged by, and the replay's timing all still read the 16 kHz file. The copy comes from a tape of the microphone's last few minutes at its own rate, cut by time as the utterance ends, so it is for sound, never for timing. The plan tells the two apart by their rate.
The first video made from a talk exposed the next layer. Through a studio microphone, Dad's voice sounded close to a phone call, because the tape was fed by the talk's own stream, and that stream runs through the browser's echo cancellation, noise suppression and automatic gain, which barge-in depends on. His ruling: leave alone what cut-in needs, and use the microphone's own sound only at a desk. So at a desk the WebUI opens the same device a second time with all three off and keeps that copy (named voice-raw-…, against voice-hq-… for a processed copy); a phone stays as its microphone is. Firekeeper needed no second capture on office's audio interface, where the family's voice kit never turns that processing on. One more guard came out of measuring: a raw copy never reaches back before the listen's own recording began, because the extra half second once carried her voice from the monitors into his file.