Transcription accuracy, per-game screen guide, and model selection - #24
Merged
MyNamesEMurray merged 6 commits intoJul 26, 2026
Merged
Conversation
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
enabled auto-merge (squash)
July 26, 2026 19:40
MyNamesEMurray
deleted the
claude/localvocal-transcription-accuracy-jxxefb
branch
July 26, 2026 19:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.Per-game screen guide
---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.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.memory.gameInfoSkip, pre-filled with 20 common Twitch categories): a guide keyed to "Just Chatting" would blend every game played beneath it.Model selection
claude-opus-5; keptclaude-opus-4-8so existing configs keep costing correctly.Cost / correctness
quietSeconds(8-minute gaps) a 5-minute entry expires between messages, so every call paid the write premium and never read.max_tokenscaps thinking and visible text together — the budget for a plain chat message was 200. Added headroom, and pinnedeffort: lowfor models known to accept the parameter.Verification
npm ci --ignore-scripts: syntax check on every JS file,config.example.jsonvalidates, 88 tests pass (1 pre-existing Windows-only TTS skip),npm run smokeOK.test/brain.test.js(172 lines) stubs the SDK client so the outgoing request body is asserted directly — budget floor,effortsent only where supported, no sampling/thinkingparameters, 1h TTL on both cache breakpoints. Plustest/speech.test.js, and additions to transcript/models/twitch/storage tests.Just Chattingcase-insensitively while a real game saved.Risk & rollout
config.example.json, so existing configs deep-merge cleanly):speech.*,memory.gameInfoEvery,memory.gameInfoSkip. README config-reference rows added for each.game-notes.jsonandmiccheck.json— both added toDATA_FILES, or "Move data folder…" would silently leave them behind. A test now assertsdataFilePaths()andDATA_FILESstay in step.effort: lowcap and the 1h TTL both cut spend; the screen guide adds tokens but sits in the cached prefix.public/overlay.htmlis 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.max_tokensinteraction 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 smokepasses locally (or CI is green)main🤖 Generated with Claude Code
https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Generated by Claude Code