161 lines
3.9 KiB
Python
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,
|
|
)
|
|
) |