From 3a77b4c1d8af4090fe35328f45fea1c432302869 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 17 Feb 2026 09:35:58 -0500 Subject: [PATCH] =?UTF-8?q?In=20services=20that=20don't=20handle=20runtime?= =?UTF-8?q?=20settings=20updates=E2=80=94or=20don't=20handle=20them=20for?= =?UTF-8?q?=20*all*=20available=20settings=E2=80=94log=20a=20warning=20abo?= =?UTF-8?q?ut=20which=20fields=20specifically=20aren't=20handled.=20Revert?= =?UTF-8?q?=20new=20apply-settings-updates=20logic=20across=20various=20se?= =?UTF-8?q?rvices,=20to=20reduce=20PR=20testing=20scope.=20This=20logic=20?= =?UTF-8?q?can=20be=20added=20service=20by=20service=20gradually=20as=20fu?= =?UTF-8?q?ture=20work.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/pipecat/services/ai_service.py | 14 +++++++++++ src/pipecat/services/assemblyai/stt.py | 24 ++++++++++--------- src/pipecat/services/aws/nova_sonic/llm.py | 24 +++++++++++++++++++ src/pipecat/services/aws/stt.py | 18 +++++++++----- src/pipecat/services/azure/stt.py | 21 +++++++++------- src/pipecat/services/cartesia/stt.py | 14 +++++++---- src/pipecat/services/cartesia/tts.py | 19 +++++++++++++++ src/pipecat/services/deepgram/flux/stt.py | 20 ++++++++++++++++ src/pipecat/services/elevenlabs/tts.py | 6 +++++ src/pipecat/services/gladia/stt.py | 16 +++++++------ .../services/google/gemini_live/llm.py | 19 +++++++++++++++ src/pipecat/services/gradium/tts.py | 2 ++ src/pipecat/services/inworld/tts.py | 19 +++++++++++++++ src/pipecat/services/playht/tts.py | 19 +++++++++++++++ src/pipecat/services/rime/tts.py | 2 ++ src/pipecat/services/sarvam/stt.py | 13 ++++++---- src/pipecat/services/sarvam/tts.py | 1 + src/pipecat/services/soniox/stt.py | 10 +++++--- src/pipecat/services/ultravox/llm.py | 1 + 19 files changed, 218 insertions(+), 44 deletions(-) diff --git a/src/pipecat/services/ai_service.py b/src/pipecat/services/ai_service.py index 2c6c10be4..64af3b4b2 100644 --- a/src/pipecat/services/ai_service.py +++ b/src/pipecat/services/ai_service.py @@ -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. diff --git a/src/pipecat/services/assemblyai/stt.py b/src/pipecat/services/assemblyai/stt.py index 2e7b1230b..a291e6903 100644 --- a/src/pipecat/services/assemblyai/stt.py +++ b/src/pipecat/services/assemblyai/stt.py @@ -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 diff --git a/src/pipecat/services/aws/nova_sonic/llm.py b/src/pipecat/services/aws/nova_sonic/llm.py index 05baba2bd..92bd0ea1f 100644 --- a/src/pipecat/services/aws/nova_sonic/llm.py +++ b/src/pipecat/services/aws/nova_sonic/llm.py @@ -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 # diff --git a/src/pipecat/services/aws/stt.py b/src/pipecat/services/aws/stt.py index 6a91c2973..dda58e2ba 100644 --- a/src/pipecat/services/aws/stt.py +++ b/src/pipecat/services/aws/stt.py @@ -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 diff --git a/src/pipecat/services/azure/stt.py b/src/pipecat/services/azure/stt.py index 319296a47..a9ecd67e7 100644 --- a/src/pipecat/services/azure/stt.py +++ b/src/pipecat/services/azure/stt.py @@ -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 diff --git a/src/pipecat/services/cartesia/stt.py b/src/pipecat/services/cartesia/stt.py index 5116965ec..a069dface 100644 --- a/src/pipecat/services/cartesia/stt.py +++ b/src/pipecat/services/cartesia/stt.py @@ -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): diff --git a/src/pipecat/services/cartesia/tts.py b/src/pipecat/services/cartesia/tts.py index cff365443..00620fdb0 100644 --- a/src/pipecat/services/cartesia/tts.py +++ b/src/pipecat/services/cartesia/tts.py @@ -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. diff --git a/src/pipecat/services/deepgram/flux/stt.py b/src/pipecat/services/deepgram/flux/stt.py index 547eca0de..14e77c2d3 100644 --- a/src/pipecat/services/deepgram/flux/stt.py +++ b/src/pipecat/services/deepgram/flux/stt.py @@ -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. diff --git a/src/pipecat/services/elevenlabs/tts.py b/src/pipecat/services/elevenlabs/tts.py index 9643fa6ba..79f05bbf8 100644 --- a/src/pipecat/services/elevenlabs/tts.py +++ b/src/pipecat/services/elevenlabs/tts.py @@ -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): diff --git a/src/pipecat/services/gladia/stt.py b/src/pipecat/services/gladia/stt.py index bb8f05e61..500b88052 100644 --- a/src/pipecat/services/gladia/stt.py +++ b/src/pipecat/services/gladia/stt.py @@ -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 diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 7a7aed08c..ecca52396 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -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. diff --git a/src/pipecat/services/gradium/tts.py b/src/pipecat/services/gradium/tts.py index bc4945bcf..947404baa 100644 --- a/src/pipecat/services/gradium/tts.py +++ b/src/pipecat/services/gradium/tts.py @@ -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: diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index 2f6a13bd1..80d95aef9 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -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. diff --git a/src/pipecat/services/playht/tts.py b/src/pipecat/services/playht/tts.py index b63e5d648..ece3f2c17 100644 --- a/src/pipecat/services/playht/tts.py +++ b/src/pipecat/services/playht/tts.py @@ -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. diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index 5f8a5cef6..e87cf9f4f 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -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: diff --git a/src/pipecat/services/sarvam/stt.py b/src/pipecat/services/sarvam/stt.py index 68427fbc4..271b12ffe 100644 --- a/src/pipecat/services/sarvam/stt.py +++ b/src/pipecat/services/sarvam/stt.py @@ -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]): diff --git a/src/pipecat/services/sarvam/tts.py b/src/pipecat/services/sarvam/tts.py index 56ce3bef0..6842eda35 100644 --- a/src/pipecat/services/sarvam/tts.py +++ b/src/pipecat/services/sarvam/tts.py @@ -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): diff --git a/src/pipecat/services/soniox/stt.py b/src/pipecat/services/soniox/stt.py index a74a14ee9..29ca33ad5 100644 --- a/src/pipecat/services/soniox/stt.py +++ b/src/pipecat/services/soniox/stt.py @@ -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 diff --git a/src/pipecat/services/ultravox/llm.py b/src/pipecat/services/ultravox/llm.py index 434a54ab6..07dc107eb 100644 --- a/src/pipecat/services/ultravox/llm.py +++ b/src/pipecat/services/ultravox/llm.py @@ -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 #