Skip to content

fix(desktop): finish voice turns while the window is minimized or occluded - #110981

Open
lesyuk wants to merge 1 commit into
NousResearch:mainfrom
lesyuk:fix/desktop-voice-vad-hidden-window
Open

lesyuk wants to merge 1 commit into
NousResearch:mainfrom
lesyuk:fix/desktop-voice-vad-hidden-window

Conversation

@lesyuk

@lesyuk lesyuk commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Voice turns started while Hermes Desktop is minimized or occluded — typically by the wake word — never finished: useMicRecorder polled its level/silence meter from requestAnimationFrame, and Chromium doesn't run rAF callbacks for a hidden window. Recording kept going until the window came back to the foreground.

This drives the meter from a ScriptProcessorNode on the recording's own audio graph — the pattern wake-client-capture.ts already relies on to keep streaming wake audio in the background. Levels stay on the old analyser's scale (float RMS × 128 / 42 = byte time-domain RMS / 42), so silenceLevel thresholds keep their meaning. The rAF loop remains as a fallback where createScriptProcessor is unavailable.

Why not timers: #110230 fixes the same root cause with setTimeout(40). Measured on Windows 11 (table below), timers in a hidden window were still throttled to ~1 Hz despite an active mic capture, so every background turn ended 0.8–1.65 s late; the audio callback fired on schedule. This PR is intentionally narrower — only use-mic-recorder, no behaviour change beyond the fix — and adds a regression test. Happy to fold it into #110230 if maintainers prefer one PR.

Related Issue

No separate issue — the root cause is described in #110230. Related: #110230

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts — level/silence evaluation moved into measure(), driven by ScriptProcessorNode.onaudioprocess; the node is disconnected in cleanup(); the analyser + rAF loop is kept as a fallback.
  • apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.test.ts (new) — window hidden, rAF inert, audio fed through a fake graph: end of speech, idle timeout, level scale, no metering after stop.

How to Test

  1. cd apps/desktop && npx vitest run --project ui src/app/chat/composer/hooks/use-mic-recorder.test.ts — against current main, 3 of 4 tests fail (onSilence never called, level frozen at 0); with this change 4/4 pass.
  2. Hermes Desktop with the wake word enabled: minimize the window, say the wake phrase and a question, stop talking — the reply is spoken without restoring the window. On main nothing happens until the window is restored.

Checklist

  • Read the Contributing Guide · [x] Conventional Commits · [x] Searched existing PRs (desktop(voice): keep wake-word conversations alive while occluded, auto-end after silence, Spanish stop phrases #110230, see above) · [x] Only related changes · [x] Added tests
  • pytest tests/ -q: this PR changes no Python. Ran the full suite anyway with scripts/run_tests.sh on Windows 11 with the CI extras: 47,629 passed, 839 failed, 5 files not collected, all in areas this PR does not touch (Windows-specific MCP OAuth, terminal/process and file-read tests). CI runs this suite on Linux.
  • Desktop checks on Windows 11, Node 24: npm run check:lint (0 errors); vitest --project ui 7,646 passed, 6 failed, and the same 6 fail with this change reverted (locale-dependent formatting, vitest suite fails under non-en-US system locale (bare .toLocaleString() / Intl.NumberFormat(undefined, ...)) #98849, and voice-prefs spying on jsdom's Storage instance); vitest --project electron 2,155 passed, 39 failed, all POSIX/macOS-specific tests that don't import the changed hook. The composer and voice suites, including the new test, pass.
  • Tested on Windows 11 (10.0.26200), Hermes v0.21.2 desktop rebuilt with this patch
  • Docs / cli-config.yaml.example / CONTRIBUTING / tool schemas — N/A · [x] Cross-platform: ScriptProcessorNode is already used by wake capture on all desktop platforms

Screenshots / Logs

Standalone Electron repro (Electron 40.10.2 / Chrome 144, default backgroundThrottling), same method as above:

window rAF (main) setTimeout(40) onaudioprocess (this PR)
occluded never fired +1.59 s +0.05 s
minimized 15 s never fired (no ticks for 15.3 s) +1.65 s +0.05 s
minimized 6 min never fired +0.79 s (ticks ~1 s apart) +0.04 s

Patched desktop, window minimized throughout (Windows 11):

18:11:26.851 wake.detected
18:11:37.485 Transcribed … 4.3s audio   (first turn also loads Whisper)
18:11:56.913 TTS audio saved            (cold Ollama model load)
18:12:08.665 Transcribed … 7.7s audio   (follow-up, still minimized)
18:12:12.681 TTS audio saved

Known, not changed here: apps/desktop/src/lib/voice-barge-in.ts also drives barge-in detection from rAF; I haven't reproduced that path.

…luded

useMicRecorder polled its level/silence meter from requestAnimationFrame.
Chromium never runs rAF callbacks for a minimized or occluded window, so a
voice turn started in the background (e.g. by the wake word) never detected
end of speech and never submitted until the window was brought back.

Drive the meter from a ScriptProcessorNode on the recording's own audio
graph instead - the same pattern wake-client-capture already relies on to
keep streaming wake audio in the background. Levels stay on the old
analyser's byte time-domain scale, so silenceLevel thresholds keep their
meaning. The rAF loop remains as a fallback where ScriptProcessorNode is
unavailable.
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) tool/tts Text-to-speech and transcription labels Sep 14, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Competing with #110230 for the same root cause (rAF frozen while the window is occluded). This PR uses a ScriptProcessorNode on the recording audio graph; #110230 uses setTimeout polling plus broader hands-free changes. Flagging for maintainers to pick one or fold this into #110230 as the author offered.

@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Comment assumes head f01e6b7706780e2d4bcd628074d7f3ea809237d4; void if moved.

KEEP — Desktop occluded mic metering (use-mic-recorder)

CHECK

One-click close / ordering

Landing-evidence

  • When merged, please reply with the merge commit SHA on main (not “completed”).

@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Comment assumes head f01e6b7706780e2d4bcd628074d7f3ea809237d4; void if moved.

KEEP — Desktop occluded mic metering (use-mic-recorder)

CHECK

One-click close / ordering

Landing-evidence

  • When merged, please reply with the merge commit SHA on main (not “completed”).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants