From 2cea1e8de060ecc7e0095d16020f64d253266a59 Mon Sep 17 00:00:00 2001 From: Assistant builder Date: Thu, 11 Jun 2026 14:54:45 +0000 Subject: [PATCH] Test 2 skeleton: openwakeword loads via --no-deps split, scores print --- bin/deploy | 5 +- .../2026-06-11-voice-assistant-prototypes.md | 25 ++++-- tests/02-wakeword/main.py | 78 +++++++++++++++++++ tests/02-wakeword/requirements-nodeps.txt | 1 + tests/02-wakeword/requirements.txt | 7 ++ 5 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 tests/02-wakeword/main.py create mode 100644 tests/02-wakeword/requirements-nodeps.txt create mode 100644 tests/02-wakeword/requirements.txt diff --git a/bin/deploy b/bin/deploy index 19f39dc..d4f564c 100644 --- a/bin/deploy +++ b/bin/deploy @@ -35,7 +35,10 @@ sshpass -p "$PI_PASS" ssh -o StrictHostKeyChecking=accept-new \ cd '$PI_PATH' [ -d .venv ] || python3 -m venv .venv .venv/bin/pip install --upgrade pip -q - .venv/bin/pip install -q -r '$TEST_DIR/requirements.txt'" 2>&1 | REDACT + .venv/bin/pip install -q -r '$TEST_DIR/requirements.txt' + if [ -f '$TEST_DIR/requirements-nodeps.txt' ]; then + .venv/bin/pip install -q --no-deps -r '$TEST_DIR/requirements-nodeps.txt' + fi" 2>&1 | REDACT echo "==> run $TEST_DIR/main.py" sshpass -p "$PI_PASS" ssh -t -o StrictHostKeyChecking=accept-new \ diff --git a/docs/superpowers/plans/2026-06-11-voice-assistant-prototypes.md b/docs/superpowers/plans/2026-06-11-voice-assistant-prototypes.md index 73765dd..e12f084 100644 --- a/docs/superpowers/plans/2026-06-11-voice-assistant-prototypes.md +++ b/docs/superpowers/plans/2026-06-11-voice-assistant-prototypes.md @@ -12,6 +12,8 @@ **Audio note:** The Pi's USB Speaker Phone only supports 48 kHz natively. We set `PA_ALSA_PLUGHW=1` at the top of every test's `main.py` (before importing sounddevice) so PortAudio opens via ALSA's `plughw:` device, which auto-resamples to the requested 16 kHz. +**openwakeword install note:** openwakeword 0.6.0 declares a hard `tflite-runtime` dep that has no Python 3.13 wheel; we install openwakeword with `--no-deps` from a separate `requirements-nodeps.txt` and pin its real runtime deps in the regular `requirements.txt`. `bin/deploy` auto-installs `requirements-nodeps.txt` after `requirements.txt` if it exists. + **Spec:** `docs/superpowers/specs/2026-06-11-voice-assistant-prototypes-design.md` --- @@ -176,7 +178,10 @@ sshpass -p "$PI_PASS" ssh -o StrictHostKeyChecking=accept-new \ cd '$PI_PATH' [ -d .venv ] || python3 -m venv .venv .venv/bin/pip install --upgrade pip -q - .venv/bin/pip install -q -r '$TEST_DIR/requirements.txt'" 2>&1 | REDACT + .venv/bin/pip install -q -r '$TEST_DIR/requirements.txt' + if [ -f '$TEST_DIR/requirements-nodeps.txt' ]; then + .venv/bin/pip install -q --no-deps -r '$TEST_DIR/requirements-nodeps.txt' + fi" 2>&1 | REDACT echo "==> run $TEST_DIR/main.py" sshpass -p "$PI_PASS" ssh -t -o StrictHostKeyChecking=accept-new \ @@ -406,15 +411,25 @@ git commit -q -m "Test 1: add playback step — record-play loop complete" **Files:** - Create: `tests/02-wakeword/requirements.txt` +- Create: `tests/02-wakeword/requirements-nodeps.txt` - Create: `tests/02-wakeword/main.py` -- [ ] **Step 1: Write `tests/02-wakeword/requirements.txt`** +- [ ] **Step 1: Write `tests/02-wakeword/requirements.txt`** — openwakeword's real runtime deps for ONNX mode, resolved normally ``` sounddevice==0.5.1 numpy>=2.0 -openwakeword==0.6.0 onnxruntime>=1.18 +scipy +scikit-learn +tqdm +requests +``` + +- [ ] **Step 1b: Write `tests/02-wakeword/requirements-nodeps.txt`** — openwakeword itself, installed with `--no-deps` so pip skips the unsatisfiable `tflite-runtime` declared dep + +``` +openwakeword==0.6.0 ``` - [ ] **Step 2: Write `tests/02-wakeword/main.py` — skeleton, prints scores but no detection logic yet** @@ -457,7 +472,7 @@ def find_usb_device() -> int: def main() -> int: print("Ensuring openwakeword onnx models are downloaded (idempotent)...") - openwakeword.utils.download_models(target_directory=None) + openwakeword.utils.download_models() print("Loading openwakeword model...") t0 = time.monotonic() @@ -583,7 +598,7 @@ def beep(device: int) -> None: def main() -> int: print("Ensuring openwakeword onnx models are downloaded (idempotent)...") - openwakeword.utils.download_models(target_directory=None) + openwakeword.utils.download_models() print("Loading openwakeword model...") t0 = time.monotonic() diff --git a/tests/02-wakeword/main.py b/tests/02-wakeword/main.py new file mode 100644 index 0000000..f3404b7 --- /dev/null +++ b/tests/02-wakeword/main.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Skeleton: load openwakeword's 'alexa' model and print scores every 80 ms. + +This task validates that the model loads, the mic stream produces 1280-sample +frames, and predict() returns a non-empty score dict. Detection + beep come +in the next task. +""" +import os +os.environ.setdefault("PA_ALSA_PLUGHW", "1") + +import sys +import time + +import numpy as np +import sounddevice as sd +import openwakeword.utils +from openwakeword.model import Model + +SAMPLE_RATE = 16_000 +FRAME_SAMPLES = 1280 # 80 ms @ 16 kHz, openwakeword's expected chunk +CHANNELS = 1 +DTYPE = "int16" +WAKEWORD = "alexa" + + +def find_usb_device() -> int: + for idx, dev in enumerate(sd.query_devices()): + name = dev["name"].lower() + if "usb" in name and dev["max_input_channels"] >= 1: + return idx + print("USB audio device not found. Devices:", file=sys.stderr) + print(sd.query_devices(), file=sys.stderr) + sys.exit(1) + + +def main() -> int: + print("Ensuring openwakeword onnx models are downloaded (idempotent)...") + openwakeword.utils.download_models() + + print("Loading openwakeword model...") + t0 = time.monotonic() + model = Model(wakeword_models=[WAKEWORD], inference_framework="onnx") + print( + f"Model loaded in {time.monotonic() - t0:.2f}s. " + f"Score keys: {list(model.models.keys())}" + ) + + device = find_usb_device() + print( + f"Streaming from device {device} ({sd.query_devices(device)['name']!r}). " + f"Will print {WAKEWORD} score 5x/s for ~10s, then exit." + ) + + start = time.monotonic() + last_print = 0.0 + with sd.InputStream( + samplerate=SAMPLE_RATE, + channels=CHANNELS, + dtype=DTYPE, + device=device, + blocksize=FRAME_SAMPLES, + ) as stream: + while time.monotonic() - start < 10.0: + frame, overflowed = stream.read(FRAME_SAMPLES) + if overflowed: + print("[status] input overflow", file=sys.stderr) + mono = frame[:, 0] + scores = model.predict(mono) + now = time.monotonic() + if now - last_print >= 0.2: + score = float(scores.get(WAKEWORD, 0.0)) + print(f"t={now - start:4.1f}s {WAKEWORD}={score:.3f}") + last_print = now + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/02-wakeword/requirements-nodeps.txt b/tests/02-wakeword/requirements-nodeps.txt new file mode 100644 index 0000000..7e39f69 --- /dev/null +++ b/tests/02-wakeword/requirements-nodeps.txt @@ -0,0 +1 @@ +openwakeword==0.6.0 diff --git a/tests/02-wakeword/requirements.txt b/tests/02-wakeword/requirements.txt new file mode 100644 index 0000000..fa3fe06 --- /dev/null +++ b/tests/02-wakeword/requirements.txt @@ -0,0 +1,7 @@ +sounddevice==0.5.1 +numpy>=2.0 +onnxruntime>=1.18 +scipy +scikit-learn +tqdm +requests