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

Keeping His Voice, on Purpose

~14 min · voice-chat, retention, recording, privacy, audio

Level 0Unlit
0 XP0/48 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Forgetful by default was a rule with a reason. When the reason changed for one surface, the rule changed there — and only there."

From Never Kept to Always Kept

When voice chat shipped on 2026-09-26 it inherited dictation's rule: the audio is discarded. Eight days later cwkPippa ran a record check across its conversations and found that 0 of 55 dictated Firekeeper turns had a recording — by Firekeeper's own design. That mattered because of a new goal: any conversation should be able to become a video later, with no live recording session, which means its ground truth — Dad's actual voice — has to survive. Dad ruled the same day, for Firekeeper and the navigation app alike: keep both voices. Asked why the copy should be 16 kHz, he answered with a question: why keep it at 16 when 48 kHz costs nothing extra?

What a Turn Now Keeps

When an utterance ends, Firekeeper takes two files: the 16 kHz recording the transcriber heard, and the same stretch at the microphone's own rate from the voice kit's tape. Both go to cwkPippa's /api/upload, and the turn names them in input_audio_ids — a record attached to the turn, never something the model reads. cwkPippa tells the two apart by sample rate. If the server is briefly away, the upload is asked again under the same rule the listening grant uses; any other failure leaves the note "The recording was not kept." and the words still go out. Nothing is kept on the Mac. And dictation did not change at all: its audio never leaves as a file and is discarded once transcribed, apart from the bounded failure rescue track 8 describes (at most ten files, and only while history is on).

utterance ends
  -> voice-<stamp>.wav       16 kHz: what the transcriber heard (replay timing, loudness)
  -> voice-hq-<stamp>.wav    mic rate, through voice processing (MacBook mic, AirPods)
     voice-raw-<stamp>.wav   mic rate, the microphone's own sound (office)
  -> POST /api/upload  (a private talk names its own folder)
  -> turn.input_audio_ids = [ids]

On this Mac: nothing.   Dictation: unchanged (discarded; failure rescue aside).

On Office, the Microphone's Own Sound

The first video made from a talk sounded like a phone call, and Dad asked for better — on one condition: if fixing it would disturb cut-in, don't. The WebUI needed a second, unprocessed opening of the device to get a clean copy, because its talk stream runs through the browser's echo cancellation. Firekeeper needed nothing new: office's 32-input interface never runs voice processing, so the talk's own channel already is the raw microphone at 48 kHz. Measured inside the app's own path, the tape was flat to 24 kHz with the room at about −54.5 dBFS. So the capture did not change, cut-in could not be touched, and the only change was the name: a copy taken without voice processing is voice-raw-…, a processed one stays voice-hq-….

The Copy Starts When the Listening Began

A raw copy keeps everything the room heard — including Pippa's own voice from the monitors. The first version cut the copy as "the utterance's length plus half a second" from the end of the tape, and that half second reached back before the listen began: a web raw copy that evening opened with 0.4 s of her voice at speech level. Now, whenever the listen's start time is known, the copy is bounded by the time since that recording began, so it cannot reach back into her reply; when the start time is missing, the function falls back to the old length plus half a second. In a speaker test that night, every copy lined up with its 16 kHz twin at 0.99–1.00 correlation within 45 ms of the start.

The Retention Rule Still Holds

Track 8 said retention is the exception with a reason, bounded. This is exactly that: a stated reason (a talk must be able to become a video), a bounded scope (talks yes, dictation no beyond its bounded failure rescue, nothing of a talk on the Mac), and one store (cwkPippa, the same seam the WebUI and the phone use, so its record check reads a Firekeeper talk with no new code). A private talk's recordings land in that talk's own folder and end with it.

Change a privacy default per surface, with its reason attached. "Forget everything" was right for an input method. A conversation that may become a video has a different purpose, so it keeps its ground truth. Neither surface borrows the other's rule; each states its own.
A raw copy hears the whole room. Bound any unprocessed recording to the moment listening began. A margin that reaches back even half a second can carry the other speaker's voice into a file labeled as one person's words.

Code

Naming what a turn keeps (FirekeeperKit VoiceChatTurn)·swift
// A copy taken without voice processing is the mic's own sound: "voice-raw-".
// A processed copy (built-in mic, AirPods) is "voice-hq-".
public static func recordingFiles(recording: Data?, original: Data?,
                                  originalIsRaw: Bool, stamp: Int) -> [SharedFile] {
    var files: [SharedFile] = []
    if let original, !original.isEmpty {
        let kind = originalIsRaw ? "raw" : "hq"
        files.append(SharedFile(name: "voice-\(kind)-\(stamp).wav",
                                contentType: "audio/wav", data: original))
    }
    if let recording, !recording.isEmpty {   // 16 kHz, what the transcriber heard
        files.append(SharedFile(name: "voice-\(stamp).wav",
                                contentType: "audio/wav", data: recording))
    }
    return files
}

// When this listen's start is known, the original never outlasts it;
// without it, the old length + 0.5 s margin is the fallback.
public static func originalSeconds(recordingSeconds: Double, sinceRecordingBegan: Double?) -> Double {
    guard recordingSeconds > 0 else { return 0 }
    let wanted = recordingSeconds + originalMarginSeconds        // 0.5 s margin
    guard let since = sinceRecordingBegan, since.isFinite, since > 0 else { return wanted }
    return min(wanted, since)
}

External links

Exercise

Pick a privacy default in something you built ("we don't keep X"). Imagine a new use that needs X kept. Write the three things a responsible change must state: the reason, the scope (which surface, which store, where it is NOT kept), and what still never changes.
Hint
The failure mode is changing the default everywhere because one surface needed it. A good change names the surface that keeps the data, the single place it is stored, and the surfaces that keep the old rule untouched — here: talks keep; dictation keeps nothing beyond its bounded failure rescue; the Mac keeps nothing of a talk.

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.