--- license: apache-2.0 library_name: openwakeword tags: - wake-word-detection - keyword-spotting - audio-classification - onnx - openwakeword language: - en pipeline_tag: audio-classification --- # hey-claude — an openWakeWord wake word Wakes on **"hey Claude"**. Runs on a CPU, about 200 KB, no GPU and no network. Trained entirely on synthetic speech on one MacBook Air (M2) — no Colab, no recordings of anyone's voice, roughly an hour end to end. ```python from openwakeword.model import Model model = Model(wakeword_models=["hey_claude.onnx"], inference_framework="onnx") # Feed 1280-sample (80 ms) chunks of 16 kHz mono int16 audio. for chunk in microphone_chunks(): if model.predict(chunk)["hey_claude"] > 0.5: print("woke up") ``` ## How well does it work? Scored against macOS `say` voices, which are **not** the voices it trained on, five voices per phrase (Samantha, Daniel, Karen, Moira, Fred). Each clip is padded with silence and run through a freshly constructed `Model`; the number is the peak score anywhere in the clip. **An empty room, 180 s on a real microphone: worst score 0.0011, zero false wakes.** This is the test that matters most for an always-listening device and the easiest to skip — see the warning further down. | Phrase | Peak | | |---|---|---| | **"hey claude"** | **0.995** on 2 of 5 voices | wakes | | "hey cloud" | 0.004 | quiet | | "hey claire" | 0.000 | quiet | | "okay cloud" | 0.001 | quiet | | "grey cloud" | 0.000 | quiet | | "hey jarvis" | 0.002 | quiet | | "what is the weather today" | 0.000 | quiet | | "can you play some music please" | 0.000 | quiet | | "I think the cloud is going to rain" | 0.000 | quiet | | "let us go outside and play" | 0.001 | quiet | | "he clawed at the door" | 0.011 | quiet (a homophone — got lucky) | | "hey clyde" | 0.991 | **false wake** | On held-back clips it never trained on: **90%** of positives wake it, **3%** of adversarial negatives falsely do. It has been used successfully with a real human voice, but every number in this table comes from synthesized speech. Waking reliably and staying quiet pull against each other here: a variant tuned for higher recall (98% held-out, 4 of 5 voices) scored 0.998 on "hey clyde", which no threshold can separate from the real phrase. This one trades some recall for a margin you can actually tune. ## Limitations — please read before shipping this **It wakes on "hey Clyde"** at 0.991 — as hard as on the real phrase, so no threshold separates them. `K L AY D` against Claude's `K L AO D` is a single vowel. If someone in the room is called Clyde, this model is not for you. **It is on the strict side.** Of five macOS `say` voices, two wake it at ~0.99 and three score low. Say the phrase clearly, and lower the threshold to 0.3 before assuming it's broken. This is a deliberate trade: a more sensitive variant reached 4 of 5 voices but woke on an empty room. **It wakes on "clawed"** (0.676). "Clawed" is `K L AO D` in CMUdict — *the same phonemes as "Claude"*. This is not fixable by training. Any wake word has this problem with its own homophones; "Claude" happens to have a common one. **Silence was the hardest problem here, and it is worth understanding why.** An earlier version of this model scored up to 0.99 on an *empty room* and fired about 4,000 times an hour. Every clip it had trained on contained audible sound, so near-silence fell outside its experience entirely and its output there was arbitrary. Padding test clips with `np.zeros` did not catch it — digital zero is not what a microphone produces. A quiet room is 1-50 RMS of preamp hiss, mains hum and fan rumble, punctuated by transients. Two things fixed it: quiet-room clips among the negatives (including knocks, clicks and creaks), and training on 27.8% of openWakeWord's negative-features dataset rather than 4.7%. Synthetic noise alone only got from 4,000 to 90 false wakes an hour; real-world audio took it to zero. **Still uncharacterised: hours of television or music.** 180 s of quiet is not the same as an evening of background noise. **English only**, and the positive voices skew US/UK. ## Training 100% synthetic. [Piper](https://github.com/rhasspy/piper) says the phrase thousands of times across many speakers; those are the positives. Phonetically similar phrases are the adversarial negatives, mixed with openWakeWord's pre-computed negative features, real MIT room impulse responses, and background noise. | | | |---|---| | Base | openWakeWord's frozen audio embedding + a 32-unit DNN head | | Positives | 2,000 train / 400 test, 11 Piper voices (~1,030 speakers, 4 corpora) | | Negatives | 7,600 train / 1,520 test — 111 phrases plus 1,600 quiet-room clips | | Negative features | 1,564,709 windows (a 27.8% slice of ACAV100M-2000h) | | Steps | 50,000 | | Hardware | MacBook Air M2, CPU only, ~8 minutes | Positive clips are filtered by Whisper: any clip that doesn't clearly say the phrase is discarded, because a slurred positive teaches the wrong sound. ### Three things that cost the most time Recorded here because they're easy to repeat and none are obvious. **Don't put a homophone in the negatives.** Listing "hey clawed" as something to reject asks the model to distinguish identical audio. It can't, so it degrades the wake word trying — held-out recall fell from 100% to 40%, and no amount of loss weighting recovered it. **Not every TTS voice can say your phrase.** Measured pass rates for "hey claude": `en_US-libritts_r` 92%, but `en_US-l2arctic` **0%**, `en_GB-aru` **0%**, `en_US-arctic` 8%. Check before spending an hour generating. **Generate both classes from the same voices.** Positives from eleven voices and negatives from three lets the model separate them by voice rather than by phrase — it scores beautifully on your own data and fails on real people. **Put silence in the negatives, and test against a real microphone.** This cost more time than everything else combined. See the limitations above. **Test for minutes, not seconds.** A 30 s silence test passed a model that a 120 s test then failed at 90 false wakes an hour. These are rare events; a short sample cannot see them. ### Evaluating it openWakeWord is a *streaming* detector and `reset()` does not clear its rolling audio buffer. Scoring several clips through one `Model` object lets each contaminate the next and quietly corrupts every number. Build a fresh `Model` per clip, pad with ~1 s of silence, and sanity-check the harness against the stock `hey_jarvis` model first — it should score ~0.999 on "hey jarvis". If it doesn't, the harness is wrong, not the model. ## Reproducing Full pipeline, including the Apple Silicon workarounds for openWakeWord's Linux/CUDA trainer: **https://github.com/greg1232/hey-claude** ```bash python train/fetch_data.py --budget-gb 5 python train/generate_clips.py --phrase "hey claude" --out train/hey_claude \ --count 2000 --count-test 400 \ --count-negative 6000 --count-negative-test 1200 \ --adversarial 60 --verify --workers 6 python train/train_local.py --training_config train/hey_claude.yml \ --augment_clips --train_model python train/test_silence.py train/hey_claude.onnx --seconds 180 ``` The same scripts train any phrase — change `--phrase` and `target_phrase`. ## Credits Built on [openWakeWord](https://github.com/dscripka/openWakeWord) by David Scripka (Apache-2.0), whose trainer, embedding model, and negative-feature dataset this depends on entirely. Speech from [Piper](https://github.com/rhasspy/piper); voices derive from LibriTTS-R, VCTK, and others under their own licenses. Room impulse responses from the MIT Acoustical Reverberation Scene Statistics Survey. Not affiliated with or endorsed by Anthropic.