In services that don't handle runtime settings updates—or don't handle them for *all* available settings—log a warning about which fields specifically aren't handled. Revert new apply-settings-updates logic across various services, to reduce PR testing scope. This logic can be added service by service gradually as future work.
Note that for services that previously handled applying updates (through methods like `set_model` and `set_language`), we're keeping the update-applying logic (some or most of which is already well-tested) and expanding it to cover all relevant settings fields. Services under this bucket are: - Deepgram STT - Deepgram Sagemaker STT - Elevenlabs STT - Google STT - Gradium STT - OpenAI STT - Speechmatics STT
This commit is contained in:
@@ -123,6 +123,20 @@ class AIService(FrameProcessor):
|
||||
|
||||
return changed
|
||||
|
||||
def _warn_unhandled_updated_settings(self, unhandled: Set[str]):
|
||||
"""Log a warning for settings changes that won't take effect at runtime.
|
||||
|
||||
Convenience helper for ``_update_settings`` overrides. Call with the
|
||||
set of field names that changed but that the service does not (yet)
|
||||
apply at runtime.
|
||||
|
||||
Args:
|
||||
unhandled: Field names that changed but are not applied.
|
||||
"""
|
||||
if unhandled:
|
||||
fields = ", ".join(sorted(unhandled))
|
||||
logger.warning(f"{self.name}: runtime update of [{fields}] is not currently supported")
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
"""Process frames and handle service lifecycle.
|
||||
|
||||
|
||||
@@ -185,10 +185,9 @@ class AssemblyAISTTService(WebsocketSTTService):
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||
"""Apply a settings update and reconnect if anything changed.
|
||||
"""Apply a settings update.
|
||||
|
||||
Any change triggers a WebSocket reconnect since all connection
|
||||
parameters are encoded in the WebSocket URL.
|
||||
Settings are stored but not applied to the active connection.
|
||||
|
||||
Args:
|
||||
update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta.
|
||||
@@ -201,15 +200,18 @@ class AssemblyAISTTService(WebsocketSTTService):
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# Re-apply manual turn mode config if vad_force_turn_endpoint is active
|
||||
# and connection_params were updated.
|
||||
if self._vad_force_turn_endpoint and "connection_params" in changed:
|
||||
self._settings.connection_params = self._configure_manual_turn_mode(
|
||||
self._settings.connection_params
|
||||
)
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# # Re-apply manual turn mode config if vad_force_turn_endpoint is active
|
||||
# # and connection_params were updated.
|
||||
# if self._vad_force_turn_endpoint and "connection_params" in changed:
|
||||
# self._settings.connection_params = self._configure_manual_turn_mode(
|
||||
# self._settings.connection_params
|
||||
# )
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
|
||||
)
|
||||
from pipecat.processors.frame_processor import FrameDirection
|
||||
from pipecat.services.llm_service import LLMService
|
||||
from pipecat.services.settings import LLMSettings
|
||||
from pipecat.utils.time import time_now_iso8601
|
||||
|
||||
try:
|
||||
@@ -302,6 +303,29 @@ class AWSNovaSonicLLMService(LLMService):
|
||||
with wave.open(file_path.open("rb"), "rb") as wav_file:
|
||||
self._assistant_response_trigger_audio = wav_file.readframes(wav_file.getnframes())
|
||||
|
||||
#
|
||||
# settings
|
||||
#
|
||||
|
||||
async def _update_settings(self, update: LLMSettings) -> set[str]:
|
||||
"""Apply a settings update.
|
||||
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
#
|
||||
# standard AIService frame handling
|
||||
#
|
||||
|
||||
@@ -141,16 +141,22 @@ class AWSTranscribeSTTService(WebsocketSTTService):
|
||||
return encoding_map.get(encoding, encoding)
|
||||
|
||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||
"""Apply a settings update, reconnecting if needed.
|
||||
"""Apply a settings update.
|
||||
|
||||
Any change to connection-relevant settings (model, language, etc.)
|
||||
triggers a WebSocket reconnect so the new configuration takes effect.
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if changed and self._websocket:
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# if changed and self._websocket:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
@@ -124,25 +124,28 @@ class AzureSTTService(STTService):
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||
"""Apply a settings update, reconfiguring the recognizer if needed.
|
||||
"""Apply a settings update.
|
||||
|
||||
When ``language`` changes the ``SpeechConfig`` is updated and the
|
||||
speech recognizer is restarted so that the new language takes effect.
|
||||
Settings are stored but not applied to the active recognizer.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if "language" in changed:
|
||||
# Convert Language enum to Azure language code if needed.
|
||||
# Convert Language enum to Azure language code for consistency.
|
||||
lang = self._settings.language
|
||||
if isinstance(lang, Language):
|
||||
lang = language_to_azure_language(lang)
|
||||
self._settings.language = lang
|
||||
self._speech_config.speech_recognition_language = lang
|
||||
|
||||
# Restart the recognizer with the new config.
|
||||
if self._speech_recognizer:
|
||||
self._speech_recognizer.stop_continuous_recognition_async()
|
||||
self._speech_recognizer.start_continuous_recognition_async()
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# if "language" in changed:
|
||||
# self._speech_config.speech_recognition_language = self._settings.language
|
||||
# if self._speech_recognizer:
|
||||
# self._speech_recognizer.stop_continuous_recognition_async()
|
||||
# self._speech_recognizer.start_continuous_recognition_async()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ class CartesiaSTTService(WebsocketSTTService):
|
||||
await self._disconnect_websocket()
|
||||
|
||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||
"""Apply a settings update and reconnect if anything changed.
|
||||
"""Apply a settings update.
|
||||
|
||||
Args:
|
||||
update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta.
|
||||
@@ -304,9 +304,15 @@ class CartesiaSTTService(WebsocketSTTService):
|
||||
Set of field names whose values actually changed.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
if changed:
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# if changed:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
async def _connect_websocket(self):
|
||||
|
||||
@@ -344,6 +344,25 @@ class CartesiaTTSService(AudioContextWordTTSService):
|
||||
"""
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: TTSSettings) -> set[str]:
|
||||
"""Apply a settings update.
|
||||
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||
"""Convert a Language enum to Cartesia language format.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from pipecat.frames.frames import (
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
)
|
||||
from pipecat.services.settings import STTSettings
|
||||
from pipecat.services.stt_service import WebsocketSTTService
|
||||
from pipecat.transcriptions.language import Language
|
||||
from pipecat.utils.time import time_now_iso8601
|
||||
@@ -329,6 +330,25 @@ class DeepgramFluxSTTService(WebsocketSTTService):
|
||||
"""
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||
"""Apply a settings update.
|
||||
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
async def start(self, frame: StartFrame):
|
||||
"""Start the Deepgram Flux STT service.
|
||||
|
||||
|
||||
@@ -516,6 +516,12 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
|
||||
await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e)
|
||||
self._context_id = None
|
||||
|
||||
if not url_changed:
|
||||
# Reconnect applies all settings; only warn about fields not handled
|
||||
# by voice settings or URL changes.
|
||||
handled = ElevenLabsTTSSettings.URL_FIELDS | ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS
|
||||
self._warn_unhandled_updated_settings(changed - handled)
|
||||
|
||||
return changed
|
||||
|
||||
async def start(self, frame: StartFrame):
|
||||
|
||||
@@ -382,8 +382,7 @@ class GladiaSTTService(WebsocketSTTService):
|
||||
async def _update_settings(self, update: GladiaSTTSettings) -> set[str]:
|
||||
"""Apply settings update.
|
||||
|
||||
Gladia sessions are fixed at creation time, so any change requires
|
||||
a full session teardown and reconnect.
|
||||
Settings are stored but not applied to the active session.
|
||||
|
||||
Args:
|
||||
update: A settings delta.
|
||||
@@ -396,11 +395,14 @@ class GladiaSTTService(WebsocketSTTService):
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# Gladia sessions are fixed — need to tear down and recreate
|
||||
self._session_url = None
|
||||
self._session_id = None
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# self._session_url = None
|
||||
# self._session_id = None
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
@@ -804,6 +804,25 @@ class GeminiLiveLLMService(LLMService):
|
||||
"""
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: LLMSettings) -> set[str]:
|
||||
"""Apply a settings update.
|
||||
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
def set_audio_input_paused(self, paused: bool):
|
||||
"""Set the audio input pause state.
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ class GradiumTTSService(InterruptibleWordTTSService):
|
||||
if self._voice_id != prev_voice:
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
else:
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
return changed
|
||||
|
||||
def _build_msg(self, text: str = "") -> dict:
|
||||
|
||||
@@ -165,6 +165,25 @@ class InworldHttpTTSService(WordTTSService):
|
||||
"""
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: TTSSettings) -> set[str]:
|
||||
"""Apply a settings update.
|
||||
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
async def start(self, frame: StartFrame):
|
||||
"""Start the Inworld TTS service.
|
||||
|
||||
|
||||
@@ -215,6 +215,25 @@ class PlayHTTTSService(InterruptibleTTSService):
|
||||
"""
|
||||
return True
|
||||
|
||||
async def _update_settings(self, update: TTSSettings) -> set[str]:
|
||||
"""Apply a settings update.
|
||||
|
||||
Settings are stored but not applied to the active connection.
|
||||
"""
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||
"""Convert a Language enum to PlayHT service language format.
|
||||
|
||||
|
||||
@@ -279,6 +279,8 @@ class RimeTTSService(AudioContextWordTTSService):
|
||||
self._settings.speaker = self._voice_id
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
else:
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
return changed
|
||||
|
||||
def _build_msg(self, text: str = "") -> dict:
|
||||
|
||||
@@ -338,11 +338,16 @@ class SarvamSTTService(STTService):
|
||||
|
||||
changed = await super()._update_settings(update)
|
||||
|
||||
if not changed:
|
||||
return changed
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# if not changed:
|
||||
# return changed
|
||||
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
return changed
|
||||
|
||||
async def set_prompt(self, prompt: Optional[str]):
|
||||
|
||||
@@ -958,6 +958,7 @@ class SarvamTTSService(InterruptibleTTSService):
|
||||
changed = await super()._update_settings(update)
|
||||
if "voice" in changed:
|
||||
await self._send_config()
|
||||
self._warn_unhandled_updated_settings(changed - {"voice"})
|
||||
return changed
|
||||
|
||||
async def _connect(self):
|
||||
|
||||
@@ -225,7 +225,7 @@ class SonioxSTTService(WebsocketSTTService):
|
||||
``input_params`` is given, its ``model`` is propagated *up* to the
|
||||
top-level field.
|
||||
|
||||
Any change triggers a WebSocket reconnect.
|
||||
Settings are stored but not applied to the active connection.
|
||||
|
||||
Args:
|
||||
update: A settings delta.
|
||||
@@ -249,8 +249,12 @@ class SonioxSTTService(WebsocketSTTService):
|
||||
self._settings.model = self._settings.input_params.model
|
||||
self.set_model_name(self._settings.model)
|
||||
|
||||
await self._disconnect()
|
||||
await self._connect()
|
||||
# TODO: someday we could reconnect here to apply updated settings.
|
||||
# Code might look something like the below:
|
||||
# await self._disconnect()
|
||||
# await self._connect()
|
||||
|
||||
self._warn_unhandled_updated_settings(changed)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
@@ -329,6 +329,7 @@ class UltravoxRealtimeLLMService(LLMService):
|
||||
changed = await super()._update_settings(update)
|
||||
if "output_medium" in changed:
|
||||
await self._update_output_medium(self._settings.output_medium)
|
||||
self._warn_unhandled_updated_settings(changed - {"output_medium"})
|
||||
return changed
|
||||
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user