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)
```
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:

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.
"""
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.

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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.")

View File

@@ -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:

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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,

View File

@@ -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.

View File

@@ -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):

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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
#

View File

@@ -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

View File

@@ -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