"""Barge-in: notice that the user started talking *while the pet is talking* so playback can be cut short mid-sentence. Deliberately dumber than the utterance VAD in mic.py. The mic hears the pet's own voice coming back out of the speakers, so a single loud frame proves nothing — this requires several consecutive frames well above the normal speech threshold (BARGE_IN_RMS_THRESHOLD defaults to 4x VAD_RMS_THRESHOLD). Takes the same injectable stream shape as mic.record_utterance, so tests feed it fake frames instead of real audio hardware. """ from __future__ import annotations import numpy as np from .. import config from .mic import AudioStream, rms class BargeInDetector: """Poll-driven: call check() repeatedly while audio plays. Each call consumes exactly one mic frame (80ms at the default frame length), which is also what paces the playback loop's polling.""" def __init__( self, stream: AudioStream, threshold: int = None, required_frames: int = None, frame_len: int = config.FRAME_LEN, ): self._stream = stream self._threshold = config.BARGE_IN_RMS_THRESHOLD if threshold is None else threshold self._required = max(1, config.BARGE_IN_FRAMES if required_frames is None else required_frames) self._frame_len = frame_len self._loud_frames = 0 @property def loud_frames(self) -> int: return self._loud_frames def reset(self) -> None: self._loud_frames = 0 def check(self) -> bool: """True once the user has been loud for long enough to count as an interruption. Never raises: a mic hiccup mid-playback should not kill the reply, it should just mean "no barge-in this frame".""" try: chunk, _ = self._stream.read(self._frame_len) except Exception: return False frame = np.asarray(chunk) if frame.ndim > 1: frame = frame[:, 0] if frame.size == 0: return False if rms(frame) >= self._threshold: self._loud_frames += 1 else: self._loud_frames = 0 # a single thump/cough shouldn't count return self._loud_frames >= self._required