Plan 3: openwakeword wrapper with threshold + cooldown

This commit is contained in:
2026-06-11 22:49:40 +00:00
parent 55257b8867
commit d0bd141082
+47
View File
@@ -0,0 +1,47 @@
"""openwakeword wrapper for the Pi client.
Loads the stock `alexa` model. Each call to `predict()` takes a 1280-sample
16 kHz mono int16 frame (produced by `client.audio.resample_24k_to_16k`).
Returns True at most once per cooldown window.
"""
import time
import numpy as np
from client.log import get_logger
_log = get_logger("wakeword")
WAKEWORD = "alexa"
DEFAULT_THRESHOLD = 0.5
DEFAULT_COOLDOWN_S = 1.5
class WakewordDetector:
def __init__(self, *, threshold: float = DEFAULT_THRESHOLD,
cooldown_s: float = DEFAULT_COOLDOWN_S) -> None:
# Lazy-import openwakeword to keep the test suite importable without it.
import openwakeword.utils
from openwakeword.model import Model
_log.info("downloading openwakeword models (idempotent)")
# No-arg form dodges the None-handling crash documented in findings.md §4.
openwakeword.utils.download_models()
_log.info("loading openwakeword model %r", WAKEWORD)
t0 = time.monotonic()
self._model = Model(wakeword_models=[WAKEWORD], inference_framework="onnx")
_log.info("openwakeword model loaded in %.2fs", time.monotonic() - t0)
self._threshold = threshold
self._cooldown_s = cooldown_s
self._last_fired = 0.0
def predict(self, frame_16k_int16: np.ndarray) -> bool:
scores = self._model.predict(frame_16k_int16)
score = float(scores.get(WAKEWORD, 0.0))
now = time.monotonic()
if score >= self._threshold and (now - self._last_fired) >= self._cooldown_s:
self._last_fired = now
_log.info("wakeword fired score=%.3f", score)
return True
return False