wakeword: the agent's name
A voice agent listens all the time, but it should act only after hearing its name, such as "Jarvis". This recipe trains the small detector that listens for that name; the command that follows it ("wake up", "search the web") is heard by speech recognition afterwards. It uses openWakeWord's frozen speech features and trains only a small scoring head on top of them, so a few hundred synthetic recordings are enough. The exported model loads in openWakeWord's runtime.
Validation plays the agent's first moment back to the detector: the user says the name and then something else, with noise underneath, and the detector must fire exactly once, on the name and not on what follows.
Rows
Two sets: positive recordings say the name and nothing else; negative recordings never say it (near misses such
as "Travis", the commands said alone, ordinary speech). Rows are audio and an optional text.
Config
# A detector for the agent's name, "Jarvis": it fires as the name ends, and what follows ("wake up", "search the web
# for ...") is the command, heard by speech recognition. Trained on openWakeWord's frozen speech features; the
# checkpoint directory holds the model, the two feature models and wakeword.json, and loads in openWakeWord's runtime:
# openwakeword.Model(wakeword_models=["<checkpoint>/jarvis.onnx"], inference_framework="onnx",
# melspec_model_path="<checkpoint>/melspectrogram.onnx", embedding_model_path="<checkpoint>/embedding_model.onnx")
# Replace <hash> with the data experiments' (`llmclick validate configs/data/wake_phrase.yaml`, wake_negatives, wake_nearmiss).
pipeline:
recipe: wakeword
name: jarvis-wake
seed: 20260920
output_dir: ./output
phrase: Jarvis # the wake word; data/wake_phrase.yaml must say it, data/wake_negatives.yaml must not
data:
positive:
- {name: local_jsonl, path: output/wake-phrase-<hash>/select/rows.jsonl}
# - {name: delimited, path: recordings/wake/metadata.csv, audio_dir: recordings/wake/wavs} # your own voice
negative:
- {name: local_jsonl, path: output/wake-negatives-<hash>/select/rows.jsonl}
- {name: local_jsonl, path: output/wake-nearmiss-<hash>/select/rows.jsonl} # data/wake_nearmiss.yaml
background: [] # directories of noise WAVs (room tone, music, a fan); empty: Gaussian noise
validation: 0.2
# followed: "Jarvis" with a command after it, as negatives, so the detector fires as the name ends and not again
augment: {rounds: 4, followed: 2, noise: 200, snr: [5, 25], gain: [-12, 0], tail: 0.3}
# negative_weight with the cosine schedule: over six seeds false accepts stayed at or under 4.7% (constant rate and equal
# weights: 3.7% to 8.8%, depending on the seed); recall stayed 100%
training: {steps: 3000, batch_size: 256, learning_rate: 1.0e-3, layer_dim: 32, blocks: 1, negative_weight: 100, schedule: cosine}
validation:
threshold: 0.5
snr: 20
min: {recall: 0.9, scenario: 0.9} # scenario: "Jarvis", then another utterance; it must fire once, on the name
max: {false_accepts: 0.05}
Use
uv run llmclick run configs/data/wake_phrase.yaml # the name, read by 300 speakers
uv run llmclick run configs/data/wake_negatives.yaml # near misses, commands, ordinary lines
uv run llmclick run configs/data/wake_nearmiss.yaml # more near misses, more speakers
uv run llmclick run configs/wakeword/jarvis.yaml # replace each <hash> first
Measure a detector on voices and hours of audio it never heard with
evaluation_wakeword.
How it works
wakeword trains the detector that wakes a voice agent: a small model that listens to the microphone all the time
and fires when it hears one phrase. It follows openWakeWord (Apache-2.0):
audio becomes a melspectrogram and then a 96-number speech embedding every 80 ms through two frozen ONNX models
(pinned by hash, downloaded once), and a small dnn head scores the last 16 embeddings (2 s). Only the head is
trained, so a few hundred synthetic recordings are enough, and the exported file loads in openWakeWord's runtime.
phrase: Jarvis # the wake word: the agent's name; what follows it is the command
data:
positive: [{name: local_jsonl, path: output/wake-phrase-<hash>/select/rows.jsonl}] # data/wake_phrase.yaml
negative: [{name: local_jsonl, path: output/wake-negatives-<hash>/select/rows.jsonl}, # data/wake_negatives.yaml
{name: local_jsonl, path: output/wake-nearmiss-<hash>/select/rows.jsonl}] # data/wake_nearmiss.yaml
background: [] # directories of noise WAVs; empty: Gaussian noise
augment: {rounds: 4, followed: 2, noise: 200, snr: [5, 25]} # followed: "Jarvis" + a command, as negatives
validation: {threshold: 0.5, min: {recall: 0.9, scenario: 0.9}, max: {false_accepts: 0.05}}
- Phrase. The wake word is the agent's name; the command after it ("Jarvis, wake up", "Jarvis, search the web
for ...") is not part of it, and speech recognition hears it once the detector has fired. Positives say the phrase
and nothing else (a positive row with a
textmust say it); negatives must never contain it, and thedatastage stops on one that does. The hardest negatives are near misses ("Travis", "Davis", "Karen"), and the commands said alone. The model file is named after the phrase:jarvis.onnx. - Recordings.
data_speechwith a multi-speaker voice (voice.speakers) andselect.per_text: nullmakes them: the name read by hundreds of speakers, each checked by Whisper. Recordings of the people who will use the agent (delimitedorlocal_jsonl) belong inpositivetoo: synthetic voices are a start, not the target. - Windows. Every training recording becomes
augment.rounds2-second windows over noise at a random SNR and gain, ending up toaugment.tailseconds after the speech. Negatives are also cut short at random, so half a phrase is a negative;augment.followedwindows per phrase put other speech after it, so the detector fires as the name ends and not again while the command that follows is spoken. All randomness comes from the seed and the recording. - Training. Balanced batches (half positive), BCE, Adam,
training.stepssteps on the CPU, seeded: a rerun after a crash trains the same detector again. - Validation. Held-out recordings in noise (
validation.snr), scored every 80 ms as the runtime scores them, a firing counted at most once a second:recallandfalse_acceptsper recording, andscenario, the agent's first moment: "Jarvis", a pause, then another held-out utterance (a command such as "Please search to the paper reference on the web", a near miss, or ordinary speech). It passes when the detector fires exactly once, while or just after the name is said. - Checkpoint.
<experiment>/validate/checkpoint/holdsjarvis.onnx, the two feature models andwakeword.json(the phrase, the files, the feature models' hashes):
from openwakeword.model import Model # pip install openwakeword
detector = Model(wakeword_models=["checkpoint/jarvis.onnx"], inference_framework="onnx",
melspec_model_path="checkpoint/melspectrogram.onnx", embedding_model_path="checkpoint/embedding_model.onnx")
score = detector.predict(chunk)["jarvis"] # chunk: 1280 samples, 16 kHz, int16; wake at >= the threshold
false_accepts is measured on the negatives you give, not on hours of real audio; openWakeWord's own models add a
large corpus of everyday speech and music for that. Add real background recordings and your own speech before
trusting the rate. openWakeWord's code is Apache-2.0 and its README puts its pre-trained models under CC BY-NC-SA 4.0;
the two feature models are derived from Google's Apache-2.0 speech_embedding, so treat a detector as
non-commercial until you have checked their terms.
Source: README.md, configs/wakeword/jarvis.yaml. This page is generated; edit the source and run npm run sync.