"""Barge-in detection, driven by a fake mic stream and a fake wake model (no audio hardware, no ONNX runtime).""" import sys from pathlib import Path import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from bolt_pet.audio.barge_in import BargeInDetector, WakeWordBargeIn, make_detector class FakeStream: """Yields frames of a given amplitude, mimicking sounddevice's (data, overflowed) 2-D int16 return shape.""" def __init__(self, amplitudes): self._amplitudes = list(amplitudes) def read(self, frames): amplitude = self._amplitudes.pop(0) if self._amplitudes else 0 data = np.full((frames, 1), amplitude, dtype=np.int16) return data, False def test_silence_never_interrupts(): detector = BargeInDetector(FakeStream([0] * 20), threshold=1000, required_frames=3) assert not any(detector.check() for _ in range(20)) def test_sustained_speech_interrupts_after_the_required_frames(): detector = BargeInDetector(FakeStream([2000] * 5), threshold=1000, required_frames=3) assert detector.check() is False assert detector.check() is False assert detector.check() is True def test_a_single_thump_does_not_interrupt(): # loud, quiet, loud, quiet ... never three in a row detector = BargeInDetector(FakeStream([2000, 0, 2000, 0, 2000, 0]), threshold=1000, required_frames=3) assert not any(detector.check() for _ in range(6)) def test_counter_resets_after_a_quiet_frame(): detector = BargeInDetector(FakeStream([2000, 2000, 0, 2000, 2000, 2000]), threshold=1000, required_frames=3) results = [detector.check() for _ in range(6)] assert results == [False, False, False, False, False, True] def test_reset_clears_progress(): detector = BargeInDetector(FakeStream([2000] * 6), threshold=1000, required_frames=3) detector.check() detector.check() detector.reset() assert detector.check() is False assert detector.loud_frames == 1 def test_a_mic_error_mid_playback_is_not_fatal(): class BrokenStream: def read(self, frames): raise OSError("device disappeared") detector = BargeInDetector(BrokenStream(), threshold=1000, required_frames=1) assert detector.check() is False # ── wake-word mode ─────────────────────────────────────────────────────────── class FakeModel: """Scores frames from a canned list, mimicking openWakeWord's {class_name: score} return. Records reset() calls.""" def __init__(self, scores): self._scores = list(scores) self.resets = 0 def predict(self, frame): score = self._scores.pop(0) if self._scores else 0.0 return {"thunderbolt": score} def reset(self): self.resets += 1 def _wake_detector(scores, threshold=0.5, amplitudes=None): stream = FakeStream(amplitudes if amplitudes is not None else [500] * len(scores)) return WakeWordBargeIn(stream, model=FakeModel(scores), threshold=threshold), stream def test_loud_noise_alone_does_not_interrupt_in_wake_mode(): """The whole point of wake mode: a slammed door is deafening and scores nothing, so the pet keeps talking.""" detector, _ = _wake_detector([0.01] * 6, amplitudes=[30000] * 6) assert not any(detector.check() for _ in range(6)) def test_the_wake_word_interrupts(): detector, _ = _wake_detector([0.1, 0.2, 0.9]) assert [detector.check() for _ in range(3)] == [False, False, True] def test_a_single_frame_is_enough_when_it_clears_the_threshold(): detector, _ = _wake_detector([0.55]) assert detector.check() is True def test_scores_just_under_the_threshold_do_not_fire(): detector, _ = _wake_detector([0.49, 0.499], threshold=0.5) assert not any(detector.check() for _ in range(2)) def test_detecting_resets_the_model_so_the_tail_is_not_reused(): model = FakeModel([0.9]) detector = WakeWordBargeIn(FakeStream([500]), model=model, threshold=0.5) assert detector.check() is True assert model.resets == 1 def test_reset_clears_the_models_audio_window_not_just_predictions(): """The regression that made the pet interrupt itself a word into every reply: openwakeword's reset() clears only the prediction buffer, so the "thunderbolt" that started the turn was still in the preprocessor's rolling window when playback began, and the first frame fed to the model re-fired on it.""" class FakePreprocessor: def __init__(self): self.raw_data_buffer = [1, 2, 3] self.feature_buffer = np.ones((120, 96)) self.melspectrogram_buffer = np.zeros((76, 32)) self.accumulated_samples = 4096 def _get_embeddings(self, audio): return np.zeros((120, 96)) model = FakeModel([0.9]) model.preprocessor = FakePreprocessor() WakeWordBargeIn(FakeStream([500]), model=model, threshold=0.5).reset() assert model.preprocessor.raw_data_buffer == [] assert model.preprocessor.accumulated_samples == 0 assert not model.preprocessor.feature_buffer.any() # blank, not the old audio assert model.preprocessor.melspectrogram_buffer.all() # restored to ones def test_the_lazy_wrapper_exposes_its_preprocessor(): """The pet holds _default_model — a lazy *wrapper* around openwakeword's Model. If the wrapper stops proxying .preprocessor, hard_reset() finds nothing to clear and silently degrades to the shallow reset that leaves the last detection in the audio window. That failure is invisible: no exception, no log, the pet just interrupts itself again.""" from bolt_pet.audio.wake_word import _OpenWakeWordModel wrapper = _OpenWakeWordModel() assert hasattr(wrapper, "preprocessor") assert wrapper.preprocessor is None # not loaded yet: a no-op, not a load class FakeInner: preprocessor = object() def reset(self): pass wrapper._model = FakeInner() assert wrapper.preprocessor is FakeInner.preprocessor def test_a_model_without_a_preprocessor_still_resets(): """Fakes in tests, and any future openwakeword whose internals moved.""" model = FakeModel([0.0]) WakeWordBargeIn(FakeStream([500]), model=model, threshold=0.5).reset() assert model.resets == 1 def test_a_callable_threshold_is_read_every_frame(): """The tray tuner's slider has to apply mid-playback, not just mid-idle.""" threshold = {"value": 0.9} detector = WakeWordBargeIn( FakeStream([500] * 2), model=FakeModel([0.6, 0.6]), threshold=lambda: threshold["value"], ) assert detector.check() is False threshold["value"] = 0.5 assert detector.check() is True def test_a_model_that_blows_up_mid_playback_is_not_fatal(): class BrokenModel: def predict(self, frame): raise RuntimeError("onnx session died") def reset(self): raise RuntimeError("still dead") detector = WakeWordBargeIn(FakeStream([500]), model=BrokenModel(), threshold=0.5) assert detector.check() is False detector.reset() # must not raise either def test_a_mic_error_is_not_fatal_in_wake_mode(): class BrokenStream: def read(self, frames): raise OSError("device disappeared") detector = WakeWordBargeIn(BrokenStream(), model=FakeModel([0.9]), threshold=0.5) assert detector.check() is False def test_make_detector_picks_the_mode(): stream = FakeStream([0]) assert isinstance(make_detector(stream, mode="wake"), WakeWordBargeIn) assert isinstance(make_detector(stream, mode="energy"), BargeInDetector) # A typo in .env shouldn't stop the pet from starting. assert isinstance(make_detector(stream, mode="waek"), BargeInDetector)