From 3b1ba57452111c69afbbab023fb59c5bd0cb9594 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 17 Feb 2026 11:49:15 -0500 Subject: [PATCH] Change `apply_update` / `_update_settings` return type from `set[str]` to `dict[str, Any]`. The dict maps each changed field name to its pre-update value, enabling services to do granular diffing of complex settings objects. Existing call-site patterns (`"field" in changed`, `if changed`, iteration) work unchanged; set-difference sites use `changed.keys() - {...}`. --- COMMUNITY_INTEGRATIONS.md | 10 +++++--- src/pipecat/services/ai_service.py | 24 +++++++++--------- src/pipecat/services/assemblyai/stt.py | 4 +-- src/pipecat/services/aws/nova_sonic/llm.py | 2 +- src/pipecat/services/aws/stt.py | 4 +-- src/pipecat/services/azure/stt.py | 4 +-- src/pipecat/services/cartesia/stt.py | 6 ++--- src/pipecat/services/cartesia/tts.py | 2 +- src/pipecat/services/deepgram/flux/stt.py | 2 +- src/pipecat/services/deepgram/stt.py | 4 +-- .../services/deepgram/stt_sagemaker.py | 4 +-- src/pipecat/services/elevenlabs/stt.py | 10 ++++---- src/pipecat/services/elevenlabs/tts.py | 10 ++++---- src/pipecat/services/fal/stt.py | 4 +-- src/pipecat/services/fish/tts.py | 6 ++--- src/pipecat/services/gladia/stt.py | 4 +-- .../services/google/gemini_live/llm.py | 2 +- src/pipecat/services/google/stt.py | 4 +-- src/pipecat/services/google/tts.py | 10 ++++---- src/pipecat/services/gradium/stt.py | 6 ++--- src/pipecat/services/gradium/tts.py | 6 ++--- src/pipecat/services/inworld/tts.py | 2 +- src/pipecat/services/llm_service.py | 4 +-- src/pipecat/services/neuphonic/tts.py | 4 +-- src/pipecat/services/nvidia/stt.py | 6 ++--- src/pipecat/services/openai/stt.py | 6 ++--- src/pipecat/services/playht/tts.py | 4 +-- src/pipecat/services/rime/tts.py | 6 ++--- src/pipecat/services/sarvam/stt.py | 6 ++--- src/pipecat/services/sarvam/tts.py | 6 ++--- src/pipecat/services/settings.py | 20 ++++++++------- src/pipecat/services/soniox/stt.py | 4 +-- src/pipecat/services/speechmatics/stt.py | 6 ++--- src/pipecat/services/stt_service.py | 6 ++--- src/pipecat/services/tts_service.py | 6 ++--- src/pipecat/services/ultravox/llm.py | 2 +- src/pipecat/services/whisper/base_stt.py | 4 +-- tests/test_settings.py | 25 +++++++++++++------ 38 files changed, 129 insertions(+), 116 deletions(-) diff --git a/COMMUNITY_INTEGRATIONS.md b/COMMUNITY_INTEGRATIONS.md index a7cfa6103..f52e30b58 100644 --- a/COMMUNITY_INTEGRATIONS.md +++ b/COMMUNITY_INTEGRATIONS.md @@ -267,10 +267,10 @@ class MySTTService(STTService): self._settings = MySTTSettings(model=model, region=region) ``` -To react to runtime setting changes, override `_update_settings`. The base implementation applies the delta to `self._settings` and returns the set of field names that changed. Your override should call `super()` first, then act on the changed fields: +To react to runtime setting changes, override `_update_settings`. The base implementation applies the delta to `self._settings` and returns a `dict` mapping each changed field name to its **pre-update** value. Your override should call `super()` first, then act on the changed fields: ```python -async def _update_settings(self, update: STTSettings) -> set[str]: +async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, reconfiguring the recognizer if needed.""" changed = await super()._update_settings(update) @@ -282,12 +282,14 @@ async def _update_settings(self, update: STTSettings) -> set[str]: return changed ``` +The dict keys work like a set for membership tests (`"language" in changed`) and truthiness (`if changed`). Use `changed.keys() - {"language"}` for set difference, or `changed["language"]` to inspect the previous value of a field. + Note that, in this example, the service requires a reconnect to apply the new language. Consider, for each setting, whether your service requires reconnection or can apply changes in-place. -If your service can't yet apply certain settings at runtime, call `self._warn_unhandled_updated_settings(changed)` with the set of unhandled field names so users get a clear log message: +If your service can't yet apply certain settings at runtime, call `self._warn_unhandled_updated_settings(changed)` with the unhandled field names so users get a clear log message: ```python -async def _update_settings(self, update: STTSettings) -> set[str]: +async def _update_settings(self, update: STTSettings) -> dict[str, Any]: changed = await super()._update_settings(update) if not changed: diff --git a/src/pipecat/services/ai_service.py b/src/pipecat/services/ai_service.py index 64af3b4b2..ec78549c2 100644 --- a/src/pipecat/services/ai_service.py +++ b/src/pipecat/services/ai_service.py @@ -10,7 +10,7 @@ Provides the foundation for all AI services in the Pipecat framework, including model management, settings handling, and frame processing lifecycle methods. """ -from typing import Any, AsyncGenerator, Dict, Set +from typing import Any, AsyncGenerator, Dict from loguru import logger @@ -97,12 +97,13 @@ class AIService(FrameProcessor): """ pass - async def _update_settings(self, update: ServiceSettings) -> Set[str]: - """Apply a settings update and return the set of changed field names. + async def _update_settings(self, update: ServiceSettings) -> Dict[str, Any]: + """Apply a settings update and return the changed fields. - The update is applied to ``_settings`` and the changed-field set is - returned. The ``model`` field is handled specially: when it changes, - ``set_model_name`` is called. + The update is applied to ``_settings`` and a dict mapping each changed + field name to its **pre-update** value is returned. The ``model`` + field is handled specially: when it changes, ``set_model_name`` is + called. Concrete services should override this method (calling ``super()``) to react to specific changed fields (e.g. reconnect on voice change). @@ -111,7 +112,7 @@ class AIService(FrameProcessor): update: A settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = self._settings.apply_update(update) @@ -119,16 +120,15 @@ class AIService(FrameProcessor): self.set_model_name(self._settings.model) if changed: - logger.info(f"{self.name}: updated settings fields: {changed}") + logger.info(f"{self.name}: updated settings fields: {set(changed)}") return changed - def _warn_unhandled_updated_settings(self, unhandled: Set[str]): + def _warn_unhandled_updated_settings(self, unhandled): """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. + Convenience helper for ``_update_settings`` overrides. Accepts any + iterable of field names (a ``dict``, ``set``, ``dict_keys``, etc.). Args: unhandled: Field names that changed but are not applied. diff --git a/src/pipecat/services/assemblyai/stt.py b/src/pipecat/services/assemblyai/stt.py index a291e6903..23b7d149b 100644 --- a/src/pipecat/services/assemblyai/stt.py +++ b/src/pipecat/services/assemblyai/stt.py @@ -184,7 +184,7 @@ class AssemblyAISTTService(WebsocketSTTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. @@ -193,7 +193,7 @@ class AssemblyAISTTService(WebsocketSTTService): update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) diff --git a/src/pipecat/services/aws/nova_sonic/llm.py b/src/pipecat/services/aws/nova_sonic/llm.py index 92bd0ea1f..91c2374e3 100644 --- a/src/pipecat/services/aws/nova_sonic/llm.py +++ b/src/pipecat/services/aws/nova_sonic/llm.py @@ -307,7 +307,7 @@ class AWSNovaSonicLLMService(LLMService): # settings # - async def _update_settings(self, update: LLMSettings) -> set[str]: + async def _update_settings(self, update: LLMSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/aws/stt.py b/src/pipecat/services/aws/stt.py index dda58e2ba..ae502e8be 100644 --- a/src/pipecat/services/aws/stt.py +++ b/src/pipecat/services/aws/stt.py @@ -15,7 +15,7 @@ import os import random import string from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -140,7 +140,7 @@ class AWSTranscribeSTTService(WebsocketSTTService): } return encoding_map.get(encoding, encoding) - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/azure/stt.py b/src/pipecat/services/azure/stt.py index a9ecd67e7..8a5b09e26 100644 --- a/src/pipecat/services/azure/stt.py +++ b/src/pipecat/services/azure/stt.py @@ -12,7 +12,7 @@ Speech SDK for real-time audio transcription. import asyncio from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -123,7 +123,7 @@ class AzureSTTService(STTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active recognizer. diff --git a/src/pipecat/services/cartesia/stt.py b/src/pipecat/services/cartesia/stt.py index a069dface..6629d05bb 100644 --- a/src/pipecat/services/cartesia/stt.py +++ b/src/pipecat/services/cartesia/stt.py @@ -13,7 +13,7 @@ the Cartesia Live transcription API for real-time speech recognition. import json import urllib.parse from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -294,14 +294,14 @@ class CartesiaSTTService(WebsocketSTTService): await self._disconnect_websocket() - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update. Args: update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) diff --git a/src/pipecat/services/cartesia/tts.py b/src/pipecat/services/cartesia/tts.py index 00620fdb0..2544d3b98 100644 --- a/src/pipecat/services/cartesia/tts.py +++ b/src/pipecat/services/cartesia/tts.py @@ -344,7 +344,7 @@ class CartesiaTTSService(AudioContextWordTTSService): """ return True - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/deepgram/flux/stt.py b/src/pipecat/services/deepgram/flux/stt.py index 14e77c2d3..dcb5e3429 100644 --- a/src/pipecat/services/deepgram/flux/stt.py +++ b/src/pipecat/services/deepgram/flux/stt.py @@ -330,7 +330,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/deepgram/stt.py b/src/pipecat/services/deepgram/stt.py index 32759069b..f52932b2c 100644 --- a/src/pipecat/services/deepgram/stt.py +++ b/src/pipecat/services/deepgram/stt.py @@ -7,7 +7,7 @@ """Deepgram speech-to-text service implementation.""" from dataclasses import dataclass, field -from typing import AsyncGenerator, Dict, Optional +from typing import Any, AsyncGenerator, Dict, Optional from loguru import logger @@ -195,7 +195,7 @@ class DeepgramSTTService(STTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, keeping ``live_options`` in sync. Top-level ``model`` and ``language`` are the source of truth. When diff --git a/src/pipecat/services/deepgram/stt_sagemaker.py b/src/pipecat/services/deepgram/stt_sagemaker.py index 08acc6530..870ded11f 100644 --- a/src/pipecat/services/deepgram/stt_sagemaker.py +++ b/src/pipecat/services/deepgram/stt_sagemaker.py @@ -15,7 +15,7 @@ languages, and various Deepgram features. import asyncio import json from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -163,7 +163,7 @@ class DeepgramSageMakerSTTService(STTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, keeping ``live_options`` in sync. Top-level ``model`` and ``language`` are the source of truth. When diff --git a/src/pipecat/services/elevenlabs/stt.py b/src/pipecat/services/elevenlabs/stt.py index d2b7a8f99..fd938c12e 100644 --- a/src/pipecat/services/elevenlabs/stt.py +++ b/src/pipecat/services/elevenlabs/stt.py @@ -16,7 +16,7 @@ import io import json from dataclasses import dataclass, field from enum import Enum -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional import aiohttp from loguru import logger @@ -294,7 +294,7 @@ class ElevenLabsSTTService(SegmentedSTTService): """ return language_to_elevenlabs_language(language) - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update. Converts language to ElevenLabs format before applying and keeps @@ -304,7 +304,7 @@ class ElevenLabsSTTService(SegmentedSTTService): update: A :class:`STTSettings` (or ``ElevenLabsSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ # Convert language to ElevenLabs format before applying if is_given(update.language) and isinstance(update.language, Language): @@ -543,7 +543,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update and reconnect if anything changed. Converts language to ElevenLabs format before applying and keeps @@ -553,7 +553,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): update: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ # Convert language to ElevenLabs format before applying if is_given(update.language) and isinstance(update.language, Language): diff --git a/src/pipecat/services/elevenlabs/tts.py b/src/pipecat/services/elevenlabs/tts.py index 79f05bbf8..022b08b94 100644 --- a/src/pipecat/services/elevenlabs/tts.py +++ b/src/pipecat/services/elevenlabs/tts.py @@ -471,7 +471,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService): voice_settings[key] = val return voice_settings or None - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update, reconnecting as needed. Uses the declarative ``URL_FIELDS`` and ``VOICE_SETTINGS_FIELDS`` @@ -482,7 +482,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService): update: A :class:`TTSSettings` (or ``ElevenLabsTTSSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) @@ -520,7 +520,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService): # 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) + self._warn_unhandled_updated_settings(changed.keys() - handled) return changed @@ -964,14 +964,14 @@ class ElevenLabsHttpTTSService(WordTTSService): def _set_voice_settings(self): return build_elevenlabs_voice_settings(self._settings) - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and rebuild voice settings. Args: update: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) if changed: diff --git a/src/pipecat/services/fal/stt.py b/src/pipecat/services/fal/stt.py index ff6628f6c..28b611865 100644 --- a/src/pipecat/services/fal/stt.py +++ b/src/pipecat/services/fal/stt.py @@ -12,7 +12,7 @@ transcription using segmented audio processing. import os from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger from pydantic import BaseModel @@ -251,7 +251,7 @@ class FalSTTService(SegmentedSTTService): """ return language_to_fal_language(language) - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, converting language if changed.""" changed = await super()._update_settings(update) diff --git a/src/pipecat/services/fish/tts.py b/src/pipecat/services/fish/tts.py index daa884af8..4da4b6673 100644 --- a/src/pipecat/services/fish/tts.py +++ b/src/pipecat/services/fish/tts.py @@ -12,7 +12,7 @@ for streaming text-to-speech synthesis with customizable voice parameters. import uuid from dataclasses import dataclass, field -from typing import AsyncGenerator, Literal, Optional +from typing import Any, AsyncGenerator, Literal, Optional from loguru import logger from pydantic import BaseModel @@ -184,7 +184,7 @@ class FishAudioTTSService(InterruptibleTTSService): """ return True - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and reconnect if needed. Any change to voice or model triggers a WebSocket reconnect. @@ -193,7 +193,7 @@ class FishAudioTTSService(InterruptibleTTSService): update: A :class:`TTSSettings` (or ``FishAudioTTSSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) if changed: diff --git a/src/pipecat/services/gladia/stt.py b/src/pipecat/services/gladia/stt.py index 500b88052..25922e7aa 100644 --- a/src/pipecat/services/gladia/stt.py +++ b/src/pipecat/services/gladia/stt.py @@ -379,7 +379,7 @@ class GladiaSTTService(WebsocketSTTService): await super().start(frame) await self._connect() - async def _update_settings(self, update: GladiaSTTSettings) -> set[str]: + async def _update_settings(self, update: GladiaSTTSettings) -> dict[str, Any]: """Apply settings update. Settings are stored but not applied to the active session. @@ -388,7 +388,7 @@ class GladiaSTTService(WebsocketSTTService): update: A settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index ecca52396..3047e258d 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -804,7 +804,7 @@ class GeminiLiveLLMService(LLMService): """ return True - async def _update_settings(self, update: LLMSettings) -> set[str]: + async def _update_settings(self, update: LLMSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/google/stt.py b/src/pipecat/services/google/stt.py index d4ffb0d91..cdd583c8e 100644 --- a/src/pipecat/services/google/stt.py +++ b/src/pipecat/services/google/stt.py @@ -630,7 +630,7 @@ class GoogleSTTService(STTService): logger.debug(f"Switching STT languages to: {languages}") await self._update_settings(GoogleSTTSettings(languages=list(languages))) - async def _update_settings(self, update: GoogleSTTSettings) -> set[str]: + async def _update_settings(self, update: GoogleSTTSettings) -> dict[str, Any]: """Apply settings update and reconnect if anything changed. Handles ``language`` from base ``set_language`` by converting it to @@ -642,7 +642,7 @@ class GoogleSTTService(STTService): update: A settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ from pipecat.services.settings import is_given diff --git a/src/pipecat/services/google/tts.py b/src/pipecat/services/google/tts.py index 9769aa665..e47aa384a 100644 --- a/src/pipecat/services/google/tts.py +++ b/src/pipecat/services/google/tts.py @@ -24,7 +24,7 @@ from pipecat.utils.tracing.service_decorators import traced_tts os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" from dataclasses import dataclass, field -from typing import AsyncGenerator, List, Literal, Optional +from typing import Any, AsyncGenerator, List, Literal, Optional from loguru import logger from pydantic import BaseModel @@ -680,7 +680,7 @@ class GoogleHttpTTSService(TTSService): """ return language_to_google_tts_language(language) - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Override to handle speaking_rate validation. Args: @@ -1024,7 +1024,7 @@ class GoogleTTSService(GoogleBaseTTSService): credentials, credentials_path ) - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Override to handle speaking_rate validation. Args: @@ -1259,14 +1259,14 @@ class GeminiTTSService(GoogleBaseTTSService): f"Current rate of {self.sample_rate}Hz may cause issues." ) - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update with voice validation. Args: update: Settings delta. Can include 'voice', 'prompt', etc. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ if is_given(update.voice) and update.voice not in self.AVAILABLE_VOICES: logger.warning(f"Voice '{update.voice}' not in known voices list. Using anyway.") diff --git a/src/pipecat/services/gradium/stt.py b/src/pipecat/services/gradium/stt.py index 3b634cbd2..381f76884 100644 --- a/src/pipecat/services/gradium/stt.py +++ b/src/pipecat/services/gradium/stt.py @@ -13,7 +13,7 @@ WebSocket API for streaming audio transcription. import base64 import json from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger from pydantic import BaseModel @@ -171,14 +171,14 @@ class GradiumSTTService(WebsocketSTTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, sync params, and reconnect. Args: update: A :class:`STTSettings` (or ``GradiumSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) if not changed: diff --git a/src/pipecat/services/gradium/tts.py b/src/pipecat/services/gradium/tts.py index 947404baa..3bffbb5bf 100644 --- a/src/pipecat/services/gradium/tts.py +++ b/src/pipecat/services/gradium/tts.py @@ -7,7 +7,7 @@ import base64 import json from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger from pydantic import BaseModel @@ -119,14 +119,14 @@ class GradiumTTSService(InterruptibleWordTTSService): """ return True - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and reconnect if voice changed. Args: update: A :class:`TTSSettings` (or ``GradiumTTSSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ prev_voice = self._voice_id changed = await super()._update_settings(update) diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index 80d95aef9..c291f3156 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -165,7 +165,7 @@ class InworldHttpTTSService(WordTTSService): """ return True - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 97d49192c..860a472c9 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -313,14 +313,14 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService): await self._cancel_sequential_runner_task() await self._cancel_summary_task() - async def _update_settings(self, update: LLMSettings) -> set[str]: + async def _update_settings(self, update: LLMSettings) -> dict[str, Any]: """Apply a settings update, handling turn-completion fields. Args: update: An LLM settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) diff --git a/src/pipecat/services/neuphonic/tts.py b/src/pipecat/services/neuphonic/tts.py index 0680de4f6..0797f9b1b 100644 --- a/src/pipecat/services/neuphonic/tts.py +++ b/src/pipecat/services/neuphonic/tts.py @@ -14,7 +14,7 @@ import asyncio import base64 import json from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional import aiohttp from loguru import logger @@ -181,7 +181,7 @@ class NeuphonicTTSService(InterruptibleTTSService): """ return language_to_neuphonic_lang_code(language) - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and reconnect with new configuration.""" changed = await super()._update_settings(update) if changed: diff --git a/src/pipecat/services/nvidia/stt.py b/src/pipecat/services/nvidia/stt.py index ff76a6900..b0d11fc2b 100644 --- a/src/pipecat/services/nvidia/stt.py +++ b/src/pipecat/services/nvidia/stt.py @@ -9,7 +9,7 @@ import asyncio from concurrent.futures import CancelledError as FuturesCancelledError from dataclasses import dataclass, field -from typing import AsyncGenerator, List, Mapping, Optional +from typing import Any, AsyncGenerator, List, Mapping, Optional from loguru import logger from pydantic import BaseModel @@ -579,14 +579,14 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): self._config = self._create_recognition_config() logger.debug(f"Initialized NvidiaSegmentedSTTService with model: {self.model_name}") - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update and sync internal state. Args: update: A :class:`STTSettings` (or ``NvidiaSegmentedSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) diff --git a/src/pipecat/services/openai/stt.py b/src/pipecat/services/openai/stt.py index 458f40133..6daefd1da 100644 --- a/src/pipecat/services/openai/stt.py +++ b/src/pipecat/services/openai/stt.py @@ -17,7 +17,7 @@ Provides two STT services: import base64 import json from dataclasses import dataclass, field -from typing import AsyncGenerator, Literal, Optional, Union +from typing import Any, AsyncGenerator, Literal, Optional, Union from loguru import logger @@ -268,7 +268,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): """ return True - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update and send session update if needed. Keeps ``_language_code`` and ``_prompt`` in sync with settings @@ -278,7 +278,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): update: A :class:`STTSettings` (or ``OpenAIRealtimeSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) diff --git a/src/pipecat/services/playht/tts.py b/src/pipecat/services/playht/tts.py index ece3f2c17..1965c9ea3 100644 --- a/src/pipecat/services/playht/tts.py +++ b/src/pipecat/services/playht/tts.py @@ -15,7 +15,7 @@ import json import struct import warnings from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional import aiohttp from loguru import logger @@ -215,7 +215,7 @@ class PlayHTTTSService(InterruptibleTTSService): """ return True - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index e87cf9f4f..9e916025c 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -13,7 +13,7 @@ using Rime's API for streaming and batch audio synthesis. import base64 import json from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional import aiohttp from loguru import logger @@ -271,7 +271,7 @@ class RimeTTSService(AudioContextWordTTSService): self._extra_msg_fields["inlineSpeedAlpha"] = ",".join(speed_vals + [str(speed)]) return f"[{text}]" - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and reconnect if voice changed.""" prev_voice = self._voice_id changed = await super()._update_settings(update) @@ -977,7 +977,7 @@ class RimeNonJsonTTSService(InterruptibleTTSService): except Exception as e: yield ErrorFrame(error=f"Unknown error occurred: {e}") - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and reconnect if necessary. Since all settings are WebSocket URL query parameters, diff --git a/src/pipecat/services/sarvam/stt.py b/src/pipecat/services/sarvam/stt.py index 271b12ffe..80e6d6ca2 100644 --- a/src/pipecat/services/sarvam/stt.py +++ b/src/pipecat/services/sarvam/stt.py @@ -13,7 +13,7 @@ can handle multiple audio formats for Indian language speech recognition. import base64 from dataclasses import dataclass, field -from typing import AsyncGenerator, Dict, Literal, Optional +from typing import Any, AsyncGenerator, Dict, Literal, Optional from loguru import logger from pydantic import BaseModel @@ -306,14 +306,14 @@ class SarvamSTTService(STTService): if self._socket_client: await self._socket_client.flush() - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, validate, sync state, and reconnect. Args: update: A :class:`STTSettings` (or ``SarvamSTTSettings``) delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. Raises: ValueError: If a setting is not supported by the current model. diff --git a/src/pipecat/services/sarvam/tts.py b/src/pipecat/services/sarvam/tts.py index 6842eda35..99a0827f5 100644 --- a/src/pipecat/services/sarvam/tts.py +++ b/src/pipecat/services/sarvam/tts.py @@ -42,7 +42,7 @@ import base64 import json from dataclasses import dataclass, field from enum import Enum -from typing import AsyncGenerator, Dict, List, Optional, Tuple +from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple import aiohttp from loguru import logger @@ -953,12 +953,12 @@ class SarvamTTSService(InterruptibleTTSService): if isinstance(frame, (LLMFullResponseEndFrame, EndFrame)): await self.flush_audio() - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a settings update and resend config if voice changed.""" changed = await super()._update_settings(update) if "voice" in changed: await self._send_config() - self._warn_unhandled_updated_settings(changed - {"voice"}) + self._warn_unhandled_updated_settings(changed.keys() - {"voice"}) return changed async def _connect(self): diff --git a/src/pipecat/services/settings.py b/src/pipecat/services/settings.py index d63e1f539..eae120842 100644 --- a/src/pipecat/services/settings.py +++ b/src/pipecat/services/settings.py @@ -19,7 +19,7 @@ Key concepts: service's current settings *and* for update objects. Fields set to ``NOT_GIVEN`` are simply skipped when applying an update. - **apply_update**: Applies a delta onto a target settings object and returns - the set of field names that actually changed. + a dict mapping each changed field name to its previous value. - **from_mapping**: Constructs a settings object from a plain dict, supporting field aliases (e.g. ``"voice_id"`` → ``"voice"``). - **Extras**: Unknown keys land in the ``extra`` dict so services that have @@ -30,7 +30,7 @@ from __future__ import annotations import copy from dataclasses import dataclass, field, fields -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional, Set, Type, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional, Type, TypeVar from loguru import logger @@ -140,8 +140,8 @@ class ServiceSettings: result.update(self.extra) return result - def apply_update(self: _S, update: _S) -> Set[str]: - """Apply *update* onto this settings object, returning changed field names. + def apply_update(self: _S, update: _S) -> Dict[str, Any]: + """Apply *update* onto this settings object, returning changed fields. Only fields in *update* that are **given** (i.e. not ``NOT_GIVEN``) are considered. A field is "changed" if its new value differs from @@ -154,17 +154,19 @@ class ServiceSettings: update: A settings object of the same type containing the delta. Returns: - The set of field names whose values actually changed. + A dict mapping each changed field name to its **pre-update** value. + Use ``changed.keys()`` for the set of names, or index with + ``changed["field"]`` to inspect the old value. Examples:: current = TTSSettings(voice="alice", language="en") delta = TTSSettings(voice="bob") changed = current.apply_update(delta) - # changed == {"voice"} + # changed == {"voice": "alice"} # current.voice == "bob", current.language == "en" """ - changed: Set[str] = set() + changed: Dict[str, Any] = {} for f in fields(self): if f.name == "extra": continue @@ -174,14 +176,14 @@ class ServiceSettings: old_val = getattr(self, f.name) if old_val != new_val: setattr(self, f.name, new_val) - changed.add(f.name) + changed[f.name] = old_val # Merge extra for key, new_val in update.extra.items(): old_val = self.extra.get(key, NOT_GIVEN) if old_val != new_val: self.extra[key] = new_val - changed.add(key) + changed[key] = old_val return changed diff --git a/src/pipecat/services/soniox/stt.py b/src/pipecat/services/soniox/stt.py index 29ca33ad5..5c4b49cbe 100644 --- a/src/pipecat/services/soniox/stt.py +++ b/src/pipecat/services/soniox/stt.py @@ -217,7 +217,7 @@ class SonioxSTTService(WebsocketSTTService): await super().start(frame) await self._connect() - async def _update_settings(self, update: SonioxSTTSettings) -> set[str]: + async def _update_settings(self, update: SonioxSTTSettings) -> dict[str, Any]: """Apply a settings update, keeping ``input_params`` in sync. Top-level ``model`` is the source of truth. When it is given in @@ -231,7 +231,7 @@ class SonioxSTTService(WebsocketSTTService): update: A settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ model_given = is_given(getattr(update, "model", NOT_GIVEN)) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 5bacc208a..c6fe0d16e 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -480,7 +480,7 @@ class SpeechmaticsSTTService(STTService): await super().start(frame) await self._connect() - async def _update_settings(self, update: SpeechmaticsSTTSettings) -> set[str]: + async def _update_settings(self, update: SpeechmaticsSTTSettings) -> dict[str, Any]: """Apply settings update, reconnecting only when necessary. Fields are classified into three categories (see @@ -497,7 +497,7 @@ class SpeechmaticsSTTService(STTService): update: A settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) @@ -505,7 +505,7 @@ class SpeechmaticsSTTService(STTService): return changed no_reconnect = SpeechmaticsSTTSettings.HOT_FIELDS | SpeechmaticsSTTSettings.LOCAL_FIELDS - needs_reconnect = bool(changed - no_reconnect) + needs_reconnect = bool(changed.keys() - no_reconnect) if needs_reconnect: # Connection-level fields changed — rebuild the SDK config diff --git a/src/pipecat/services/stt_service.py b/src/pipecat/services/stt_service.py index e92095297..d6dd31824 100644 --- a/src/pipecat/services/stt_service.py +++ b/src/pipecat/services/stt_service.py @@ -236,19 +236,19 @@ class STTService(AIService): await super().cleanup() await self._cancel_ttfb_timeout() - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply an STT settings update. Handles ``model`` (via parent). Does **not** call ``set_language`` — concrete services should override this method and handle language changes (including any reconnect logic) based on the returned - changed-field set. + changed-field dict. Args: update: An STT settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ changed = await super()._update_settings(update) return changed diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index a696e538d..bb5cff69f 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -432,20 +432,20 @@ class TTSService(AIService): if not (agg_type == aggregation_type and func == transform_function) ] - async def _update_settings(self, update: TTSSettings) -> set[str]: + async def _update_settings(self, update: TTSSettings) -> dict[str, Any]: """Apply a TTS settings update. Handles ``model`` (via parent) and syncs ``_voice_id`` when voice changes. Translates language values before applying. Does **not** call ``set_voice`` or ``set_model`` directly — concrete services should override this method and handle reconnect logic based on the - returned changed-field set. + returned changed-field dict. Args: update: A TTS settings delta. Returns: - Set of field names whose values actually changed. + Dict mapping changed field names to their previous values. """ # Translate language *before* applying so the stored value is canonical if is_given(update.language) and update.language is not None: diff --git a/src/pipecat/services/ultravox/llm.py b/src/pipecat/services/ultravox/llm.py index 07dc107eb..ef8baacb4 100644 --- a/src/pipecat/services/ultravox/llm.py +++ b/src/pipecat/services/ultravox/llm.py @@ -329,7 +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"}) + self._warn_unhandled_updated_settings(changed.keys() - {"output_medium"}) return changed # diff --git a/src/pipecat/services/whisper/base_stt.py b/src/pipecat/services/whisper/base_stt.py index 6ff85efeb..a67ad1cbc 100644 --- a/src/pipecat/services/whisper/base_stt.py +++ b/src/pipecat/services/whisper/base_stt.py @@ -11,7 +11,7 @@ interface, including language mapping, metrics generation, and error handling. """ from dataclasses import dataclass, field -from typing import AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional from loguru import logger from openai import AsyncOpenAI @@ -174,7 +174,7 @@ class BaseWhisperSTTService(SegmentedSTTService): def _create_client(self, api_key: Optional[str], base_url: Optional[str]): return AsyncOpenAI(api_key=api_key, base_url=base_url) - async def _update_settings(self, update: STTSettings) -> set[str]: + async def _update_settings(self, update: STTSettings) -> dict[str, Any]: """Apply a settings update, syncing instance variables. Keeps ``_language``, ``_prompt``, and ``_temperature`` in sync with diff --git a/tests/test_settings.py b/tests/test_settings.py index 62583b00b..71d66fc35 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -100,7 +100,8 @@ class TestApplyUpdate: current = TTSSettings(voice="alice", language="en") delta = TTSSettings(voice="bob") changed = current.apply_update(delta) - assert changed == {"voice"} + assert changed.keys() == {"voice"} + assert changed["voice"] == "alice" # old value assert current.voice == "bob" assert current.language == "en" @@ -108,14 +109,14 @@ class TestApplyUpdate: current = TTSSettings(voice="alice", language="en") delta = TTSSettings(voice="alice") changed = current.apply_update(delta) - assert changed == set() + assert changed == {} assert current.voice == "alice" def test_apply_update_not_given_skipped(self): current = TTSSettings(voice="alice", language="en") delta = TTSSettings() # all NOT_GIVEN changed = current.apply_update(delta) - assert changed == set() + assert changed == {} assert current.voice == "alice" assert current.language == "en" @@ -123,7 +124,9 @@ class TestApplyUpdate: current = LLMSettings(temperature=0.7, max_tokens=100) delta = LLMSettings(temperature=0.9, max_tokens=200, top_p=0.95) changed = current.apply_update(delta) - assert changed == {"temperature", "max_tokens", "top_p"} + assert changed.keys() == {"temperature", "max_tokens", "top_p"} + assert changed["temperature"] == 0.7 + assert changed["max_tokens"] == 100 assert current.temperature == 0.9 assert current.max_tokens == 200 assert current.top_p == 0.95 @@ -135,6 +138,7 @@ class TestApplyUpdate: delta.extra = {"speed": 1.2} changed = current.apply_update(delta) assert "speed" in changed + assert changed["speed"] == 1.0 # old value assert current.extra == {"speed": 1.2, "stability": 0.5} def test_apply_update_extra_no_change(self): @@ -143,13 +147,14 @@ class TestApplyUpdate: delta = TTSSettings() delta.extra = {"speed": 1.0} changed = current.apply_update(delta) - assert changed == set() + assert changed == {} def test_apply_update_model_field(self): current = ServiceSettings(model="old-model") delta = ServiceSettings(model="new-model") changed = current.apply_update(delta) - assert changed == {"model"} + assert changed.keys() == {"model"} + assert changed["model"] == "old-model" assert current.model == "new-model" def test_apply_update_none_is_a_valid_value(self): @@ -165,6 +170,7 @@ class TestApplyUpdate: delta = TTSSettings(language="en") changed = current.apply_update(delta) assert "language" in changed + assert changed["language"] is None # old value was None assert current.language == "en" @@ -293,7 +299,9 @@ class TestRoundtrip: delta = TTSSettings.from_mapping(raw) changed = current.apply_update(delta) - assert changed == {"voice", "speed"} + assert changed.keys() == {"voice", "speed"} + assert changed["voice"] == "alice" + assert changed["speed"] == 1.0 assert current.voice == "bob" assert current.language == "en" assert current.extra["speed"] == 1.2 @@ -303,6 +311,7 @@ class TestRoundtrip: current = LLMSettings(model="gpt-4o", temperature=0.7) delta = LLMSettings.from_mapping({"model": "gpt-4o-mini", "temperature": 0.9}) changed = current.apply_update(delta) - assert changed == {"model", "temperature"} + assert changed.keys() == {"model", "temperature"} + assert changed["model"] == "gpt-4o" assert current.model == "gpt-4o-mini" assert current.temperature == 0.9