feat: establish voice assistant V1 baseline
This commit is contained in:
146
src/voice_assistant/turn_manager.py
Normal file
146
src/voice_assistant/turn_manager.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class TurnState(StrEnum):
|
||||
LISTENING = "listening"
|
||||
PROCESSING = "processing"
|
||||
SPEAKING = "speaking"
|
||||
COMPLETED = "completed"
|
||||
INTERRUPTED = "interrupted"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Turn:
|
||||
turn_id: int
|
||||
state: TurnState = TurnState.LISTENING
|
||||
partial_text: str = ""
|
||||
final_text: str | None = None
|
||||
|
||||
|
||||
class TurnManager:
|
||||
def __init__(self) -> None:
|
||||
self._active_turn: Turn | None = None
|
||||
|
||||
@property
|
||||
def active_turn(self) -> Turn | None:
|
||||
return self._active_turn
|
||||
|
||||
def start(self, turn_id: int) -> Turn:
|
||||
if self._active_turn is not None:
|
||||
raise RuntimeError(
|
||||
"A turn is already active."
|
||||
)
|
||||
|
||||
self._active_turn = Turn(
|
||||
turn_id=turn_id,
|
||||
state=TurnState.LISTENING,
|
||||
)
|
||||
|
||||
return self._active_turn
|
||||
|
||||
def on_partial(
|
||||
self,
|
||||
turn_id: int,
|
||||
text: str,
|
||||
) -> None:
|
||||
turn = self._require_active_turn(turn_id)
|
||||
|
||||
if turn.state != TurnState.LISTENING:
|
||||
raise RuntimeError(
|
||||
f"Cannot receive partial transcript while "
|
||||
f"turn is {turn.state.value}."
|
||||
)
|
||||
|
||||
turn.partial_text = text
|
||||
|
||||
def on_final(
|
||||
self,
|
||||
turn_id: int,
|
||||
text: str,
|
||||
) -> None:
|
||||
turn = self._require_active_turn(turn_id)
|
||||
|
||||
if turn.state != TurnState.LISTENING:
|
||||
raise RuntimeError(
|
||||
f"Cannot receive final transcript while "
|
||||
f"turn is {turn.state.value}."
|
||||
)
|
||||
|
||||
turn.final_text = text
|
||||
turn.partial_text = text
|
||||
turn.state = TurnState.PROCESSING
|
||||
|
||||
def on_agent_response(
|
||||
self,
|
||||
turn_id: int,
|
||||
) -> None:
|
||||
turn = self._require_active_turn(turn_id)
|
||||
|
||||
if turn.state != TurnState.PROCESSING:
|
||||
raise RuntimeError(
|
||||
f"Cannot start speaking while "
|
||||
f"turn is {turn.state.value}."
|
||||
)
|
||||
|
||||
turn.state = TurnState.SPEAKING
|
||||
|
||||
def on_tts_complete(
|
||||
self,
|
||||
turn_id: int,
|
||||
) -> None:
|
||||
turn = self._require_active_turn(turn_id)
|
||||
|
||||
if turn.state != TurnState.SPEAKING:
|
||||
raise RuntimeError(
|
||||
f"Cannot complete TTS while "
|
||||
f"turn is {turn.state.value}."
|
||||
)
|
||||
|
||||
turn.state = TurnState.COMPLETED
|
||||
|
||||
def stop(self, turn_id: int) -> Turn:
|
||||
"""
|
||||
Return the active turn when its complete lifecycle has finished.
|
||||
|
||||
A turn may only be stopped after the TTS stage has completed and
|
||||
the semantic state has reached COMPLETED.
|
||||
"""
|
||||
turn = self._require_active_turn(turn_id)
|
||||
|
||||
if turn.state != TurnState.COMPLETED:
|
||||
raise RuntimeError(
|
||||
f"Cannot stop turn while turn is {turn.state.value}."
|
||||
)
|
||||
|
||||
return turn
|
||||
|
||||
def interrupt(self, turn_id: int) -> Turn:
|
||||
turn = self._require_active_turn(turn_id)
|
||||
|
||||
turn.state = TurnState.INTERRUPTED
|
||||
return turn
|
||||
|
||||
def clear(self, turn_id: int) -> None:
|
||||
"""
|
||||
Clear the active turn after its lifecycle has been handled.
|
||||
|
||||
Turn identity validation is delegated to _require_active_turn().
|
||||
"""
|
||||
self._require_active_turn(turn_id)
|
||||
self._active_turn = None
|
||||
|
||||
|
||||
def _require_active_turn(self, turn_id: int) -> Turn:
|
||||
if self._active_turn is None:
|
||||
raise RuntimeError(
|
||||
"There is no active turn."
|
||||
)
|
||||
|
||||
if self._active_turn.turn_id != turn_id:
|
||||
raise ValueError(
|
||||
f"Unexpected turn_id={turn_id}; "
|
||||
f"active_turn_id={self._active_turn.turn_id}."
|
||||
)
|
||||
|
||||
return self._active_turn
|
||||
Reference in New Issue
Block a user