feat(backend): send the campaign vocabulary as hotwords, on a switch (#577) #578

Merged
claude-bot merged 2 commits from feat/577-asr-hotwords into main 2026-09-09 10:03:07 +00:00
Contributor

Closes #577.

What. The bundled WhisperX server accepts the campaign vocabulary either as an initial_prompt (what the app has always sent) or as hotwords, faster-whisper's purpose-built term bias. This adds VocabularyStyle.HOTWORDS and a worker switch, QB_ASR_VOCABULARY_STYLE=initial_prompt|hotwords|none, defaulting to initial_prompt so nothing changes for anyone who does not set it. hotwords falls back to the prompt, with a warning, on a server that does not declare it; none is the control arm and skips the wiki query entirely. The style actually used is stamped on the summarisation run (vocabulary_style, migration 4f5a6b7c8d9e, nullable, not backfilled) and surfaced on the run API, so a comparison can be read back later.

Verified against the live server, not assumed. With an invented fixture clip that says "Stillbrook": no vocabulary → Stillbrook; hotwords="Stilbroque, …"Stilbrooke; a misspelt field name → Stillbrook (so the field is read, not silently dropped); and both fields together → the prompt wins and hotwords are ignored. That last point is why the request tests assert the other field is absent, not merely that the right one is present: sending both would be a comparison that never runs.

Design notes. caps.vocabulary holds the effective style after reconciling the switch with /health, so the request field, the log line, the capability panel and the stored value cannot disagree. The style is captured right after resolve_asr_vocabulary, before per-span failures can invalidate the capability cache. The switch is scoped to the bundled adapter: the OpenAI-compatible path has only prompt on the wire. One accepted gap: with none set, the capability panel's remedy text still suggests updating the server; documented in OPERATIONS.md rather than fixed, since fixing it means carrying the raw declaration separately.

Tests: 21 new in test_asr_hotwords.py; full backend suite 2,985 passed; migration upgraded, downgraded and re-upgraded on a real database.

🤖 Generated with Claude Code

Closes #577. **What.** The bundled WhisperX server accepts the campaign vocabulary either as an `initial_prompt` (what the app has always sent) or as `hotwords`, faster-whisper's purpose-built term bias. This adds `VocabularyStyle.HOTWORDS` and a worker switch, `QB_ASR_VOCABULARY_STYLE=initial_prompt|hotwords|none`, defaulting to `initial_prompt` so nothing changes for anyone who does not set it. `hotwords` falls back to the prompt, with a warning, on a server that does not declare it; `none` is the control arm and skips the wiki query entirely. The style actually used is stamped on the summarisation run (`vocabulary_style`, migration `4f5a6b7c8d9e`, nullable, not backfilled) and surfaced on the run API, so a comparison can be read back later. **Verified against the live server, not assumed.** With an invented fixture clip that says "Stillbrook": no vocabulary → *Stillbrook*; `hotwords="Stilbroque, …"` → *Stilbrooke*; a misspelt field name → *Stillbrook* (so the field is read, not silently dropped); and both fields together → the prompt wins and hotwords are ignored. That last point is why the request tests assert the other field is *absent*, not merely that the right one is present: sending both would be a comparison that never runs. **Design notes.** `caps.vocabulary` holds the effective style after reconciling the switch with `/health`, so the request field, the log line, the capability panel and the stored value cannot disagree. The style is captured right after `resolve_asr_vocabulary`, before per-span failures can invalidate the capability cache. The switch is scoped to the bundled adapter: the OpenAI-compatible path has only `prompt` on the wire. One accepted gap: with `none` set, the capability panel's remedy text still suggests updating the server; documented in OPERATIONS.md rather than fixed, since fixing it means carrying the raw declaration separately. Tests: 21 new in `test_asr_hotwords.py`; full backend suite 2,985 passed; migration upgraded, downgraded and re-upgraded on a real database. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(backend): send the campaign vocabulary as hotwords, on a switch (#577)
Some checks failed
CI / Docker image build (pull_request) Successful in 26s
CI / Bot/backend version sync (pull_request) Successful in 50s
CI / Backend lint (ruff) (pull_request) Failing after 56s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m26s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m4s
CI / Bot tests and audit (pull_request) Successful in 2m22s
CI / Backend migration, tests, and audit (pull_request) Successful in 11m33s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m6s
33d3d40e2f
The bundled WhisperX server takes the campaign's proper nouns two ways.
`initial_prompt` is a sentence the decoder is told it has just transcribed,
so the model conditions on the style of the list as well as its contents;
`hotwords` is faster-whisper's dedicated bias for exactly this job. Verified
against the live server that they are alternatives rather than layers — both
occupy the same slot in the prompt, and a request carrying both is served as
though only the initial prompt were there — so exactly one is ever sent.

Which is better is an empirical question about one deployment's audio, its
model and its campaign's names, and nothing in here can answer it. So this
is a switch plus a record of which arm each run was on, not a decision:

- `QB_ASR_VOCABULARY_STYLE` — `initial_prompt` (default), `hotwords`, `none`.
  Unset makes byte-identical requests to every release before this one, even
  though the current server declares both mechanisms. Validated at boot,
  because a typo would otherwise be silent: the pipeline keeps working and
  every run of the comparison lands on the default arm.
- `hotwords` against a server that does not declare it falls back to the
  prompt and warns on each capability probe, rather than quietly running the
  comparison on the arm the operator was trying to leave.
- `none` is the control arm and is genuinely free — the wiki is not queried,
  because nothing would read the answer.
- New nullable `summarisation_runs.vocabulary_style` (migration
  `4f5a6b7c8d9e`, raw DDL, no CHECK or enum since the style set belongs to
  the provider layer), surfaced as `vocabulary_style` on the summarisation-run
  responses. NULL is "not recorded" — a run predating the column, or a
  re-summarisation reading someone else's transcript. Captured before the
  first request rather than after transcription, because a failed span drops
  the capability cache and would otherwise stamp a prompt-biased run "none".

The switch reaches the bundled adapter only; a managed provider's vocabulary
is whatever its own wire shape offers.

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 08:59:01 +00:00
style: format the vocabulary_style migration with the ruff version CI pins (#577)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m1s
CI / Docker image build (pull_request) Successful in 20s
CI / Backend lint (ruff) (pull_request) Successful in 27s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Bot tests and audit (pull_request) Successful in 1m59s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m33s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m47s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m25s
0be6d2d356
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude-bot deleted branch feat/577-asr-hotwords 2026-09-09 10:03:08 +00:00
Sign in to join this conversation.
No description provided.