mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-08-12 07:55:33 -06:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d341d0617 | ||
|
|
a1b22200e0 |
97
README.md
97
README.md
@@ -7,7 +7,7 @@
|
||||
<a href="https://taterassistant.com">taterassistant.com</a>
|
||||
</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.
|
||||
|
||||
@@ -22,7 +22,7 @@ docker pull ghcr.io/tatertotterson/microwakeword:latest
|
||||
Tagged releases also publish matching immutable image tags:
|
||||
|
||||
```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
|
||||
@@ -30,7 +30,7 @@ Python 3.13 TensorFlow build for `sm_120`:
|
||||
|
||||
```bash
|
||||
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
|
||||
@@ -51,19 +51,19 @@ docker run -d \
|
||||
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`
|
||||
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.
|
||||
|
||||
The flags:
|
||||
|
||||
- `--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.
|
||||
- `-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:
|
||||
|
||||
@@ -71,31 +71,37 @@ Open:
|
||||
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
|
||||
|
||||
- `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.
|
||||
- `Firmware` pulls verified prebuilt Tater firmware images from GitHub and flashes supported satellites over OTA.
|
||||
- Popup consoles show colorized training and firmware logs while long-running jobs are active.
|
||||
- `Wake Words` lists locally trained JSON/model links for live wake-word switching in Tater.
|
||||
- Popup consoles show colorized training logs while long-running jobs are active.
|
||||
|
||||
---
|
||||
|
||||
## 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 Close Misses` toggles upload of near misses.
|
||||
- `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
|
||||
/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`.
|
||||
- Verifies downloaded images by size and SHA before upload.
|
||||
- Auto-detects ESPHome devices with mDNS when the container is running with host networking.
|
||||
- Allows manual IP or hostname entry if discovery does not find the device.
|
||||
- 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.
|
||||
- Copy the JSON URL into the Tater Native satellite settings to switch wake words live.
|
||||
- Open the JSON or model links directly for quick inspection.
|
||||
- The JSON includes the matching model path plus Tater tuning metadata.
|
||||
- No firmware flashing happens from this trainer app anymore.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
The trainer also syncs firmware-ready artifacts into:
|
||||
The trainer also syncs Tater-ready wake-word artifacts into:
|
||||
|
||||
```text
|
||||
/data/trained_wake_words/<wake_word>.tflite
|
||||
/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
|
||||
- training environments
|
||||
- trained models
|
||||
- downloaded firmware images
|
||||
|
||||
---
|
||||
|
||||
@@ -261,7 +298,7 @@ That removes:
|
||||
- Negative samples are optional but useful for reducing false wakes.
|
||||
- The UI server is `trainer_server.py`.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ from microwakeword.data import FeatureHandler
|
||||
from microwakeword.inference import Model
|
||||
|
||||
|
||||
DEFAULT_WINDOW_SIZES = [3, 4, 5, 6, 7]
|
||||
DEFAULT_TARGET_FAPH = float(os.environ.get("MWW_CALIBRATION_TARGET_FAPH", "1.0"))
|
||||
DEFAULT_WINDOW_SIZES = [4, 5, 6, 7]
|
||||
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_POSITIVE_SKIP_SLICES = int(
|
||||
os.environ.get("MWW_CALIBRATION_POSITIVE_SKIP_SLICES", "25")
|
||||
)
|
||||
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"))
|
||||
|
||||
|
||||
|
||||
@@ -302,11 +302,11 @@ TRAIN_ARGS=(
|
||||
--test_tflite_streaming_quantized 1
|
||||
--use_weights best_weights
|
||||
mixednet
|
||||
--pointwise_filters "64,64,64,64"
|
||||
--pointwise_filters "128,128,128,128"
|
||||
--repeat_in_block "1,1,1,1"
|
||||
--mixconv_kernel_sizes "[5], [7,11], [9,15], [23]"
|
||||
--residual_connection "0,0,0,0"
|
||||
--first_conv_filters 32
|
||||
--first_conv_filters 64
|
||||
--first_conv_kernel_size 5
|
||||
--stride 2
|
||||
)
|
||||
@@ -386,6 +386,7 @@ fi
|
||||
TRAINING_DONE="false"
|
||||
|
||||
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
|
||||
echo "✅ Training complete (GPU path)."
|
||||
TRAINING_DONE="true"
|
||||
@@ -495,8 +496,10 @@ from pathlib import Path
|
||||
json_path = Path(os.environ["JSON_PATH"])
|
||||
calibration_path = Path(os.environ.get("CALIBRATION_PATH", ""))
|
||||
language = (os.environ.get("LANGUAGE", "en") or "en").strip().lower()
|
||||
probability_cutoff = 0.97
|
||||
sliding_window_size = 5
|
||||
probability_cutoff = 0.85
|
||||
sliding_window_size = 4
|
||||
strict_min_close_miss_threshold = 0.68
|
||||
calibration = {}
|
||||
|
||||
if calibration_path.exists():
|
||||
try:
|
||||
@@ -510,21 +513,63 @@ if calibration_path.exists():
|
||||
except Exception as exc:
|
||||
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 = {
|
||||
"type": "micro",
|
||||
"wake_word": os.environ["WAKE_WORD_TITLE"],
|
||||
"label": os.environ["WAKE_WORD_TITLE"].replace("_", " ").title(),
|
||||
"author": "Tater Totterson",
|
||||
"website": "https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git",
|
||||
"model": os.environ["TFLITE_FILENAME"],
|
||||
"trained_languages": [language],
|
||||
"version": 2,
|
||||
"model_format": "tflite_stream_state_internal_quant",
|
||||
"quantization": "int8",
|
||||
"sample_rate": 16000,
|
||||
"micro": {
|
||||
"probability_cutoff": round(probability_cutoff, 2),
|
||||
"probability_cutoff": probability_cutoff,
|
||||
"sliding_window_size": sliding_window_size,
|
||||
"feature_step_size": 10,
|
||||
"tensor_arena_size": 30000,
|
||||
"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")
|
||||
PY
|
||||
|
||||
2
run.sh
2
run.sh
@@ -30,7 +30,6 @@ install_ui_deps() {
|
||||
"fastapi==${FASTAPI_VERSION}" \
|
||||
"uvicorn[standard]==${UVICORN_VERSION}" \
|
||||
"python-multipart==${PY_MULTIPART_VERSION}" \
|
||||
"zeroconf>=0.132.2" \
|
||||
"silero-vad>=5.0.0" \
|
||||
"numpy>=1.24.0"
|
||||
}
|
||||
@@ -79,7 +78,6 @@ exact = {
|
||||
minimum = {
|
||||
"silero-vad": "5.0.0",
|
||||
"numpy": "1.24.0",
|
||||
"zeroconf": "0.132.2",
|
||||
}
|
||||
present = ("torch",)
|
||||
|
||||
|
||||
1963
static/index.html
1963
static/index.html
File diff suppressed because it is too large
Load Diff
1735
trainer_server.py
1735
trainer_server.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user