diff --git a/src/pipecat/services/asyncai/tts.py b/src/pipecat/services/asyncai/tts.py index d4e2eabdc..3b1296752 100644 --- a/src/pipecat/services/asyncai/tts.py +++ b/src/pipecat/services/asyncai/tts.py @@ -9,8 +9,8 @@ import asyncio import base64 import json -from dataclasses import dataclass, field -from typing import Any, AsyncGenerator, Mapping, Optional, Self +from dataclasses import dataclass +from typing import Any, AsyncGenerator, Optional import aiohttp from loguru import logger @@ -27,7 +27,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import AudioContextTTSService, TextAggregationMode, TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -75,28 +75,9 @@ def language_to_async_language(language: Language) -> Optional[str]: @dataclass class AsyncAITTSSettings(TTSSettings): - """Settings for Async AI TTS services. + """Settings for Async AI TTS services.""" - Parameters: - output_container: Audio container format (e.g. "raw"). - output_encoding: Audio encoding format (e.g. "pcm_s16le"). - output_sample_rate: Audio sample rate in Hz. - """ - - output_container: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - output_encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - output_sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - - @classmethod - def from_mapping(cls, settings: Mapping[str, Any]) -> Self: - """Construct settings from a plain dict, destructuring legacy nested ``output_format``.""" - flat = dict(settings) - nested = flat.pop("output_format", None) - if isinstance(nested, dict): - flat.setdefault("output_container", nested.get("container")) - flat.setdefault("output_encoding", nested.get("encoding")) - flat.setdefault("output_sample_rate", nested.get("sample_rate")) - return super().from_mapping(flat) + pass class AsyncAITTSService(AudioContextTTSService): @@ -176,9 +157,6 @@ class AsyncAITTSService(AudioContextTTSService): model="async_flash_v1.0", voice=None, language=None, - output_container=container, - output_encoding=encoding, - output_sample_rate=0, ) # 2. Apply direct init arg overrides (deprecated) @@ -215,6 +193,11 @@ class AsyncAITTSService(AudioContextTTSService): self._api_version = version self._url = url + # Init-only audio format config (not runtime-updatable). + self._output_container = container + self._output_encoding = encoding + self._output_sample_rate = 0 # Set in start() + self._receive_task = None self._keepalive_task = None @@ -262,7 +245,7 @@ class AsyncAITTSService(AudioContextTTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.output_sample_rate = self.sample_rate + self._output_sample_rate = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): @@ -319,9 +302,9 @@ class AsyncAITTSService(AudioContextTTSService): "model_id": self._settings.model, "voice": {"mode": "id", "id": self._settings.voice}, "output_format": { - "container": self._settings.output_container, - "encoding": self._settings.output_encoding, - "sample_rate": self._settings.output_sample_rate, + "container": self._output_container, + "encoding": self._output_encoding, + "sample_rate": self._output_sample_rate, }, "language": self._settings.language, } @@ -567,9 +550,6 @@ class AsyncAIHttpTTSService(TTSService): model="async_flash_v1.0", voice=None, language=None, - output_container=container, - output_encoding=encoding, - output_sample_rate=0, ) # 2. Apply direct init arg overrides (deprecated) @@ -602,6 +582,11 @@ class AsyncAIHttpTTSService(TTSService): self._base_url = url self._api_version = version + # Init-only audio format config (not runtime-updatable). + self._output_container = container + self._output_encoding = encoding + self._output_sample_rate = 0 # Set in start() + self._session = aiohttp_session def can_generate_metrics(self) -> bool: @@ -630,7 +615,7 @@ class AsyncAIHttpTTSService(TTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.output_sample_rate = self.sample_rate + self._output_sample_rate = self.sample_rate @traced_tts async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: @@ -653,9 +638,9 @@ class AsyncAIHttpTTSService(TTSService): "transcript": text, "voice": voice_config, "output_format": { - "container": self._settings.output_container, - "encoding": self._settings.output_encoding, - "sample_rate": self._settings.output_sample_rate, + "container": self._output_container, + "encoding": self._output_encoding, + "sample_rate": self._output_sample_rate, }, "language": self._settings.language, } diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index f4aa85dbc..112459c5a 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -73,7 +73,6 @@ class AzureTTSSettings(TTSSettings): Parameters: emphasis: Emphasis level for speech ("strong", "moderate", "reduced"). - language: Language for synthesis. Defaults to English (US). pitch: Voice pitch adjustment (e.g., "+10%", "-5Hz", "high"). rate: Speech rate adjustment (e.g., "1.0", "1.25", "slow", "fast"). role: Voice role for expression (e.g., "YoungAdultFemale"). @@ -83,7 +82,6 @@ class AzureTTSSettings(TTSSettings): """ emphasis: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - language: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) pitch: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) rate: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) role: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -143,7 +141,6 @@ class AzureBaseTTSService: *, api_key: str, region: str, - voice: str = "en-US-SaraNeural", ): """Initialize Azure-specific configuration. @@ -152,7 +149,6 @@ class AzureBaseTTSService: Args: api_key: Azure Cognitive Services subscription key. region: Azure region identifier (e.g., "eastus", "westus2"). - voice: Voice name to use for synthesis. Defaults to "en-US-SaraNeural". """ self._api_key = api_key self._region = region @@ -343,7 +339,7 @@ class AzureTTSService(TTSService, AzureBaseTTSService): ) # Initialize Azure-specific functionality from mixin - self._init_azure_base(api_key=api_key, region=region, voice=default_settings.voice) + self._init_azure_base(api_key=api_key, region=region) self._speech_config = None self._speech_synthesizer = None @@ -842,7 +838,7 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService): ) # Initialize Azure-specific functionality from mixin - self._init_azure_base(api_key=api_key, region=region, voice=default_settings.voice) + self._init_azure_base(api_key=api_key, region=region) self._speech_config = None self._speech_synthesizer = None diff --git a/src/pipecat/services/deepgram/sagemaker/tts.py b/src/pipecat/services/deepgram/sagemaker/tts.py index 9457cc1db..0129abedb 100644 --- a/src/pipecat/services/deepgram/sagemaker/tts.py +++ b/src/pipecat/services/deepgram/sagemaker/tts.py @@ -14,7 +14,7 @@ streaming audio output. import asyncio import json -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -33,20 +33,16 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.aws.sagemaker.bidi_client import SageMakerBidiClient -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import TTSService from pipecat.utils.tracing.service_decorators import traced_tts @dataclass class DeepgramSageMakerTTSSettings(TTSSettings): - """Settings for Deepgram SageMaker TTS service. + """Settings for Deepgram SageMaker TTS service.""" - Parameters: - encoding: Audio encoding format (e.g. "linear16"). - """ - - encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class DeepgramSageMakerTTSService(TTSService): @@ -107,10 +103,9 @@ class DeepgramSageMakerTTSService(TTSService): voice = voice or "aura-2-helena-en" default_settings = DeepgramSageMakerTTSSettings( - model=voice, + model=None, voice=voice, language=None, - encoding=encoding, ) if settings is not None: default_settings.apply_update(settings) @@ -126,6 +121,7 @@ class DeepgramSageMakerTTSService(TTSService): self._endpoint_name = endpoint_name self._region = region + self._encoding = encoding self._client: Optional[SageMakerBidiClient] = None self._response_task: Optional[asyncio.Task] = None diff --git a/src/pipecat/services/deepgram/tts.py b/src/pipecat/services/deepgram/tts.py index b54cd23dc..95db998e2 100644 --- a/src/pipecat/services/deepgram/tts.py +++ b/src/pipecat/services/deepgram/tts.py @@ -11,7 +11,7 @@ for generating speech from text using various voice models. """ import json -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional import aiohttp @@ -30,7 +30,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import TTSService, WebsocketTTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -47,13 +47,9 @@ except ModuleNotFoundError as e: @dataclass class DeepgramTTSSettings(TTSSettings): - """Settings for Deepgram TTS service. + """Settings for Deepgram TTS service.""" - Parameters: - encoding: Audio encoding format (linear16, mulaw, alaw). - """ - - encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class DeepgramTTSService(WebsocketTTSService): @@ -105,10 +101,9 @@ class DeepgramTTSService(WebsocketTTSService): # 1. Initialize default_settings with hardcoded defaults default_settings = DeepgramTTSSettings( - model="aura-2-helena-en", + model=None, voice="aura-2-helena-en", language=None, - encoding=encoding, ) # 2. Apply direct init arg overrides (deprecated) @@ -132,6 +127,7 @@ class DeepgramTTSService(WebsocketTTSService): self._api_key = api_key self._base_url = base_url + self._encoding = encoding self._receive_task = None self._context_id: Optional[str] = None @@ -236,7 +232,7 @@ class DeepgramTTSService(WebsocketTTSService): # Build WebSocket URL with query parameters params = [] params.append(f"model={self._settings.voice}") - params.append(f"encoding={self._settings.encoding}") + params.append(f"encoding={self._encoding}") params.append(f"sample_rate={self.sample_rate}") url = f"{self._base_url}/v1/speak?{'&'.join(params)}" @@ -422,10 +418,9 @@ class DeepgramHttpTTSService(TTSService): """ # 1. Initialize default_settings with hardcoded defaults default_settings = DeepgramTTSSettings( - model="aura-2-helena-en", + model=None, voice="aura-2-helena-en", language=None, - encoding=encoding, ) # 2. Apply direct init arg overrides (deprecated) @@ -447,6 +442,7 @@ class DeepgramHttpTTSService(TTSService): self._api_key = api_key self._session = aiohttp_session self._base_url = base_url + self._encoding = encoding def can_generate_metrics(self) -> bool: """Check if the service can generate metrics. @@ -476,7 +472,7 @@ class DeepgramHttpTTSService(TTSService): params = { "model": self._settings.voice, - "encoding": self._settings.encoding, + "encoding": self._encoding, "sample_rate": self.sample_rate, "container": "none", } diff --git a/src/pipecat/services/elevenlabs/tts.py b/src/pipecat/services/elevenlabs/tts.py index 2447427ab..c22455e7a 100644 --- a/src/pipecat/services/elevenlabs/tts.py +++ b/src/pipecat/services/elevenlabs/tts.py @@ -201,9 +201,6 @@ class ElevenLabsTTSSettings(TTSSettings): style: Style control for voice expression (0.0 to 1.0). use_speaker_boost: Whether to use speaker boost enhancement. speed: Voice speed control (0.7 to 1.2). - auto_mode: Whether to enable automatic mode optimization. - enable_ssml_parsing: Whether to parse SSML tags in text. - enable_logging: Whether to enable ElevenLabs logging. apply_text_normalization: Text normalization mode ("auto", "on", "off"). """ @@ -212,9 +209,6 @@ class ElevenLabsTTSSettings(TTSSettings): style: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) use_speaker_boost: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) speed: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - auto_mode: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - enable_ssml_parsing: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - enable_logging: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) apply_text_normalization: Literal["auto", "on", "off"] | None | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) @@ -228,8 +222,6 @@ class ElevenLabsTTSSettings(TTSSettings): {"stability", "similarity_boost", "style", "use_speaker_boost", "speed"} ) - _aliases: ClassVar[Dict[str, str]] = {"voice_id": "voice"} - @dataclass class ElevenLabsHttpTTSSettings(TTSSettings): @@ -255,8 +247,6 @@ class ElevenLabsHttpTTSSettings(TTSSettings): default_factory=lambda: NOT_GIVEN ) - _aliases: ClassVar[Dict[str, str]] = {"voice_id": "voice"} - def calculate_word_times( alignment_info: Mapping[str, Any], @@ -430,6 +420,11 @@ class ElevenLabsTTSService(AudioContextTTSService): model="eleven_turbo_v2_5", ) + # Track init-only URL params through the override chain + _auto_mode = True + _enable_ssml_parsing = None + _enable_logging = None + # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: _warn_deprecated_param("voice_id", ElevenLabsTTSSettings, "voice") @@ -456,11 +451,11 @@ class ElevenLabsTTSService(AudioContextTTSService): if params.speed is not None: default_settings.speed = params.speed if params.auto_mode is not None: - default_settings.auto_mode = str(params.auto_mode).lower() + _auto_mode = str(params.auto_mode).lower() if params.enable_ssml_parsing is not None: - default_settings.enable_ssml_parsing = params.enable_ssml_parsing + _enable_ssml_parsing = params.enable_ssml_parsing if params.enable_logging is not None: - default_settings.enable_logging = params.enable_logging + _enable_logging = params.enable_logging if params.apply_text_normalization is not None: default_settings.apply_text_normalization = params.apply_text_normalization if _pronunciation_dictionary_locators is None: @@ -485,6 +480,11 @@ class ElevenLabsTTSService(AudioContextTTSService): self._api_key = api_key self._url = url + # Init-only WebSocket URL params (not runtime-updatable). + self._auto_mode = _auto_mode + self._enable_ssml_parsing = _enable_ssml_parsing + self._enable_logging = _enable_logging + self._output_format = "" # initialized in start() self._voice_settings = self._set_voice_settings() self._pronunciation_dictionary_locators = _pronunciation_dictionary_locators @@ -518,20 +518,7 @@ class ElevenLabsTTSService(AudioContextTTSService): return language_to_elevenlabs_language(language) def _set_voice_settings(self): - ts = self._settings - voice_setting_keys = [ - "stability", - "similarity_boost", - "style", - "use_speaker_boost", - "speed", - ] - voice_settings = {} - for key in voice_setting_keys: - val = getattr(ts, key, None) - if val is not None: - voice_settings[key] = val - return voice_settings or None + return build_elevenlabs_voice_settings(self._settings) async def _update_settings(self, delta: TTSSettings) -> dict[str, Any]: """Apply a settings delta, reconnecting as needed. @@ -670,13 +657,13 @@ class ElevenLabsTTSService(AudioContextTTSService): voice_id = self._settings.voice model = self._settings.model output_format = self._output_format - url = f"{self._url}/v1/text-to-speech/{voice_id}/multi-stream-input?model_id={model}&output_format={output_format}&auto_mode={self._settings.auto_mode}" + url = f"{self._url}/v1/text-to-speech/{voice_id}/multi-stream-input?model_id={model}&output_format={output_format}&auto_mode={self._auto_mode}" - if self._settings.enable_ssml_parsing: - url += f"&enable_ssml_parsing={self._settings.enable_ssml_parsing}" + if self._enable_ssml_parsing: + url += f"&enable_ssml_parsing={self._enable_ssml_parsing}" - if self._settings.enable_logging: - url += f"&enable_logging={self._settings.enable_logging}" + if self._enable_logging: + url += f"&enable_logging={self._enable_logging}" if self._settings.apply_text_normalization is not None: url += f"&apply_text_normalization={self._settings.apply_text_normalization}" diff --git a/src/pipecat/services/fish/tts.py b/src/pipecat/services/fish/tts.py index af02b4e99..78b021c51 100644 --- a/src/pipecat/services/fish/tts.py +++ b/src/pipecat/services/fish/tts.py @@ -52,25 +52,19 @@ class FishAudioTTSSettings(TTSSettings): """Settings for Fish Audio TTS service. Parameters: - fish_sample_rate: Audio sample rate sent to the API. latency: Latency mode ("normal" or "balanced"). Defaults to "normal". - format: Audio output format. normalize: Whether to normalize audio output. Defaults to True. prosody_speed: Speech speed multiplier (0.5-2.0). Defaults to 1.0. prosody_volume: Volume adjustment in dB. Defaults to 0. reference_id: Reference ID of the voice model. """ - fish_sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) latency: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - format: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) normalize: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) prosody_speed: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) prosody_volume: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) reference_id: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - _aliases: ClassVar[Dict[str, str]] = {"voice_id": "voice", "sample_rate": "fish_sample_rate"} - @classmethod def from_mapping(cls, settings: Mapping[str, Any]) -> Self: """Construct settings from a plain dict, destructuring legacy nested ``prosody``.""" @@ -179,9 +173,7 @@ class FishAudioTTSService(InterruptibleTTSService): default_settings = FishAudioTTSSettings( model="s1", voice=None, - fish_sample_rate=0, latency="normal", - format=output_format, normalize=True, prosody_speed=1.0, prosody_volume=0, @@ -228,6 +220,10 @@ class FishAudioTTSService(InterruptibleTTSService): self._receive_task = None self._request_id = None + # Init-only audio format config (not runtime-updatable). + self._fish_sample_rate = 0 # Set in start() + self._output_format = output_format + def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -262,7 +258,7 @@ class FishAudioTTSService(InterruptibleTTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.fish_sample_rate = self.sample_rate + self._fish_sample_rate = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): @@ -312,9 +308,9 @@ class FishAudioTTSService(InterruptibleTTSService): # Send initial start message with ormsgpack request_settings = { - "sample_rate": self._settings.fish_sample_rate, + "sample_rate": self._fish_sample_rate, "latency": self._settings.latency, - "format": self._settings.format, + "format": self._output_format, "normalize": self._settings.normalize, "prosody": { "speed": self._settings.prosody_speed, diff --git a/src/pipecat/services/google/tts.py b/src/pipecat/services/google/tts.py index 22ba47821..7cf2ee996 100644 --- a/src/pipecat/services/google/tts.py +++ b/src/pipecat/services/google/tts.py @@ -494,7 +494,6 @@ class GoogleHttpTTSSettings(TTSSettings): Range [0.25, 2.0]. volume: Volume adjustment (e.g., "loud", "soft", "+6dB"). emphasis: Emphasis level for the text. - language: Language for synthesis. Defaults to English. gender: Voice gender preference. google_style: Google-specific voice style. """ @@ -506,7 +505,6 @@ class GoogleHttpTTSSettings(TTSSettings): emphasis: Literal["strong", "moderate", "reduced", "none"] | None | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) - language: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) gender: Literal["male", "female", "neutral"] | None | _NotGiven = field( default_factory=lambda: NOT_GIVEN ) @@ -520,11 +518,9 @@ class GoogleStreamTTSSettings(TTSSettings): """Settings for Google streaming TTS service. Parameters: - language: Language for synthesis. Defaults to English. speaking_rate: The speaking rate, in the range [0.25, 2.0]. """ - language: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) speaking_rate: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -533,13 +529,11 @@ class GeminiTTSSettings(TTSSettings): """Settings for Gemini TTS service. Parameters: - language: Language for synthesis. Defaults to English. prompt: Optional style instructions for how to synthesize the content. multi_speaker: Whether to enable multi-speaker support. speaker_configs: List of speaker configurations for multi-speaker mode. """ - language: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) multi_speaker: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) speaker_configs: list[dict[str, Any]] | None | _NotGiven = field( diff --git a/src/pipecat/services/gradium/tts.py b/src/pipecat/services/gradium/tts.py index 3befb4505..2ade14367 100644 --- a/src/pipecat/services/gradium/tts.py +++ b/src/pipecat/services/gradium/tts.py @@ -6,7 +6,7 @@ import base64 import json -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -22,7 +22,7 @@ from pipecat.frames.frames import ( TTSStartedFrame, TTSStoppedFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import AudioContextTTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -40,13 +40,9 @@ SAMPLE_RATE = 48000 @dataclass class GradiumTTSSettings(TTSSettings): - """Settings for the Gradium TTS service. + """Settings for the Gradium TTS service.""" - Parameters: - output_format: Audio output format. - """ - - output_format: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class GradiumTTSService(AudioContextTTSService): @@ -108,7 +104,6 @@ class GradiumTTSService(AudioContextTTSService): model="default", voice="YTpq7expH9539ERJ", language=None, - output_format="pcm", ) # 2. Apply direct init arg overrides (deprecated) diff --git a/src/pipecat/services/groq/tts.py b/src/pipecat/services/groq/tts.py index 6f9a610e2..18b623fc8 100644 --- a/src/pipecat/services/groq/tts.py +++ b/src/pipecat/services/groq/tts.py @@ -9,7 +9,7 @@ import io import wave from dataclasses import dataclass, field -from typing import AsyncGenerator, ClassVar, Dict, Optional +from typing import AsyncGenerator, Optional from loguru import logger from pydantic import BaseModel @@ -39,16 +39,10 @@ class GroqTTSSettings(TTSSettings): """Settings for the Groq TTS service. Parameters: - output_format: Audio output format. speed: Speech speed multiplier. Defaults to 1.0. - groq_sample_rate: Audio sample rate. """ - output_format: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) speed: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - groq_sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - - _aliases: ClassVar[Dict[str, str]] = {"voice_id": "voice", "sample_rate": "groq_sample_rate"} class GroqTTSService(TTSService): @@ -122,9 +116,7 @@ class GroqTTSService(TTSService): model="canopylabs/orpheus-v1-english", voice="autumn", language="en", - output_format=output_format, speed=1.0, - groq_sample_rate=sample_rate, ) # 2. Apply direct init arg overrides (deprecated) diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index f650ce620..a1421b2ab 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -71,8 +71,6 @@ class InworldTTSSettings(TTSSettings): """Settings for Inworld TTS services. Parameters: - audio_encoding: Audio encoding format (e.g. LINEAR16). - audio_sample_rate: Audio sample rate in Hz. speaking_rate: Speaking rate for speech synthesis. temperature: Temperature for speech synthesis. auto_mode: Whether to use auto mode. Recommended when texts are sent @@ -84,8 +82,6 @@ class InworldTTSSettings(TTSSettings): timestamp_transport_strategy: Strategy for timestamp transport ("ASYNC" or "SYNC"). """ - audio_encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - audio_sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) speaking_rate: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) temperature: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) auto_mode: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -93,7 +89,6 @@ class InworldTTSSettings(TTSSettings): timestamp_transport_strategy: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) _aliases: ClassVar[Dict[str, str]] = { - "voice_id": "voice", "voiceId": "voice", "modelId": "model", "applyTextNormalization": "apply_text_normalization", @@ -107,8 +102,6 @@ class InworldTTSSettings(TTSSettings): flat = dict(settings) nested = flat.pop("audioConfig", None) if isinstance(nested, dict): - flat.setdefault("audio_encoding", nested.get("audioEncoding")) - flat.setdefault("audio_sample_rate", nested.get("sampleRateHertz")) flat.setdefault("speaking_rate", nested.get("speakingRate")) return super().from_mapping(flat) @@ -184,8 +177,6 @@ class InworldHttpTTSService(TTSService): model="inworld-tts-1.5-max", voice="Ashley", language=None, - audio_encoding=encoding, - audio_sample_rate=0, speaking_rate=None, temperature=None, timestamp_transport_strategy="ASYNC", @@ -239,6 +230,10 @@ class InworldHttpTTSService(TTSService): self._cumulative_time = 0.0 + # Init-only audio format config (not runtime-updatable). + self._audio_encoding = encoding + self._audio_sample_rate = 0 # Set in start() + def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -254,7 +249,7 @@ class InworldHttpTTSService(TTSService): frame: The start frame. """ await super().start(frame) - self._settings.audio_sample_rate = self.sample_rate + self._audio_sample_rate = self.sample_rate async def stop(self, frame: EndFrame): """Stop the Inworld TTS service. @@ -334,8 +329,8 @@ class InworldHttpTTSService(TTSService): logger.debug(f"{self}: Generating TTS [{text}] (streaming={self._streaming})") audio_config = { - "audioEncoding": self._settings.audio_encoding, - "sampleRateHertz": self._settings.audio_sample_rate, + "audioEncoding": self._audio_encoding, + "sampleRateHertz": self._audio_sample_rate, } if self._settings.speaking_rate is not None: audio_config["speakingRate"] = self._settings.speaking_rate @@ -611,8 +606,6 @@ class InworldTTSService(AudioContextTTSService): model="inworld-tts-1.5-max", voice="Ashley", language=None, - audio_encoding=encoding, - audio_sample_rate=0, speaking_rate=None, temperature=None, apply_text_normalization=None, @@ -686,6 +679,10 @@ class InworldTTSService(AudioContextTTSService): # Track the end time of the last word in the current generation self._generation_end_time = 0.0 + # Init-only audio format config (not runtime-updatable). + self._audio_encoding = encoding + self._audio_sample_rate = 0 # Set in start() + def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -701,7 +698,7 @@ class InworldTTSService(AudioContextTTSService): frame: The start frame. """ await super().start(frame) - self._settings.audio_sample_rate = self.sample_rate + self._audio_sample_rate = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): @@ -1051,8 +1048,8 @@ class InworldTTSService(AudioContextTTSService): context_id: The context ID. """ audio_config = { - "audioEncoding": self._settings.audio_encoding, - "sampleRateHertz": self._settings.audio_sample_rate, + "audioEncoding": self._audio_encoding, + "sampleRateHertz": self._audio_sample_rate, } if self._settings.speaking_rate is not None: audio_config["speakingRate"] = self._settings.speaking_rate diff --git a/src/pipecat/services/kokoro/tts.py b/src/pipecat/services/kokoro/tts.py index 60dfb65ce..0646923f3 100644 --- a/src/pipecat/services/kokoro/tts.py +++ b/src/pipecat/services/kokoro/tts.py @@ -7,7 +7,7 @@ """Kokoro TTS service implementation using kokoro-onnx.""" import os -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import AsyncGenerator, Optional @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( TTSStartedFrame, TTSStoppedFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -91,13 +91,9 @@ def language_to_kokoro_language(language: Language) -> str: @dataclass class KokoroTTSSettings(TTSSettings): - """Settings for the Kokoro TTS service. + """Settings for the Kokoro TTS service.""" - Parameters: - lang_code: Kokoro language code for synthesis. - """ - - lang_code: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class KokoroTTSService(TTSService): @@ -156,7 +152,6 @@ class KokoroTTSService(TTSService): model=None, voice=None, language=language_to_kokoro_language(Language.EN), - lang_code=language_to_kokoro_language(Language.EN), ) # 2. Apply direct init arg overrides (deprecated) @@ -168,9 +163,7 @@ class KokoroTTSService(TTSService): if params is not None: _warn_deprecated_param("params", KokoroTTSSettings) if not settings: - lang_code = language_to_kokoro_language(params.language) - default_settings.language = lang_code - default_settings.lang_code = lang_code + default_settings.language = language_to_kokoro_language(params.language) # 4. Apply settings delta (canonical API, always wins) if settings is not None: @@ -181,8 +174,6 @@ class KokoroTTSService(TTSService): **kwargs, ) - self._lang_code = default_settings.lang_code - model_file = Path(model_path) if model_path else KOKORO_CACHE_DIR / "kokoro-v1.0.onnx" voices = Path(voices_path) if voices_path else KOKORO_CACHE_DIR / "voices-v1.0.bin" @@ -196,6 +187,17 @@ class KokoroTTSService(TTSService): """Indicate that this service supports TTFB and usage metrics.""" return True + def language_to_service_language(self, language: Language) -> str: + """Convert a Language enum to kokoro-onnx language format. + + Args: + language: The language to convert. + + Returns: + The kokoro-onnx language code. + """ + return language_to_kokoro_language(language) + @traced_tts async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: """Synthesize speech from text using kokoro-onnx. @@ -215,7 +217,7 @@ class KokoroTTSService(TTSService): yield TTSStartedFrame(context_id=context_id) stream = self._kokoro.create_stream( - text, voice=self._settings.voice, lang=self._lang_code, speed=1.0 + text, voice=self._settings.voice, lang=self._settings.language, speed=1.0 ) async for samples, sample_rate in stream: diff --git a/src/pipecat/services/lmnt/tts.py b/src/pipecat/services/lmnt/tts.py index 58468bc00..9ee6d8d60 100644 --- a/src/pipecat/services/lmnt/tts.py +++ b/src/pipecat/services/lmnt/tts.py @@ -7,7 +7,7 @@ """LMNT text-to-speech service implementation.""" import json -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, AsyncGenerator, Optional from loguru import logger @@ -17,14 +17,13 @@ from pipecat.frames.frames import ( EndFrame, ErrorFrame, Frame, - InterruptionFrame, StartFrame, TTSAudioRawFrame, TTSStartedFrame, TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import InterruptibleTTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -75,13 +74,9 @@ def language_to_lmnt_language(language: Language) -> Optional[str]: @dataclass class LmntTTSSettings(TTSSettings): - """Settings for LMNT TTS service. + """Settings for LMNT TTS service.""" - Parameters: - format: Audio output format. Defaults to "raw". - """ - - format: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class LmntTTSService(InterruptibleTTSService): @@ -130,7 +125,6 @@ class LmntTTSService(InterruptibleTTSService): model="blizzard", voice=None, language=self.language_to_service_language(language), - format="raw", ) # 2. Apply direct init arg overrides (deprecated) @@ -156,6 +150,7 @@ class LmntTTSService(InterruptibleTTSService): ) self._api_key = api_key + self._output_format = "raw" self._receive_task = None self._context_id: Optional[str] = None @@ -262,7 +257,7 @@ class LmntTTSService(InterruptibleTTSService): init_msg = { "X-API-Key": self._api_key, "voice": self._settings.voice, - "format": self._settings.format, + "format": self._output_format, "sample_rate": self.sample_rate, "language": self._settings.language, "model": self._settings.model, diff --git a/src/pipecat/services/minimax/tts.py b/src/pipecat/services/minimax/tts.py index 2be25aef9..efe8c1fd9 100644 --- a/src/pipecat/services/minimax/tts.py +++ b/src/pipecat/services/minimax/tts.py @@ -12,7 +12,7 @@ for streaming text-to-speech synthesis. import json from dataclasses import dataclass, field -from typing import Any, AsyncGenerator, ClassVar, Dict, Mapping, Optional, Self +from typing import Any, AsyncGenerator, Mapping, Optional, Self import aiohttp from loguru import logger @@ -100,10 +100,6 @@ class MiniMaxTTSSettings(TTSSettings): "disgusted", "surprised", "calm", "fluent"). text_normalization: Enable text normalization (Chinese/English). latex_read: Enable LaTeX formula reading. - audio_bitrate: Audio bitrate in bps. - audio_format: Audio output format. - audio_channel: Number of audio channels. - audio_sample_rate: Audio sample rate in Hz. language_boost: Language boost string for multilingual support. """ @@ -114,14 +110,8 @@ class MiniMaxTTSSettings(TTSSettings): emotion: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) text_normalization: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) latex_read: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - audio_bitrate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - audio_format: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - audio_channel: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - audio_sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) language_boost: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - _aliases: ClassVar[Dict[str, str]] = {"voice_id": "voice"} - @classmethod def from_mapping(cls, settings: Mapping[str, Any]) -> Self: """Construct settings from a plain dict, destructuring legacy nested dicts. @@ -140,13 +130,6 @@ class MiniMaxTTSSettings(TTSSettings): flat.setdefault("text_normalization", voice.get("text_normalization")) flat.setdefault("latex_read", voice.get("latex_read")) - audio = flat.pop("audio_setting", None) - if isinstance(audio, dict): - flat.setdefault("audio_bitrate", audio.get("bitrate")) - flat.setdefault("audio_format", audio.get("format")) - flat.setdefault("audio_channel", audio.get("channel")) - flat.setdefault("audio_sample_rate", audio.get("sample_rate")) - return super().from_mapping(flat) @@ -258,10 +241,6 @@ class MiniMaxHttpTTSService(TTSService): emotion=None, text_normalization=None, latex_read=None, - audio_bitrate=128000, - audio_format="pcm", - audio_channel=1, - audio_sample_rate=0, ) # 2. Apply direct init arg overrides (deprecated) @@ -335,6 +314,12 @@ class MiniMaxHttpTTSService(TTSService): self._base_url = f"{base_url}?GroupId={group_id}" self._session = aiohttp_session + # Init-only audio format config + self._audio_bitrate = 128000 + self._audio_format = "pcm" + self._audio_channel = 1 + self._audio_sample_rate = 0 # Set in start() + def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -361,7 +346,7 @@ class MiniMaxHttpTTSService(TTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.audio_sample_rate = self.sample_rate + self._audio_sample_rate = self.sample_rate logger.debug(f"MiniMax TTS initialized with sample_rate: {self.sample_rate}") @traced_tts @@ -399,10 +384,10 @@ class MiniMaxHttpTTSService(TTSService): # Build audio_setting dict for API audio_setting = { - "bitrate": self._settings.audio_bitrate, - "format": self._settings.audio_format, - "channel": self._settings.audio_channel, - "sample_rate": self._settings.audio_sample_rate, + "bitrate": self._audio_bitrate, + "format": self._audio_format, + "channel": self._audio_channel, + "sample_rate": self._audio_sample_rate, } # Create payload from settings diff --git a/src/pipecat/services/neuphonic/tts.py b/src/pipecat/services/neuphonic/tts.py index f6cc6bf68..da14ec2e5 100644 --- a/src/pipecat/services/neuphonic/tts.py +++ b/src/pipecat/services/neuphonic/tts.py @@ -80,13 +80,9 @@ class NeuphonicTTSSettings(TTSSettings): Parameters: speed: Speech speed multiplier. Defaults to 1.0. - encoding: Audio encoding format. - sampling_rate: Audio sample rate. """ speed: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - encoding: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - sampling_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) class NeuphonicTTSService(InterruptibleTTSService): @@ -160,8 +156,6 @@ class NeuphonicTTSService(InterruptibleTTSService): voice=None, language=self.language_to_service_language(Language.EN), speed=1.0, - encoding=encoding, - sampling_rate=sample_rate, ) # 2. Apply direct init arg overrides (deprecated) @@ -200,6 +194,8 @@ class NeuphonicTTSService(InterruptibleTTSService): self._receive_task = None self._keepalive_task = None self._context_id: Optional[str] = None + self._encoding = encoding + self._sampling_rate = sample_rate def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -327,8 +323,8 @@ class NeuphonicTTSService(InterruptibleTTSService): tts_config = { "lang_code": self._settings.language, "speed": self._settings.speed, - "encoding": self._settings.encoding, - "sampling_rate": self._settings.sampling_rate, + "encoding": self._encoding, + "sampling_rate": self._sampling_rate, "voice_id": self._settings.voice, } @@ -503,8 +499,6 @@ class NeuphonicHttpTTSService(TTSService): voice=None, language=self.language_to_service_language(Language.EN) or "en", speed=1.0, - encoding=encoding, - sampling_rate=sample_rate, ) # 2. Apply direct init arg overrides (deprecated) @@ -536,6 +530,7 @@ class NeuphonicHttpTTSService(TTSService): self._api_key = api_key self._session = aiohttp_session self._base_url = url.rstrip("/") + self._encoding = encoding def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -629,7 +624,7 @@ class NeuphonicHttpTTSService(TTSService): payload = { "text": text, "lang_code": self._settings.language, - "encoding": self._settings.encoding, + "encoding": self._encoding, "sampling_rate": self.sample_rate, "speed": self._settings.speed, } diff --git a/src/pipecat/services/resembleai/tts.py b/src/pipecat/services/resembleai/tts.py index f0a8a988a..e1c1cf68a 100644 --- a/src/pipecat/services/resembleai/tts.py +++ b/src/pipecat/services/resembleai/tts.py @@ -8,8 +8,8 @@ import base64 import json -from dataclasses import dataclass, field -from typing import AsyncGenerator, ClassVar, Dict, Optional +from dataclasses import dataclass +from typing import AsyncGenerator, Optional from loguru import logger @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( TTSStartedFrame, TTSStoppedFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import AudioContextTTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -38,22 +38,9 @@ except ModuleNotFoundError as e: @dataclass class ResembleAITTSSettings(TTSSettings): - """Settings for Resemble AI TTS service. + """Settings for Resemble AI TTS service.""" - Parameters: - precision: PCM bit depth (PCM_32, PCM_24, PCM_16, or MULAW). - output_format: Audio format (wav or mp3). - resemble_sample_rate: Audio sample rate sent to the API. - """ - - precision: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - output_format: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - resemble_sample_rate: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - - _aliases: ClassVar[Dict[str, str]] = { - "voice_id": "voice", - "sample_rate": "resemble_sample_rate", - } + pass class ResembleAITTSService(AudioContextTTSService): @@ -100,21 +87,12 @@ class ResembleAITTSService(AudioContextTTSService): model=None, voice=None, language=None, - precision="PCM_16", - output_format="wav", - resemble_sample_rate=22050, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: _warn_deprecated_param("voice_id", ResembleAITTSSettings, "voice") default_settings.voice = voice_id - if precision is not None: - default_settings.precision = precision - if output_format is not None: - default_settings.output_format = output_format - if sample_rate is not None: - default_settings.resemble_sample_rate = sample_rate # 3. No params for this service @@ -133,6 +111,11 @@ class ResembleAITTSService(AudioContextTTSService): self._api_key = api_key self._url = url + # Init-only audio format config (not runtime-updatable). + self._precision = precision or "PCM_16" + self._output_format = output_format or "wav" + self._resemble_sample_rate = 0 # Set in start() + self._websocket = None self._request_id_counter = 0 self._receive_task = None @@ -174,9 +157,9 @@ class ResembleAITTSService(AudioContextTTSService): "data": text, "binary_response": False, # Use JSON frames to get timestamps "request_id": self._request_id_counter, # ResembleAI only accepts number - "output_format": self._settings.output_format, - "sample_rate": self._settings.resemble_sample_rate, - "precision": self._settings.precision, + "output_format": self._output_format, + "sample_rate": self._resemble_sample_rate, + "precision": self._precision, "no_audio_header": True, } @@ -190,7 +173,7 @@ class ResembleAITTSService(AudioContextTTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.resemble_sample_rate = self.sample_rate + self._resemble_sample_rate = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index 613d58f3a..04ebcd5fc 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -76,8 +76,6 @@ class RimeTTSSettings(TTSSettings): """Settings for Rime WS JSON and HTTP TTS services. Parameters: - audioFormat: Audio output format. - samplingRate: Audio sample rate. segment: Text segmentation mode ("immediate", "bySentence", "never"). speedAlpha: Speech speed multiplier (mistv2 only). reduceLatency: Whether to reduce latency at potential quality cost (mistv2 only). @@ -91,8 +89,6 @@ class RimeTTSSettings(TTSSettings): top_p: Cumulative probability threshold (arcana only, 0.0-1.0). """ - audioFormat: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - samplingRate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) segment: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) speedAlpha: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) reduceLatency: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -113,16 +109,12 @@ class RimeNonJsonTTSSettings(TTSSettings): """Settings for Rime non-JSON WS TTS service. Parameters: - audioFormat: Audio output format. - samplingRate: Audio sample rate. segment: Text segmentation mode ("immediate", "bySentence", "never"). repetition_penalty: Token repetition penalty (1.0-2.0). temperature: Sampling temperature (0.0-1.0). top_p: Cumulative probability threshold (0.0-1.0). """ - audioFormat: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - samplingRate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) segment: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) repetition_penalty: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) temperature: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -230,8 +222,6 @@ class RimeTTSService(AudioContextTTSService): default_settings = RimeTTSSettings( model="arcana", voice=None, - audioFormat="pcm", - samplingRate=0, # updated in start() language=None, segment=None, inlineSpeedAlpha=None, @@ -293,6 +283,10 @@ class RimeTTSService(AudioContextTTSService): **kwargs, ) + # Init-only audio format fields (not runtime-updatable) + self._audio_format = "pcm" + self._sampling_rate = 0 # updated in start() + if not text_aggregator: # Always skip tags added for spelled-out text # Note: This is primarily to support backwards compatibility. @@ -342,8 +336,8 @@ class RimeTTSService(AudioContextTTSService): params: dict[str, Any] = { "speaker": self._settings.voice, "modelId": self._settings.model, - "audioFormat": self._settings.audioFormat, - "samplingRate": self._settings.samplingRate, + "audioFormat": self._audio_format, + "samplingRate": self._sampling_rate, } if self._settings.language is not None: params["lang"] = self._settings.language @@ -437,7 +431,7 @@ class RimeTTSService(AudioContextTTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.samplingRate = self.sample_rate + self._sampling_rate = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): @@ -737,8 +731,6 @@ class RimeHttpTTSService(TTSService): model="mistv2", voice=None, language="eng", - audioFormat="pcm", - samplingRate=0, segment=None, speedAlpha=None, reduceLatency=None, @@ -789,6 +781,9 @@ class RimeHttpTTSService(TTSService): self._session = aiohttp_session self._base_url = "https://users.rime.ai/v1/rime-tts" + # Init-only audio format fields (not runtime-updatable) + self._audio_format = "pcm" + def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -974,8 +969,6 @@ class RimeNonJsonTTSService(InterruptibleTTSService): default_settings = RimeNonJsonTTSSettings( voice=None, model="arcana", - audioFormat=audio_format, - samplingRate=sample_rate, language=None, segment=None, repetition_penalty=None, @@ -1017,6 +1010,11 @@ class RimeNonJsonTTSService(InterruptibleTTSService): settings=default_settings, **kwargs, ) + + # Init-only audio format fields (not runtime-updatable) + self._audio_format = audio_format + self._sampling_rate = sample_rate + self._api_key = api_key self._url = url # Add any extra parameters for future compatibility @@ -1053,7 +1051,7 @@ class RimeNonJsonTTSService(InterruptibleTTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.samplingRate = self.sample_rate + self._sampling_rate = self.sample_rate await self._connect() async def stop(self, frame: EndFrame): @@ -1101,8 +1099,8 @@ class RimeNonJsonTTSService(InterruptibleTTSService): settings_dict = { "speaker": self._settings.voice, "modelId": self._settings.model, - "audioFormat": self._settings.audioFormat, - "samplingRate": self._settings.samplingRate, + "audioFormat": self._audio_format, + "samplingRate": self._sampling_rate, } if self._settings.language is not None: settings_dict["lang"] = self._settings.language diff --git a/src/pipecat/services/sarvam/tts.py b/src/pipecat/services/sarvam/tts.py index cf00dd8c7..659a58bdd 100644 --- a/src/pipecat/services/sarvam/tts.py +++ b/src/pipecat/services/sarvam/tts.py @@ -250,7 +250,6 @@ class SarvamHttpTTSSettings(TTSSettings): """Settings for Sarvam HTTP TTS service. Parameters: - language: Sarvam language code. enable_preprocessing: Whether to enable text preprocessing. Defaults to False. **Note:** Always enabled for bulbul:v3-beta (cannot be disabled). pace: Speech pace multiplier. Defaults to 1.0. @@ -263,16 +262,13 @@ class SarvamHttpTTSSettings(TTSSettings): temperature: Controls output randomness for bulbul:v3-beta (0.01 to 1.0). Lower values = more deterministic, higher = more random. Defaults to 0.6. **Note:** Only supported for bulbul:v3-beta. Ignored for v2. - sample_rate: Audio sample rate. """ - language: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) enable_preprocessing: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) pace: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) pitch: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) loudness: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) temperature: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - sarvam_sample_rate: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @dataclass @@ -280,19 +276,12 @@ class SarvamTTSSettings(TTSSettings): """Settings for Sarvam WebSocket TTS service. Parameters: - language: Sarvam language code (e.g. ``"hi-IN"``). Uses the standard - ``TTSSettings.language`` field. - speech_sample_rate: Audio sample rate as string. enable_preprocessing: Enable text preprocessing. Defaults to False. **Note:** Always enabled for bulbul:v3-beta. min_buffer_size: Minimum characters to buffer before generating audio. Lower values reduce latency but may affect quality. Defaults to 50. max_chunk_length: Maximum characters processed in a single chunk. Controls memory usage and processing efficiency. Defaults to 150. - output_audio_codec: Audio codec format. Options: linear16, mulaw, alaw, - opus, flac, aac, wav, mp3. Defaults to "linear16". - output_audio_bitrate: Audio bitrate (32k, 64k, 96k, 128k, 192k). - Defaults to "128k". pace: Speech pace multiplier. Defaults to 1.0. - bulbul:v2: Range 0.3 to 3.0 - bulbul:v3-beta: Range 0.5 to 2.0 @@ -307,12 +296,9 @@ class SarvamTTSSettings(TTSSettings): _aliases: ClassVar[Dict[str, str]] = {"target_language_code": "language"} - speech_sample_rate: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) enable_preprocessing: bool | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) min_buffer_size: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) max_chunk_length: int | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - output_audio_codec: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - output_audio_bitrate: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) pace: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) pitch: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) loudness: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -587,7 +573,6 @@ class SarvamHttpTTSService(TTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings.sarvam_sample_rate = self.sample_rate @traced_tts async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: @@ -881,12 +866,9 @@ class SarvamTTSService(InterruptibleTTSService): model="bulbul:v2", voice="anushka", language="en-IN", - speech_sample_rate="22050", enable_preprocessing=False, min_buffer_size=50, max_chunk_length=150, - output_audio_codec="linear16", - output_audio_bitrate="128k", pace=1.0, pitch=None, loudness=None, @@ -901,6 +883,10 @@ class SarvamTTSService(InterruptibleTTSService): _warn_deprecated_param("voice_id", SarvamTTSSettings, "voice") default_settings.voice = voice_id + # Init-only audio format fields (not runtime-updatable) + output_audio_codec = "linear16" + output_audio_bitrate = "128k" + # 3. Apply params overrides — only if settings not provided if params is not None: _warn_deprecated_param("params", SarvamTTSSettings) @@ -916,9 +902,9 @@ class SarvamTTSService(InterruptibleTTSService): if params.max_chunk_length is not None: default_settings.max_chunk_length = params.max_chunk_length if params.output_audio_codec is not None: - default_settings.output_audio_codec = params.output_audio_codec + output_audio_codec = params.output_audio_codec if params.output_audio_bitrate is not None: - default_settings.output_audio_bitrate = params.output_audio_bitrate + output_audio_bitrate = params.output_audio_bitrate if params.pace is not None: default_settings.pace = params.pace if params.pitch is not None: @@ -943,7 +929,6 @@ class SarvamTTSService(InterruptibleTTSService): # Set default sample rate based on model if not specified if sample_rate is None: sample_rate = self._config.default_sample_rate - default_settings.speech_sample_rate = str(sample_rate) # Set default voice based on model if not specified via any mechanism if voice_id is None and (settings is None or settings.voice is NOT_GIVEN): @@ -986,6 +971,11 @@ class SarvamTTSService(InterruptibleTTSService): **kwargs, ) + # Init-only audio format fields (not runtime-updatable) + self._speech_sample_rate = str(sample_rate) + self._output_audio_codec = output_audio_codec + self._output_audio_bitrate = output_audio_bitrate + # WebSocket endpoint URL with model query parameter self._websocket_url = f"{url}?model={resolved_model}" self._api_key = api_key @@ -1022,7 +1012,7 @@ class SarvamTTSService(InterruptibleTTSService): await super().start(frame) # WebSocket API expects sample rate as string - self._settings.speech_sample_rate = str(self.sample_rate) + self._speech_sample_rate = str(self.sample_rate) await self._connect() async def stop(self, frame: EndFrame): @@ -1140,12 +1130,12 @@ class SarvamTTSService(InterruptibleTTSService): config_data = { "target_language_code": self._settings.language, "speaker": self._settings.voice, - "speech_sample_rate": self._settings.speech_sample_rate, + "speech_sample_rate": self._speech_sample_rate, "enable_preprocessing": self._settings.enable_preprocessing, "min_buffer_size": self._settings.min_buffer_size, "max_chunk_length": self._settings.max_chunk_length, - "output_audio_codec": self._settings.output_audio_codec, - "output_audio_bitrate": self._settings.output_audio_bitrate, + "output_audio_codec": self._output_audio_codec, + "output_audio_bitrate": self._output_audio_bitrate, "pace": self._settings.pace, "model": self._settings.model, } diff --git a/src/pipecat/services/xtts/tts.py b/src/pipecat/services/xtts/tts.py index adf11da6a..ea7cb0b8b 100644 --- a/src/pipecat/services/xtts/tts.py +++ b/src/pipecat/services/xtts/tts.py @@ -10,7 +10,7 @@ This module provides integration with Coqui XTTS streaming server for text-to-speech synthesis using local Docker deployment. """ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import AsyncGenerator, Dict, Optional import aiohttp @@ -25,7 +25,7 @@ from pipecat.frames.frames import ( TTSStartedFrame, TTSStoppedFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import TTSSettings, _warn_deprecated_param from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -72,13 +72,9 @@ def language_to_xtts_language(language: Language) -> Optional[str]: @dataclass class XTTSTTSSettings(TTSSettings): - """Settings for XTTS TTS service. + """Settings for XTTS TTS service.""" - Parameters: - base_url: Base URL of the XTTS streaming server. - """ - - base_url: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + pass class XTTSService(TTSService): @@ -123,7 +119,6 @@ class XTTSService(TTSService): model=None, voice=None, language=self.language_to_service_language(language), - base_url=base_url, ) # 2. Apply direct init arg overrides (deprecated) @@ -140,6 +135,10 @@ class XTTSService(TTSService): settings=default_settings, **kwargs, ) + + # Init-only fields (not runtime-updatable) + self._base_url = base_url + self._studio_speakers: Optional[Dict[str, Any]] = None self._aiohttp_session = aiohttp_session @@ -175,7 +174,7 @@ class XTTSService(TTSService): if self._studio_speakers: return - async with self._aiohttp_session.get(self._settings.base_url + "/studio_speakers") as r: + async with self._aiohttp_session.get(self._base_url + "/studio_speakers") as r: if r.status != 200: text = await r.text() await self.push_error( @@ -203,7 +202,7 @@ class XTTSService(TTSService): embeddings = self._studio_speakers[self._settings.voice] - url = self._settings.base_url + "/tts_stream" + url = self._base_url + "/tts_stream" payload = { "text": text.replace(".", "").replace("*", ""),