import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import numpy as np import pytest from bolt_pet.audio import wake_word class _FakeStream: """Yields a fixed sequence of frames, then silence forever.""" def __init__(self, frames, frame_len): self._frames = list(frames) self._frame_len = frame_len def read(self, frames): if self._frames: frame = self._frames.pop(0) else: frame = np.zeros(self._frame_len, dtype=np.int16) return frame.reshape(-1, 1), False def _frame(frame_len): return np.zeros(frame_len, dtype=np.int16) class _FakeModel: """Reports the given score sequence (one dict per predict() call, then repeats the last entry) and records reset() calls.""" def __init__(self, score_sequence): self._scores = list(score_sequence) self.reset_calls = 0 def predict(self, frame): if self._scores: return self._scores.pop(0) return {"thunderbolt": 0.0} def reset(self): self.reset_calls += 1 def test_returns_true_and_resets_on_detection(monkeypatch): frame_len = 1280 monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) # 1 frame monkeypatch.setattr(wake_word.config, "WAKE_WORD_THRESHOLD", 0.5) stream = _FakeStream([_frame(frame_len)] * 3, frame_len) model = _FakeModel([{"thunderbolt": 0.1}, {"thunderbolt": 0.9}]) detected = wake_word.listen_for_wake_word(stream, model=model) assert detected is True assert model.reset_calls == 1 def test_returns_false_when_should_continue_goes_false_first(): frame_len = 1280 stream = _FakeStream([_frame(frame_len)] * 5, frame_len) model = _FakeModel([{"thunderbolt": 0.0}] * 5) calls = {"n": 0} def should_continue(): calls["n"] += 1 return calls["n"] <= 3 detected = wake_word.listen_for_wake_word( stream, should_continue=should_continue, model=model, ) assert detected is False assert model.reset_calls == 0 def test_custom_threshold_is_respected(monkeypatch): frame_len = 1280 monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) stream = _FakeStream([_frame(frame_len)] * 3, frame_len) model = _FakeModel([{"thunderbolt": 0.6}] * 3) calls = {"n": 0} def should_continue(): calls["n"] += 1 return calls["n"] <= 3 detected = wake_word.listen_for_wake_word( stream, should_continue=should_continue, model=model, threshold=0.7, ) assert detected is False def test_on_tick_fires_once_per_check_interval(monkeypatch): frame_len = 1280 monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) # 1 frame/check frames = [_frame(frame_len) for _ in range(5)] stream = _FakeStream(frames, frame_len) model = _FakeModel([{"thunderbolt": 0.0}] * 5) ticks = {"n": 0} state = {"i": 0} def should_continue(): state["i"] += 1 return state["i"] <= 5 wake_word.listen_for_wake_word( stream, should_continue=should_continue, model=model, on_tick=lambda: ticks.__setitem__("n", ticks["n"] + 1), ) assert ticks["n"] == 5 # one check-interval per frame at this config def test_on_tick_interval_can_span_multiple_frames(monkeypatch): frame_len = 1280 monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len) monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000) monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.24) # 3 frames/check frames = [_frame(frame_len) for _ in range(6)] stream = _FakeStream(frames, frame_len) model = _FakeModel([{"thunderbolt": 0.0}] * 6) ticks = {"n": 0} state = {"i": 0} def should_continue(): state["i"] += 1 return state["i"] <= 6 wake_word.listen_for_wake_word( stream, should_continue=should_continue, model=model, on_tick=lambda: ticks.__setitem__("n", ticks["n"] + 1), ) assert ticks["n"] == 2 # 6 frames / 3 frames-per-check