A custom "Hey Donut" wake word for Home Assistant voice assistants. Runs entirely on-device with microWakeWord — no cloud, no account, nothing leaves your house.

Two files, 62 KB. Drop them in and your assistant answers to Donut.

model/hey_donut.tflite    62 KB   int8 quantized streaming model
model/hey_donut.json      356 B   microWakeWord v2 manifest

The full training pipeline is in training/ if you'd rather teach it your name instead.


v2 (2026-09-16): trained on how the phrase is actually said

model/hey_donut_v2.* is the model to use. Said at speed, the T in "Donut" is a glottal stop ("hey donuh"), and v1 had only ever heard Piper's crisp "hey donut." - worse, Piper's T-less renderings transcribe as "Hey Donna", which v1 was trained to reject. v2 adds 11,000 T-less and fast-delivery positives and drops "hey donna" from the near-misses. On 1,800 held-out clean clips, detection rate at a cutoff of 0.85:

Phrase (200 clips each) v1 v2
"hey donut", crisp T 51.0% 95.5%
"hey doe nuh" 60.0% 96.5%
"hey dohnuh" 52.0% 92.5%
"hey donna" (near-miss) 0.0% 27.0%
"hey don't", bare "donut", "do not", "hey there" 0% 0-1%

Framework table for v2 on its augmented test set: cutoff 0.95 = 9.0% missed / 0 false accepts per hour; 0.77 = 4.9% / 0.19; 0.47 = 2.9% / 0.38. The manifest ships 0.95; 0.85 is a fair working point. Full numbers in training/eval_v2_results.txt; how it was made in training/README.md.

How well does v1 work?

Measured on the held-out test set at the shipped cutoff of 0.72:

Metric Value
False rejection rate 4.1% (about 96 of 100 attempts wake it)
False accepts per hour 0.000
AUC 0.032

Lower cutoffs trade accuracy for twitchiness, if you want them:

Cutoff Missed wakes False accepts/hour
0.72 (default) 4.1% 0.000
0.37 2.0% 0.19
0.25 1.7% 0.56
0.10 1.2% 1.9

It was trained against deliberate near-misses so ordinary conversation doesn't set it off: donut, donuts, doughnut, do not, hey donna, hey dolly, the donut shop, a donut please, hey there.


Install

ESPHome voice satellites

Copy both files somewhere your ESPHome build can reach, and reference the manifest:

micro_wake_word:
  models:
    - model: hey_donut.json

Requires ESPHome 2024.7.0 or newer (tensor_arena_size is 37000).

ThirdReality / Buildroot-based speakers

These store wake words as ordinary files on a writable overlay — no firmware flash needed:

scp model/hey_donut.* root@<speaker-ip>:/usr/share/thirdreality/wakewords/microwakeword/

Restart the voice service, then pick Hey Donut in Home Assistant under the satellite's wake word selector.

Then tune it

Home Assistant exposes a sensitivity control per satellite:

number.<your_satellite>_wake_word_1_sensitivity

This overrides the manifest's probability_cutoff and it is the cutoff: higher is stricter, lower wakes more easily. (Verified in the source of the Open Home Foundation's linux-voice-assistant, which the ThirdReality speakers run: the number is stored as wake_word_1_threshold and passed to microWakeWord as probability_cutoff.) Start at the manifest's value and adjust only after reading the next section, which will save you an evening.


Before you tune anything, move the microphone

The single biggest factor in whether a wake word works is where the microphone physically sits — not the threshold, not the gain, not the noise suppression setting.

A satellite that missed roughly 70% of attempts was fixed completely by moving it out from behind a monitor and away from a loudspeaker. Detection went to 10/10 at ten feet with no configuration change at all. Six rounds of threshold tuning before that had produced incoherent results — worse in both directions — which is itself the diagnostic:

If turning a dial up and turning it down both make things worse, the environment is moving, not the dial. Stop tuning and look at the room.

Things that hurt: hard surfaces right behind the mic, a speaker cone anywhere near it, being inside a shelf or cabinet, sitting behind a screen.

Also worth knowing: a satellite cannot hear a wake word while it is already listening, processing or responding — roughly a nine-second window. Attempts inside that window aren't detection failures. Count wake-ups from the satellite's own state history rather than by feel, and try ten in a row so you get a rate instead of an impression.


Train your own wake word

The pipeline in training/ is generic — change the phrase and run it. It generates its own training audio, so you never record yourself saying anything.

Positives come from piper-sample-generator using a multi-speaker LibriTTS-R checkpoint: 12,000 samples across 500 speakers at five speaking rates. Negatives are precomputed spectrogram features from the microWakeWord dataset. Total training data is around 44 GB, almost all of it downloaded or generated — which is why it isn't in this repo.

cd training
docker build -t wakeword-train .
docker run --rm -v /your/workspace:/w wakeword-train bash -c "
  /w/gen.sh &&          # synthesize positives + adversarial near-misses
  /w/getneg.sh &&       # fetch negative feature sets
  python /w/get_aug.py &&      # room impulse responses + background noise
  python /w/build_features.py && # spectrograms
  python /w/make_config.py &&
  /w/train.sh"
File Does
gen.sh synthesizes positives and the adversarial near-miss set
getneg.sh downloads precomputed negative features
get_aug.py MIT room impulse responses + background clips for augmentation
build_features.py turns audio into spectrograms with augmentation
make_config.py writes training_parameters.yaml
train.sh trains and exports the quantized streaming .tflite

Picking a phrase that actually works

Three syllables with a hard stop consonant. Bare "Donut" was tried and rejected on acoustic grounds — two syllables with a soft onset, and it false-triggered on "do not" and "donuts" constantly. Adding "Hey" gave it the length and the attack it needed.

Whatever you choose, generate an adversarial set of near-misses for it (see gen.sh) and weight them heavily. That list is most of why this model doesn't wake up during dinner.

It trains on CPU

This model was trained on spare CPU cores under nice, not a GPU — roughly 10,000 steps. TensorFlow's Python version requirements are the reason the pipeline is containerized.


Security note for anyone opening up a voice speaker

Several consumer voice satellites ship with SSH, Telnet and ADB all reachable on the LAN and a vendor-documented default root password. ADB over TCP is the worst of the three: it grants root with no authentication at all, so there is no password to get wrong. These are devices with always-on microphones.

If you're going to put a custom wake word on one, change the default password and close the services you aren't using while you're in there.