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() - {...}.

This commit is contained in:
Paul Kompfner
2026-02-17 11:49:15 -05:00
parent 02c2778b8d
commit 3b1ba57452
38 changed files with 129 additions and 116 deletions

View File

@@ -267,10 +267,10 @@ class MySTTService(STTService):
self._settings = MySTTSettings(model=model, region=region) 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 ```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.""" """Apply a settings update, reconfiguring the recognizer if needed."""
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
@@ -282,12 +282,14 @@ async def _update_settings(self, update: STTSettings) -> set[str]:
return changed 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. 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 ```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) changed = await super()._update_settings(update)
if not changed: if not changed:

View File

@@ -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. 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 from loguru import logger
@@ -97,12 +97,13 @@ class AIService(FrameProcessor):
""" """
pass pass
async def _update_settings(self, update: ServiceSettings) -> Set[str]: async def _update_settings(self, update: ServiceSettings) -> Dict[str, Any]:
"""Apply a settings update and return the set of changed field names. """Apply a settings update and return the changed fields.
The update is applied to ``_settings`` and the changed-field set is The update is applied to ``_settings`` and a dict mapping each changed
returned. The ``model`` field is handled specially: when it changes, field name to its **pre-update** value is returned. The ``model``
``set_model_name`` is called. field is handled specially: when it changes, ``set_model_name`` is
called.
Concrete services should override this method (calling ``super()``) Concrete services should override this method (calling ``super()``)
to react to specific changed fields (e.g. reconnect on voice change). to react to specific changed fields (e.g. reconnect on voice change).
@@ -111,7 +112,7 @@ class AIService(FrameProcessor):
update: A settings delta. update: A settings delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = self._settings.apply_update(update) changed = self._settings.apply_update(update)
@@ -119,16 +120,15 @@ class AIService(FrameProcessor):
self.set_model_name(self._settings.model) self.set_model_name(self._settings.model)
if changed: if changed:
logger.info(f"{self.name}: updated settings fields: {changed}") logger.info(f"{self.name}: updated settings fields: {set(changed)}")
return 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. """Log a warning for settings changes that won't take effect at runtime.
Convenience helper for ``_update_settings`` overrides. Call with the Convenience helper for ``_update_settings`` overrides. Accepts any
set of field names that changed but that the service does not (yet) iterable of field names (a ``dict``, ``set``, ``dict_keys``, etc.).
apply at runtime.
Args: Args:
unhandled: Field names that changed but are not applied. unhandled: Field names that changed but are not applied.

View File

@@ -184,7 +184,7 @@ class AssemblyAISTTService(WebsocketSTTService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.
@@ -193,7 +193,7 @@ class AssemblyAISTTService(WebsocketSTTService):
update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta. update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -307,7 +307,7 @@ class AWSNovaSonicLLMService(LLMService):
# settings # settings
# #
async def _update_settings(self, update: LLMSettings) -> set[str]: async def _update_settings(self, update: LLMSettings) -> dict[str, Any]:
"""Apply a settings update. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -15,7 +15,7 @@ import os
import random import random
import string import string
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
@@ -140,7 +140,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
} }
return encoding_map.get(encoding, encoding) 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -12,7 +12,7 @@ Speech SDK for real-time audio transcription.
import asyncio import asyncio
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
@@ -123,7 +123,7 @@ class AzureSTTService(STTService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active recognizer. Settings are stored but not applied to the active recognizer.

View File

@@ -13,7 +13,7 @@ the Cartesia Live transcription API for real-time speech recognition.
import json import json
import urllib.parse import urllib.parse
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
@@ -294,14 +294,14 @@ class CartesiaSTTService(WebsocketSTTService):
await self._disconnect_websocket() 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. """Apply a settings update.
Args: Args:
update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta. update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -344,7 +344,7 @@ class CartesiaTTSService(AudioContextWordTTSService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -330,7 +330,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -7,7 +7,7 @@
"""Deepgram speech-to-text service implementation.""" """Deepgram speech-to-text service implementation."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Dict, Optional from typing import Any, AsyncGenerator, Dict, Optional
from loguru import logger from loguru import logger
@@ -195,7 +195,7 @@ class DeepgramSTTService(STTService):
""" """
return True 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. """Apply a settings update, keeping ``live_options`` in sync.
Top-level ``model`` and ``language`` are the source of truth. When Top-level ``model`` and ``language`` are the source of truth. When

View File

@@ -15,7 +15,7 @@ languages, and various Deepgram features.
import asyncio import asyncio
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
@@ -163,7 +163,7 @@ class DeepgramSageMakerSTTService(STTService):
""" """
return True 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. """Apply a settings update, keeping ``live_options`` in sync.
Top-level ``model`` and ``language`` are the source of truth. When Top-level ``model`` and ``language`` are the source of truth. When

View File

@@ -16,7 +16,7 @@ import io
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
import aiohttp import aiohttp
from loguru import logger from loguru import logger
@@ -294,7 +294,7 @@ class ElevenLabsSTTService(SegmentedSTTService):
""" """
return language_to_elevenlabs_language(language) 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. """Apply a settings update.
Converts language to ElevenLabs format before applying and keeps Converts language to ElevenLabs format before applying and keeps
@@ -304,7 +304,7 @@ class ElevenLabsSTTService(SegmentedSTTService):
update: A :class:`STTSettings` (or ``ElevenLabsSTTSettings``) delta. update: A :class:`STTSettings` (or ``ElevenLabsSTTSettings``) delta.
Returns: 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 # Convert language to ElevenLabs format before applying
if is_given(update.language) and isinstance(update.language, Language): if is_given(update.language) and isinstance(update.language, Language):
@@ -543,7 +543,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
""" """
return True 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. """Apply a settings update and reconnect if anything changed.
Converts language to ElevenLabs format before applying and keeps Converts language to ElevenLabs format before applying and keeps
@@ -553,7 +553,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
update: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTSettings``) delta. update: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTSettings``) delta.
Returns: 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 # Convert language to ElevenLabs format before applying
if is_given(update.language) and isinstance(update.language, Language): if is_given(update.language) and isinstance(update.language, Language):

View File

@@ -471,7 +471,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
voice_settings[key] = val voice_settings[key] = val
return voice_settings or None 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. """Apply a settings update, reconnecting as needed.
Uses the declarative ``URL_FIELDS`` and ``VOICE_SETTINGS_FIELDS`` Uses the declarative ``URL_FIELDS`` and ``VOICE_SETTINGS_FIELDS``
@@ -482,7 +482,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
update: A :class:`TTSSettings` (or ``ElevenLabsTTSSettings``) delta. update: A :class:`TTSSettings` (or ``ElevenLabsTTSSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
@@ -520,7 +520,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
# Reconnect applies all settings; only warn about fields not handled # Reconnect applies all settings; only warn about fields not handled
# by voice settings or URL changes. # by voice settings or URL changes.
handled = ElevenLabsTTSSettings.URL_FIELDS | ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS 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 return changed
@@ -964,14 +964,14 @@ class ElevenLabsHttpTTSService(WordTTSService):
def _set_voice_settings(self): def _set_voice_settings(self):
return build_elevenlabs_voice_settings(self._settings) 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. """Apply a settings update and rebuild voice settings.
Args: Args:
update: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSSettings``) delta. update: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if changed: if changed:

View File

@@ -12,7 +12,7 @@ transcription using segmented audio processing.
import os import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -251,7 +251,7 @@ class FalSTTService(SegmentedSTTService):
""" """
return language_to_fal_language(language) 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.""" """Apply a settings update, converting language if changed."""
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -12,7 +12,7 @@ for streaming text-to-speech synthesis with customizable voice parameters.
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Literal, Optional from typing import Any, AsyncGenerator, Literal, Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -184,7 +184,7 @@ class FishAudioTTSService(InterruptibleTTSService):
""" """
return True 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. """Apply a settings update and reconnect if needed.
Any change to voice or model triggers a WebSocket reconnect. Any change to voice or model triggers a WebSocket reconnect.
@@ -193,7 +193,7 @@ class FishAudioTTSService(InterruptibleTTSService):
update: A :class:`TTSSettings` (or ``FishAudioTTSSettings``) delta. update: A :class:`TTSSettings` (or ``FishAudioTTSSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if changed: if changed:

View File

@@ -379,7 +379,7 @@ class GladiaSTTService(WebsocketSTTService):
await super().start(frame) await super().start(frame)
await self._connect() 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. """Apply settings update.
Settings are stored but not applied to the active session. Settings are stored but not applied to the active session.
@@ -388,7 +388,7 @@ class GladiaSTTService(WebsocketSTTService):
update: A settings delta. update: A settings delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -804,7 +804,7 @@ class GeminiLiveLLMService(LLMService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -630,7 +630,7 @@ class GoogleSTTService(STTService):
logger.debug(f"Switching STT languages to: {languages}") logger.debug(f"Switching STT languages to: {languages}")
await self._update_settings(GoogleSTTSettings(languages=list(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. """Apply settings update and reconnect if anything changed.
Handles ``language`` from base ``set_language`` by converting it to Handles ``language`` from base ``set_language`` by converting it to
@@ -642,7 +642,7 @@ class GoogleSTTService(STTService):
update: A settings delta. update: A settings delta.
Returns: 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 from pipecat.services.settings import is_given

View File

@@ -24,7 +24,7 @@ from pipecat.utils.tracing.service_decorators import traced_tts
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
from dataclasses import dataclass, field 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 loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -680,7 +680,7 @@ class GoogleHttpTTSService(TTSService):
""" """
return language_to_google_tts_language(language) 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. """Override to handle speaking_rate validation.
Args: Args:
@@ -1024,7 +1024,7 @@ class GoogleTTSService(GoogleBaseTTSService):
credentials, credentials_path 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. """Override to handle speaking_rate validation.
Args: Args:
@@ -1259,14 +1259,14 @@ class GeminiTTSService(GoogleBaseTTSService):
f"Current rate of {self.sample_rate}Hz may cause issues." 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. """Apply a settings update with voice validation.
Args: Args:
update: Settings delta. Can include 'voice', 'prompt', etc. update: Settings delta. Can include 'voice', 'prompt', etc.
Returns: 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: 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.") logger.warning(f"Voice '{update.voice}' not in known voices list. Using anyway.")

View File

@@ -13,7 +13,7 @@ WebSocket API for streaming audio transcription.
import base64 import base64
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -171,14 +171,14 @@ class GradiumSTTService(WebsocketSTTService):
""" """
return True 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. """Apply a settings update, sync params, and reconnect.
Args: Args:
update: A :class:`STTSettings` (or ``GradiumSTTSettings``) delta. update: A :class:`STTSettings` (or ``GradiumSTTSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if not changed: if not changed:

View File

@@ -7,7 +7,7 @@
import base64 import base64
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -119,14 +119,14 @@ class GradiumTTSService(InterruptibleWordTTSService):
""" """
return True 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. """Apply a settings update and reconnect if voice changed.
Args: Args:
update: A :class:`TTSSettings` (or ``GradiumTTSSettings``) delta. update: A :class:`TTSSettings` (or ``GradiumTTSSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
prev_voice = self._voice_id prev_voice = self._voice_id
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -165,7 +165,7 @@ class InworldHttpTTSService(WordTTSService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -313,14 +313,14 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
await self._cancel_sequential_runner_task() await self._cancel_sequential_runner_task()
await self._cancel_summary_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. """Apply a settings update, handling turn-completion fields.
Args: Args:
update: An LLM settings delta. update: An LLM settings delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -14,7 +14,7 @@ import asyncio
import base64 import base64
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
import aiohttp import aiohttp
from loguru import logger from loguru import logger
@@ -181,7 +181,7 @@ class NeuphonicTTSService(InterruptibleTTSService):
""" """
return language_to_neuphonic_lang_code(language) 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.""" """Apply a settings update and reconnect with new configuration."""
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if changed: if changed:

View File

@@ -9,7 +9,7 @@
import asyncio import asyncio
from concurrent.futures import CancelledError as FuturesCancelledError from concurrent.futures import CancelledError as FuturesCancelledError
from dataclasses import dataclass, field 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 loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -579,14 +579,14 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
self._config = self._create_recognition_config() self._config = self._create_recognition_config()
logger.debug(f"Initialized NvidiaSegmentedSTTService with model: {self.model_name}") 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. """Apply a settings update and sync internal state.
Args: Args:
update: A :class:`STTSettings` (or ``NvidiaSegmentedSTTSettings``) delta. update: A :class:`STTSettings` (or ``NvidiaSegmentedSTTSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -17,7 +17,7 @@ Provides two STT services:
import base64 import base64
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Literal, Optional, Union from typing import Any, AsyncGenerator, Literal, Optional, Union
from loguru import logger from loguru import logger
@@ -268,7 +268,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
""" """
return True 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. """Apply a settings update and send session update if needed.
Keeps ``_language_code`` and ``_prompt`` in sync with settings Keeps ``_language_code`` and ``_prompt`` in sync with settings
@@ -278,7 +278,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
update: A :class:`STTSettings` (or ``OpenAIRealtimeSTTSettings``) delta. update: A :class:`STTSettings` (or ``OpenAIRealtimeSTTSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)

View File

@@ -15,7 +15,7 @@ import json
import struct import struct
import warnings import warnings
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
import aiohttp import aiohttp
from loguru import logger from loguru import logger
@@ -215,7 +215,7 @@ class PlayHTTTSService(InterruptibleTTSService):
""" """
return True 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. """Apply a settings update.
Settings are stored but not applied to the active connection. Settings are stored but not applied to the active connection.

View File

@@ -13,7 +13,7 @@ using Rime's API for streaming and batch audio synthesis.
import base64 import base64
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
import aiohttp import aiohttp
from loguru import logger from loguru import logger
@@ -271,7 +271,7 @@ class RimeTTSService(AudioContextWordTTSService):
self._extra_msg_fields["inlineSpeedAlpha"] = ",".join(speed_vals + [str(speed)]) self._extra_msg_fields["inlineSpeedAlpha"] = ",".join(speed_vals + [str(speed)])
return f"[{text}]" 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.""" """Apply a settings update and reconnect if voice changed."""
prev_voice = self._voice_id prev_voice = self._voice_id
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
@@ -977,7 +977,7 @@ class RimeNonJsonTTSService(InterruptibleTTSService):
except Exception as e: except Exception as e:
yield ErrorFrame(error=f"Unknown error occurred: {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. """Apply a settings update and reconnect if necessary.
Since all settings are WebSocket URL query parameters, Since all settings are WebSocket URL query parameters,

View File

@@ -13,7 +13,7 @@ can handle multiple audio formats for Indian language speech recognition.
import base64 import base64
from dataclasses import dataclass, field 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 loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -306,14 +306,14 @@ class SarvamSTTService(STTService):
if self._socket_client: if self._socket_client:
await self._socket_client.flush() 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. """Apply a settings update, validate, sync state, and reconnect.
Args: Args:
update: A :class:`STTSettings` (or ``SarvamSTTSettings``) delta. update: A :class:`STTSettings` (or ``SarvamSTTSettings``) delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
Raises: Raises:
ValueError: If a setting is not supported by the current model. ValueError: If a setting is not supported by the current model.

View File

@@ -42,7 +42,7 @@ import base64
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import AsyncGenerator, Dict, List, Optional, Tuple from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple
import aiohttp import aiohttp
from loguru import logger from loguru import logger
@@ -953,12 +953,12 @@ class SarvamTTSService(InterruptibleTTSService):
if isinstance(frame, (LLMFullResponseEndFrame, EndFrame)): if isinstance(frame, (LLMFullResponseEndFrame, EndFrame)):
await self.flush_audio() 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.""" """Apply a settings update and resend config if voice changed."""
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if "voice" in changed: if "voice" in changed:
await self._send_config() await self._send_config()
self._warn_unhandled_updated_settings(changed - {"voice"}) self._warn_unhandled_updated_settings(changed.keys() - {"voice"})
return changed return changed
async def _connect(self): async def _connect(self):

View File

@@ -19,7 +19,7 @@ Key concepts:
service's current settings *and* for update objects. Fields set to service's current settings *and* for update objects. Fields set to
``NOT_GIVEN`` are simply skipped when applying an update. ``NOT_GIVEN`` are simply skipped when applying an update.
- **apply_update**: Applies a delta onto a target settings object and returns - **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, - **from_mapping**: Constructs a settings object from a plain dict,
supporting field aliases (e.g. ``"voice_id"`` → ``"voice"``). supporting field aliases (e.g. ``"voice_id"`` → ``"voice"``).
- **Extras**: Unknown keys land in the ``extra`` dict so services that have - **Extras**: Unknown keys land in the ``extra`` dict so services that have
@@ -30,7 +30,7 @@ from __future__ import annotations
import copy import copy
from dataclasses import dataclass, field, fields 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 from loguru import logger
@@ -140,8 +140,8 @@ class ServiceSettings:
result.update(self.extra) result.update(self.extra)
return result return result
def apply_update(self: _S, update: _S) -> Set[str]: def apply_update(self: _S, update: _S) -> Dict[str, Any]:
"""Apply *update* onto this settings object, returning changed field names. """Apply *update* onto this settings object, returning changed fields.
Only fields in *update* that are **given** (i.e. not ``NOT_GIVEN``) Only fields in *update* that are **given** (i.e. not ``NOT_GIVEN``)
are considered. A field is "changed" if its new value differs from 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. update: A settings object of the same type containing the delta.
Returns: 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:: Examples::
current = TTSSettings(voice="alice", language="en") current = TTSSettings(voice="alice", language="en")
delta = TTSSettings(voice="bob") delta = TTSSettings(voice="bob")
changed = current.apply_update(delta) changed = current.apply_update(delta)
# changed == {"voice"} # changed == {"voice": "alice"}
# current.voice == "bob", current.language == "en" # current.voice == "bob", current.language == "en"
""" """
changed: Set[str] = set() changed: Dict[str, Any] = {}
for f in fields(self): for f in fields(self):
if f.name == "extra": if f.name == "extra":
continue continue
@@ -174,14 +176,14 @@ class ServiceSettings:
old_val = getattr(self, f.name) old_val = getattr(self, f.name)
if old_val != new_val: if old_val != new_val:
setattr(self, f.name, new_val) setattr(self, f.name, new_val)
changed.add(f.name) changed[f.name] = old_val
# Merge extra # Merge extra
for key, new_val in update.extra.items(): for key, new_val in update.extra.items():
old_val = self.extra.get(key, NOT_GIVEN) old_val = self.extra.get(key, NOT_GIVEN)
if old_val != new_val: if old_val != new_val:
self.extra[key] = new_val self.extra[key] = new_val
changed.add(key) changed[key] = old_val
return changed return changed

View File

@@ -217,7 +217,7 @@ class SonioxSTTService(WebsocketSTTService):
await super().start(frame) await super().start(frame)
await self._connect() 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. """Apply a settings update, keeping ``input_params`` in sync.
Top-level ``model`` is the source of truth. When it is given in Top-level ``model`` is the source of truth. When it is given in
@@ -231,7 +231,7 @@ class SonioxSTTService(WebsocketSTTService):
update: A settings delta. update: A settings delta.
Returns: 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)) model_given = is_given(getattr(update, "model", NOT_GIVEN))

View File

@@ -480,7 +480,7 @@ class SpeechmaticsSTTService(STTService):
await super().start(frame) await super().start(frame)
await self._connect() 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. """Apply settings update, reconnecting only when necessary.
Fields are classified into three categories (see Fields are classified into three categories (see
@@ -497,7 +497,7 @@ class SpeechmaticsSTTService(STTService):
update: A settings delta. update: A settings delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
@@ -505,7 +505,7 @@ class SpeechmaticsSTTService(STTService):
return changed return changed
no_reconnect = SpeechmaticsSTTSettings.HOT_FIELDS | SpeechmaticsSTTSettings.LOCAL_FIELDS no_reconnect = SpeechmaticsSTTSettings.HOT_FIELDS | SpeechmaticsSTTSettings.LOCAL_FIELDS
needs_reconnect = bool(changed - no_reconnect) needs_reconnect = bool(changed.keys() - no_reconnect)
if needs_reconnect: if needs_reconnect:
# Connection-level fields changed — rebuild the SDK config # Connection-level fields changed — rebuild the SDK config

View File

@@ -236,19 +236,19 @@ class STTService(AIService):
await super().cleanup() await super().cleanup()
await self._cancel_ttfb_timeout() 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. """Apply an STT settings update.
Handles ``model`` (via parent). Does **not** call ``set_language`` Handles ``model`` (via parent). Does **not** call ``set_language``
— concrete services should override this method and handle language — concrete services should override this method and handle language
changes (including any reconnect logic) based on the returned changes (including any reconnect logic) based on the returned
changed-field set. changed-field dict.
Args: Args:
update: An STT settings delta. update: An STT settings delta.
Returns: Returns:
Set of field names whose values actually changed. Dict mapping changed field names to their previous values.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
return changed return changed

View File

@@ -432,20 +432,20 @@ class TTSService(AIService):
if not (agg_type == aggregation_type and func == transform_function) 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. """Apply a TTS settings update.
Handles ``model`` (via parent) and syncs ``_voice_id`` when voice Handles ``model`` (via parent) and syncs ``_voice_id`` when voice
changes. Translates language values before applying. Does **not** changes. Translates language values before applying. Does **not**
call ``set_voice`` or ``set_model`` directly — concrete services call ``set_voice`` or ``set_model`` directly — concrete services
should override this method and handle reconnect logic based on the should override this method and handle reconnect logic based on the
returned changed-field set. returned changed-field dict.
Args: Args:
update: A TTS settings delta. update: A TTS settings delta.
Returns: 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 # Translate language *before* applying so the stored value is canonical
if is_given(update.language) and update.language is not None: if is_given(update.language) and update.language is not None:

View File

@@ -329,7 +329,7 @@ class UltravoxRealtimeLLMService(LLMService):
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if "output_medium" in changed: if "output_medium" in changed:
await self._update_output_medium(self._settings.output_medium) 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 return changed
# #

View File

@@ -11,7 +11,7 @@ interface, including language mapping, metrics generation, and error handling.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional from typing import Any, AsyncGenerator, Optional
from loguru import logger from loguru import logger
from openai import AsyncOpenAI from openai import AsyncOpenAI
@@ -174,7 +174,7 @@ class BaseWhisperSTTService(SegmentedSTTService):
def _create_client(self, api_key: Optional[str], base_url: Optional[str]): def _create_client(self, api_key: Optional[str], base_url: Optional[str]):
return AsyncOpenAI(api_key=api_key, base_url=base_url) 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. """Apply a settings update, syncing instance variables.
Keeps ``_language``, ``_prompt``, and ``_temperature`` in sync with Keeps ``_language``, ``_prompt``, and ``_temperature`` in sync with

View File

@@ -100,7 +100,8 @@ class TestApplyUpdate:
current = TTSSettings(voice="alice", language="en") current = TTSSettings(voice="alice", language="en")
delta = TTSSettings(voice="bob") delta = TTSSettings(voice="bob")
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert changed == {"voice"} assert changed.keys() == {"voice"}
assert changed["voice"] == "alice" # old value
assert current.voice == "bob" assert current.voice == "bob"
assert current.language == "en" assert current.language == "en"
@@ -108,14 +109,14 @@ class TestApplyUpdate:
current = TTSSettings(voice="alice", language="en") current = TTSSettings(voice="alice", language="en")
delta = TTSSettings(voice="alice") delta = TTSSettings(voice="alice")
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert changed == set() assert changed == {}
assert current.voice == "alice" assert current.voice == "alice"
def test_apply_update_not_given_skipped(self): def test_apply_update_not_given_skipped(self):
current = TTSSettings(voice="alice", language="en") current = TTSSettings(voice="alice", language="en")
delta = TTSSettings() # all NOT_GIVEN delta = TTSSettings() # all NOT_GIVEN
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert changed == set() assert changed == {}
assert current.voice == "alice" assert current.voice == "alice"
assert current.language == "en" assert current.language == "en"
@@ -123,7 +124,9 @@ class TestApplyUpdate:
current = LLMSettings(temperature=0.7, max_tokens=100) current = LLMSettings(temperature=0.7, max_tokens=100)
delta = LLMSettings(temperature=0.9, max_tokens=200, top_p=0.95) delta = LLMSettings(temperature=0.9, max_tokens=200, top_p=0.95)
changed = current.apply_update(delta) 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.temperature == 0.9
assert current.max_tokens == 200 assert current.max_tokens == 200
assert current.top_p == 0.95 assert current.top_p == 0.95
@@ -135,6 +138,7 @@ class TestApplyUpdate:
delta.extra = {"speed": 1.2} delta.extra = {"speed": 1.2}
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert "speed" in changed assert "speed" in changed
assert changed["speed"] == 1.0 # old value
assert current.extra == {"speed": 1.2, "stability": 0.5} assert current.extra == {"speed": 1.2, "stability": 0.5}
def test_apply_update_extra_no_change(self): def test_apply_update_extra_no_change(self):
@@ -143,13 +147,14 @@ class TestApplyUpdate:
delta = TTSSettings() delta = TTSSettings()
delta.extra = {"speed": 1.0} delta.extra = {"speed": 1.0}
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert changed == set() assert changed == {}
def test_apply_update_model_field(self): def test_apply_update_model_field(self):
current = ServiceSettings(model="old-model") current = ServiceSettings(model="old-model")
delta = ServiceSettings(model="new-model") delta = ServiceSettings(model="new-model")
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert changed == {"model"} assert changed.keys() == {"model"}
assert changed["model"] == "old-model"
assert current.model == "new-model" assert current.model == "new-model"
def test_apply_update_none_is_a_valid_value(self): def test_apply_update_none_is_a_valid_value(self):
@@ -165,6 +170,7 @@ class TestApplyUpdate:
delta = TTSSettings(language="en") delta = TTSSettings(language="en")
changed = current.apply_update(delta) changed = current.apply_update(delta)
assert "language" in changed assert "language" in changed
assert changed["language"] is None # old value was None
assert current.language == "en" assert current.language == "en"
@@ -293,7 +299,9 @@ class TestRoundtrip:
delta = TTSSettings.from_mapping(raw) delta = TTSSettings.from_mapping(raw)
changed = current.apply_update(delta) 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.voice == "bob"
assert current.language == "en" assert current.language == "en"
assert current.extra["speed"] == 1.2 assert current.extra["speed"] == 1.2
@@ -303,6 +311,7 @@ class TestRoundtrip:
current = LLMSettings(model="gpt-4o", temperature=0.7) current = LLMSettings(model="gpt-4o", temperature=0.7)
delta = LLMSettings.from_mapping({"model": "gpt-4o-mini", "temperature": 0.9}) delta = LLMSettings.from_mapping({"model": "gpt-4o-mini", "temperature": 0.9})
changed = current.apply_update(delta) 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.model == "gpt-4o-mini"
assert current.temperature == 0.9 assert current.temperature == 0.9