Skip to content

Reply only when addressed, plus a beta release line - #25

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

Reply only when addressed, plus a beta release line#25
MyNamesEMurray merged 3 commits into
mainfrom
claude/localvocal-transcription-accuracy-jxxefb

Conversation

@MyNamesEMurray

Copy link
Copy Markdown
Owner

Summary

Cuts the dominant cost driver — the cast was treating almost everything the streamer said as a question aimed at it — and adds a beta release line so changes like this can be tested on a real stream without merging or compiling anything.

Changes

Voice replies only when the cast is actually addressed

A 2.5h stream on v3.7.0 cost $4 on Opus, ~5x the README's estimate. The per-message price wasn't the story; the message count was. onSpeech treated any speech within 120s of a cast message as an answer to it — but a streamer talks almost continuously, so an extra generation fired after nearly every cast message.

  • A voice reply now needs the speech to be addressed: a ghost named out loud, or a prompt answer to a question one just asked.
  • Nothing is lost when it isn't — the transcript window is read by the next scheduled generation either way, so undirected talk still reaches the cast, it just no longer buys its own API call.
  • The question rule is on a short fuse (voiceAnswerWindowSeconds, 30s): the "curious" archetype ends a lot of messages with "?", so a generous window would match nearly everything and undo the point.
  • Name matching is whole-word, so "wispy" doesn't summon Wisp. This signal is only reliable because the correction map already keeps ghost names intact through transcription.
  • cadence.voiceReplyRequiresAddress: false restores the old behaviour. voiceReplyDelaySeconds / voiceReplyWindowSeconds were hardcoded and are now config. All hot-appliable.

Cost visibility — hover the cost pill for the share of input served from cache and the average output tokens per message (a one-line message is ~60; far above that means thinking is doing the spending). cache_read_input_tokens was already collected and then merged into a single total, which hid exactly the number needed to diagnose the above.

Beta release line — Actions → Beta Release → pick a branch. Tags vX.Y.Z-beta.N off the newest stable tag, builds the Windows installer, publishes a GitHub prerelease. Stable installs can't see it: the -beta.N suffix makes electron-builder write beta.yml, and a stable install only polls latest.yml.

app.updateChannel (auto | stable | beta) — because a beta install does not find its own way back. electron-updater follows a channel file, not a version comparison, so a stable release (which publishes only latest.yml) is never offered to a beta install. stable is the deliberate way home, and the only case that sets allowDowngrade.

Verification

  • Full CI reproduced locally from a clean npm ci --ignore-scripts: syntax check, config validation, 95 tests pass (1 pre-existing Windows-only skip), npm run smoke OK.
  • 8 new loop tests pin the voice gating: named ghost replies, undirected narration doesn't, a prompt answer counts but a late one doesn't, a statement isn't a question, "wispy" doesn't match "Wisp", the opt-out still works, and being addressed can't bypass the rate floor or staleness window.
  • Beta version maths tested against a scratch repo seeded with the real tag history: correct base from the newest stable tag, beta.10 follows beta.9 (numeric sort), and a beta tag never becomes the base for the next one.
  • Update-channel decision matrix checked across every setting × build combination — allowDowngrade is true in exactly one (explicitly chose stable while on a prerelease).
  • All three workflow files parse as YAML.

Risk & rollout

  • Two pre-existing release paths had to be fixed first, or the first beta tag would have broken stable releases. auto-release.yml picked the highest v* tag to bump from; a beta tag sorts above the release it precedes, and splitting 3.8.0-beta.1 on . puts 0-beta.1 into $patch. Verified to fail with invalid arithmetic operator, which under Actions' bash -e fails the job — every stable release blocked until someone deleted the tag. Now filtered to ^v[0-9]+\.[0-9]+\.[0-9]+$. Separately, release.yml's v* tag trigger also matches beta tags, and publishing one there would emit latest.yml — pushing a test build to every install. Its job is now guarded.
  • Behaviour change worth watching: the cast will speak noticeably less often in response to talking. That's the intent, but if it now feels unresponsive, voiceReplyRequiresAddress: false restores the old behaviour immediately (no restart).
  • Config additions, all defaulted: cadence.voiceReply*, cadence.voiceAnswerWindowSeconds, app.updateChannel. README rows added.
  • Not verifiable here: the updater channel logic needs a packaged Windows build to exercise electron-updater for real — which is exactly what the beta line now enables. The first beta build is the test of the beta machinery.
  • Ethics guarantees untouched: public/overlay.html is not modified; the 🤖 AI badge and "simulated viewers" watermark are unchanged.
  • Release-Bump: minor → v3.7.0 would tag v3.8.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 3 commits July 26, 2026 23:29
A 2.5h stream on v3.7.0 cost $4 on Opus — about 5x the README's estimate. The
per-message price was not the story; the message *count* was. onSpeech treated
any speech within 120s of a cast message as an answer to it, but a streamer
talks almost continuously — narrating a fight, thinking out loud, talking to a
co-op partner. That fired an extra generation after nearly every cast message,
roughly doubling spend, and made the cast read as interrupting rather than
listening.

A voice reply now needs the speech to be addressed: a ghost named out loud, or
a prompt answer to a question one just asked. Nothing is lost when it isn't —
the transcript window is read by the next scheduled generation either way, so
undirected talk still reaches the cast, it just no longer buys its own API
call.

The question rule is on a deliberately short fuse (voiceAnswerWindowSeconds,
30s): the "curious" archetype ends a lot of messages with "?", so a generous
window would match nearly everything and undo the point. Name matching is
whole-word, so "wispy" doesn't summon Wisp — and the correction map already
works to keep ghost names intact through transcription, which is what makes
this signal reliable.

cadence.voiceReplyRequiresAddress (default true) restores the old behaviour
when false. voiceReplyDelaySeconds and voiceReplyWindowSeconds were hardcoded;
both are now config. All hot-appliable.

Also surfaces the two numbers that would have diagnosed this in minutes: hover
the cost pill for the share of input served from cache and the average output
tokens per message (a one-line chat message is ~60; far above that is thinking
doing the spending). cache_read_input_tokens was already collected and merged
into a single total, which hid it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Test builds can now be produced from any branch without merging and without
anyone compiling: Actions -> Beta Release -> pick a branch. It tags
vX.Y.Z-beta.N off the newest stable tag, builds the Windows installer, and
publishes a GitHub prerelease.

Isolation from the stable line is structural rather than procedural: the
-beta.N suffix makes electron-builder write the update feed as beta.yml, and a
stable install only ever polls latest.yml, so it cannot be offered a test
build.

Two pre-existing paths had to be fixed first, or the first beta tag would have
broken stable releases:

- auto-release.yml picked the highest v* tag to bump from. A beta tag sorts
  ABOVE the release it precedes, and splitting 3.8.0-beta.1 on "." puts
  "0-beta.1" into $patch — verified to fail with "invalid arithmetic operator",
  which under Actions' bash -e fails the job. Every stable release would have
  been blocked until someone deleted the tag. Now filtered to
  ^v[0-9]+\.[0-9]+\.[0-9]+$.
- release.yml triggers on tags v*, which matches beta tags too. Publishing one
  there would produce a non-prerelease carrying latest.yml — pushing a test
  build to every stable install. Its job is now guarded against prerelease
  tags.

Beta numbering sorts numerically, so beta.10 follows beta.9 rather than
beta.1. Verified against a scratch repo seeded with the real tag history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Correcting something I got wrong when adding the beta line: I described a
stable release as superseding its own betas by semver, carrying testers home.
It does not. electron-updater follows a channel *file*, not a version
comparison — a stable release publishes only latest.yml, so beta.yml goes on
advertising the last beta and a tester is never offered the real release.
Installing a beta was therefore a one-way door.

app.updateChannel (Settings -> App, advanced):

- auto (default) — follow whatever build is running. This is exactly the
  previous behaviour: stable installs read latest.yml, beta builds read
  beta.yml. Nothing changes for anyone who never touches a beta.
- stable — the way back. Forces latest.yml, and is the only case that sets
  allowDowngrade, because returning from 3.8.0-beta.5 to 3.7.0 is a version
  decrease and the escape hatch would otherwise silently do nothing.
- beta — opt in without hunting for an installer.

allowDowngrade is deliberately scoped to "explicitly chose stable while running
a prerelease" so a normal install can never be walked backwards. An
unrecognised or missing value falls back to auto rather than erroring.

Verified the decision matrix across every setting x build combination:
allowDowngrade is true in exactly one of them.

Not verifiable here — this needs a packaged Windows build to exercise
electron-updater for real, which is precisely what the beta line now makes
possible. The first beta is the test of the beta machinery.

Release-Bump: minor

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 23:51
@MyNamesEMurray
MyNamesEMurray merged commit 11b761e into main Jul 26, 2026
2 checks passed
@MyNamesEMurray
MyNamesEMurray deleted the claude/localvocal-transcription-accuracy-jxxefb branch July 26, 2026 23:52
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