fix(backend): send vocabulary terms whole, and catch the pieces that come back (#574, #576) #632

Merged
claude-bot merged 3 commits from fix/574-576-vocabulary-echo into main 2026-09-12 20:58:57 +00:00
Collaborator

Two halves of the same loop: what goes to Whisper in the initial_prompt, and what comes back out of it on quiet audio. #574 is why #576 has one more shape to catch.

#574 — terms were sent malformed

clean_term shaved _EDGE_PUNCTUATION off both ends unconditionally, so the campaign's own names reached the ASR as Viquilonto (Viq (unbalanced) and Whispering Lantern Supply Co (truncated).

The trailing end is now walked one character at a time and stops at a closing bracket whose opener is inside the term, or a full stop that ends an abbreviation. Leading punctuation still goes unconditionally — nothing typed in front of a name belongs to it. A stray Aramore) still loses its bracket, because nothing in the term opened it.

#576 — the echo rule only caught whole terms

On the session that motivated #569, the whole-term rule dropped one segment; nine echoes survived because Whisper gives back a piece: "Fulfillment", "Mimic", "Mimic's" — each alone on a line, several times, across different speakers.

Two new rules, both whole-segment like everything else in this filter:

  • Fragments. A segment whose tokens are a contiguous one- or two-word piece of a non-person term. A token qualifies on length (≥5), or on having been capitalised in the term if it is ≥4, so Office and Prophetic build fragments and of does not. Single-letter tokens — what normalisation leaves of a possessive — are ignored when matching, which is how Mimic's is recognised as the same echo as Mimic.
  • Doubled names. A person's name repeated two or more times with nothing else. The allow-list still keeps the single form.

The one judgement call in here — worth your eye

The issue asked for a third condition: "and the segment is not also an ordinary dictionary word that people say alone." I did not implement that, and the effect is that a fragment which is also an ordinary word — Office is the realistic example — is dropped when it is the entire segment. A player who says just "Office." loses that one-word line.

I took that trade deliberately, for the reason the module already states at the top of the section: "the cost of a false positive is one contentless line, which is what lets the rules be exact rather than clever." The alternative is shipping a dictionary to decide whether a word is ordinary, and being wrong about it in a way nobody can review — a second thing to be wrong, about text that is already gone.

It is reversible in one line (raise _ECHO_FRAGMENT_MIN_LETTERS, or require two tokens for a fragment) if you would rather keep those lines. Say so and I will.

Tests

tests/test_asr_vocabulary.py and tests/test_hallucination_filter.py, following each file's existing structure. Nine tests were confirmed to fail against the pre-fix source (3 for #574, 6 for #576); the rest are guards that pass either way by design — a function word from a term, a fragment inside a sentence, a fragment of a person's name, the single form of a doubled name, and a stray bracket still being stripped.

Backend suite: 3449 passed, 13 skipped. ruff check and ruff format --check clean under CI's pinned 0.4.4.

Closes #574
Closes #576

🤖 Generated with Claude Code

Two halves of the same loop: what goes to Whisper in the `initial_prompt`, and what comes back out of it on quiet audio. #574 is why #576 has one more shape to catch. ## #574 — terms were sent malformed `clean_term` shaved `_EDGE_PUNCTUATION` off both ends unconditionally, so the campaign's own names reached the ASR as `Viquilonto (Viq` (unbalanced) and `Whispering Lantern Supply Co` (truncated). The trailing end is now walked one character at a time and stops at a closing bracket whose opener is inside the term, or a full stop that ends an abbreviation. Leading punctuation still goes unconditionally — nothing typed in front of a name belongs to it. A stray `Aramore)` still loses its bracket, because nothing in the term opened it. ## #576 — the echo rule only caught whole terms On the session that motivated #569, the whole-term rule dropped **one** segment; nine echoes survived because Whisper gives back a *piece*: "Fulfillment", "Mimic", "Mimic's" — each alone on a line, several times, across different speakers. Two new rules, both whole-segment like everything else in this filter: - **Fragments.** A segment whose tokens are a contiguous one- or two-word piece of a non-person term. A token qualifies on length (≥5), or on having been capitalised in the term if it is ≥4, so `Office` and `Prophetic` build fragments and `of` does not. Single-letter tokens — what normalisation leaves of a possessive — are ignored when matching, which is how `Mimic's` is recognised as the same echo as `Mimic`. - **Doubled names.** A person's name repeated two or more times with nothing else. The allow-list still keeps the single form. ## The one judgement call in here — worth your eye The issue asked for a third condition: *"and the segment is not also an ordinary dictionary word that people say alone."* **I did not implement that**, and the effect is that a fragment which is also an ordinary word — `Office` is the realistic example — is dropped when it is the entire segment. A player who says just "Office." loses that one-word line. I took that trade deliberately, for the reason the module already states at the top of the section: *"the cost of a false positive is one contentless line, which is what lets the rules be exact rather than clever."* The alternative is shipping a dictionary to decide whether a word is ordinary, and being wrong about it in a way nobody can review — a second thing to be wrong, about text that is already gone. It is reversible in one line (raise `_ECHO_FRAGMENT_MIN_LETTERS`, or require two tokens for a fragment) if you would rather keep those lines. Say so and I will. ## Tests `tests/test_asr_vocabulary.py` and `tests/test_hallucination_filter.py`, following each file's existing structure. **Nine tests were confirmed to fail against the pre-fix source** (3 for #574, 6 for #576); the rest are guards that pass either way by design — a function word from a term, a fragment inside a sentence, a fragment of a *person's* name, the single form of a doubled name, and a stray bracket still being stripped. Backend suite: `3449 passed, 13 skipped`. `ruff check` and `ruff format --check` clean under CI's pinned 0.4.4. Closes #574 Closes #576 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`clean_term` shaved `_EDGE_PUNCTUATION` off both ends unconditionally, so the
campaign's own names reached Whisper malformed: "Viquilonto (Viq)" was sent as
"Viquilonto (Viq" and "Whispering Lantern Supply Co." as "…Supply Co".

The intent — drop a stray comma or quote a GM typed — is right; the rule was
too broad. The trailing end is now walked one character at a time and stops at
a closing bracket whose opener is inside the term, or a full stop that ends an
abbreviation. Leading punctuation still goes unconditionally: nothing typed in
front of a name belongs to it.

An unbalanced bracket in an `initial_prompt` is one more shape for the decoder
to echo back out (#569), and a truncated abbreviation is simply the wrong name
— the list exists to tell the model what this table calls things.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whole-term echo rule dropped one segment on the session that motivated
#569; nine echoes survived, because Whisper gives back a *piece* of a term
rather than the term. "Fulfillment" (the last word of *Office of Prophetic
Fulfillment*), "Mimic" and "Mimic's" (the head of *Mimic's Slime*), each alone
on a line, several times, across different speakers, on near-silent audio.

Two rules, both whole-segment like every other rule in this filter:

- A segment whose tokens are a contiguous one- or two-word piece of a
  non-person term is an echo. A token qualifies on length, or on having been
  capitalised in the term if it is at least four letters, so "Office" and
  "Prophetic" build fragments and "of" does not. Single-letter tokens — what
  normalisation leaves of a possessive — are ignored when matching, which is
  how "Mimic's" is recognised as the same echo as "Mimic".
- A person's name repeated two or more times with nothing else is an echo. The
  allow-list still keeps the single form: "Wilhelmina?" is a complete
  utterance, "Wilhelmina, Wilhelmina" is the prompt talking.

The accepted cost is stated at the new helper: a fragment that is also an
ordinary word ("Office") is dropped when it is the entire segment. That is the
trade this module already documents — a false positive costs one contentless
line — and the alternative is shipping a dictionary to decide whether a word is
ordinary, and being wrong about it unreviewably.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(changelog): transcript cleanup fixes (#574, #576)
All checks were successful
CI / Docker image build (pull_request) Successful in 22s
CI / Bot/backend version sync (pull_request) Successful in 39s
CI / Backend lint (ruff) (pull_request) Successful in 40s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 48s
CI / Bot tests and audit (pull_request) Successful in 2m13s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m26s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m54s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 21m41s
fd55493219
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch fix/574-576-vocabulary-echo 2026-09-12 20:58:58 +00:00
Sign in to join this conversation.
No description provided.