Test 2 skeleton: openwakeword loads via --no-deps split, scores print
This commit is contained in:
+4
-1
@@ -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 \
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1 @@
|
||||
openwakeword==0.6.0
|
||||
@@ -0,0 +1,7 @@
|
||||
sounddevice==0.5.1
|
||||
numpy>=2.0
|
||||
onnxruntime>=1.18
|
||||
scipy
|
||||
scikit-learn
|
||||
tqdm
|
||||
requests
|
||||
Reference in New Issue
Block a user