Files
vocero-client/speech_to_speech/api/openai_realtime/runtime_config.py
T
2026-08-26 17:42:00 -04:00

82 lines
3.3 KiB
Python

from openai.types.realtime import RealtimeSessionCreateRequest
from openai.types.realtime.realtime_audio_config import RealtimeAudioConfig
from openai.types.realtime.realtime_audio_config_input import RealtimeAudioConfigInput
from openai.types.realtime.realtime_audio_config_output import RealtimeAudioConfigOutput
from pydantic import BaseModel, ConfigDict, Field, field_validator
from speech_to_speech.LLM.chat import Chat
def _apply_update(current: BaseModel, update: BaseModel) -> None:
"""Apply explicitly-set fields from *update* onto *current* in-place,
recursing into nested BaseModel children so partial nested updates
don't overwrite unset fields.
Only fields present in update.model_fields_set (i.e. actually
sent by the client) are considered.
"""
for field_name in update.model_fields_set:
new_val = getattr(update, field_name)
old_val = getattr(current, field_name, None)
if isinstance(new_val, BaseModel) and isinstance(old_val, BaseModel):
_apply_update(old_val, new_val)
else:
setattr(current, field_name, new_val)
class RuntimeConfig(BaseModel):
"""
Shared mutable configuration written by the RealtimeService on
session.update and read by pipeline handlers (VAD, LLM, TTS) during
processing. Python's GIL makes simple attribute reads/writes atomic,
so no explicit locking is needed for primitive values.
The canonical state lives in 'session' (a full
'RealtimeSessionCreateRequest').
"""
model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
chat: Chat = Field(default_factory=lambda: Chat(10))
session: RealtimeSessionCreateRequest = Field(
default_factory=lambda: RealtimeSessionCreateRequest(type="realtime"),
validate_default=True,
)
@field_validator("session", mode="after")
@classmethod
def _ensure_audio_structure(cls, v: RealtimeSessionCreateRequest) -> RealtimeSessionCreateRequest:
"""Guarantee 'audio.input' and 'audio.output' are never None."""
if v.audio is None:
v.audio = RealtimeAudioConfig()
if v.audio.input is None:
v.audio.input = RealtimeAudioConfigInput()
if v.audio.output is None:
v.audio.output = RealtimeAudioConfigOutput()
return v
@property
def interrupt_response_enabled(self) -> bool:
"""Whether barge-in should cancel an active response.
Reads 'turn_detection.interrupt_response' from the session config,
handling both Pydantic models ('ServerVad') and plain dicts.
Defaults to 'True' (OpenAI API default).
"""
assert self.session.audio is not None and self.session.audio.input is not None
td = self.session.audio.input.turn_detection
if td is None:
return True
if hasattr(td, "interrupt_response"):
val = td.interrupt_response
elif isinstance(td, dict):
val = td.get("interrupt_response", True)
else:
return True
return val if val is not None else True
def apply_session_update(self, update: RealtimeSessionCreateRequest) -> None:
"""Merge non-None, explicitly-set fields from 'update' into the
current 'session', preserving any fields not present in the update."""
_apply_update(self.session, update)