diff --git a/README.md b/README.md
index 754491d..e70f264 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
taterassistant.com
-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:v10
+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:v10-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:v10` 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:v10-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 device 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:
@@ -80,8 +80,8 @@ If you change `REC_PORT`, open that port instead and use the same port in the sa
- `Trainer` starts a wake-word session, shows positive/negative sample counts, and launches training.
- `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.
---
@@ -207,23 +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 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/Tater-Native-Firmware`](https://github.com/TaterTotterson/Tater-Native-Firmware).
-- Verifies downloaded images by size and SHA before upload.
-- Auto-detects compatible 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.
-- Leaves Wi-Fi, Tater server, and pairing setup to the satellite setup portal after USB flash.
-- 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.
-> **Tater only:** these native firmware images connect to Tater. They are not Home Assistant or ESPHome satellite firmware.
-
-You usually only flash for firmware updates. New satellites, or devices not already running Tater Native Firmware `v1`, 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.
---
@@ -236,14 +229,14 @@ Successful runs produce timestamped training output folders such as:
/data/output/---/.json
```
-The trainer also syncs firmware-ready artifacts into:
+The trainer also syncs Tater-ready wake-word artifacts into:
```text
/data/trained_wake_words/.tflite
/data/trained_wake_words/.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:
@@ -296,7 +289,6 @@ That removes:
- cached datasets
- training environments
- trained models
-- downloaded firmware images
---
@@ -306,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 in Tater for Tater Native satellites, and on device entities for older ESPHome satellites.
+- Trainer capture settings live in Tater for Tater Native satellites, and on device entities for older ESPHome satellites.
---
diff --git a/cli/calibrate_detector.py b/cli/calibrate_detector.py
index 5cd56da..aad90d9 100644
--- a/cli/calibrate_detector.py
+++ b/cli/calibrate_detector.py
@@ -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"))
diff --git a/cli/wake_word_sample_trainer b/cli/wake_word_sample_trainer
index c25c290..1719ed1 100644
--- a/cli/wake_word_sample_trainer
+++ b/cli/wake_word_sample_trainer
@@ -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,9 @@ 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():
@@ -515,7 +517,10 @@ 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(0.01, probability_cutoff - 0.19), 3)))
+close_miss_threshold = max(
+ 0.01,
+ min(0.99, round(max(strict_min_close_miss_threshold, probability_cutoff - 0.17), 3)),
+)
meta = {
"type": "micro",
diff --git a/run.sh b/run.sh
index b65d833..ea9f41f 100644
--- a/run.sh
+++ b/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",)
diff --git a/static/index.html b/static/index.html
index 634c0aa..d95bf0a 100644
--- a/static/index.html
+++ b/static/index.html
@@ -348,6 +348,13 @@
font-size: 13px;
}
+ .runtimeWakeWordActions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
.runtimeWakeWordLinks {
display: grid;
gap: 10px;
@@ -370,6 +377,12 @@
margin-bottom: 4px;
}
+ .runtimeWakeWordUrls {
+ display: grid;
+ gap: 3px;
+ margin-top: 6px;
+ }
+
.runtimeWakeWordUrl {
display: block;
overflow: hidden;
@@ -968,157 +981,6 @@
margin-top: 2px;
}
- .firmwareLogModal {
- position: fixed;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 20px;
- background: rgba(0, 0, 0, 0.64);
- opacity: 0;
- visibility: hidden;
- pointer-events: none;
- z-index: 12000;
- }
-
- .firmwareLogModal.active {
- visibility: visible;
- pointer-events: auto;
- animation: firmwareLogBackdropIn 280ms ease-out both;
- }
-
- .firmwareLogDialog {
- width: min(1320px, 98vw);
- height: min(94vh, 1040px);
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 18px;
- border-radius: 22px;
- border: 1px solid rgba(255,255,255,0.12);
- background:
- linear-gradient(180deg, rgba(17, 20, 28, 0.88), rgba(8, 10, 16, 0.96)),
- rgba(8, 10, 16, 0.88);
- box-shadow: 0 28px 84px rgba(0,0,0,0.58);
- backdrop-filter: blur(18px) saturate(1.12);
- overflow: hidden;
- opacity: 0;
- transform: translateY(14px) scale(0.97);
- }
-
- .firmwareLogModal.active .firmwareLogDialog {
- animation: firmwareLogDialogIn 300ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
- }
-
- .firmwareLogHeader {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 16px;
- }
-
- .firmwareLogTitle {
- margin: 0;
- font-size: 18px;
- }
-
- .firmwareLogMeta {
- margin: 6px 0 0;
- color: var(--muted);
- font-size: 13px;
- }
-
- .firmwareLogStatus {
- color: #b7c7e6;
- font-size: 13px;
- }
-
- .firmwareLogConsole {
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- border: 1px solid rgba(255,138,42,0.22);
- border-radius: 16px;
- padding: 12px 14px;
- background:
- linear-gradient(180deg, rgba(15, 18, 25, 0.98), rgba(10, 12, 18, 0.98)),
- radial-gradient(circle at top, rgba(255,138,42,0.08), transparent 40%);
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,0.05),
- inset 0 -1px 0 rgba(0,0,0,0.35);
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
- font-size: 13px;
- line-height: 1.45;
- color: #dbe8ff;
- }
-
- .firmwareLogLine {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- gap: 10px;
- align-items: start;
- padding: 2px 0;
- }
-
- .firmwareLogLevel {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 58px;
- border-radius: 999px;
- padding: 2px 8px;
- font-size: 11px;
- font-weight: 800;
- letter-spacing: 0.04em;
- border: 1px solid rgba(255,255,255,0.12);
- white-space: nowrap;
- }
-
- .firmwareLogMessage {
- white-space: pre-wrap;
- word-break: break-word;
- }
-
- .firmwareLogLine.tone-info .firmwareLogLevel {
- background: rgba(70, 120, 255, 0.16);
- border-color: rgba(108, 152, 255, 0.28);
- color: #bdd3ff;
- }
-
- .firmwareLogLine.tone-warn .firmwareLogLevel {
- background: rgba(245, 167, 36, 0.14);
- border-color: rgba(245, 167, 36, 0.32);
- color: #ffd696;
- }
-
- .firmwareLogLine.tone-error .firmwareLogLevel {
- background: rgba(226, 76, 76, 0.15);
- border-color: rgba(255, 111, 111, 0.34);
- color: #ffc1c1;
- }
-
- .firmwareLogLine.tone-debug .firmwareLogLevel {
- background: rgba(92, 214, 178, 0.14);
- border-color: rgba(92, 214, 178, 0.28);
- color: #bffff0;
- }
-
- .firmwareLogEmpty {
- color: var(--muted);
- }
-
- @keyframes firmwareLogBackdropIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
-
- @keyframes firmwareLogDialogIn {
- from { opacity: 0; transform: translateY(14px) scale(0.97); }
- 62% { transform: translateY(-3px) scale(1.01); }
- to { opacity: 1; transform: translateY(0) scale(1); }
- }
-
.consoleMuted { color: #93a0b5; }
.consoleText { color: #d8e2f1; }
.consoleCmd { color: #89d4ff; }
@@ -1263,22 +1125,6 @@
height: min(82vh, 760px);
padding: 14px;
}
- .firmwareLogModal {
- padding: 12px;
- }
- .firmwareLogDialog {
- width: 100%;
- height: 88vh;
- padding: 14px;
- }
- .firmwareLogHeader {
- flex-direction: column;
- align-items: stretch;
- }
- .firmwareLogLine {
- grid-template-columns: 1fr;
- gap: 4px;
- }
.consoleHeader {
flex-direction: column;
align-items: stretch;
@@ -1311,14 +1157,14 @@
microWakeWord Trainer Studio
-
Train wake words, review captured clips, and flash Tater Native firmware from one local workspace.
+
Train wake words, review captured clips, and publish live model URLs for Tater Native satellites.
-
+
@@ -1552,22 +1398,22 @@
-
Firmware Studio
-
Prebuilt Tater Firmware Flasher
-
Flash only for firmware updates. If this is a new satellite or it is not already on Tater Native Firmware v1, do one USB flash first, then use this tab for fast OTA updates.
-
- 1 Pick firmware
- 2 Select target
- 3 Flash OTA or USB
+
Wake Word Catalog
+
Trained Wake Words
+
Every completed training run publishes a local JSON package and model file. Copy the JSON URL into the native satellite settings in Tater to switch wake words live.
+
+ 1 Train model
+ 2 Copy JSON URL
+ 3 Paste in Tater
- Flasher idle
+ Catalog idle
- Tater only
- These native firmware images connect to Tater. They are not Home Assistant or ESPHome satellite firmware.
+ Tater Native
+ These generated wake-word URLs are for Tater Native satellites and are loaded from the satellite settings in Tater.
@@ -1576,89 +1422,19 @@
v1
Live Model Switching
-
Tater Native satellites can swap wake words live
-
No reflash is needed after training. Copy a trained wake word JSON URL below and paste it into the native satellite settings in Tater.
+
Copy trained wake-word URLs
+
The JSON file points Tater and the satellite at the matching model plus trainer tuning values.
- No reflash needed
+
+ No reflash needed
+
+
Train a wake word first, then its live model URL will appear here.
-
-
-
-
-
- 1
-
-
Firmware Image
-
Choose a prebuilt Tater firmware image from the Tater Native Firmware releases.
-
-
-
-
-
-
-
-
-
- 2
-
-
Target Device
-
Auto-detect a device or enter the OTA target manually.
-
-
- Not scanned
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 3
-
-
Flash Firmware
-
The console shows download, verification, OTA upload, USB flash, and reboot progress.
-
-
-
-
-
-
-
-
-
-
Use Browser USB Flash for new satellites, recovery, or devices not already running Tater Native Firmware v1. After USB flash, join the satellite setup hotspot and finish Wi-Fi, Tater server, and pairing in the device setup page. Chrome or Edge must be used on localhost or HTTPS.