Skip to content

Transcription accuracy, per-game screen guide, and model selection - #24

Merged
MyNamesEMurray merged 6 commits into
mainfrom
claude/localvocal-transcription-accuracy-jxxefb
Jul 26, 2026
Merged

Transcription accuracy, per-game screen guide, and model selection#24
MyNamesEMurray merged 6 commits into
mainfrom
claude/localvocal-transcription-accuracy-jxxefb

Conversation

@MyNamesEMurray

Copy link
Copy Markdown
Owner

Summary

Makes voice awareness meaningfully more accurate without adding a second speech model, teaches the cast to read each game's screen so it can react to game state rather than surface appearance, and opens up model selection. Also fixes a live issue where thinking tokens were sharing a budget sized for a one-line chat message.

Changes

Transcription accuracy (LocalVocal stays the transcriber)

  • src/speech.js — new: hallucination filtering, a per-streamer correction map, word-level alignment, WER, and derivation of corrections from a known reference reading. Whisper's silence filler ("Thank you.", "please subscribe", stuck repetition) is matched whole-line only, so a real sentence containing those words survives.
  • Mic check (Settings → Voice) — generates a reading script from your ghost names, channel and vocabulary, listens through the live LocalVocal path, and reports your real word error rate plus one-click fixes. It is a measurement, not training: nothing is uploaded and no model is modified. Past scores persist so a LocalVocal change can be judged by a number.
  • Click-to-fix — click a 🎙 line in the deck feed, retype it, press Enter; the changed words become corrections and apply to the very next line, no save or restart.
  • The cast is told which proper nouns matter, so it recovers a near-miss the map hasn't learned yet.
  • Warns once when voice is configured, OBS is connected, and nothing has ever arrived for 10 minutes — a wrong path currently looks identical to the cast ignoring you.

Per-game screen guide

  • New ---GAMEINFO--- tail section: the cast learns where the HUD is, what a death/menu screen looks like, and where your overlay sits, then reuses it on later streams of that game. Written on generations that already happen (no extra API calls) and refined across a session, so it self-corrects rather than baking in the first frame.
  • Stored per game in game-notes.json, injected into the cached context block, and visible/editable at Settings → Stream — it's treated as truth on every message, so a wrong line has to be fixable.
  • Nothing is learned under a category that isn't one game (memory.gameInfoSkip, pre-filled with 20 common Twitch categories): a guide keyed to "Just Chatting" would blend every game played beneath it.

Model selection

  • Settings → Brain → Custom model ID… accepts any model the key can reach, so a new release isn't gated behind an app update. The cost hint says plainly when there are no published rates instead of implying a price it can't compute.
  • Catalog: added claude-opus-5; kept claude-opus-4-8 so existing configs keep costing correctly.

Cost / correctness

  • Prompt cache moved to the 1h TTL. At quietSeconds (8-minute gaps) a 5-minute entry expires between messages, so every call paid the write premium and never read.
  • Current-generation models think by default and max_tokens caps thinking and visible text together — the budget for a plain chat message was 200. Added headroom, and pinned effort: low for models known to accept the parameter.

Verification

  • Full CI reproduced locally from a clean npm ci --ignore-scripts: syntax check on every JS file, config.example.json validates, 88 tests pass (1 pre-existing Windows-only TTS skip), npm run smoke OK.
  • New tests: test/brain.test.js (172 lines) stubs the SDK client so the outgoing request body is asserted directly — budget floor, effort sent only where supported, no sampling/thinking parameters, 1h TTL on both cache breakpoints. Plus test/speech.test.js, and additions to transcript/models/twitch/storage tests.
  • End-to-end against a live headless server: click-to-fix corrected the next transcript line automatically; mic check went 8% → 0% projected and carried history between runs; the skip list refused Just Chatting case-insensitively while a real game saved.
  • Browser-driven (Chromium): deck click-to-fix — Enter commits, Escape abandons, blur does not silently save a half-typed edit; Settings model picker prefill/validation/round-trip; game-guide picker loads and saves a past game's guide. No JS errors.

Risk & rollout

  • Config additions (all defaulted in config.example.json, so existing configs deep-merge cleanly): speech.*, memory.gameInfoEvery, memory.gameInfoSkip. README config-reference rows added for each.
  • New data files game-notes.json and miccheck.json — both added to DATA_FILES, or "Move data folder…" would silently leave them behind. A test now asserts dataFilePaths() and DATA_FILES stay in step.
  • Cost: should go down. The effort: low cap and the 1h TTL both cut spend; the screen guide adds tokens but sits in the cached prefix.
  • Ethics guarantees untouched: public/overlay.html is not modified in this PR — the 🤖 AI badge and the "simulated viewers" watermark are unchanged. Nothing added here posts anywhere; all new writes are local files.
  • Not yet measured against a live key: the thinking/max_tokens interaction follows documented behaviour, not an observed truncation. The deck's cost pill is the one-minute check. Likewise the silent-transcript warning requires a connected OBS, which wasn't available here.
  • Release-Bump: minor → v3.6.3 would tag v3.7.0.

Checklist

  • npm run smoke passes locally (or CI is green)
  • Follows the design language in DESIGN.md (if UI changed)
  • AI-labeling / "simulated viewers" guarantees intact (if bot output or overlay changed)
  • Docs updated (README / config reference) if behavior or config changed
  • Targets main

🤖 Generated with Claude Code

https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP


Generated by Claude Code

claude added 6 commits July 25, 2026 17:52
LocalVocal stays the transcriber; this adds the text-side layer around it,
plus a way to measure how well it is actually doing on this streamer's mic.

- src/speech.js: hallucination filtering (whisper's "Thank you." / "[Music]" /
  repetition loops on silence, matched whole-line so real sentences survive),
  a per-streamer correction map, word-level alignment, WER, and derivation of
  correction candidates from a known reference reading.
- Mic check (Settings -> Voice): generates a short script from the cast names,
  channel and custom vocabulary, listens through the live LocalVocal path, and
  reports the real word error rate plus one-click fixes for the names it got
  wrong. It is a measurement, not training - no audio leaves the machine and
  no model is modified.
- Guards so a sloppy reading cannot produce sloppy rules: mismatch segments
  past a few words are discarded, both sides must resemble each other, and a
  partly-mangled multi-word name widens to the whole name ("hollow night ->
  Hollow Knight", never "night -> Knight").
- Corrections apply live via HOT_PATHS, so accepting fixes mid-stream takes
  effect on the next line. The cast is also told which proper nouns matter, so
  it recovers near-misses the map has not learned yet.
- Voice replies are suppressed while a mic check runs, and an abandoned check
  self-expires.
- README: config reference rows plus LocalVocal tuning guidance (bigger .en
  model, longer buffer over caption latency, partials off, clean audio).

Release-Bump: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
LocalVocal only ever runs on the mic device, so there is no music in the
audio. The filter still matters — more so, if anything: whisper invents
filler during near-silence, and a mic-only track is mostly near-silence
between sentences. Documentation only; no behavior change. The bracketed
sound-event case stays, since a line with no words in it is never speech.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
The Anthropic model dropdown was a closed list of three, so a model released
after a build shipped - or an older one worth pinning - was unreachable
without an app update. The OpenAI-compatible path already had a free-text
model field; this gives the Claude path the same escape hatch.

- Settings -> Brain -> "Custom model ID..." reveals a text field. The saved id
  stays the source of truth: one that isn't in the catalog shows in that field
  rather than the dropdown silently snapping to a different model.
- The hint says plainly when the cost meter has no rates for the id, instead of
  implying a price it can't compute. Prefix-aware, so a dated snapshot of a
  known model is still recognised as priced.
- "Test brain" uses the typed id, so it can be verified before saving; an empty
  custom id is rejected on save.
- The setup wizard carries a custom id as its own option, so re-running it no
  longer blanks the model (an unmatched value left the select with no
  selection, and saved an empty string).
- Catalog: add claude-opus-5, keep 4.8 so existing configs keep costing
  correctly, and fix the stale Opus hourly estimate (~$0.60 -> ~$0.33; it is
  1.7x Sonnet at current rates, not 3x). Same correction in the README.
- Tests cover isKnownModel and assert no catalog id shadows another by prefix,
  which would misprice a dated id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Five things, all aimed at the cast reacting to what is actually happening.

Prompt cache TTL 5m -> 1h. The two breakpoints in brain.js sit under Sonnet's
1024-token minimum today, so they are inert; more importantly the default
5-minute entry expires between messages at `quietSeconds` (8 minutes), so every
call would pay the write premium and never read. A 1h write costs 2x instead of
1.25x but survives the gaps.

Screen guide (---GAMEINFO---). The cast learns how to READ each game - HUD
layout, what a death or menu screen looks like, where the streamer's overlay
sits - and reuses it on later streams of that game. Written as a tail section
on generations that already happen (no extra API calls) and refined across a
session, so it self-corrects instead of baking in whatever the first frame was.
Lives in the cached context block, keyed by game in game-notes.json. Visible
and editable at Settings -> Stream, including guides from past streams: it is
treated as truth on every message, so a wrong line has to be fixable.

Click-to-fix in the deck. Click a mishear in the 🎙 feed, retype it, press
Enter; the words that changed become corrections and apply to the very next
line. deriveCorrections gains options because the gates that keep the mic check
honest are wrong here - the streamer is not guessing at what they said.

Silent-transcript warning. Voice on, OBS connected, and nothing EVER heard for
ten minutes is a wrong path or a missing filter, and it looks exactly like the
cast ignoring you. Gated on "nothing ever", not "nothing lately", so a streamer
who simply has not talked yet is never nagged.

Mic check history. Past scores persist, so a LocalVocal change is judged by a
number rather than a feeling.

Also: game-notes.json and miccheck.json are added to DATA_FILES, or "Move data
folder..." would silently leave them behind - with a test asserting
dataFilePaths and DATA_FILES stay in step. New test/brain.test.js stubs the
transport to cover tail parsing and prompt assembly.

Release-Bump: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
The guide is keyed by category name, so a stream sitting under "Just Chatting"
while several games get played would accumulate one guide blending all of them
and accurately describing none. Twitch categories are not all games, and the
OBS fallback can report a browser or Discord just as easily.

memory.gameInfoSkip ships with the 20 common non-game Twitch categories.
isGameCategory (twitch.js, where category semantics already live) matches whole
names, case- and space-insensitively — "Music" skips, "Music Racer" does not.

Gated in three places so the behaviour is consistent rather than merely
unwritten: the loop asks canLearnGame() before spending output tokens on a
guide the server would refuse to store; reads return blank; and POST refuses a
hand-written guide for a skipped category, since one could never be read back
and accepting it would just look broken. The cast is still told the category as
usual — only the guide is gated. Settings says which category is being skipped
rather than claiming no game was detected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Current-generation models (Sonnet 5, Opus 5) run adaptive thinking when the
`thinking` parameter is omitted, and max_tokens caps thinking AND visible text
together. The budget for a plain chat message was 200 tokens, so reasoning was
sharing a ceiling meant for a one-line Twitch message — truncating it, and
billing the reasoning at output rates on every generation.

- 600 tokens of headroom on every call. A cap costs nothing unused, works on
  every model including the OpenAI-compatible path, and needs no per-model
  knowledge.
- output_config.effort = low for models known to accept it. effort defaults to
  `high`, which on ~40 messages an hour is real money and real latency, and the
  voice reply has a 6s target. Held constant, not varied per call, so the
  cached prefix stays byte-stable.
- MODELS carries an `effort` capability flag; supportsEffort() answers false
  for unknown ids. A custom model ID therefore gets the headroom but not the
  cap: being wrong that way costs a little money, while sending a parameter an
  unknown model rejects would 400 every generation.

Thinking is deliberately left ON. The same call that writes a throwaway
one-liner may also be merging session notes or the screen guide, and disabling
it on Opus 5 carries a documented risk of reasoning and stray tags leaking into
the visible response — which here goes straight to the on-stream overlay.

Tests stub the SDK client rather than callAnthropic so the outgoing request
body is asserted directly: budget floor, effort sent only where supported, no
sampling or explicit thinking parameters, and the 1h TTL on both breakpoints.

Not yet measured against a live key — this follows the documented behaviour
change, not an observed truncation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
@MyNamesEMurray
MyNamesEMurray enabled auto-merge (squash) July 26, 2026 19:40
@MyNamesEMurray
MyNamesEMurray merged commit 741e2e7 into main Jul 26, 2026
2 checks passed
@MyNamesEMurray
MyNamesEMurray deleted the claude/localvocal-transcription-accuracy-jxxefb branch July 26, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants