Skip to content

interkelstar/microwakeword-trainer

Repository files navigation

microWakeWord Trainer

Config-driven pipeline for training custom wake word models using the microWakeWord framework.

microWakeWord trains a MixedNet/Inception streaming CNN that is exported as a fully quantised TFLite model. The final model is 50–300 KB and is designed to run with pymicro_wakeword (used by linux-voice-assistant) and ESPHome on microcontrollers.

Background

I built this because I wanted a custom Russian wake word — "Джарвис" (Jarvis) — for my home voice assistant. Russian isn't supported by any of the off-the-shelf wake word solutions I found, and recording hundreds of your own voice clips felt like the wrong approach. I wanted a fully synthetic training pipeline: pick your phrases, pick your voices, run the script, get a model.

I started with openWakeWord and got something working, but the gap between training accuracy and real-world recall was frustrating. Switching to microWakeWord's streaming CNN architecture gave much better results, and the model is a fraction of the size.

The pipeline is fully language-agnostic — the Russian example configs are just that. Any language with a Piper voice model should work the same way.

Results on Russian "Джарвис"

Trained on 50k Piper TTS clips across 4 Russian voices (Dmitri, Denis, Irina, Ruslan) plus ElevenLabs positives (~314k positive / ~353k negative feature windows). Evaluated on real audio held out of training: 50 organic activations recorded during daily use, and 140 mined hard false-positive clips (TV speech, clicks, knocks):

Metric Result
Recall — 50 real activations (held out of training) 47 / 50 (94%)
False positives — 140 mined hard cases 1 / 140
Previous model on the same test sets 50/50 recall, but 91/140 FP
Production cutoff in daily use 0.9
Model size 82.5 KB (streaming, uint8 quantised)
Val accuracy 99.77%

The model runs continuously in production. The biggest win came from FP-mining: collecting 208 real false-positive clips from daily use and re-extracting negative features with dense windowing (1 window per 10 ms) dropped hard-case false positives from 91/140 to 1/140. Lowering the cutoff from 0.97 to 0.9 then recovered 5 more real activations at no measurable FP cost — the 3 remaining misses sit far below the threshold (~0.2–0.5) and are not recoverable by tuning alone.


Requirements

  • Linux (tested on Ubuntu 22.04, Debian 12, WSL2)
  • Python 3.9+ with pip
  • ~15 GB disk space for training data
  • NVIDIA GPU strongly recommended (training takes 8–14 hours on CPU)
  • Internet access for HuggingFace downloads

Quick Start

# Clone the repo and set up the environment (one time only)
git clone https://github.com/interkelstar/microwakeword-trainer.git
cd microwakeword-trainer
chmod +x setup_mww.sh && ./setup_mww.sh

# Copy the example config and customise it for your wake word
cp example_ru_jarvis.yaml my_wakeword.yaml
# Edit my_wakeword.yaml: set model_name, target_phrases, voices, negatives

# Run the full training pipeline
./run_mww.sh --config my_wakeword.yaml

Output files created at the project root after training:

  • <model_name>_mww.tflite — TFLite model ready for pymicro_wakeword
  • <model_name>_mww.json — Deployment manifest with inference parameters

Training Phases

The pipeline runs these phases in order:

Phase What it does Time (CPU)
setup Verify microwakeword, TF, pymicro_features are installed seconds
voices Download Piper ONNX voice models from HuggingFace 1–5 min
generate Synthesise positive + adversarial negative TTS clips via piper 6–10 hours
features Extract pymicro_features spectrograms + download HF negatives 1–3 hours
train Build and train the MixedNet model with SpecAugment 0.5–3 hours
export Convert to streaming TFLite (uint8 quantised) + write JSON manifest 5–15 min
test Evaluate TFLite model on test clips (optional, not run in "all") minutes

Run individual phases with --phase <name>:

./run_mww.sh --config my_wakeword.yaml --phase train
./run_mww.sh --config my_wakeword.yaml --phase export
./run_mww.sh --config my_wakeword.yaml --phase test

Config Reference

See example_ru_jarvis.yaml for a fully annotated example. Key sections:

model_name: ru_jarvis        # Output filename prefix (ASCII, no spaces)

target_phrases:              # What the model should fire on
  - Джарвис
  - Джарвис!
  - Джарвис?

training:
  n_samples: 50000           # TTS clips for training
  n_samples_val: 10000       # TTS clips for validation

voices:
  primary:                   # For positive clips + same-language negatives
    base_url: https://huggingface.co/rhasspy/piper-voices/resolve/main/ru/ru_RU
    models:
      ru_RU-dmitri-medium: dmitri/medium
  secondary:                 # For cross-language negative phrases (optional)
    base_url: https://huggingface.co/rhasspy/piper-voices/resolve/main
    models:
      en_US-amy-medium: en/en_US/amy/medium

negative_phrases:            # Acoustically similar words to reject
  - Джар
  - Алиса
  - Jarring                  # Latin script → synthesised with secondary voices

microwakeword:
  architecture: mixednet     # mixednet (default) or inception
  spectrogram_length: 204    # input frames per inference window
  stride: 3                  # frames consumed per streaming step (3 × 10ms = 30ms)
  training_steps: 10000      # gradient steps
  batch_size: 128
  learning_rate: 0.001
  positive_class_weight: 1.5 # raise if recall is too low
  negative_class_weight: 1   # raise if false positive rate is too high
  probability_cutoff: 0.9    # detection threshold (used at inference time)
  sliding_window_size: 10    # averaging window size (used at inference time)

Understanding spectrogram_length and stride

microWakeWord uses pymicro_features to extract 40-bin mel spectrogram frames at 10 ms intervals. The model processes stride frames per inference call and maintains a ring buffer of internal state.

The spectrogram_length determines how many frames the model can look back when making each decision. Larger values give more context but increase model size and training memory.

probability_cutoff and sliding_window_size are used only at inference time — re-run --phase export after changing them.

Deployment

Copy both output files to your linux-voice-assistant config directory:

cp ru_jarvis_mww.tflite /path/to/linux-voice-assistant/models/
cp ru_jarvis_mww.json   /path/to/linux-voice-assistant/models/

The JSON manifest format:

{
  "type": "micro",
  "wake_word": "Джарвис",
  "model": "ru_jarvis_mww.tflite",
  "micro": {
    "probability_cutoff": 0.9,
    "sliding_window_size": 10,
    "feature_step_size": 10
  }
}

The type: micro field is required — the linux-voice-assistant loader uses it to select the pymicro_wakeword inference backend. The exporter also writes author, trained_languages, version, micro.tensor_arena_size, and micro.minimum_esphome_version for ESPHome compatibility.

To change the detection threshold without retraining, edit probability_cutoff in the JSON file directly, or change it in your YAML and re-run --phase export.

Iterative Improvement — Reducing False Positives

The most effective way to reduce false positives is to collect negative examples from your real environment and retrain.

Workflow:

  1. Run the assistant for a day and note when it triggers incorrectly.
  2. Record the audio that caused false positives as a WAV file (16 kHz, mono).
  3. Copy the WAV to training/output/<model_name>/user_negatives/.
  4. Re-run feature extraction and training:
./run_mww.sh --config my_wakeword.yaml --phase features
./run_mww.sh --config my_wakeword.yaml --phase train
./run_mww.sh --config my_wakeword.yaml --phase export

The user_negatives/ directory is processed with dense windowing (1 window per 10 ms) to maximise coverage of the triggering audio.

Important: Do NOT put actual wake word recordings in user_negatives/. Only put audio that triggered incorrectly (TV speech, background noise, etc.).

Testing a Model

Use test_mww.py to evaluate a trained model against WAV files. The script simulates the exact pymicro_wakeword inference loop including the sliding window averaging:

# Test against all WAV files in record/
.venv/bin/python3 test_mww.py

# Test specific files
.venv/bin/python3 test_mww.py record/sample1.wav record/sample2.wav

# Output shows per-frame probabilities and a TRIGGERED / no trigger result

The default model and config paths are set at the top of test_mww.py; override them with --model my_model_mww.tflite (the JSON manifest is found automatically next to the model).

Two companion tools help with false-positive mining:

# Batch-test a directory of FP clips at the production cutoff
.venv/bin/python3 batch_test_fps.py --model my_model_mww.tflite fp_clips/

# A/B compare two model versions on the same FP clips
.venv/bin/python3 ab_test_fps.py old_mww.tflite new_mww.tflite fp_clips/

ElevenLabs High-Quality Positives (Optional)

generate_elevenlabs.py generates additional high-quality TTS clips using the ElevenLabs API (requires an API key with credits). These are automatically picked up during feature extraction and can improve the naturalness of the positive class:

.venv/bin/python generate_elevenlabs.py --api-key YOUR_KEY --config my_wakeword.yaml

Clips are saved to training/output/<model_name>/elevenlabs_positive/.

Disk Space

After a full training run, the training/ directory contains:

training/piper_binary/           # piper executable + shared libs (~150 MB)
training/piper_models/           # ONNX voice models (~280 MB for 4 voices)
training/output/<model_name>/
  positive_train/                # TTS positive clips (~4 GB for 50k)
  positive_test/                 # TTS validation clips (~800 MB for 10k)
  negative_train/                # Adversarial negative TTS clips
  negative_test/
  elevenlabs_positive/           # ElevenLabs clips (optional)
  user_negatives/                # Real environment negatives (optional)
  mww_features/                  # Extracted spectrograms + HF negatives
  model/                         # Saved Keras model weights

To clean up after training:

rm -rf .venv training/

How It Works

microWakeWord uses the pymicro_features frontend (matching the on-device inference pipeline on microcontrollers):

  1. Audio is chunked into 10 ms frames of 160 samples at 16 kHz.
  2. Each chunk is processed by MicroFrontend from the pymicro-features package, producing a 40-bin log-mel spectrogram frame.
  3. The streaming model processes stride frames per call, maintaining internal convolutional state across calls.
  4. A sliding window of sliding_window_size consecutive probabilities is averaged; when the mean exceeds probability_cutoff, the wake word fires.

Feature scale: pymicro_features outputs values in [0, ~26] (float32). This range must match at training time and inference time. The HuggingFace negative dataset (kahrendt/microwakeword) uses a uint16 scale and is automatically rescaled to match during feature extraction.

License

MIT

About

Config-driven pipeline for training custom microWakeWord models — streaming TFLite wake word detector for linux-voice-assistant and ESPHome

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages