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.
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 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
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 by David Scripka (Apache-2.0), whose trainer, embedding model, and negative-feature dataset this depends on entirely. Speech from 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.