""" In-memory mock STT provider for the Voice Assistant. Purpose: Simulate a realtime STT provider without external services. Responsibilities: - Simulate transcription session lifecycle. - Validate incoming audio chunks. - Expose partial and final transcript events. - Support normal stop and immediate abort. - Provide deterministic behavior for automated tests. Non-responsibilities: - Performing real speech recognition. - Managing network connections. - Managing voice turns or Gateway sessions. - Executing Agent or TTS operations. Architecture: Tests / VoicePipeline | v STTAdapter | v MockSTTAdapter | v Async event queue """ import asyncio from collections.abc import AsyncIterator from .adapter import STTAdapter from .models import TranscriptEvent class MockSTTAdapter(STTAdapter): def __init__(self) -> None: self._events: asyncio.Queue[ TranscriptEvent | None ] = asyncio.Queue() self._started = False async def start_session(self) -> None: self._started = True self._events = asyncio.Queue() async def send_audio(self, audio: bytes) -> None: if not self._started: raise RuntimeError( "STT session has not been started." ) if not audio: raise ValueError( "Audio chunk cannot be empty." ) async def stop_session(self) -> None: if not self._started: return self._started = False await self._events.put(None) async def abort_session(self) -> None: """ Abort transcription and discard pending transcript events. Unlike stop_session(), abort_session() must not deliver queued partial or final transcription results. """ if not self._started: return # Reject further audio and transcript events. self._started = False # Discard queued transcript events from the aborted session. while True: try: self._events.get_nowait() except asyncio.QueueEmpty: break # Unblock consumers waiting for the next event. await self._events.put(None) def events(self) -> AsyncIterator[TranscriptEvent]: return self._event_stream() async def _event_stream( self, ) -> AsyncIterator[TranscriptEvent]: while True: event = await self._events.get() if event is None: break yield event async def emit_partial(self, text: str) -> None: if not self._started: raise RuntimeError( "STT session has not been started." ) await self._events.put( TranscriptEvent( type="partial", text=text, ) ) async def emit_final(self, text: str) -> None: if not self._started: raise RuntimeError( "STT session has not been started." ) await self._events.put( TranscriptEvent( type="final", text=text, ) ) def emit_partial_now(self, text: str) -> None: if not self._started: raise RuntimeError( "STT session has not been started." ) self._events.put_nowait( TranscriptEvent( type="partial", text=text, ) ) def emit_final_now(self, text: str) -> None: if not self._started: raise RuntimeError( "STT session has not been started." ) self._events.put_nowait( TranscriptEvent( type="final", text=text, ) )