80bef6f524
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""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
|