Record D-WS2a: hint measurements, the 120-token truncation rule, no faster-whisper hotwords
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
edb9b332b9
commit
378aff6a7b
@@ -1224,3 +1224,87 @@ whole-file output is equally bad, which is exactly what the absolute bar
|
||||
exists to catch.
|
||||
**Consequence:** full `bun test` ≈ 19 min on CPU (en_alien ≈ 20 min of it
|
||||
on a cold run); the fast suite excludes this file.
|
||||
|
||||
## D-WS2a — "Names and terms" vocabulary hint as a Whisper decoder prompt, cut to 120 tokens
|
||||
**Status:** Accepted (2026-10-29) — spec 001, Goal 2. Placeholder id; the
|
||||
integrator renumbers it.
|
||||
**Context:** users want names and jargon spelled their way. Whisper takes a
|
||||
"previous text" prompt (`<|startofprev|> … <|startoftranscript|>`).
|
||||
Transformers.js 4.2.0 — and 4.3.2, checked in workstream 1's worktree —
|
||||
declare `prompt_ids` in the Whisper generation config but never use it (the
|
||||
parameter is commented out in `WhisperForConditionalGeneration.generate`),
|
||||
and the pipeline's `_decode_asr` prints every non-special decoder token, so
|
||||
a prompt passed as `decoder_input_ids` would appear in the transcript.
|
||||
**Decision:**
|
||||
- UI: an optional "Names and terms" textarea for every model
|
||||
(`VocabularyHint`), raw text kept in localStorage (`whisper-vocabulary-hint`),
|
||||
≤ 1000 characters, sent as `prompt` on the worker's transcribe message, on
|
||||
the server engine's query string, and on the live WebSocket URL.
|
||||
- `src/lib/hint.ts` normalizes (one term per line or comma-separated →
|
||||
`"a, b, c"`, control characters dropped) and owns the **truncation rule**:
|
||||
prompt text is `" " + hint` ending in a full stop (unless it ends in
|
||||
`.?!`); over budget → the longest prefix of whole space-separated words
|
||||
that fits (dangling `,`/`;` removed, full stop re-added) — the start of the
|
||||
list is kept, the end dropped; if not even the first word fits, its first
|
||||
`budget` tokens. Budget **120 tokens** (Whisper allows 223). The Python twin
|
||||
`fit_prompt_ids` in `server-py/sidecar.py` is parity-tested.
|
||||
- Browser: `src/worker/prompt.ts` builds `<|startofprev|> + ids` once per pass
|
||||
and passes it through the ASR pipeline as a private generate kwarg; a
|
||||
one-time wrapper around `model.generate` prepends it to the init tokens of
|
||||
every 30 s window, caps `max_new_tokens` so prompt + init + new ≤ 448
|
||||
decoder positions (D-021's 440 otherwise), and slices it off the output.
|
||||
Live passes use the same path.
|
||||
- Server: `parseLanguageAndTask` validates `prompt` (400 above 1000 chars) for
|
||||
the HTTP and WebSocket routes; the sidecar passes the cut token ids as
|
||||
faster-whisper's `initial_prompt`. **No `hotwords`.**
|
||||
**Measurements** (en_alien, 888 s gaming livestream, whole file; WER vs
|
||||
`tests/audio_files/en_alien.txt`, single runs; hinted terms counted as
|
||||
maxwell / voices of the void / slenderman / runescape, reference 9/1/1/1;
|
||||
hint "Maxwell, Voices of the Void, Slenderman, RuneScape, UFO" = 21 tokens):
|
||||
- Browser path, **onnxruntime-node CPU**, catalog WASM dtypes (turbo q4,
|
||||
base.en q8), the worker's prompt code:
|
||||
|
||||
| model | hint | prompt tokens | WER | terms |
|
||||
|---|---|---|---|---|
|
||||
| large-v3-turbo | none | 0 | 0.255 | 9/1/0/1 |
|
||||
| large-v3-turbo | short list | 21 | **0.120** | 9/1/1/1 |
|
||||
| large-v3-turbo | 54-name realistic list | 120 | 0.253 | 9/1/1/1 |
|
||||
| large-v3-turbo | 54-name realistic list | 221 | 0.205 | 10/1/1/1 |
|
||||
| large-v3-turbo | 300 synthetic "TermN" | 120 | 0.266 | 9/1/1/1 |
|
||||
| base.en | none | 0 | 0.339 | 7/1/0/1 |
|
||||
| base.en | short list, no final full stop | 21 | 0.566 | 4/1/1/0 |
|
||||
| base.en | short list | 21 | 0.319 | 7/1/1/1 |
|
||||
| base.en | realistic list | 32 / 64 / 96 / 120 | 0.286 / 0.306 / 0.305 / 0.284 | 9/1/1/1 at 32 and 120 |
|
||||
| base.en | realistic list | 160 / 218 | 0.386 / **0.476** | 8/1/1/1, 6/1/1/0 |
|
||||
| base.en | 300 synthetic "TermN" | 96 / 120 / 223 | 0.530 / 0.474 / **1.050** | |
|
||||
|
||||
Wall-clock per whole-file pass (turbo 141 s without a hint, 129–206 s
|
||||
with) varied with machine load more than with the hint; the extra cost is
|
||||
only the prompt's decoder positions per window.
|
||||
- Without a final full stop the prompt made base.en drop punctuation and loop
|
||||
(punctuation 239 → 183) — hence the full stop.
|
||||
- Short windows (live): 10 cases × 2 models (silence 1/3 s, noise 1–3 s,
|
||||
2 s speech snippets from en_aiproblem and da_festuge), with and without the
|
||||
short hint: **0 / 20** outputs contained a hint word the no-hint output
|
||||
lacked. One 2 s Danish snippet on turbo (language pinned to `da`) came out
|
||||
as English words with the English hint ("I have been at an order June
|
||||
August." vs "at den orde tøde i august.").
|
||||
- Server path, faster-whisper 1.2.1 large-v3-turbo int8 CPU, short hint:
|
||||
none 0.118 (9/1/0/1), `initial_prompt` 0.085, `hotwords` 0.077, both 0.112
|
||||
— all with 9/1/1/1. Hotwords did not improve hinted-word recall over
|
||||
`initial_prompt` and combining them lost WER, so the plan's bar ("only if it
|
||||
measurably helps") is not met.
|
||||
**Why 120 tokens:** with prompts near Whisper's 223-token cap the small
|
||||
model degraded badly (base.en 0.339 → 0.476 realistic, 1.05 synthetic —
|
||||
repetition and dropped text), while at ≤ 120 tokens a realistic list stayed
|
||||
at or below the no-hint WER on both models. A junk list still hurts base.en
|
||||
at 96–120 tokens (0.47–0.53); the UI says "put the most important names
|
||||
first" once the text passes ~300 characters (≈ 120 tokens of names), and
|
||||
short lists measured best everywhere.
|
||||
**Rejected:** Transformers.js `prompt_ids` (unused in 4.2.0/4.3.2);
|
||||
patching `_decode_asr` (a wrapper avoids touching the transformers patch);
|
||||
the full 223-token budget (above); per-model budgets (one rule, one parity
|
||||
test; revisit if a larger list is wanted on turbo).
|
||||
**Consequence:** every window pays up to 121 extra decoder positions with a
|
||||
long hint. WebGPU behaviour of the prompt path is unmeasured here (CPU
|
||||
only) — needs the owner's browser check.
|
||||
|
||||
@@ -339,6 +339,12 @@ cached.
|
||||
`language`/`task` the same way the HTTP route does, and
|
||||
`tests/server-live.test.ts` covers the bridge against a fake sidecar.
|
||||
|
||||
## Spec 001 — Transcription quality and speed
|
||||
- [x] Vocabulary hint "Names and terms" (D-WS2a) — optional field for every
|
||||
model, remembered in localStorage, sent as `prompt` on file and live
|
||||
passes (browser and server engines); cut to 120 tokens at a whole word.
|
||||
faster-whisper gets `initial_prompt` only (hotwords measured, no gain).
|
||||
|
||||
---
|
||||
|
||||
## Deliberately out of scope (for now)
|
||||
@@ -372,6 +378,10 @@ Found while bootstrapping agent docs (D-036); all fixed on 2026-10-29.
|
||||
— Light theme via tokens + self-hosted Inter (D-037). Also fixed: the
|
||||
Backend select overflowed its panel on 390 px-wide phones.
|
||||
|
||||
- [ ] An English "Names and terms" hint can pull a short Danish live window
|
||||
toward English words (one 2 s da_festuge snippet on turbo, D-WS2a).
|
||||
Not addressed: suggest same-language hints if it shows up in use.
|
||||
|
||||
## Recurring
|
||||
- [ ] Maintain `AGENTS.md`:
|
||||
- **Add a rule only in response to a real failure.** When an agent repeatedly
|
||||
|
||||
@@ -46,7 +46,7 @@ sidecar applies the same truncation rule (Python twin, parity-tested) and passes
|
||||
above it — URL-safe since it travels as a query param).
|
||||
- `normalizeHint`: drop control chars, one term per line or comma-separated → lines trimmed, empty
|
||||
lines dropped, joined with `", "`, whitespace collapsed. Empty → `""` (= no prompt).
|
||||
- `PROMPT_TOKEN_BUDGET = 223` (= 448/2 − 1, the openai/faster-whisper prompt cap).
|
||||
- `PROMPT_TOKEN_BUDGET = 120` (measured; was 223 = 448/2 − 1, see D-WS2a).
|
||||
- **Truncation rule:** encode `" " + hint`; if > budget, keep the longest prefix of whole
|
||||
(space-separated) words that fits, trailing `,;` stripped from the kept text; if not even the
|
||||
first word fits, its first 223 tokens. The start of the list is kept, the end dropped.
|
||||
@@ -179,12 +179,12 @@ Scratch scripts in `/tmp/ws2/` (not committed), importing the worktree's modules
|
||||
`"Maxwell, Voices of the Void"`: WER (test normalizer), count of `maxwell` and `voices of the
|
||||
void` vs reference counts, wall-clock, punctuation count (a prompt without a final full stop may
|
||||
suppress punctuation — if so, `normalizeHint` appends `.` and Task 1 tests are updated).
|
||||
- [ ] Short-window hallucination: 1–3 s low-noise and 2 s speech snippets with/without hint, turbo
|
||||
- [x] Short-window hallucination: 1–3 s low-noise and 2 s speech snippets with/without hint, turbo
|
||||
+ small model; output must not contain hint words that are absent from the audio.
|
||||
- [ ] Server path (venv, `mobiuslabsgmbh/faster-whisper-large-v3-turbo` cached, int8 CPU):
|
||||
- [x] Server path (venv, `mobiuslabsgmbh/faster-whisper-large-v3-turbo` cached, int8 CPU):
|
||||
en_alien none / initial_prompt / hotwords / both. Keep hotwords only if it measurably beats
|
||||
initial_prompt alone on hinted-word recall without WER loss. Implement the outcome in sidecar.py.
|
||||
- [ ] Write D-WS2a (bottom of `docs/Decisions.md`) with numbers labelled CPU/onnxruntime-node,
|
||||
- [x] Write D-WS2a (bottom of `docs/Decisions.md`) with numbers labelled CPU/onnxruntime-node,
|
||||
truncation rule, wrapper rationale, 4.3.2 check. Architecture.md data-flow note; WorkBreakdown
|
||||
tick/noticed. Commit.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user