from __future__ import annotations import asyncio import audioop import uuid from realtime_voice_service.core.session import CallSession from realtime_voice_service.core.vad import SileroVADDetector from realtime_voice_service.providers.base import BaseLLM, BaseTTS, MockSTT from realtime_voice_service.transports.audiosocket import ( AUDIO_SOCKET_PACKET_PCM16, AUDIO_SOCKET_PACKET_UUID, AudioSocketServer, encode_audio_packet, encode_packet, read_packet, ) from realtime_voice_service.transports.base import BaseMediaTransport def _speech_frame() -> bytes: return (1200).to_bytes(2, "little", signed=True) * 160 def _silence_frame() -> bytes: return b"\x00\x00" * 160 class _FakeTransport(BaseMediaTransport): def __init__(self, schedule: list[tuple[float, bytes | None]]) -> None: super().__init__(transport_id="test-session", sample_rate_hz=8000, frame_duration_ms=20) self._schedule = list(schedule) self.sent_audio: list[bytes] = [] self.closed = False @property def protocol(self) -> str: return "fake" async def receive_audio(self) -> bytes | None: if not self._schedule: return None delay_seconds, payload = self._schedule.pop(0) if delay_seconds: await asyncio.sleep(delay_seconds) return payload async def send_audio(self, audio_chunk: bytes) -> None: self.sent_audio.append(audio_chunk) await asyncio.sleep(0.02) async def close(self) -> None: self.closed = True class _StreamingEchoLLM(BaseLLM): async def generate_stream(self, transcript: str, context: list): del context await asyncio.sleep(0.02) yield f"reply to {transcript}" class _LongMockTTS(BaseTTS): async def synthesize_stream(self, text: str): del text await asyncio.sleep(0.01) frame_count = 14 for _ in range(frame_count): await asyncio.sleep(0.01) yield b"\x10\x00" * 160 def _window_prediction(window: bytes) -> float: return 0.9 if audioop.rms(window, 2) >= 250 else 0.0 def test_call_session_barge_in_increments_generation_epoch(): transport = _FakeTransport( [ (0.0, _speech_frame()), (0.0, _speech_frame()), (0.0, _silence_frame()), (0.0, _silence_frame()), (0.0, _silence_frame()), (0.11, _speech_frame()), (0.0, _speech_frame()), (0.0, _silence_frame()), (0.0, _silence_frame()), (0.0, _silence_frame()), (0.8, None), ] ) session = CallSession( session_id="test-session", transport=transport, vad=SileroVADDetector( sample_rate_hz=transport.sample_rate_hz, speech_end_silence_ms=32, speech_pad_ms=0, prediction_fn=_window_prediction, ), stt=MockSTT( latency_ms=5, sample_rate_hz=transport.sample_rate_hz, scripted_transcripts=["first request", "second request"], ), llm=_StreamingEchoLLM(), tts=_LongMockTTS(), ) asyncio.run(session.run()) assert transport.closed is True assert session.generation_epoch == 1 assert session.interruptions == ["barge-in"] assert ("user", "first request") in session.conversation assert ("user", "second request") in session.conversation assert ("assistant", "reply to first request") not in session.conversation assert ("assistant", "reply to second request") in session.conversation assert transport.sent_audio assert set(session.last_latency_ms) >= {"stt_latency", "ttft", "ttfa"} def test_audiosocket_server_decodes_uuid_and_echoes_pcm(): received_session_ids: list[str] = [] received_payloads: list[bytes] = [] async def _scenario() -> None: async def _handler(transport) -> None: received_session_ids.append(transport.transport_id) pcm = await transport.receive_audio() received_payloads.append(pcm or b"") if pcm is not None: await transport.send_audio(pcm) server = AudioSocketServer(host="127.0.0.1", port=0, session_handler=_handler) await server.start() media_uuid = str(uuid.uuid4()) reader, writer = await asyncio.open_connection("127.0.0.1", server.bound_port) writer.write(encode_packet(AUDIO_SOCKET_PACKET_UUID, uuid.UUID(media_uuid).bytes)) writer.write(encode_audio_packet(_speech_frame())) await writer.drain() packet_type, payload = await read_packet(reader, timeout_seconds=1.0) assert packet_type == AUDIO_SOCKET_PACKET_PCM16 assert payload == _speech_frame() writer.close() await writer.wait_closed() await asyncio.sleep(0.05) await server.stop() asyncio.run(_scenario()) assert len(received_session_ids) == 1 assert received_session_ids[0] assert received_payloads == [_speech_frame()]