STT glossary drift: consumers.yaml stt_prompt is hand-maintained and diverges from mVoice lexicon.yaml (missing mBrian → 'mBryan') #10

Open
opened 2026-07-25 09:30:31 +00:00 by mAi · 2 comments

Symptom

m's proper noun mBrian transcribes as mBryan in production PWA voice. mVoice's own STT renders it correctly; the divergence is in the production path.

Root cause

The STT bias vocabulary exists in two places and they have drifted:

  • mVoice ~/.config/mvoice/lexicon.yaml — the intended source of truth, 14 stt_bias terms, includes mBrian. Editable by hand and via the #12 lexicon feedback API.

  • mGPUmanager config/consumers.yaml stt_prompt — 11 hand-typed terms, forwarded to whisper.cpp as the per-request prompt. Verified value:

    stt_prompt: "Dania, Matthias, Otto, Jule, Hogan Lovells, mRock, mRiver, mBreeze, mDock, mLake, mGPUmanager"
    

    mBrian is absent. So the production STT is not biased toward it, and whisper hears the more common-looking mBryan.

The consequence is larger than one word: the production STT path never sees mVoice's lexicon. Every term added through the #12 feedback API — the whole point of which was to let Otto fix a misrecognition without an ssh + yaml-edit + reload — lands in lexicon.yaml and never reaches consumers.yaml, so it has no effect on what m actually experiences.

Fix direction

Generate consumers.yaml's stt_prompt (or whatever mGPUmanager forwards as the whisper prompt) from mVoice's lexicon.yaml stt_bias terms, rather than maintaining a second hand-typed list. Then lexicon.yaml is genuinely the single source and the #12 API works end to end. Decide the mechanism (build-time generation, a shared file, or mGPUmanager reading lexicon.yaml directly) on the mGPUmanager side.

Note there is also a static --prompt "Dania, Matthias, Otto, Jule, Hogan Lovells" baked into whisper-server.service itself; consumers.yaml notes the per-request prompt overrides it. Whatever the fix, collapse to one source rather than three.

Acceptance

  • mBrian transcribes as mBrian in the production PWA path, verified live.
  • A term added via the mVoice #12 lexicon feedback API takes effect in production STT without a hand-edit to mGPUmanager config.
  • No regression on the terms currently in stt_prompt.

Provenance

Surfaced by apollo during m/mVoice #22; both config values independently re-verified by mvoice/head before filing (lexicon.yaml has 14 stt_bias terms incl. mBrian; consumers.yaml stt_prompt has 11 without it). Filed in mGPUmanager because that is where the drifted config and the forwarding logic live. Left unassigned — mGPUmanager owns the priority call.

Related: m/mVoice #22, #12; and the -l de language-forcing defect filed alongside.

## Symptom m's proper noun **mBrian** transcribes as **mBryan** in production PWA voice. mVoice's own STT renders it correctly; the divergence is in the production path. ## Root cause The STT bias vocabulary exists in **two** places and they have drifted: - **mVoice `~/.config/mvoice/lexicon.yaml`** — the intended source of truth, 14 `stt_bias` terms, **includes `mBrian`**. Editable by hand and via the #12 lexicon feedback API. - **mGPUmanager `config/consumers.yaml` `stt_prompt`** — 11 hand-typed terms, forwarded to whisper.cpp as the per-request `prompt`. Verified value: ``` stt_prompt: "Dania, Matthias, Otto, Jule, Hogan Lovells, mRock, mRiver, mBreeze, mDock, mLake, mGPUmanager" ``` `mBrian` is absent. So the production STT is not biased toward it, and whisper hears the more common-looking `mBryan`. The consequence is larger than one word: **the production STT path never sees mVoice's lexicon.** Every term added through the #12 feedback API — the whole point of which was to let Otto fix a misrecognition without an ssh + yaml-edit + reload — lands in lexicon.yaml and never reaches `consumers.yaml`, so it has no effect on what m actually experiences. ## Fix direction Generate `consumers.yaml`'s `stt_prompt` (or whatever mGPUmanager forwards as the whisper prompt) **from** mVoice's lexicon.yaml `stt_bias` terms, rather than maintaining a second hand-typed list. Then lexicon.yaml is genuinely the single source and the #12 API works end to end. Decide the mechanism (build-time generation, a shared file, or mGPUmanager reading lexicon.yaml directly) on the mGPUmanager side. Note there is also a *static* `--prompt "Dania, Matthias, Otto, Jule, Hogan Lovells"` baked into `whisper-server.service` itself; consumers.yaml notes the per-request prompt overrides it. Whatever the fix, collapse to one source rather than three. ## Acceptance - `mBrian` transcribes as `mBrian` in the production PWA path, verified live. - A term added via the mVoice #12 lexicon feedback API takes effect in production STT without a hand-edit to mGPUmanager config. - No regression on the terms currently in `stt_prompt`. ## Provenance Surfaced by apollo during `m/mVoice` #22; both config values independently re-verified by mvoice/head before filing (lexicon.yaml has 14 stt_bias terms incl. mBrian; consumers.yaml stt_prompt has 11 without it). Filed in mGPUmanager because that is where the drifted config and the forwarding logic live. Left unassigned — mGPUmanager owns the priority call. Related: `m/mVoice` #22, #12; and the `-l de` language-forcing defect filed alongside.
Author

Collapsed to one source: the broker reads mVoice's lexicon

Branch mai/curie/mgpumanager-stt-truth-9.
Commits: 730fe90 · cbebade

Mechanism, and what it costs

A new consumer field stt_prompt_file names ~/.config/mvoice/lexicon.yaml. The broker stats it per STT request and re-reads on an mtime or size change, so a term m adds through mVoice's feedback API applies to the next transcription with no restart on either side. mVoice writes the file atomically (tmp + rename), so a partial read is not possible.

Rejected alternatives:

Option Why not
Generate stt_prompt at build time A term added through the #12 API would need a rebuild and a deploy. Fails this issue's acceptance outright.
Fetch the lexicon from mVoice over HTTP Puts mVoice back in the STT hot path, which the routing deliberately removed, and needs a new endpoint in a second repo. STT would degrade whenever mVoice is unloaded.

The cost. mGPUmanager takes a read-only dependency on a file mVoice owns — three fields of it: word, aliases, stt_bias. The two services must share a host. They do, both on mRock, but that is now load-bearing where it was incidental, and a schema change in mVoice is a silent break here. Two things bound the damage: the broker reads only those three fields, and an unreadable or empty lexicon falls back to stt_prompt in consumers.yaml rather than leaving STT unbiased.

Aliases too, not only terms

The adapter also applies the lexicon's aliases, the same rewrite mVoice's canonicalize_stt performs. Aliases are the repair m reaches for when a name comes back wrong — that is what the #12 API is for — so honouring the prompt while ignoring the aliases would have left the feedback API half a no-op on the production path. Whole-word, case-insensitive, canonical spelling wins.

This is a behaviour change worth objecting to if it is unwanted: an alias now rewrites the transcript on the PWA path exactly as it already does on mVoice's own path. If m says "Daniel" and the lexicon lists Daniel as an alias of Dania, the transcript says Dania.

Lexicon changes, all made through the #12 API

Backup on mRock: ~/.config/mvoice/lexicon.yaml.bak-curie.

  • Added the 7 terms consumers.yaml had and the lexicon did not: Matthias, Jule, mRiver, mBreeze, mDock, mLake, mGPUmanager. Without these, switching the source would have regressed them.
  • Added mBryan as an alias of mBrian — the misrecognition this issue reports.
  • Renamed otto to Otto. The prompt influences casing, and the production list carried Otto. One API call reverts it if the lowercase spelling was deliberate.
  • Added the 5 terms that existed only in the legacy glossary.txt: mAi, goldi, RaceTrack, Juliane Rein, Juliane Brandes.

Result: 26 stt_bias terms, a strict superset of the 11 that were in stt_prompt. No regression on any of them.

The prompt format

A bare comma-joined term list, terminated by a full stop. Not mVoice's "Glossar: … ." — a German label is German text in the initial prompt and would bias the language auto-detection that #9 just unblocked.

The full stop is not decoration. Live on the German reference clip, same audio, same terms:

prompt "…, Juliane Brandes"   →  " … in Düsseldorf heute\n"
prompt "…, Juliane Brandes."  →  " … in Düsseldorf heute.\n"

Detail on #9.

What did NOT collapse — two things, stated plainly

1. whisper-server.service --prompt stays. The per-request prompt overrides it, so it does not govern PWA voice. But it is the only biasing left for a caller that sends no prompt of its own, and there is one: mAi/chats/transcribe-audio.sh transcribes WhatsApp voice notes straight against :8178, sending language=de and no prompt. Removing the launch prompt would silently degrade WhatsApp voice notes. Moving that caller onto the broker is mAi's change, not this one — worth a follow-up issue there.

2. There is a fourth copy: ~/.config/mvoice/glossary.txt. mVoice merges it at load and its own docs call it transitional. Its 5 unique terms are now in lexicon.yaml too, so the broker sees them, but retiring the file is mVoice's call.

Acceptance

  • A term added through the #12 API reaches production with no mGPUmanager config edit: verified by mechanism (mtime re-read, TestLexiconCacheRereadsOnChange) and by the 12 entries above, all written through that API.
  • No regression on the 11 terms: the live prompt is a superset.
  • mBrian transcribing as mBrian live is not verified. mBrian is now in the production prompt with mBryan as an alias, which is the fix, but espeak-ng cannot pronounce it — the synthetic clip came out as "im Bayern", which no prompt can repair. That last criterion needs m's own voice.

Tests

TestSTTPromptComesFromLexicon, TestSTTPromptFallsBackToStatic, TestSTTCanonicalizesAliases, TestParseLexiconTakesOnlyBiasedWords, TestParseLexiconNilWithoutBiasedWords, TestCanonicalizeRewritesWholeWordsOnly, TestLexiconCacheRereadsOnChange, TestLexiconCacheNilForMissingFile, TestLoadSTTPromptFile. Each fix was re-run as a negative control with the fix disabled, and each test failed.

Design note: docs/designs/issue-9-10-stt-language-and-lexicon.md.

## Collapsed to one source: the broker reads mVoice's lexicon Branch `mai/curie/mgpumanager-stt-truth-9`. Commits: [730fe90](https://mgit.msbls.de/m/mGPUmanager/commit/730fe90) · [cbebade](https://mgit.msbls.de/m/mGPUmanager/commit/cbebade) ### Mechanism, and what it costs A new consumer field `stt_prompt_file` names `~/.config/mvoice/lexicon.yaml`. The broker stats it per STT request and re-reads on an mtime or size change, so a term m adds through mVoice's feedback API applies to the next transcription with no restart on either side. mVoice writes the file atomically (tmp + rename), so a partial read is not possible. Rejected alternatives: | Option | Why not | |---|---| | Generate `stt_prompt` at build time | A term added through the #12 API would need a rebuild and a deploy. Fails this issue's acceptance outright. | | Fetch the lexicon from mVoice over HTTP | Puts mVoice back in the STT hot path, which the routing deliberately removed, and needs a new endpoint in a second repo. STT would degrade whenever mVoice is unloaded. | **The cost.** mGPUmanager takes a read-only dependency on a file mVoice owns — three fields of it: `word`, `aliases`, `stt_bias`. The two services must share a host. They do, both on mRock, but that is now load-bearing where it was incidental, and a schema change in mVoice is a silent break here. Two things bound the damage: the broker reads only those three fields, and an unreadable or empty lexicon falls back to `stt_prompt` in `consumers.yaml` rather than leaving STT unbiased. ### Aliases too, not only terms The adapter also applies the lexicon's `aliases`, the same rewrite mVoice's `canonicalize_stt` performs. Aliases are the repair m reaches for when a name comes back wrong — that is what the #12 API is *for* — so honouring the prompt while ignoring the aliases would have left the feedback API half a no-op on the production path. Whole-word, case-insensitive, canonical spelling wins. This is a behaviour change worth objecting to if it is unwanted: an alias now rewrites the transcript on the PWA path exactly as it already does on mVoice's own path. If m says "Daniel" and the lexicon lists `Daniel` as an alias of `Dania`, the transcript says `Dania`. ### Lexicon changes, all made through the #12 API Backup on mRock: `~/.config/mvoice/lexicon.yaml.bak-curie`. - Added the 7 terms `consumers.yaml` had and the lexicon did not: `Matthias`, `Jule`, `mRiver`, `mBreeze`, `mDock`, `mLake`, `mGPUmanager`. Without these, switching the source would have regressed them. - Added `mBryan` as an alias of `mBrian` — the misrecognition this issue reports. - Renamed `otto` to `Otto`. The prompt influences casing, and the production list carried `Otto`. One API call reverts it if the lowercase spelling was deliberate. - Added the 5 terms that existed only in the legacy `glossary.txt`: `mAi`, `goldi`, `RaceTrack`, `Juliane Rein`, `Juliane Brandes`. Result: 26 `stt_bias` terms, a strict superset of the 11 that were in `stt_prompt`. No regression on any of them. ### The prompt format A bare comma-joined term list, terminated by a full stop. Not mVoice's `"Glossar: … ."` — a German label is German text in the initial prompt and would bias the language auto-detection that #9 just unblocked. The full stop is not decoration. Live on the German reference clip, same audio, same terms: ``` prompt "…, Juliane Brandes" → " … in Düsseldorf heute\n" prompt "…, Juliane Brandes." → " … in Düsseldorf heute.\n" ``` Detail on #9. ### What did NOT collapse — two things, stated plainly **1. `whisper-server.service --prompt` stays.** The per-request prompt overrides it, so it does not govern PWA voice. But it is the only biasing left for a caller that sends no prompt of its own, and there is one: `mAi/chats/transcribe-audio.sh` transcribes WhatsApp voice notes straight against `:8178`, sending `language=de` and no prompt. Removing the launch prompt would silently degrade WhatsApp voice notes. Moving that caller onto the broker is mAi's change, not this one — worth a follow-up issue there. **2. There is a fourth copy: `~/.config/mvoice/glossary.txt`.** mVoice merges it at load and its own docs call it transitional. Its 5 unique terms are now in `lexicon.yaml` too, so the broker sees them, but retiring the file is mVoice's call. ### Acceptance - A term added through the #12 API reaches production with no mGPUmanager config edit: verified by mechanism (mtime re-read, `TestLexiconCacheRereadsOnChange`) and by the 12 entries above, all written through that API. - No regression on the 11 terms: the live prompt is a superset. - `mBrian` transcribing as `mBrian` live is **not** verified. `mBrian` is now in the production prompt with `mBryan` as an alias, which is the fix, but espeak-ng cannot pronounce it — the synthetic clip came out as "im Bayern", which no prompt can repair. That last criterion needs m's own voice. ### Tests `TestSTTPromptComesFromLexicon`, `TestSTTPromptFallsBackToStatic`, `TestSTTCanonicalizesAliases`, `TestParseLexiconTakesOnlyBiasedWords`, `TestParseLexiconNilWithoutBiasedWords`, `TestCanonicalizeRewritesWholeWordsOnly`, `TestLexiconCacheRereadsOnChange`, `TestLexiconCacheNilForMissingFile`, `TestLoadSTTPromptFile`. Each fix was re-run as a negative control with the fix disabled, and each test failed. Design note: `docs/designs/issue-9-10-stt-language-and-lexicon.md`.
Author

Deployed, and the acceptance criterion is demonstrated live

On main 7b2a0ba. Broker deployed to mRock 12:05.

"A term added via the mVoice #12 API takes effect in production without hand-editing mGPUmanager config"

Run end to end against production, nothing else touched — no config edit, no restart of either service:

1. Added a term through mVoice's API alone:

POST mrock:8766/api/lexicon/entry {"word":"Testsatz","aliases":["Test Satz"],"stt_bias":true}
→ {"action":"created"}

2. Same audio, production broker mrock:8770/v1/stt:

before: "… das ist ein deutscher Test Satz über das Wetter …"
after:  "… das ist ein deutscher Testsatz über das Wetter …"

3. Deleted the term again, transcribed again — the rewrite is gone, back to Test Satz.

The test term was removed; m's lexicon is left as it was, at 28 entries.

That closes the loop the issue described: the API edit reached the model that actually transcribes m's voice, in the next request, with no ssh and no yaml edit on this side.

The other two criteria

  • No regression on the 11 terms. The live prompt is 26 stt_bias terms, a strict superset. Verified after the deploy.
  • mBrian transcribes as mBrian live — not verified, and it cannot be from here. mBrian is in the production prompt now with mBryan as an alias, which is the fix. But espeak-ng cannot say it: the synthetic clip came out as "im Bayern", an acoustic miss no prompt repairs. This one needs m's own voice.

Deploy notes

stt_prompt_file: ~/.config/mvoice/lexicon.yaml is live in mRock's consumers.yaml; the previous file is kept as consumers.yaml.bak-curie. The host-side comfyui vram_resident_mib: 6000 hand-edit was preserved (repo says 13000). whisper-server.service launch --prompt now ends in a full stop, unit backed up as .bak-curie2.

After the restart: /healthz ok, /v1/status all four consumers healthy, POST /v1/gamemode 200 and released cleanly. mDock failover untouched and answering.

## Deployed, and the acceptance criterion is demonstrated live On main [7b2a0ba](https://mgit.msbls.de/m/mGPUmanager/commit/7b2a0ba). Broker deployed to mRock 12:05. ### "A term added via the mVoice #12 API takes effect in production without hand-editing mGPUmanager config" Run end to end against production, nothing else touched — no config edit, no restart of either service: **1.** Added a term through mVoice's API alone: ``` POST mrock:8766/api/lexicon/entry {"word":"Testsatz","aliases":["Test Satz"],"stt_bias":true} → {"action":"created"} ``` **2.** Same audio, production broker `mrock:8770/v1/stt`: ``` before: "… das ist ein deutscher Test Satz über das Wetter …" after: "… das ist ein deutscher Testsatz über das Wetter …" ``` **3.** Deleted the term again, transcribed again — the rewrite is gone, back to `Test Satz`. The test term was removed; m's lexicon is left as it was, at 28 entries. That closes the loop the issue described: the API edit reached the model that actually transcribes m's voice, in the next request, with no ssh and no yaml edit on this side. ### The other two criteria - **No regression on the 11 terms.** The live prompt is 26 `stt_bias` terms, a strict superset. Verified after the deploy. - **`mBrian` transcribes as `mBrian` live — not verified, and it cannot be from here.** `mBrian` is in the production prompt now with `mBryan` as an alias, which is the fix. But espeak-ng cannot say it: the synthetic clip came out as "im Bayern", an acoustic miss no prompt repairs. This one needs m's own voice. ### Deploy notes `stt_prompt_file: ~/.config/mvoice/lexicon.yaml` is live in mRock's `consumers.yaml`; the previous file is kept as `consumers.yaml.bak-curie`. The host-side `comfyui vram_resident_mib: 6000` hand-edit was preserved (repo says 13000). `whisper-server.service` launch `--prompt` now ends in a full stop, unit backed up as `.bak-curie2`. After the restart: `/healthz` ok, `/v1/status` all four consumers healthy, `POST /v1/gamemode` 200 and released cleanly. mDock failover untouched and answering.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/mGPUmanager#10
No description provided.