Files
voiceagent/src/voice_assistant/stt/mock.py

161 lines
3.9 KiB
Python

"""
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,
)
)