Skip to content

The transcript filter is a projection, not a parameter

Adityo Guni Waluyo

Changing a filter rule doesn't mean re-running whisper. The filter is a projection of stored stdout, stamped with a sha256 of the rule.

TL;DR

When the profanity filter changed, the author's first instinct was re-running Whisper on every transcript, even though the audio never changed. The fix: treat filtering as a cheap projection of already-cached Whisper output, not a transcription step. Stamp artifacts with a phrase-list hash, so rule changes mean instant re-projection with zero model calls.

The profanity filter rule in my audio pipeline changed. One line in the phrase list. My first reflex: re-run whisper on every transcript I already had. Hundreds of audio files, inference paid twice, while the audio itself had not changed at all.

The first design I sketched looked tidy on paper: attach filter_version to the sidecar cache key. Rule changes, key changes, every old result automatically goes stale. The problem was the upgrade path: every existing source had to be re-transcribed by hand. Luckily the idea got reversed in the next design iteration, before anyone executed it.

Re-running whisper is not the cheap option

My mistake started with an assumption: that the filter is part of transcription. It isn't. whisper's transcribe() reads the entire file, processes it through a sliding 30-second window, and fills each window with autoregressive sequence-to-sequence predictions [1]. Running that again just to re-filter means full inference from zero.

"Just use the turbo model, it's fast." The turbo model is indeed an optimized version of large-v3, faster with a minimal degradation in accuracy [1]. It's still a real model run. The compute is just as heavy to produce text that is, from the filtering point of view, already sitting complete on disk.

The filter is a projection, not a transcription parameter

Here's the actual turning point: the whisper output is already stored. The stdout sits in the sidecar. The filter rule is just a layer that projects that stdout into its final form. When the rule changes, only the projection reruns, never the transcription. Zero model calls.

So every derived artifact can be validated, the rule gets stamped with a filter_version: a sha256 hash of the phrase list, computed with hashlib.file_digest [2].

import hashlib

with open("phrases.txt", "rb") as f:
    filter_version = hashlib.file_digest(f, "sha256").hexdigest()

Because it's a hash of the list's content, the marker is order-independent (shuffle the phrases and the value stays the same) and it changes automatically whenever any component of the rule changes. Where it lives matters too: filter_version lives in the event frontmatter, next to the data, not in the cache key. The transcription cache key is untouched, so old results stay valid and nothing needs migrating. The frontmatter checker is strict as well: every window in a source has to match the filter_version on its row, so mixed versions inside one source get caught immediately.

The final design has one _filter_projection function shared by transcribe, refilter, and assemble. Three consumers, one definition of the rule, byte-identical results on every path. That's what makes the determinism claim testable: a refilter with the same version is idempotent, and re-projecting from an old version produces output byte-for-byte identical to a fresh transcription. There's a test for it, not just hope.

Writing refilter results is atomic per source too: write to a .tmp file first, then os.replace over the final name. That function silently replaces the destination when it exists, and a successful rename is atomic, a POSIX guarantee [3]. Killed mid-refilter? Worst case you find a stray .tmp file; the final transcript never lands half-written.

When the producer is expensive, keep the raw output

The general pattern: a pipeline with an expensive producer and cheap consumers of its output should store the raw output once, derive the views from it, and stamp each view with a content hash of the rule that produced it. Changing the rule? Re-project, and the command is a one-liner. Auditing? Compare the hash in each artifact's frontmatter to know which rule produced it. The question "which filter version produced this transcript" becomes answerable without guessing from the file contents.

After this episode I get suspicious whenever I see a design whose answer is "invalidate the cache and recompute everything". Sometimes that really is the right call. But often the raw data is already on disk, and only the way of looking at it has to change. Now a filter rule change in my pipeline is a non-event: edit the phrase list, a new hash appears, the re-projection runs, done in seconds without a single model call. Short path, testable output, and no part of the system panics. Honestly, it should have been this way from the start.

Sources

  1. OpenAI Whisper, README (transcribe() mechanics and the turbo model)
  2. Python docs, hashlib.file_digest
  3. Python docs, os.replace

Related articles