Skip to content
C.W.K.
Stream
Lesson 07 of 07 · published

A Talk's Record, Checked

~15 min · records, audit, recordings, utf-16

Level 0Muted
0 XP0/57 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete
"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

WhatWhere it lives
His words and their timeHis row in the conversation's database
His voiceThe uploads his row's voice_meta.audio_ids name
Her words and their timeHer row
How her words reached the screenThe session log's streamed deltas
Her voiceTakes 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.

Code

How the check rules one dictated turn (a verbatim excerpt from inside a loop)·python
# backend/services/talk_record.py (cwkPippa), verbatim: Dad's side of the check
            if user:
                span(user.get("sent_at"))
                dad["turns"] += 1
                dad["unlogged"] += 0 if user.get("logged") else 1
                dad["edited"] += 1 if user.get("edited") else 0
                if user.get("input_modality") == "dictated":
                    dad["dictated"] += 1
                    where = {"id": user.get("id"), "at": user.get("sent_at"), "door": door(user.get("request_id"))}
                    if user.get("lost_audio_ids"):
                        dad["lost"].append({**where, "audio_ids": user["lost_audio_ids"]})
                    elif not (user.get("recordings") or user.get("hq_recordings") or user.get("other_audio")):
                        dad["not_kept"].append(where)
                    else:
                        dad["kept"] += 1
                        # Also kept at the microphone's own rate.
                        dad["hq"] += 1 if user.get("hq_recordings") else 0
Why a position needs the screen's count: screen_len, and an emoji before a unit's end·python
# screen_len is verbatim from backend/services/voice_turn.py (cwkPippa); the rest is a demo.
def screen_len(text: str) -> int:
    """``text``'s length as the screen counts it: UTF-16 code units, what a
    JavaScript string's ``length`` is. Every position a client reports or
    receives about a reply (a unit's end, how far Dad heard) is one of these.
    The two counts differ only past the Basic Multilingual Plane (an emoji)."""
    return len(text) + sum(1 for char in text if ord(char) > 0xFFFF)


reply = "좋아 🎉 오늘 끝냈어. 내일은 쉬자."
first_unit = reply[: reply.index(".") + 1]          # the first spoken unit
print("python len:", len(first_unit))               # code points
print("screen len:", screen_len(first_unit))        # what the screen reports
# A unit end the screen sends back, read as a code-point index, lands one
# character late for every emoji before it:
end = screen_len(first_unit)
print(repr(reply[:end]))

# Output:
# python len: 12
# screen len: 13
# '좋아 🎉 오늘 끝냈어. '

External links

Exercise

Pick a feature of yours that writes a record in two places (a row and a file, a row and a cache). Write the check: for each row, does the thing it names still exist; for each file, does some row name it? Run it over real data and sort every miss into not kept, lost, or expired by design. Then run the sketch and change the emoji to a plain letter: what happens to the two counts?
Hint
Check both directions. A row that names a missing file is lost; a file no row names is the watch's bug, and only a reverse scan finds it. Keep the check read-only, and give 'expired by design' its own bucket, or every retention policy will look like data loss. With a plain letter the two counts agree, which is exactly why a code-point index passes every test that has no emoji in it.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.