Files
livekit--agents/tests/test_audio_recognition_push_audio.py
2026-07-13 13:39:38 +08:00

82 lines
2.8 KiB
Python

from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from livekit import rtc
from livekit.agents.voice.audio_recognition import AudioRecognition
pytestmark = pytest.mark.unit
def _make_frame(byte: int = 0x11, samples: int = 160, sample_rate: int = 16000) -> rtc.AudioFrame:
data = bytes([byte, byte]) * samples
return rtc.AudioFrame(
data=data,
sample_rate=sample_rate,
num_channels=1,
samples_per_channel=samples,
)
def _make_recognition() -> AudioRecognition:
"""Build an AudioRecognition stub with just the attributes ``push_audio`` reads."""
ar = object.__new__(AudioRecognition)
ar._sample_rate = None # type: ignore[attr-defined]
ar._stt_pipeline = MagicMock() # type: ignore[attr-defined]
# the input anchor lives on the pipeline (see _STTPipeline.input_started_at)
ar._stt_pipeline.input_started_at = None # type: ignore[attr-defined]
ar._vad_ch = MagicMock() # type: ignore[attr-defined]
ar._interruption_ch = MagicMock() # type: ignore[attr-defined]
ar._session = MagicMock() # type: ignore[attr-defined]
ar._turn_detector_stream = None # type: ignore[attr-defined]
return ar
def test_push_audio_routes_real_frame_everywhere_by_default() -> None:
ar = _make_recognition()
frame = _make_frame()
ar._push_audio(frame)
ar._stt_pipeline.audio_ch.send_nowait.assert_called_once_with(frame)
ar._vad_ch.send_nowait.assert_called_once_with(frame)
ar._session.amd.push_audio.assert_called_once_with(frame)
ar._interruption_ch.send_nowait.assert_called_once_with(frame)
def test_push_audio_substitutes_stt_frame_only_on_stt_path() -> None:
ar = _make_recognition()
real = _make_frame(byte=0x11)
silence = _make_frame(byte=0x00)
ar._push_audio(real, stt_frame=silence)
# STT pipeline sees the substitute (silence), nothing else does.
ar._stt_pipeline.audio_ch.send_nowait.assert_called_once_with(silence)
ar._vad_ch.send_nowait.assert_called_once_with(real)
ar._session.amd.push_audio.assert_called_once_with(real)
ar._interruption_ch.send_nowait.assert_called_once_with(real)
def test_push_audio_skips_optional_consumers_when_unset() -> None:
ar = _make_recognition()
ar._stt_pipeline = None # type: ignore[attr-defined]
ar._vad_ch = None # type: ignore[attr-defined]
ar._interruption_ch = None # type: ignore[attr-defined]
ar._session.amd = None
# Should not raise even when every downstream consumer is absent.
ar._push_audio(_make_frame())
def test_push_audio_records_sample_rate_and_input_start() -> None:
ar = _make_recognition()
frame = _make_frame(sample_rate=24000)
ar._push_audio(frame)
assert ar._sample_rate == 24000 # type: ignore[attr-defined]
assert ar._input_started_at is not None # type: ignore[attr-defined]