Files
vocero-client/speech_to_speech/pipeline/messages.py
T
2026-08-26 17:42:00 -04:00

173 lines
6.2 KiB
Python

"""Single source of truth for inter-component pipeline messages.
Typed :class:`PipelineMessage` subclasses replace the ad-hoc tuples that
previously flowed between STT, LLM, LMOutputProcessor and TTS stages.
Binary sentinels carried on the audio/output queue are plain ``bytes``
constants.
"""
from __future__ import annotations
from time import perf_counter
from typing import Final, Literal, Optional, TypeAlias
import numpy as np
from openai.types.realtime.realtime_response_create_params import RealtimeResponseCreateParams
from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
from pydantic import BaseModel, ConfigDict, Field
from speech_to_speech.api.openai_realtime.runtime_config import RuntimeConfig
# ── Base class ────────────────────────────────────────────────────────
class PipelineMessage(BaseModel):
"""Base for all typed pipeline messages.
The ``tag`` field acts as a Pydantic discriminator so a ``Union`` of
subtypes can be validated from raw dicts when needed.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
tag: str
# ── VAD → STT ─────────────────────────────────────────────────────────
class VADAudio(PipelineMessage):
"""Audio segment from VAD, with optional mode for realtime transcription."""
tag: Literal["vad_audio"] = "vad_audio"
audio: np.ndarray
mode: Literal["progressive", "final"] | None = None
turn_id: str | None = None
turn_revision: int | None = None
created_at_s: float = Field(default_factory=perf_counter)
# ── STT → TranscriptionNotifier → LLM ────────────────────────────────
class PartialTranscription(PipelineMessage):
"""Live partial transcription (consumed by TranscriptionNotifier, not forwarded to LLM)."""
tag: Literal["partial_transcription"] = "partial_transcription"
text: str
turn_id: str | None = None
turn_revision: int | None = None
class Transcription(PipelineMessage):
"""Final transcription result."""
tag: Literal["transcription"] = "transcription"
text: str
language_code: Optional[str] = None
turn_id: str | None = None
turn_revision: int | None = None
speech_stopped_at_s: float | None = None
# ── LLM → LMOutputProcessor ──────────────────────────────────────────
class LLMResponseChunk(PipelineMessage):
"""One sentence/chunk of the LLM response."""
tag: Literal["llm_response_chunk"] = "llm_response_chunk"
text: str
language_code: Optional[str] = None
tools: list[ResponseFunctionToolCall] = Field(default_factory=list)
runtime_config: RuntimeConfig | None = None
response: RealtimeResponseCreateParams | None = None
turn_id: str | None = None
turn_revision: int | None = None
speech_stopped_at_s: float | None = None
cancel_generation: int | None = None
class TokenUsage(PipelineMessage):
"""Token count report (side-channel, not forwarded to TTS)."""
tag: Literal["token_usage"] = "token_usage"
input_tokens: int
output_tokens: int
turn_id: str | None = None
turn_revision: int | None = None
class EndOfResponse(PipelineMessage):
"""Sentinel marking the end of a response.
``error`` is set when generation could not start (e.g. an out-of-band
response whose ``input`` failed validation); the output processor turns it
into a ``response.done(status="failed")`` while still closing the response
normally for pipeline cleanup.
"""
tag: Literal["end_of_response"] = "end_of_response"
turn_id: str | None = None
turn_revision: int | None = None
cancel_generation: int | None = None
error: str | None = None
# ── LMOutputProcessor → TTS ──────────────────────────────────────────
class TTSInput(PipelineMessage):
"""Text to synthesize with per-response context."""
tag: Literal["tts_input"] = "tts_input"
text: str
language_code: Optional[str] = None
runtime_config: RuntimeConfig | None = None
response: RealtimeResponseCreateParams | None = None
turn_id: str | None = None
turn_revision: int | None = None
speech_stopped_at_s: float | None = None
cancel_generation: int | None = None
class AudioOutput(PipelineMessage):
"""Audio queue item tagged with the response generation that produced it."""
tag: Literal["audio_output"] = "audio_output"
audio: bytes | np.ndarray
cancel_generation: int | None = None
# ── Realtime service → LLM ────────────────────────────────────────────
class GenerateResponseRequest(PipelineMessage):
"""Triggers LLM generation for a realtime session.
Carries everything the LM handler needs to produce a response so it
never has to reach back into shared objects. ``runtime_config``
holds the per-connection session config *and* the conversation chat;
``response`` carries per-response overrides from ``response.create``.
Downstream handlers resolve each attribute by preferring the
per-response value over the session default.
"""
tag: Literal["generate_response"] = "generate_response"
runtime_config: RuntimeConfig
response: RealtimeResponseCreateParams | None = None
language_code: Optional[str] = None
turn_id: str | None = None
turn_revision: int | None = None
speech_stopped_at_s: float | None = None
# ── Binary sentinels (audio/output queue) ─────────────────────────────
AUDIO_RESPONSE_DONE: Final[bytes] = b"__RESPONSE_DONE__"
PIPELINE_END: Final[bytes] = b"END"
PipelineEndSentinel: TypeAlias = Literal[b"END"]
AudioResponseDoneSentinel: TypeAlias = Literal[b"__RESPONSE_DONE__"]
SentinelMessage: TypeAlias = PipelineEndSentinel | AudioResponseDoneSentinel