fix(voice): stop wake re-fire loop and empty-transcript error spam

Two bugs surfaced by the desktop wake conversation:

1. Runaway loop: wake -> voice -> resume -> wake fired again within
   ~200ms. openWakeWord keeps its rolling feature buffer across
   pause/resume, so on resume it immediately re-scored the "hey jarvis"
   captured before the pause and re-fired, reopening a session and
   restarting voice in a tight cycle. Reset the engine buffer on every
   detector (re)start so resume begins from clean audio.

2. Empty-transcript toast: a silent re-listen returns
   success:false / "… STT returned empty transcript", which the desktop
   transcribe endpoint turned into a 400 -> thrown error -> "Voice
   transcription failed" notification on every silent gap. Treat an empty
   transcript as no-speech: return {ok, transcript: ""} so the voice loop
   quietly re-listens. Real failures still 4xx/5xx.
This commit is contained in:
Brooklyn Nicholson
2026-06-27 11:31:37 -05:00
committed by Teknium
parent c597b4c47b
commit edb12bf423
3 changed files with 46 additions and 5 deletions
+18 -1
View File
@@ -140,6 +140,10 @@ class _Engine:
def process(self, frame) -> bool: # frame: 1-D int16 ndarray
raise NotImplementedError
def reset(self) -> None:
"""Clear any internal audio/feature buffer (called on every (re)start)."""
pass
def close(self) -> None:
pass
@@ -189,12 +193,17 @@ class _OpenWakeWordEngine(_Engine):
scores = self._model.predict(frame)
return any(score >= self._threshold for score in scores.values())
def close(self) -> None:
def reset(self) -> None:
# Clears openWakeWord's rolling feature/prediction buffer so stale audio
# captured before a pause can't re-fire the moment we resume.
try:
self._model.reset()
except Exception:
pass
def close(self) -> None:
self.reset()
class _PorcupineEngine(_Engine):
"""Picovoice Porcupine — premium, on-device, needs an access key."""
@@ -356,6 +365,14 @@ class WakeWordDetector:
logger.error("wake word: failed to open microphone: %s", e)
return
# Drop any buffered audio/feature state so a resume right after a voice
# turn can't immediately re-fire on audio captured before the pause (the
# wake → voice → resume → wake runaway loop).
try:
self.engine.reset()
except Exception:
pass
logger.info("wake word: listening (frame=%d, rate=%d)", frame_length, SAMPLE_RATE)
try:
while not self._stop.is_set():