mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-08-12 16:05:34 -06:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89260f1f14 | ||
|
|
0140dfb56f | ||
|
|
1fc7d80bae | ||
|
|
31a6388da4 | ||
|
|
85c2d6334b | ||
|
|
5f6f108c85 | ||
|
|
bb5033c5fb | ||
|
|
8a8f4a82d9 | ||
|
|
ed120e91ab | ||
|
|
7d8ebd6637 | ||
|
|
874f273d0b | ||
|
|
04249f414d | ||
|
|
6a0d60d569 | ||
|
|
8df17599c2 | ||
|
|
280e8f8de4 | ||
|
|
b582a6cade | ||
|
|
196ab8c0e7 | ||
|
|
134f607bef | ||
|
|
4a9e2f2cde | ||
|
|
7c246856df | ||
|
|
3705dabc09 | ||
|
|
1dcf48209f | ||
|
|
4f44bef8d5 | ||
|
|
98fa879db1 | ||
|
|
dfac549430 | ||
|
|
775a78326b | ||
|
|
429be4cc67 | ||
|
|
2e6179ec32 |
126
.github/workflows/docker-publish.yml
vendored
Normal file
126
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
name: Publish Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docker-publish-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: tatertotterson/microwakeword
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Docker image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=mww-trainer-nvidia-docker
|
||||||
|
cache-to: type=gha,mode=max,scope=mww-trainer-nvidia-docker
|
||||||
|
|
||||||
|
- name: Docker metadata (Blackwell)
|
||||||
|
id: meta-blackwell
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=raw,value=blackwell
|
||||||
|
type=ref,event=tag,suffix=-blackwell
|
||||||
|
|
||||||
|
- name: Build and push Blackwell image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: dockerfile.blackwell
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-blackwell.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-blackwell.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=mww-trainer-nvidia-docker-blackwell
|
||||||
|
cache-to: type=gha,mode=max,scope=mww-trainer-nvidia-docker-blackwell
|
||||||
|
|
||||||
|
- name: Create release notes
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
title="microWakeWord Nvidia Trainer ${TAG_NAME}"
|
||||||
|
generated_notes="$(mktemp)"
|
||||||
|
release_notes="$(mktemp)"
|
||||||
|
|
||||||
|
gh api "repos/${REPO}/releases/generate-notes" \
|
||||||
|
-f tag_name="${TAG_NAME}" \
|
||||||
|
-f target_commitish="${GITHUB_SHA}" \
|
||||||
|
--jq '.body' > "${generated_notes}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "## Docker Images"
|
||||||
|
echo
|
||||||
|
echo "- \`ghcr.io/tatertotterson/microwakeword:${TAG_NAME}\`"
|
||||||
|
echo "- \`ghcr.io/tatertotterson/microwakeword:latest\`"
|
||||||
|
echo "- \`ghcr.io/tatertotterson/microwakeword:${TAG_NAME}-blackwell\`"
|
||||||
|
echo "- \`ghcr.io/tatertotterson/microwakeword:blackwell\`"
|
||||||
|
echo
|
||||||
|
cat "${generated_notes}"
|
||||||
|
} > "${release_notes}"
|
||||||
|
|
||||||
|
if gh release view "${TAG_NAME}" >/dev/null 2>&1; then
|
||||||
|
gh release edit "${TAG_NAME}" \
|
||||||
|
--title "${title}" \
|
||||||
|
--notes-file "${release_notes}" \
|
||||||
|
--latest \
|
||||||
|
--verify-tag
|
||||||
|
else
|
||||||
|
gh release create "${TAG_NAME}" \
|
||||||
|
--title "${title}" \
|
||||||
|
--notes-file "${release_notes}" \
|
||||||
|
--latest \
|
||||||
|
--verify-tag
|
||||||
|
fi
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
personal_samples/*
|
personal_samples/*
|
||||||
data/
|
data/
|
||||||
|
trim_history/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
59
README.md
59
README.md
@@ -1,9 +1,13 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h1>microWakeWord NVIDIA Docker Trainer UI</h1>
|
<a href="https://taterassistant.com">
|
||||||
<img width="800" alt="microWakeWord NVIDIA trainer screenshot" src="https://github.com/user-attachments/assets/694f4cb7-e4d8-4e2b-80ec-b40fb41cbfff" />
|
<img src="images/tater-repo-logo.png" alt="microWakeWord Trainer" width="460"/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 align="center">
|
||||||
|
<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 ESPHome 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 prebuilt Tater firmware flashing.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -15,6 +19,25 @@ Real samples come from device-captured wake audio, close misses, or manual uploa
|
|||||||
docker pull ghcr.io/tatertotterson/microwakeword:latest
|
docker pull ghcr.io/tatertotterson/microwakeword:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tagged releases also publish matching immutable image tags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/tatertotterson/microwakeword:v5
|
||||||
|
```
|
||||||
|
|
||||||
|
RTX 50-series / Blackwell GPUs use a separate image with CUDA 12.8 and a
|
||||||
|
Python 3.13 TensorFlow build for `sm_120`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/tatertotterson/microwakeword:blackwell
|
||||||
|
docker pull ghcr.io/tatertotterson/microwakeword:v5-blackwell
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the Blackwell image only for RTX 50-series cards. It includes the
|
||||||
|
community-built TensorFlow wheel from
|
||||||
|
[chivitiH/tensorflow-blackwell-python313](https://github.com/chivitiH/tensorflow-blackwell-python313),
|
||||||
|
which is unofficial and licensed CC BY-NC 4.0.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Run The Container
|
## Run The Container
|
||||||
@@ -28,6 +51,11 @@ 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`.
|
||||||
|
For RTX 50-series cards, use `ghcr.io/tatertotterson/microwakeword:blackwell`
|
||||||
|
or a pinned tag such as `ghcr.io/tatertotterson/microwakeword:v5-blackwell`
|
||||||
|
in the same `docker run` command.
|
||||||
|
|
||||||
The flags:
|
The flags:
|
||||||
|
|
||||||
- `--gpus all` enables GPU acceleration.
|
- `--gpus all` enables GPU acceleration.
|
||||||
@@ -52,14 +80,14 @@ If you change `REC_PORT`, open that port instead and use the same port in the ES
|
|||||||
- `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 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` builds the latest `microWakeWords` ESPHome YAMLs from GitHub and flashes VoicePE or Satellite1 over OTA.
|
- `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.
|
- Popup consoles show colorized training and firmware 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 build and flash the VoicePE or Satellite1 YAMLs directly from that repo.
|
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.
|
||||||
|
|
||||||
After flashing, the device exposes ESPHome entities for capture setup:
|
After flashing, the device exposes ESPHome entities for capture setup:
|
||||||
|
|
||||||
@@ -141,6 +169,8 @@ Personal samples are optional. Training can run with zero personal samples after
|
|||||||
|
|
||||||
Reviewed negative samples are converted into `/data/work/reviewed_negative_features/` and inserted into the training YAML as a hard-negative feature set when present.
|
Reviewed negative samples are converted into `/data/work/reviewed_negative_features/` and inserted into the training YAML as a hard-negative feature set when present.
|
||||||
|
|
||||||
|
On RTX 50-series / Blackwell GPUs, the Blackwell Docker image keeps sample generation and augmentation in the normal Python 3.12 trainer environment, then runs only the TensorFlow training/export stage in `/data/.venv-blackwell` with Python 3.13 and the Blackwell-native TensorFlow wheel.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Language Support
|
## Language Support
|
||||||
@@ -173,18 +203,18 @@ After those assets are prepared, later runs reuse the local copies unless the mo
|
|||||||
|
|
||||||
## Firmware Flashing
|
## Firmware Flashing
|
||||||
|
|
||||||
The `Firmware` tab builds and flashes Tater firmware for supported ESPHome sats.
|
The `Firmware` tab flashes prebuilt Tater firmware for supported ESPHome satellites.
|
||||||
|
|
||||||
- Downloads the latest firmware YAML templates from `TaterTotterson/microWakeWords` on GitHub.
|
- Downloads the latest prebuilt firmware manifest plus OTA and USB factory images from `TaterTotterson/microWakeWords`.
|
||||||
- Lets you choose `VoicePE` or `Satellite1`.
|
- Verifies downloaded images by size and SHA before upload.
|
||||||
- Auto-detects ESPHome devices with mDNS when the container is running with host networking.
|
- 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.
|
- Allows manual IP or hostname entry if discovery does not find the device.
|
||||||
- Saves firmware form values so you do not re-enter sounds and URLs every run.
|
- Saves the selected OTA target for each firmware family.
|
||||||
- Lists locally trained wake words from `/data/trained_wake_words/` for easy model selection.
|
- Flashes the prebuilt factory image over Browser USB for first installs or recovery when opened in Chrome or Edge.
|
||||||
- Builds with ESPHome and flashes OTA.
|
- Lists locally trained wake words from `/data/trained_wake_words/` for live model switching.
|
||||||
- Streams ESPHome output in a colorized firmware console.
|
- Streams download, verification, and OTA upload progress in a colorized firmware console.
|
||||||
|
|
||||||
Firmware YAMLs are intentionally pulled from GitHub each time. There is no local fallback path in the trainer UI.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -221,7 +251,7 @@ That removes:
|
|||||||
- cached datasets
|
- cached datasets
|
||||||
- training environments
|
- training environments
|
||||||
- trained models
|
- trained models
|
||||||
- firmware build caches
|
- downloaded firmware images
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -241,3 +271,4 @@ Built on top of:
|
|||||||
|
|
||||||
- [microWakeWord](https://github.com/kahrendt/microWakeWord)
|
- [microWakeWord](https://github.com/kahrendt/microWakeWord)
|
||||||
- [piper-sample-generator](https://github.com/rhasspy/piper-sample-generator)
|
- [piper-sample-generator](https://github.com/rhasspy/piper-sample-generator)
|
||||||
|
- [tensorflow-blackwell-python313](https://github.com/chivitiH/tensorflow-blackwell-python313) for the optional RTX 50-series / Blackwell image
|
||||||
|
|||||||
112
cli/setup_blackwell_venv
Executable file
112
cli/setup_blackwell_venv
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROGDIR="$(dirname "$(realpath "$0")")"
|
||||||
|
ROOTDIR="$(dirname "${PROGDIR}")"
|
||||||
|
|
||||||
|
KNOWN_ARGS=( data-dir force python )
|
||||||
|
source "${PROGDIR}/shell.functions"
|
||||||
|
|
||||||
|
if [ ${#UNKNOWN_ARGS[@]} -gt 0 ] ; then
|
||||||
|
echo "Unknown argument(s): ${UNKNOWN_ARGS[*]}" >&2
|
||||||
|
HELP=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${HELP}" == "true" ] ; then
|
||||||
|
cat <<EOF >&2
|
||||||
|
Usage: setup_blackwell_venv [ --data-dir=/data ] [ --force ] [ --python=python3.13 ]
|
||||||
|
|
||||||
|
Creates /data/.venv-blackwell for RTX 50 / Blackwell TensorFlow training.
|
||||||
|
Sample generation and augmentation continue to use /data/.venv.
|
||||||
|
|
||||||
|
Environment overrides:
|
||||||
|
MWW_BLACKWELL_TF_WHEEL_URL: TensorFlow Blackwell wheel URL.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "${DATA_DIR}" ] && DATA_DIR="$(realpath "${DATA_DIR}")"
|
||||||
|
[ -d "${DATA_DIR}" ] || {
|
||||||
|
echo "Data directory '${DATA_DIR}' doesn't exist." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
PYTHON="${PYTHON:-python3.13}"
|
||||||
|
VENV="${DATA_DIR}/.venv-blackwell"
|
||||||
|
MARKER="${VENV}/.mww-blackwell-venv"
|
||||||
|
TF_WHEEL_URL="${MWW_BLACKWELL_TF_WHEEL_URL:-https://github.com/chivitiH/tensorflow-blackwell-python313/releases/download/v2.22.0-selfbuilt/tensorflow-2.22.0.dev0+selfbuilt-cp313-cp313-linux_x86_64.whl}"
|
||||||
|
|
||||||
|
if ! command -v "${PYTHON}" >/dev/null 2>&1 ; then
|
||||||
|
echo "Python 3.13 is required for the Blackwell TensorFlow wheel. Missing: ${PYTHON}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${FORCE:-false}" != "true" ] && [ -x "${VENV}/bin/python" ] && [ -f "${MARKER}" ] ; then
|
||||||
|
echo " Blackwell TensorFlow venv found (skipping setup_blackwell_venv)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== Setting up Blackwell TensorFlow environment ${VENV} ====="
|
||||||
|
rm -rf "${VENV}" || :
|
||||||
|
"${PYTHON}" -m venv --upgrade-deps "${VENV}"
|
||||||
|
source "${VENV}/bin/activate"
|
||||||
|
|
||||||
|
export PIP_PROGRESS_BAR=off
|
||||||
|
export PIP_NO_COLOR=1
|
||||||
|
export PIP_QUIET=0
|
||||||
|
|
||||||
|
pip_install() {
|
||||||
|
if $VERBOSE ; then
|
||||||
|
pip install "$@" || return 1
|
||||||
|
else
|
||||||
|
{ pip install "$@" || return 1 ; } | stdbuf -i0 -o0 tr -d '[:print:]' | stdbuf -i0 -o0 tr '\n' '.'
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
echo " ===== Installing Blackwell TensorFlow wheel ====="
|
||||||
|
pip_install --upgrade pip setuptools wheel
|
||||||
|
pip_install "${TF_WHEEL_URL}"
|
||||||
|
|
||||||
|
echo " ===== Installing microWakeWord training dependencies ====="
|
||||||
|
pip_install \
|
||||||
|
audiomentations \
|
||||||
|
audio_metadata \
|
||||||
|
datasets \
|
||||||
|
mmap_ninja \
|
||||||
|
pymicro-features \
|
||||||
|
pyyaml \
|
||||||
|
webrtcvad-wheels \
|
||||||
|
ai-edge-litert \
|
||||||
|
numpy-minmax \
|
||||||
|
numpy-rms \
|
||||||
|
absl-py \
|
||||||
|
"numpy==2.3.5"
|
||||||
|
|
||||||
|
echo " ===== Checking microwakeword ====="
|
||||||
|
MWW="${DATA_DIR}/tools/microWakeWord"
|
||||||
|
if [ ! -d "${MWW}" ] || [ -n "$(git -C "${MWW}" status --porcelain 2>/dev/null || true)" ] ; then
|
||||||
|
rm -rf "${MWW}" || :
|
||||||
|
mkdir -p "${DATA_DIR}/tools"
|
||||||
|
echo " Cloning micro-wake-word to ${DATA_DIR}/tools"
|
||||||
|
git clone https://github.com/TaterTotterson/micro-wake-word "${MWW}" &>/dev/null
|
||||||
|
fi
|
||||||
|
echo " Installing microwakeword into Blackwell venv"
|
||||||
|
pip_install --no-deps -e "${MWW}"
|
||||||
|
|
||||||
|
echo " ===== Testing Blackwell TensorFlow environment ====="
|
||||||
|
"${VENV}/bin/python" - <<'PY'
|
||||||
|
import tensorflow as tf
|
||||||
|
from ai_edge_litert.interpreter import Interpreter
|
||||||
|
from microwakeword.data import FeatureHandler
|
||||||
|
from microwakeword.inference import Model
|
||||||
|
|
||||||
|
print("TensorFlow:", tf.__version__)
|
||||||
|
print("CUDA build:", tf.test.is_built_with_cuda())
|
||||||
|
print("GPU:", tf.config.list_physical_devices("GPU"))
|
||||||
|
print("microWakeWord Blackwell imports available")
|
||||||
|
PY
|
||||||
|
|
||||||
|
touch "${MARKER}"
|
||||||
|
echo "Blackwell TensorFlow environment ready: ${VENV}"
|
||||||
@@ -25,9 +25,9 @@ fi
|
|||||||
mkdir -p "${DATA_DIR}/training_datasets/downloads" || :
|
mkdir -p "${DATA_DIR}/training_datasets/downloads" || :
|
||||||
cd "${DATA_DIR}/training_datasets"
|
cd "${DATA_DIR}/training_datasets"
|
||||||
|
|
||||||
AUDIO_URL="https://mcdermottlab.mit.edu/Reverb/IRMAudio/Audio.zip"
|
HF_RIR_REPO_ID="TaterTotterson/MIT_environmental_impulse_responses"
|
||||||
AUDIO_ZIPFILE="MIT_RIR_Audio.zip"
|
HF_RIR_API_URL="https://huggingface.co/api/datasets/${HF_RIR_REPO_ID}"
|
||||||
AUDIO_ZIP="./downloads/${AUDIO_ZIPFILE}"
|
HF_RIR_SOURCE_KEY="hf_mit_environmental_impulse_responses"
|
||||||
AUDIO_DIR="./mit_rirs"
|
AUDIO_DIR="./mit_rirs"
|
||||||
mkdir -p "${AUDIO_DIR}" || :
|
mkdir -p "${AUDIO_DIR}" || :
|
||||||
AUDIO16K_DIR="./mit_rirs_16k"
|
AUDIO16K_DIR="./mit_rirs_16k"
|
||||||
@@ -35,10 +35,92 @@ mkdir -p "${AUDIO16K_DIR}" || :
|
|||||||
AUDIO_FILECOUNT="./downloads/mit_rir_filecount"
|
AUDIO_FILECOUNT="./downloads/mit_rir_filecount"
|
||||||
AUDIO_IN_GLOB="*.wav"
|
AUDIO_IN_GLOB="*.wav"
|
||||||
|
|
||||||
declare -A filecounts=( [${AUDIO_ZIPFILE}]=0 )
|
declare -A filecounts=( [${HF_RIR_SOURCE_KEY}]=0 )
|
||||||
get_filecounts filecounts "${AUDIO_FILECOUNT}"
|
get_filecounts filecounts "${AUDIO_FILECOUNT}"
|
||||||
|
|
||||||
echo "===== Checking MIT_RIR ====="
|
echo "===== Checking MIT environmental RIRs ====="
|
||||||
|
|
||||||
|
download_hf_mit_rirs() {
|
||||||
|
source ${DATA_DIR}/.venv/bin/activate
|
||||||
|
python - "${HF_RIR_REPO_ID}" "${HF_RIR_API_URL}" "${AUDIO_DIR}" <<-'EOF'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
repo_id = sys.argv[1]
|
||||||
|
api_url = sys.argv[2]
|
||||||
|
audio_dir = Path(sys.argv[3])
|
||||||
|
audio_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
request = urllib.request.Request(api_url, headers={"User-Agent": "WakeWordTrainer/1.0"})
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
metadata = json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
files = sorted(
|
||||||
|
sibling.get("rfilename", "")
|
||||||
|
for sibling in metadata.get("siblings", [])
|
||||||
|
if str(sibling.get("rfilename", "")).startswith("16khz/")
|
||||||
|
and str(sibling.get("rfilename", "")).lower().endswith(".wav")
|
||||||
|
)
|
||||||
|
if not files:
|
||||||
|
raise SystemExit("Hugging Face MIT RIR dataset did not list any 16khz WAV files")
|
||||||
|
|
||||||
|
print(f" Found {len(files)} MIT environmental RIR files on Hugging Face mirror", flush=True)
|
||||||
|
downloaded = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
def download_file(url: str, target: Path, rel: str):
|
||||||
|
tmp = target.with_suffix(target.suffix + ".incomplete")
|
||||||
|
for attempt in range(1, 4):
|
||||||
|
try:
|
||||||
|
if tmp.exists():
|
||||||
|
tmp.unlink()
|
||||||
|
with urllib.request.urlopen(url, timeout=30) as response:
|
||||||
|
with tmp.open("wb") as out:
|
||||||
|
while True:
|
||||||
|
chunk = response.read(1024 * 64)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
out.write(chunk)
|
||||||
|
if not tmp.exists() or tmp.stat().st_size == 0:
|
||||||
|
raise RuntimeError("empty download")
|
||||||
|
tmp.replace(target)
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
if tmp.exists():
|
||||||
|
tmp.unlink()
|
||||||
|
if attempt == 3:
|
||||||
|
raise RuntimeError(f"download failed for {rel}: {exc}") from exc
|
||||||
|
print(f" Retry {attempt}/2 for {rel}: {exc}", flush=True)
|
||||||
|
time.sleep(2 * attempt)
|
||||||
|
|
||||||
|
total = len(files)
|
||||||
|
for idx, rel in enumerate(files, start=1):
|
||||||
|
target = audio_dir / rel
|
||||||
|
if target.exists() and target.stat().st_size > 0:
|
||||||
|
skipped += 1
|
||||||
|
if idx == 1 or idx % 25 == 0 or idx == total:
|
||||||
|
print(f" MIT RIR download progress: {idx}/{total} files ({downloaded} downloaded, {skipped} reused)", flush=True)
|
||||||
|
continue
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
encoded = urllib.parse.quote(rel, safe="/")
|
||||||
|
url = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{encoded}"
|
||||||
|
if idx == 1 or idx % 25 == 0 or idx == total:
|
||||||
|
print(f" Downloading MIT RIR {idx}/{total}: {rel}", flush=True)
|
||||||
|
download_file(url, target, rel)
|
||||||
|
if not target.exists() or target.stat().st_size == 0:
|
||||||
|
raise SystemExit(f"download failed for {rel}")
|
||||||
|
downloaded += 1
|
||||||
|
if idx == 1 or idx % 25 == 0 or idx == total:
|
||||||
|
print(f" MIT RIR download progress: {idx}/{total} files ({downloaded} downloaded, {skipped} reused)", flush=True)
|
||||||
|
|
||||||
|
print(f" Hugging Face MIT environmental RIR download complete ({downloaded} downloaded, {skipped} reused)", flush=True)
|
||||||
|
print(f" MIT environmental RIR files available: {len(files)}", flush=True)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
converter() {
|
converter() {
|
||||||
source ${DATA_DIR}/.venv/bin/activate
|
source ${DATA_DIR}/.venv/bin/activate
|
||||||
@@ -58,9 +140,9 @@ rir_out = Path(sys.argv[2])
|
|||||||
|
|
||||||
waves = list(rir_in.rglob("*.wav"))
|
waves = list(rir_in.rglob("*.wav"))
|
||||||
try:
|
try:
|
||||||
print(" MIT RIR normalizing to 16k…")
|
print(" MIT environmental RIR normalizing to 16k…")
|
||||||
# Normalize to 16k mono
|
# Normalize to 16k mono
|
||||||
for p in tqdm(waves, desc=" MIT_RIR (resample 16k mono)"):
|
for p in tqdm(waves, desc=" MIT environmental RIR (resample 16k mono)"):
|
||||||
outfile = Path(rir_out / p.name)
|
outfile = Path(rir_out / p.name)
|
||||||
if outfile.exists():
|
if outfile.exists():
|
||||||
continue
|
continue
|
||||||
@@ -70,14 +152,14 @@ try:
|
|||||||
if sr != 16000:
|
if sr != 16000:
|
||||||
a, _ = librosa.load(p, sr=16000, mono=True)
|
a, _ = librosa.load(p, sr=16000, mono=True)
|
||||||
write_wav(outfile, a, 16000)
|
write_wav(outfile, a, 16000)
|
||||||
print(" MIT RIR normalization complete")
|
print(" MIT environmental RIR normalization complete")
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
print(f" MIT RIR fallback failed: {e2}")
|
print(f" MIT environmental RIR preparation failed: {e2}")
|
||||||
raise
|
raise
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_filecount=${filecounts[${AUDIO_ZIPFILE}]}
|
expected_filecount=${filecounts[${HF_RIR_SOURCE_KEY}]}
|
||||||
actual_filecount=$(find "${AUDIO16K_DIR}" -name '*.wav' 2>/dev/null | wc -l) || :
|
actual_filecount=$(find "${AUDIO16K_DIR}" -name '*.wav' 2>/dev/null | wc -l) || :
|
||||||
write_filecount=false
|
write_filecount=false
|
||||||
|
|
||||||
@@ -85,24 +167,16 @@ if [ "${actual_filecount}" -ne 0 ] && [ "${actual_filecount}" -eq "${expected_fi
|
|||||||
echo " Existing ${AUDIO16K_DIR} valid"
|
echo " Existing ${AUDIO16K_DIR} valid"
|
||||||
else
|
else
|
||||||
actual_filecount=$(find "${AUDIO_DIR}" -name "${AUDIO_IN_GLOB}" 2>/dev/null | wc -l) || :
|
actual_filecount=$(find "${AUDIO_DIR}" -name "${AUDIO_IN_GLOB}" 2>/dev/null | wc -l) || :
|
||||||
if [ "${actual_filecount}" -eq 0 ] || [ "${actual_filecount}" -ne "${expected_filecount}" ] ; then
|
if [ "${actual_filecount}" -eq 0 ] || [ "${expected_filecount}" -eq 0 ] || [ "${actual_filecount}" -ne "${expected_filecount}" ] ; then
|
||||||
if [ ! -f "${AUDIO_ZIP}" ] ; then
|
|
||||||
echo " Downloading ${AUDIO_ZIPFILE}"
|
|
||||||
curl -sfL "${AUDIO_URL}" -o "${AUDIO_ZIP}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf "${AUDIO_DIR}" || :
|
rm -rf "${AUDIO_DIR}" || :
|
||||||
echo " Unzipping ${AUDIO_ZIPFILE}"
|
mkdir -p "${AUDIO_DIR}" || :
|
||||||
unzip -u -q -d "${AUDIO_DIR}" "${AUDIO_ZIP}"
|
echo " Downloading MIT environmental impulse responses from Hugging Face mirror"
|
||||||
fi
|
download_hf_mit_rirs
|
||||||
if "${CLEANUP_ARCHIVES}" && [ -f "${AUDIO_ZIP}" ] ; then
|
|
||||||
echo " Cleaning up ${AUDIO_ZIPFILE}"
|
|
||||||
rm -rf "${AUDIO_ZIP}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
converter
|
converter
|
||||||
actual_filecount=$(find "${AUDIO16K_DIR}" -name "*.wav" 2>/dev/null | wc -l) || :
|
actual_filecount=$(find "${AUDIO16K_DIR}" -name "*.wav" 2>/dev/null | wc -l) || :
|
||||||
filecounts[${AUDIO_ZIPFILE}]="${actual_filecount}"
|
filecounts[${HF_RIR_SOURCE_KEY}]="${actual_filecount}"
|
||||||
write_filecount=true
|
write_filecount=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -110,15 +184,10 @@ if ${write_filecount} ; then
|
|||||||
write_filecounts filecounts "${AUDIO_FILECOUNT}"
|
write_filecounts filecounts "${AUDIO_FILECOUNT}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if "${CLEANUP_ARCHIVES}" && [ -f "${AUDIO_ZIP}" ] ; then
|
|
||||||
echo " Cleaning up ${AUDIO_ZIPFILE}"
|
|
||||||
rm -rf "${AUDIO_ZIP}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if "${CLEANUP_INTERMEDIATE_FILES}" && [ -d "${AUDIO_DIR}" ]; then
|
if "${CLEANUP_INTERMEDIATE_FILES}" && [ -d "${AUDIO_DIR}" ]; then
|
||||||
echo " Cleaning up ${AUDIO_DIR}"
|
echo " Cleaning up ${AUDIO_DIR}"
|
||||||
rm -rf "${AUDIO_DIR}"
|
rm -rf "${AUDIO_DIR}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " MIT_RIR complete"
|
echo " MIT environmental RIRs complete"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -84,6 +84,21 @@ if [ "${IS_BLACKWELL}" = "true" ]; then
|
|||||||
echo "ℹ️ Using GPU compatibility retries; CPU fallback is ${ALLOW_CPU_FALLBACK} (override with MWW_ALLOW_CPU_FALLBACK=true|false)."
|
echo "ℹ️ Using GPU compatibility retries; CPU fallback is ${ALLOW_CPU_FALLBACK} (override with MWW_ALLOW_CPU_FALLBACK=true|false)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
BLACKWELL_TF_MODE="${MWW_BLACKWELL_TF:-auto}"
|
||||||
|
BLACKWELL_TF_REQUIRED="false"
|
||||||
|
BLACKWELL_TF_ACTIVE="false"
|
||||||
|
case "${BLACKWELL_TF_MODE,,}" in
|
||||||
|
1|true|yes|on|required)
|
||||||
|
BLACKWELL_TF_REQUIRED="true"
|
||||||
|
;;
|
||||||
|
0|false|no|off|disabled)
|
||||||
|
BLACKWELL_TF_MODE="disabled"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
BLACKWELL_TF_MODE="auto"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Enable driver-side PTX JIT fallback when ptxas/nvlink are unavailable.
|
# Enable driver-side PTX JIT fallback when ptxas/nvlink are unavailable.
|
||||||
if [ -z "${XLA_FLAGS:-}" ]; then
|
if [ -z "${XLA_FLAGS:-}" ]; then
|
||||||
export XLA_FLAGS="--xla_gpu_unsafe_fallback_to_driver_on_ptxas_not_found"
|
export XLA_FLAGS="--xla_gpu_unsafe_fallback_to_driver_on_ptxas_not_found"
|
||||||
@@ -238,6 +253,32 @@ fi
|
|||||||
echo " Wrote training_parameters.yaml"
|
echo " Wrote training_parameters.yaml"
|
||||||
rm -rf "${WORK_DIR}/trained_models/wakeword"
|
rm -rf "${WORK_DIR}/trained_models/wakeword"
|
||||||
|
|
||||||
|
if [ "${IS_BLACKWELL}" = "true" ] && [ "${BLACKWELL_TF_MODE}" != "disabled" ]; then
|
||||||
|
BLACKWELL_SETUP="${PROGDIR}/setup_blackwell_venv"
|
||||||
|
BLACKWELL_PYTHON="${DATA_DIR}/.venv-blackwell/bin/python"
|
||||||
|
|
||||||
|
if [ -x "${BLACKWELL_SETUP}" ] && command -v python3.13 >/dev/null 2>&1; then
|
||||||
|
echo "↪️ Preparing Blackwell-native TensorFlow training environment."
|
||||||
|
if "${BLACKWELL_SETUP}" --data-dir="${DATA_DIR}"; then
|
||||||
|
PYTHON_BIN="${BLACKWELL_PYTHON}"
|
||||||
|
BLACKWELL_TF_ACTIVE="true"
|
||||||
|
echo "✅ Blackwell TensorFlow training enabled: ${PYTHON_BIN}"
|
||||||
|
else
|
||||||
|
if [ "${BLACKWELL_TF_REQUIRED}" = "true" ]; then
|
||||||
|
echo "❌ Blackwell TensorFlow setup failed and MWW_BLACKWELL_TF is required." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "⚠️ Blackwell TensorFlow setup failed; continuing with compatibility retries."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "${BLACKWELL_TF_REQUIRED}" = "true" ]; then
|
||||||
|
echo "❌ Blackwell TensorFlow was required, but python3.13/setup_blackwell_venv is unavailable." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "ℹ️ Blackwell TensorFlow image support not available; continuing with compatibility retries."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
wake_word_filename="$(
|
wake_word_filename="$(
|
||||||
echo "${WAKE_WORD}" \
|
echo "${WAKE_WORD}" \
|
||||||
| tr '[:upper:]' '[:lower:]' \
|
| tr '[:upper:]' '[:lower:]' \
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
# System deps
|
# System deps
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3.12 python3.12-venv python3.12-dev python3-pip python-is-python3 \
|
python3.12 python3.12-venv python3.12-dev python3-pip python-is-python3 \
|
||||||
git wget curl unzip patch ca-certificates nano less \
|
git wget curl unzip patch ninja-build ca-certificates nano less \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /data
|
&& mkdir -p /data
|
||||||
|
|
||||||
|
|||||||
54
dockerfile.blackwell
Normal file
54
dockerfile.blackwell
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# RTX 50 / Blackwell image
|
||||||
|
FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV CUDA_HOME=/usr/local/cuda
|
||||||
|
ENV PATH=/usr/local/cuda/bin:${PATH}
|
||||||
|
ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:${LD_LIBRARY_PATH}
|
||||||
|
ENV MWW_BLACKWELL_IMAGE=1
|
||||||
|
ENV MWW_BLACKWELL_TF=auto
|
||||||
|
ENV MWW_BLACKWELL_TF_WHEEL_URL=https://github.com/chivitiH/tensorflow-blackwell-python313/releases/download/v2.22.0-selfbuilt/tensorflow-2.22.0.dev0+selfbuilt-cp313-cp313-linux_x86_64.whl
|
||||||
|
|
||||||
|
# System deps. Python 3.12 remains the main trainer/runtime venv, while
|
||||||
|
# Python 3.13 is used only for the Blackwell TensorFlow training step.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
software-properties-common ca-certificates curl git wget unzip patch \
|
||||||
|
ninja-build nano less \
|
||||||
|
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
python3.12 python3.12-venv python3.12-dev \
|
||||||
|
python3.13 python3.13-venv python3.13-dev \
|
||||||
|
python3-pip python-is-python3 \
|
||||||
|
&& ldconfig \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir -p /data
|
||||||
|
|
||||||
|
# Trainer UI port
|
||||||
|
EXPOSE 8789
|
||||||
|
|
||||||
|
# Script root
|
||||||
|
WORKDIR /root/mww-scripts
|
||||||
|
|
||||||
|
# Bash environment
|
||||||
|
COPY --chown=root:root --chmod=0755 .bashrc /root/
|
||||||
|
|
||||||
|
# Root-level entrypoints
|
||||||
|
COPY --chown=root:root --chmod=0755 \
|
||||||
|
train_wake_word \
|
||||||
|
run.sh \
|
||||||
|
trainer_server.py \
|
||||||
|
requirements.txt \
|
||||||
|
/root/mww-scripts/
|
||||||
|
|
||||||
|
# CLI folder
|
||||||
|
COPY --chown=root:root cli/ /root/mww-scripts/cli/
|
||||||
|
|
||||||
|
# Make all CLI scripts executable (avoids "Permission denied")
|
||||||
|
RUN chmod -R a+x /root/mww-scripts/cli
|
||||||
|
|
||||||
|
# Static UI for trainer
|
||||||
|
COPY --chown=root:root --chmod=0644 static/index.html /root/mww-scripts/static/index.html
|
||||||
|
|
||||||
|
# trainer server
|
||||||
|
CMD ["/bin/bash", "-lc", "/root/mww-scripts/run.sh"]
|
||||||
BIN
images/tater-repo-logo.png
Normal file
BIN
images/tater-repo-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 590 KiB |
67
run.sh
67
run.sh
@@ -17,7 +17,6 @@ PIN_FILE="${VENV_DIR}/.pinned_installed"
|
|||||||
FASTAPI_VERSION="${REC_FASTAPI_VERSION:-0.115.6}"
|
FASTAPI_VERSION="${REC_FASTAPI_VERSION:-0.115.6}"
|
||||||
UVICORN_VERSION="${REC_UVICORN_VERSION:-0.30.6}"
|
UVICORN_VERSION="${REC_UVICORN_VERSION:-0.30.6}"
|
||||||
PY_MULTIPART_VERSION="${REC_PY_MULTIPART_VERSION:-0.0.9}"
|
PY_MULTIPART_VERSION="${REC_PY_MULTIPART_VERSION:-0.0.9}"
|
||||||
ESPHOME_VERSION="${REC_ESPHOME_VERSION:-2026.4.0}"
|
|
||||||
|
|
||||||
echo "microWakeWord Trainer UI (Docker)"
|
echo "microWakeWord Trainer UI (Docker)"
|
||||||
echo "-> ROOTDIR: ${ROOTDIR}"
|
echo "-> ROOTDIR: ${ROOTDIR}"
|
||||||
@@ -26,6 +25,16 @@ echo "-> URL: http://localhost:${PORT}/"
|
|||||||
|
|
||||||
mkdir -p "${DATA_DIR}"
|
mkdir -p "${DATA_DIR}"
|
||||||
|
|
||||||
|
install_ui_deps() {
|
||||||
|
${PIP} install \
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Trainer UI venv (separate)
|
# Trainer UI venv (separate)
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
@@ -40,32 +49,54 @@ source "${VENV_DIR}/bin/activate"
|
|||||||
if [[ ! -f "${PIN_FILE}" ]]; then
|
if [[ ! -f "${PIN_FILE}" ]]; then
|
||||||
echo "Installing pinned trainer UI deps"
|
echo "Installing pinned trainer UI deps"
|
||||||
${PIP} install -U pip setuptools wheel
|
${PIP} install -U pip setuptools wheel
|
||||||
${PIP} install \
|
install_ui_deps
|
||||||
"fastapi==${FASTAPI_VERSION}" \
|
|
||||||
"uvicorn[standard]==${UVICORN_VERSION}" \
|
|
||||||
"python-multipart==${PY_MULTIPART_VERSION}" \
|
|
||||||
"esphome==${ESPHOME_VERSION}"
|
|
||||||
touch "${PIN_FILE}"
|
touch "${PIN_FILE}"
|
||||||
else
|
else
|
||||||
echo "Reusing existing trainer UI venv (no upgrades)"
|
echo "Reusing existing trainer UI venv (no upgrades)"
|
||||||
if ! "${PY}" - "${ESPHOME_VERSION}" <<'PY' >/dev/null 2>&1
|
if ! "${PY}" - "${FASTAPI_VERSION}" "${UVICORN_VERSION}" "${PY_MULTIPART_VERSION}" <<'PY' >/dev/null 2>&1
|
||||||
import importlib.metadata
|
import importlib.metadata as md
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
expected = sys.argv[1]
|
fastapi_version, uvicorn_version, multipart_version = sys.argv[1:4]
|
||||||
installed = importlib.metadata.version("esphome")
|
|
||||||
raise SystemExit(0 if installed == expected else 1)
|
def version_tuple(value):
|
||||||
|
parts = []
|
||||||
|
for token in str(value).replace("-", ".").split("."):
|
||||||
|
if token.isdigit():
|
||||||
|
parts.append(int(token))
|
||||||
|
else:
|
||||||
|
digits = "".join(ch for ch in token if ch.isdigit())
|
||||||
|
if digits:
|
||||||
|
parts.append(int(digits))
|
||||||
|
break
|
||||||
|
return tuple(parts)
|
||||||
|
|
||||||
|
exact = {
|
||||||
|
"fastapi": fastapi_version,
|
||||||
|
"uvicorn": uvicorn_version,
|
||||||
|
"python-multipart": multipart_version,
|
||||||
|
}
|
||||||
|
minimum = {
|
||||||
|
"silero-vad": "5.0.0",
|
||||||
|
"numpy": "1.24.0",
|
||||||
|
"zeroconf": "0.132.2",
|
||||||
|
}
|
||||||
|
present = ("torch",)
|
||||||
|
|
||||||
|
for package, expected in exact.items():
|
||||||
|
if md.version(package) != expected:
|
||||||
|
raise SystemExit(1)
|
||||||
|
for package, minimum_version in minimum.items():
|
||||||
|
if version_tuple(md.version(package)) < version_tuple(minimum_version):
|
||||||
|
raise SystemExit(1)
|
||||||
|
for package in present:
|
||||||
|
md.version(package)
|
||||||
PY
|
PY
|
||||||
then
|
then
|
||||||
echo "Firmware tab dependencies missing or stale; installing ESPHome firmware dependencies"
|
echo "UI dependencies missing or stale; installing recorder dependencies"
|
||||||
${PIP} install \
|
install_ui_deps
|
||||||
"fastapi==${FASTAPI_VERSION}" \
|
|
||||||
"uvicorn[standard]==${UVICORN_VERSION}" \
|
|
||||||
"python-multipart==${PY_MULTIPART_VERSION}" \
|
|
||||||
"esphome==${ESPHOME_VERSION}"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Trainer server env
|
# Trainer server env
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|||||||
2070
static/index.html
2070
static/index.html
File diff suppressed because it is too large
Load Diff
1392
trainer_server.py
1392
trainer_server.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user