Back to DevLog

Building a Wake Word Detector with Vosk Keyphrase Spotting

3 min read

I built hands-free wake-word listening for Foreman across four rounds of live microphone testing. The detector uses Vosk keyphrase spotting, and the wake phrase is name-derived: "hey" plus the assistant name lowercased.

Vosk Over openWakeWord for Licensing

I started with openWakeWord, then pivoted mid-build to Vosk. openWakeWord's code is Apache 2.0, but its stock models are CC BY-NC-SA 4.0, which is non-commercial. Vosk's toolkit and vosk-model-small-en-us-0.15 are both Apache 2.0, fully permissive.

The Detector is Swappable

I isolated the detector behind a tiny interface: phrase, reset(), and accept(pcm)->bool. The swap point is _make_detector(). A dedicated acoustic wake model can replace Vosk without touching the mic capture pipeline.

Round 2: Passive Gate and False Wakes

The first live test showed captures with no apparent wake word. There's no parallel hotkey path in wake mode, so these were false wakes from the detector. I hardened the partial match to fire only on the just-spoken tail, not accumulated partials. I added a frames-discarded counter to prove passive audio is dropped, and a --debug flag that prints every partial and final.

Round 3: Recognizer Degradation and Free-Form Recognition

Live debug output showed the recognizer going deaf after about 1600 frames of partials, then 1700 frames of silence despite speech. Vosk degrades on a long unbroken stream. I now rebuild the recognizer from the loaded model after every endpoint and every 30 seconds.

The small model cannot produce the name "emma". Live partials were "hey mom", "hey member", "a m m", "him", "hey". A literal-phrase grammar never fired. I switched to free-form recognition and added _spotted() shape-matching of the real mishearings. This is a privacy-posture shift: passive audio is now recognized on-device, still discarded and never stored or sent.

Round 4: Spoken Acknowledgment and Native-Rate Capture

I added a spoken acknowledgment so the user hears an answer when the wake word is detected. The acknowledgment is a short random phrase like "Yes?" or "I'm here." spoken via the TTS pipeline. I confirmed the user cannot self-wake during the acknowledgment because the microphone is closed in that window.

I fixed an audio-quality bug. Capture was resampling each 100ms block from 44100 Hz to 16000 Hz and concatenating them, creating per-block artifacts. Now it keeps native-rate blocks and resamples the whole command buffer once, the same way hotkey mode does. I added 200ms of settle time before capture starts.

I added split-phrase assist. A bare "hey" is remembered, and any m-word within 1.5 seconds completes the wake. Every wake appends its partials to memory/wake_tuning.log.

Offline Verification

I built scripts/wake_gate_test.py with 16 hermetic and real checks as the committed offline proof. The test covers the discard counter, mic-closed-in-ack-window, native whole-buffer capture, settle time, the _spotted() string logic, and real detector behavior including silence, noise, rebuild, reset, split assist, and tuning log.

Share this post