End recording when the user stops, not the music

Silero VAD answers "is this voice-like?", which music, TV and podcasts
satisfy just as well as the user does. With Spotify playing through the
device's own speakers the VAD never went quiet, so record_utterance never
saw the end of the command and ran to the 20 s cap on master and slave
alike -- and music alone was enough to defeat the "no speech during the
grace window" early return.

Wrap the VAD in a SpeechEndpointer that tracks the background loudness of
the recent window and only reports speech while a frame is clearly louder
than it. Both wake-word loops feed their idle audio through observe(), so
the gate is already calibrated when a turn starts, and only non-voice
frames update the background so a long sentence cannot raise the gate onto
itself. Onset/release hysteresis keeps trailing quiet syllables.

The master owns the knob (wake_word.voice_gate_ratio, 0 = off) and pushes
it to satellites in the wake-word block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
JavisandCopilot committed 2026-10-10 13:04:13 +02:00
1 parent 74bc1be2d6
commit 1fa75e58fc
16 files changed
+597 -22

No files matched your search

+5
View File
@@ -27,6 +27,11 @@ screen (GUI mode) and played back through the device's speakers.
4. **Turns** — After the wake word, the utterance is recorded locally
(Silero VAD or RMS end-pointing), uploaded as WAV to `POST /api/v1/turn`,
and the master's reply text + synthesized speech are returned and played.
Recording ends when *your voice* stops: Silero rates music and TV as
speech, so a loudness gate (`wake_word.voice_gate_ratio`, pushed by the
master) only counts audio that is clearly louder than the background the
wake-word loop measured. Without it, music from this device's own speakers
kept the recorder running until the hard length cap.
When the master marks a reply as expecting a follow-up, the slave starts
listening immediately after playback without requiring another wake word.
If the master's turn fails it returns its spoken error phrase instead, so a