mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-08-12 16:05:34 -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>
|
<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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
2
run.sh
@@ -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",)
|
||||||
|
|
||||||
|
|||||||
1965
static/index.html
1965
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