2 Commits
v9 ... v11

Author SHA1 Message Date
MasterPhooey
3d341d0617 Release NVIDIA WakeWord Trainer v11 2026-07-12 12:02:36 -05:00
MasterPhooey
a1b22200e0 Point Docker trainer at native Tater firmware 2026-07-11 08:35:38 -05:00
6 changed files with 291 additions and 3569 deletions

View File

@@ -7,7 +7,7 @@
<a href="https://taterassistant.com">taterassistant.com</a> <a href="https://taterassistant.com">taterassistant.com</a>
</h3> </h3>
Train custom microWakeWord models in Docker with NVIDIA/CUDA acceleration, generated Piper samples, device-captured samples, reviewed false-wake negatives, live training logs, and prebuilt Tater firmware flashing. Train custom microWakeWord models in Docker with NVIDIA/CUDA acceleration, generated Piper samples, device-captured samples, reviewed false-wake negatives, live training logs, and local wake-word links for Tater Native satellites.
Real samples come from device-captured wake audio, close misses, or manual uploads. Every saved sample is normalized to `16 kHz / mono / 16-bit PCM WAV` before training. Real samples come from device-captured wake audio, close misses, or manual uploads. Every saved sample is normalized to `16 kHz / mono / 16-bit PCM WAV` before training.
@@ -22,7 +22,7 @@ docker pull ghcr.io/tatertotterson/microwakeword:latest
Tagged releases also publish matching immutable image tags: Tagged releases also publish matching immutable image tags:
```bash ```bash
docker pull ghcr.io/tatertotterson/microwakeword:v5 docker pull ghcr.io/tatertotterson/microwakeword:v11
``` ```
RTX 50-series / Blackwell GPUs use a separate image with CUDA 12.8 and a RTX 50-series / Blackwell GPUs use a separate image with CUDA 12.8 and a
@@ -30,7 +30,7 @@ Python 3.13 TensorFlow build for `sm_120`:
```bash ```bash
docker pull ghcr.io/tatertotterson/microwakeword:blackwell docker pull ghcr.io/tatertotterson/microwakeword:blackwell
docker pull ghcr.io/tatertotterson/microwakeword:v5-blackwell docker pull ghcr.io/tatertotterson/microwakeword:v11-blackwell
``` ```
Use the Blackwell image only for RTX 50-series cards. It includes the Use the Blackwell image only for RTX 50-series cards. It includes the
@@ -51,19 +51,19 @@ docker run -d \
ghcr.io/tatertotterson/microwakeword:latest ghcr.io/tatertotterson/microwakeword:latest
``` ```
Use a version tag such as `ghcr.io/tatertotterson/microwakeword:v5` when you want to pin a known release instead of tracking `latest`. Use a version tag such as `ghcr.io/tatertotterson/microwakeword:v11` when you want to pin a known release instead of tracking `latest`.
For RTX 50-series cards, use `ghcr.io/tatertotterson/microwakeword:blackwell` For RTX 50-series cards, use `ghcr.io/tatertotterson/microwakeword:blackwell`
or a pinned tag such as `ghcr.io/tatertotterson/microwakeword:v5-blackwell` or a pinned tag such as `ghcr.io/tatertotterson/microwakeword:v11-blackwell`
in the same `docker run` command. in the same `docker run` command.
The flags: The flags:
- `--gpus all` enables GPU acceleration. - `--gpus all` enables GPU acceleration.
- `--network host` lets the container receive mDNS/zeroconf traffic for ESPHome auto-detect. - `--network host` exposes the trainer server directly so satellites can send captured audio and load trained wake-word files.
- `-e REC_PORT=8789` sets the trainer web UI and captured-audio port. Change this value if `8789` is already in use. - `-e REC_PORT=8789` sets the trainer web UI and captured-audio port. Change this value if `8789` is already in use.
- `-v $(pwd):/data` persists models, downloaded voices, datasets, samples, and firmware caches. - `-v $(pwd):/data` persists models, downloaded voices, datasets, samples, and generated wake-word artifacts.
Host networking is recommended for the Firmware tab's mDNS device discovery. Manual IP flashing and captured-audio uploads can still work without host networking if the trainer port is reachable, but auto-detect may not see devices from Docker bridge networking. If you do not use host networking, publish the trainer port and make sure satellites can reach it from your LAN.
Open: Open:
@@ -71,31 +71,37 @@ Open:
http://localhost:8789 http://localhost:8789
``` ```
If you change `REC_PORT`, open that port instead and use the same port in the ESPHome `Trainer App URL`. If you change `REC_PORT`, open that port instead and use the same port in the satellite `Trainer App URL`.
--- ---
## What The UI Does ## What The UI Does
- `Trainer` starts a wake-word session, shows positive/negative sample counts, and launches training. - `Trainer` starts a wake-word session, shows positive/negative sample counts, and launches training.
- `Captured Audio` reviews clips sent by ESPHome sats, including wake hits, close misses, and false wakes. - `Captured Audio` reviews clips sent by Tater Native or ESPHome sats, including wake hits, close misses, and false wakes.
- `Samples` plays, removes, clears, and manually imports personal or negative samples. - `Samples` plays, removes, clears, and manually imports personal or negative samples.
- `Firmware` pulls verified prebuilt Tater firmware images from GitHub and flashes supported satellites over OTA. - `Wake Words` lists locally trained JSON/model links for live wake-word switching in Tater.
- Popup consoles show colorized training and firmware logs while long-running jobs are active. - Popup consoles show colorized training logs while long-running jobs are active.
--- ---
## Captured Audio Workflow ## Captured Audio Workflow
To collect samples from a sat, flash it with the Tater firmware from [TaterTotterson/microWakeWords](https://github.com/TaterTotterson/microWakeWords). The `Firmware` tab can pull verified prebuilt OTA images from that repo for fast firmware updates. To collect samples from a sat, point its trainer feedback setting at this app. Tater Native satellites use the native settings popup in Tater. Older ESPHome satellites can still use their device entities.
After flashing, the device exposes ESPHome entities for capture setup: For Tater Native satellites, enable trainer feedback in Tater:
- `Send Good Wakes To Trainer` toggles upload of confirmed wake-word triggers.
- `Send Close Misses To Trainer` toggles upload of near misses.
- `Trainer App URL` sets the trainer address, for example `http://trainer.local:8789` or `http://<trainer-ip>:8789`.
For older ESPHome firmware, the equivalent capture setup is exposed as device entities:
- `Capture Wake Audio` toggles upload of wake-word triggers. - `Capture Wake Audio` toggles upload of wake-word triggers.
- `Capture Close Misses` toggles upload of near misses. - `Capture Close Misses` toggles upload of near misses.
- `Trainer App URL` sets the trainer address, for example `http://<trainer-ip>:8789`. - `Trainer App URL` sets the trainer address, for example `http://<trainer-ip>:8789`.
ESPHome devices can send raw captured audio to: Satellites send raw captured audio to:
```text ```text
/api/upload_captured_audio_raw /api/upload_captured_audio_raw
@@ -201,20 +207,16 @@ After those assets are prepared, later runs reuse the local copies unless the mo
--- ---
## Firmware Flashing ## Trained Wake Words
The `Firmware` tab flashes prebuilt Tater firmware for supported ESPHome satellites. The `Wake Words` tab lists locally trained wake-word packages from `/data/trained_wake_words/`.
- Downloads the latest prebuilt firmware manifest plus OTA and USB factory images from `TaterTotterson/microWakeWords`. - Copy the JSON URL into the Tater Native satellite settings to switch wake words live.
- Verifies downloaded images by size and SHA before upload. - Open the JSON or model links directly for quick inspection.
- Auto-detects ESPHome devices with mDNS when the container is running with host networking. - The JSON includes the matching model path plus Tater tuning metadata.
- Allows manual IP or hostname entry if discovery does not find the device. - No firmware flashing happens from this trainer app anymore.
- Saves the selected OTA target for each firmware family.
- Flashes the prebuilt factory image over Browser USB for first installs or recovery when opened in Chrome or Edge.
- Lists locally trained wake words from `/data/trained_wake_words/` for live model switching.
- Streams download, verification, and OTA upload progress in a colorized firmware console.
You usually only flash for firmware updates. New satellites, or devices older than Tater firmware `3.0.3`, need one USB flash first before OTA updates and live wake-word switching are available. Use the main Tater app for satellite firmware updates and USB flashing.
--- ---
@@ -227,14 +229,50 @@ Successful runs produce timestamped training output folders such as:
/data/output/<timestamp>-<wake_word>-<samples>-<steps>/<wake_word>.json /data/output/<timestamp>-<wake_word>-<samples>-<steps>/<wake_word>.json
``` ```
The trainer also syncs firmware-ready artifacts into: The trainer also syncs Tater-ready wake-word artifacts into:
```text ```text
/data/trained_wake_words/<wake_word>.tflite /data/trained_wake_words/<wake_word>.tflite
/data/trained_wake_words/<wake_word>.json /data/trained_wake_words/<wake_word>.json
``` ```
The firmware tab uses `/data/trained_wake_words/` to populate the wake-word dropdown. The `Wake Words` tab uses `/data/trained_wake_words/` to populate the local wake-word links.
The JSON keeps the standard microWakeWord fields for compatibility:
```json
{
"micro": {
"probability_cutoff": 0.97,
"sliding_window_size": 5
}
}
```
It also includes Tater Native metadata used by newer satellites and the Tater settings UI:
```json
{
"model_format": "tflite_stream_state_internal_quant",
"quantization": "int8",
"sample_rate": 16000,
"tater_native": {
"format_version": 1,
"wake_threshold": 0.97,
"wake_sliding_window": 5,
"close_miss_threshold": 0.78,
"frontend": {
"name": "tflm_microfrontend",
"sample_rate": 16000,
"feature_duration_ms": 30,
"feature_step_ms": 10,
"feature_size": 40
}
}
}
```
Calibration metrics are included under `calibration` so false accepts/hour and recall can be surfaced in the UI.
--- ---
@@ -251,7 +289,6 @@ That removes:
- cached datasets - cached datasets
- training environments - training environments
- trained models - trained models
- downloaded firmware images
--- ---
@@ -261,7 +298,7 @@ That removes:
- Negative samples are optional but useful for reducing false wakes. - Negative samples are optional but useful for reducing false wakes.
- The UI server is `trainer_server.py`. - The UI server is `trainer_server.py`.
- The launcher is `run.sh`. - The launcher is `run.sh`.
- Firmware capture settings live on the ESPHome device and can be toggled from the device entities after flashing. - Trainer capture settings live in Tater for Tater Native satellites, and on device entities for older ESPHome satellites.
--- ---

View File

@@ -18,14 +18,14 @@ from microwakeword.data import FeatureHandler
from microwakeword.inference import Model from microwakeword.inference import Model
DEFAULT_WINDOW_SIZES = [3, 4, 5, 6, 7] DEFAULT_WINDOW_SIZES = [4, 5, 6, 7]
DEFAULT_TARGET_FAPH = float(os.environ.get("MWW_CALIBRATION_TARGET_FAPH", "1.0")) DEFAULT_TARGET_FAPH = float(os.environ.get("MWW_CALIBRATION_TARGET_FAPH", "0.25"))
DEFAULT_COOLDOWN_SLICES = int(os.environ.get("MWW_CALIBRATION_COOLDOWN_SLICES", "25")) DEFAULT_COOLDOWN_SLICES = int(os.environ.get("MWW_CALIBRATION_COOLDOWN_SLICES", "25"))
DEFAULT_POSITIVE_SKIP_SLICES = int( DEFAULT_POSITIVE_SKIP_SLICES = int(
os.environ.get("MWW_CALIBRATION_POSITIVE_SKIP_SLICES", "25") os.environ.get("MWW_CALIBRATION_POSITIVE_SKIP_SLICES", "25")
) )
DEFAULT_CUTOFF_STEP = float(os.environ.get("MWW_CALIBRATION_CUTOFF_STEP", "0.01")) DEFAULT_CUTOFF_STEP = float(os.environ.get("MWW_CALIBRATION_CUTOFF_STEP", "0.01"))
DEFAULT_CUTOFF_MIN = float(os.environ.get("MWW_CALIBRATION_CUTOFF_MIN", "0.00")) DEFAULT_CUTOFF_MIN = float(os.environ.get("MWW_CALIBRATION_CUTOFF_MIN", "0.85"))
DEFAULT_CUTOFF_MAX = float(os.environ.get("MWW_CALIBRATION_CUTOFF_MAX", "1.00")) DEFAULT_CUTOFF_MAX = float(os.environ.get("MWW_CALIBRATION_CUTOFF_MAX", "1.00"))

View File

@@ -302,11 +302,11 @@ TRAIN_ARGS=(
--test_tflite_streaming_quantized 1 --test_tflite_streaming_quantized 1
--use_weights best_weights --use_weights best_weights
mixednet mixednet
--pointwise_filters "64,64,64,64" --pointwise_filters "128,128,128,128"
--repeat_in_block "1,1,1,1" --repeat_in_block "1,1,1,1"
--mixconv_kernel_sizes "[5], [7,11], [9,15], [23]" --mixconv_kernel_sizes "[5], [7,11], [9,15], [23]"
--residual_connection "0,0,0,0" --residual_connection "0,0,0,0"
--first_conv_filters 32 --first_conv_filters 64
--first_conv_kernel_size 5 --first_conv_kernel_size 5
--stride 2 --stride 2
) )
@@ -386,6 +386,7 @@ fi
TRAINING_DONE="false" TRAINING_DONE="false"
echo "🏋️ Starting model training and TFLite export (this is the longest stage)…" echo "🏋️ Starting model training and TFLite export (this is the longest stage)…"
echo "🧠 Model quality: high_accuracy_plus"
if run_attempt "Attempt 1/3: GPU training (default runtime profile)" ; then if run_attempt "Attempt 1/3: GPU training (default runtime profile)" ; then
echo "✅ Training complete (GPU path)." echo "✅ Training complete (GPU path)."
TRAINING_DONE="true" TRAINING_DONE="true"
@@ -495,8 +496,10 @@ from pathlib import Path
json_path = Path(os.environ["JSON_PATH"]) json_path = Path(os.environ["JSON_PATH"])
calibration_path = Path(os.environ.get("CALIBRATION_PATH", "")) calibration_path = Path(os.environ.get("CALIBRATION_PATH", ""))
language = (os.environ.get("LANGUAGE", "en") or "en").strip().lower() language = (os.environ.get("LANGUAGE", "en") or "en").strip().lower()
probability_cutoff = 0.97 probability_cutoff = 0.85
sliding_window_size = 5 sliding_window_size = 4
strict_min_close_miss_threshold = 0.68
calibration = {}
if calibration_path.exists(): if calibration_path.exists():
try: try:
@@ -510,21 +513,63 @@ if calibration_path.exists():
except Exception as exc: except Exception as exc:
print(f"⚠️ Failed to read detector calibration ({exc}); using defaults.") print(f"⚠️ Failed to read detector calibration ({exc}); using defaults.")
probability_cutoff = round(probability_cutoff, 3)
sliding_window_size = max(1, min(10, int(sliding_window_size)))
selected_metrics = calibration.get("selected_metrics") if isinstance(calibration.get("selected_metrics"), dict) else {}
evaluation = calibration.get("evaluation") if isinstance(calibration.get("evaluation"), dict) else {}
close_miss_threshold = max(
0.01,
min(0.99, round(max(strict_min_close_miss_threshold, probability_cutoff - 0.17), 3)),
)
meta = { meta = {
"type": "micro", "type": "micro",
"wake_word": os.environ["WAKE_WORD_TITLE"], "wake_word": os.environ["WAKE_WORD_TITLE"],
"label": os.environ["WAKE_WORD_TITLE"].replace("_", " ").title(),
"author": "Tater Totterson", "author": "Tater Totterson",
"website": "https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git", "website": "https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git",
"model": os.environ["TFLITE_FILENAME"], "model": os.environ["TFLITE_FILENAME"],
"trained_languages": [language], "trained_languages": [language],
"version": 2, "version": 2,
"model_format": "tflite_stream_state_internal_quant",
"quantization": "int8",
"sample_rate": 16000,
"micro": { "micro": {
"probability_cutoff": round(probability_cutoff, 2), "probability_cutoff": probability_cutoff,
"sliding_window_size": sliding_window_size, "sliding_window_size": sliding_window_size,
"feature_step_size": 10, "feature_step_size": 10,
"tensor_arena_size": 30000, "tensor_arena_size": 30000,
"minimum_esphome_version": "2024.7.0", "minimum_esphome_version": "2024.7.0",
}, },
"tater_native": {
"format_version": 1,
"wake_threshold": probability_cutoff,
"wake_sliding_window": sliding_window_size,
"close_miss_threshold": close_miss_threshold,
"frontend": {
"name": "tflm_microfrontend",
"sample_rate": 16000,
"feature_duration_ms": 30,
"feature_step_ms": 10,
"feature_size": 40,
"input_feature_frames": 2,
"lower_band_limit": 125.0,
"upper_band_limit": 7500.0,
},
"recommended_for": ["tater-native-satellite", "voice-pe"],
},
"calibration": {
"target_false_accepts_per_hour": calibration.get("target_false_accepts_per_hour"),
"selected_false_accepts_per_hour_limit": calibration.get("selected_false_accepts_per_hour_limit"),
"recall": selected_metrics.get("recall"),
"false_accepts_per_hour": selected_metrics.get("false_accepts_per_hour"),
"ambient_hours": selected_metrics.get("ambient_hours"),
"positive_dataset": evaluation.get("positive_dataset"),
"ambient_dataset": evaluation.get("ambient_dataset"),
"positive_tracks": evaluation.get("positive_tracks"),
"ambient_tracks": evaluation.get("ambient_tracks"),
"generated_at": calibration.get("generated_at"),
},
} }
json_path.write_text(json.dumps(meta, indent=4) + "\n", encoding="utf-8") json_path.write_text(json.dumps(meta, indent=4) + "\n", encoding="utf-8")
PY PY

2
run.sh
View File

@@ -30,7 +30,6 @@ install_ui_deps() {
"fastapi==${FASTAPI_VERSION}" \ "fastapi==${FASTAPI_VERSION}" \
"uvicorn[standard]==${UVICORN_VERSION}" \ "uvicorn[standard]==${UVICORN_VERSION}" \
"python-multipart==${PY_MULTIPART_VERSION}" \ "python-multipart==${PY_MULTIPART_VERSION}" \
"zeroconf>=0.132.2" \
"silero-vad>=5.0.0" \ "silero-vad>=5.0.0" \
"numpy>=1.24.0" "numpy>=1.24.0"
} }
@@ -79,7 +78,6 @@ exact = {
minimum = { minimum = {
"silero-vad": "5.0.0", "silero-vad": "5.0.0",
"numpy": "1.24.0", "numpy": "1.24.0",
"zeroconf": "0.132.2",
} }
present = ("torch",) present = ("torch",)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff