feat(webapp): snap misheard character names to what the GM says they are (#579) #581

Merged
claude-bot merged 1 commit from feat/579-heard-as-aliases into main 2026-09-09 15:42:11 +00:00
Contributor

Closes #579.

Why. On a real session "Viq" was transcribed as Vic, Vick or Nick in all 22 mentions, and no amount of vocabulary bias changes that (#577 measured prompt and hotwords as equivalent): Whisper will not overturn a common English word the audio already matches. So the GM says what a name is heard as, and the pipeline corrects it.

What. campaign_characters.heard_as (JSONB list, migration 6b7c8d9e0f1a), exposed on the character create/update/response schemas with validation (at most 20, each 1–50 characters, no case-insensitive duplicates, and a 400 for an alias that equals the character's own name or another character's name in the campaign). A pure snap_names step in audio_service, whole-word and case-insensitive, longest alias first, possessives handled, "Victor" untouched by "Vic", applied in process_audio after the hallucination filter and before the merge; the order is pinned by a test because snapping first would let the echo rule delete a corrected one-word line. Counts per canonical name land on summarisation_runs.name_snaps and the run API; the log carries only totals since the keys are campaign content. The same aliases join the ASR vocabulary at the alias tier. The eval harness runs the step in the production position (a no-op on the fixture manifest). On the campaign page the character editor gains an "Also heard as" field and the card shows the list.

Calls a reviewer should see. null on PATCH means "unchanged", [] clears. Collisions are refused at write time and dropped again at read time, and an alias two characters both claim is dropped for both rather than first-wins. Word-timing entries are corrected but not counted, so name_snaps is per mention. A GM acting on another member's character may set this field, unlike the sheet fields, because what the transcriber mishears is a property of the recording.

Known gap, filed as #580. Aliases do not survive a campaign export/import round-trip.

Tests: backend full suite 3,024 passed (new test_name_snapping.py); frontend 846 passed, eslint and build clean; migration upgraded, downgraded and re-upgraded on a real database; formatted with CI's pinned ruff.

🤖 Generated with Claude Code

Closes #579. **Why.** On a real session "Viq" was transcribed as Vic, Vick or Nick in all 22 mentions, and no amount of vocabulary bias changes that (#577 measured prompt and hotwords as equivalent): Whisper will not overturn a common English word the audio already matches. So the GM says what a name is heard as, and the pipeline corrects it. **What.** `campaign_characters.heard_as` (JSONB list, migration `6b7c8d9e0f1a`), exposed on the character create/update/response schemas with validation (at most 20, each 1–50 characters, no case-insensitive duplicates, and a 400 for an alias that equals the character's own name or another character's name in the campaign). A pure `snap_names` step in `audio_service`, whole-word and case-insensitive, longest alias first, possessives handled, "Victor" untouched by "Vic", applied in `process_audio` after the hallucination filter and before the merge; the order is pinned by a test because snapping first would let the echo rule delete a corrected one-word line. Counts per canonical name land on `summarisation_runs.name_snaps` and the run API; the log carries only totals since the keys are campaign content. The same aliases join the ASR vocabulary at the alias tier. The eval harness runs the step in the production position (a no-op on the fixture manifest). On the campaign page the character editor gains an "Also heard as" field and the card shows the list. **Calls a reviewer should see.** `null` on PATCH means "unchanged", `[]` clears. Collisions are refused at write time and dropped again at read time, and an alias two characters both claim is dropped for both rather than first-wins. Word-timing entries are corrected but not counted, so `name_snaps` is per mention. A GM acting on another member's character may set this field, unlike the sheet fields, because what the transcriber mishears is a property of the recording. **Known gap, filed as #580.** Aliases do not survive a campaign export/import round-trip. Tests: backend full suite 3,024 passed (new `test_name_snapping.py`); frontend 846 passed, eslint and build clean; migration upgraded, downgraded and re-upgraded on a real database; formatted with CI's pinned ruff. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(webapp): snap misheard character names to what the GM says they are (#579)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 1m7s
CI / Backend lint (ruff) (pull_request) Successful in 1m10s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m42s
CI / Bot tests and audit (pull_request) Successful in 2m43s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m54s
CI / Docker image build (pull_request) Successful in 4m33s
CI / Backend migration, tests, and audit (pull_request) Successful in 11m14s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m16s
f2e2e0edba
Whisper will not overturn a common English word the audio genuinely matches.
On the session that motivated this, "Viq" came back as Vic, Vick or Nick in
all 22 mentions and "Idani" as Adani/Bidani/Dani in 5 of 31 — with the
campaign's names sent as an initial_prompt *and*, since #577, as hotwords,
which measured no difference between them. So this stops arguing with the
decoder: a GM lists what a character's name is heard as, and whole-word
matches are corrected afterwards, deterministically.

It matters twice. The transcript reads wrong, and `beat_service.validate_beats`
checks a beat's actor against the speaker labels and character names, so a line
saying "Vic" cannot support a beat about Viq.

- `campaign_characters.heard_as` (JSONB NOT NULL DEFAULT '[]') and
  `summarisation_runs.name_snaps` (JSONB, nullable), migration `6b7c8d9e0f1a`.
  NULL name_snaps is "not measured" and `{}` is "measured, nothing to fix" —
  the convention `hallucination_drops` set.
- `audio_service.snap_names` is pure: whole word, case-insensitive, longest
  alias first, `(?<!\w)`/`(?!\w)` boundaries so the possessive comes along
  ("Vic's" -> "Viq's") and "Victor" does not. Word timings are corrected but
  not counted; segments are never mutated.
- Wired into `process_audio` after the hallucination filter and before the
  merge. That order is the safety argument: snapping first would hand the
  prompt-echo rule a corrected one-word line indistinguishable from a
  vocabulary echo, and it would delete somebody's line.
- The listed misspellings also join the ASR vocabulary at TIER_ALIAS, which
  sounds backwards and is the point — the snap needs a consistently wrong
  spelling, not a good one.
- Validation refuses an alias that is the character's own name or another
  character's name in the same campaign; `build_alias_map` drops both cases
  again at read time, plus an alias two characters claim, because a rename or
  a restore can create a collision no request was wrong about.
- Counts, never names, in the log line. The names go to the run record, where
  the speaker legend already is, and out as `name_snaps` on the run response.

Backend: 3024 passed, 13 skipped. Frontend: 846 passed, eslint and build clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-09 15:26:54 +00:00
claude-bot deleted branch feat/579-heard-as-aliases 2026-09-09 15:42:13 +00:00
Sign in to join this conversation.
No description provided.