From f4e9825c03b14648704e296745af40a9e7f54c9f Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Feb 2026 10:48:16 -0500 Subject: [PATCH] Remove `self._voice_id` from TTS Service implementations in favor of `self._settings.voice` --- .claude/skills/cleanup/SKILL.md | 3 +-- src/pipecat/services/asyncai/tts.py | 9 +++---- src/pipecat/services/aws/tts.py | 6 ++--- src/pipecat/services/azure/tts.py | 7 ++--- src/pipecat/services/camb/tts.py | 5 ++-- src/pipecat/services/cartesia/tts.py | 10 +++---- src/pipecat/services/deepgram/tts.py | 9 +++---- src/pipecat/services/elevenlabs/tts.py | 8 +++--- src/pipecat/services/google/tts.py | 26 +++++++++---------- src/pipecat/services/gradium/tts.py | 4 +-- src/pipecat/services/groq/tts.py | 4 +-- src/pipecat/services/hathora/tts.py | 6 ++--- src/pipecat/services/hume/tts.py | 4 +-- src/pipecat/services/inworld/tts.py | 4 +-- src/pipecat/services/kokoro/tts.py | 5 ++-- src/pipecat/services/lmnt/tts.py | 4 +-- src/pipecat/services/minimax/tts.py | 8 +++--- src/pipecat/services/neuphonic/tts.py | 7 ++--- src/pipecat/services/openai/tts.py | 5 ++-- src/pipecat/services/playht/tts.py | 9 +++---- src/pipecat/services/resembleai/tts.py | 6 ++--- src/pipecat/services/rime/tts.py | 10 +++---- src/pipecat/services/sarvam/tts.py | 25 ++++++++---------- src/pipecat/services/tts_service.py | 24 +++++++++-------- src/pipecat/services/xtts/tts.py | 5 ++-- .../utils/tracing/service_decorators.py | 5 ++-- 26 files changed, 103 insertions(+), 115 deletions(-) diff --git a/.claude/skills/cleanup/SKILL.md b/.claude/skills/cleanup/SKILL.md index c0f4945b7..5e699d588 100644 --- a/.claude/skills/cleanup/SKILL.md +++ b/.claude/skills/cleanup/SKILL.md @@ -291,9 +291,8 @@ class NewTTSService(TTSService): voice: Voice identifier to use. **kwargs: Additional arguments passed to the parent service. """ - super().__init__(**kwargs) + super().__init__(voice=voice, **kwargs) self._api_key = api_key - self._voice_id = voice ``` --- diff --git a/src/pipecat/services/asyncai/tts.py b/src/pipecat/services/asyncai/tts.py index c19aa08f8..323fa3906 100644 --- a/src/pipecat/services/asyncai/tts.py +++ b/src/pipecat/services/asyncai/tts.py @@ -153,6 +153,7 @@ class AsyncAITTSService(AudioContextTTSService): pause_frame_processing=True, push_stop_frames=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -173,7 +174,6 @@ class AsyncAITTSService(AudioContextTTSService): ) self.set_model_name(model) - self._voice_id = voice_id self._receive_task = None self._keepalive_task = None @@ -278,7 +278,7 @@ class AsyncAITTSService(AudioContextTTSService): ) init_msg = { "model_id": self._model_name, - "voice": {"mode": "id", "id": self._voice_id}, + "voice": {"mode": "id", "id": self._settings.voice}, "output_format": { "container": self._settings.output_container, "encoding": self._settings.output_encoding, @@ -497,7 +497,7 @@ class AsyncAIHttpTTSService(TTSService): params: Additional input parameters for voice customization. **kwargs: Additional arguments passed to the parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or AsyncAIHttpTTSService.InputParams() @@ -514,7 +514,6 @@ class AsyncAIHttpTTSService(TTSService): if params.language else None, ) - self._voice_id = voice_id self.set_model_name(model) self._session = aiohttp_session @@ -561,7 +560,7 @@ class AsyncAIHttpTTSService(TTSService): logger.debug(f"{self}: Generating TTS [{text}]") try: - voice_config = {"mode": "id", "id": self._voice_id} + voice_config = {"mode": "id", "id": self._settings.voice} await self.start_ttfb_metrics() payload = { "model_id": self._model_name, diff --git a/src/pipecat/services/aws/tts.py b/src/pipecat/services/aws/tts.py index e223a1abc..a277bc0b2 100644 --- a/src/pipecat/services/aws/tts.py +++ b/src/pipecat/services/aws/tts.py @@ -195,7 +195,7 @@ class AWSPollyTTSService(TTSService): params: Additional input parameters for voice customization. **kwargs: Additional arguments passed to parent TTSService class. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or AWSPollyTTSService.InputParams() @@ -222,8 +222,6 @@ class AWSPollyTTSService(TTSService): self._resampler = create_stream_resampler() - self._voice_id = voice_id - def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -299,7 +297,7 @@ class AWSPollyTTSService(TTSService): "Text": ssml, "TextType": "ssml", "OutputFormat": "pcm", - "VoiceId": self._voice_id, + "VoiceId": self._settings.voice, "Engine": self._settings.engine, # AWS only supports 8000 and 16000 for PCM. We select 16000. "SampleRate": "16000", diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index b69e60b69..2d4c01dc9 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -165,12 +165,12 @@ class AzureBaseTTSService: role=params.role, style=params.style, style_degree=params.style_degree, + voice=voice, volume=params.volume, ) self._api_key = api_key self._region = region - self._voice_id = voice self._speech_synthesizer = None def language_to_service_language(self, language: Language) -> Optional[str]: @@ -194,7 +194,7 @@ class AzureBaseTTSService: f"" - f"" + f"" "" ) @@ -295,6 +295,7 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): push_stop_frames=True, pause_frame_processing=True, sample_rate=sample_rate, + voice=voice, **kwargs, ) @@ -733,7 +734,7 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService): params: Voice and synthesis parameters configuration. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice, **kwargs) # Initialize Azure-specific functionality from mixin self._init_azure_base(api_key=api_key, region=region, voice=voice, params=params) diff --git a/src/pipecat/services/camb/tts.py b/src/pipecat/services/camb/tts.py index 40dabd17e..ec0853424 100644 --- a/src/pipecat/services/camb/tts.py +++ b/src/pipecat/services/camb/tts.py @@ -213,7 +213,7 @@ class CambTTSService(TTSService): params: Additional voice parameters. If None, uses defaults. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) self._api_key = api_key self._timeout = timeout @@ -238,7 +238,6 @@ class CambTTSService(TTSService): ) self.set_model_name(model) - self._voice_id = voice_id self._client = None @@ -299,7 +298,7 @@ class CambTTSService(TTSService): # Build SDK parameters tts_kwargs: Dict[str, Any] = { "text": text, - "voice_id": self._voice_id, + "voice_id": self._settings.voice, "language": self._settings.language, "speech_model": self.model_name, "output_configuration": StreamTtsOutputConfiguration(format="pcm_s16le"), diff --git a/src/pipecat/services/cartesia/tts.py b/src/pipecat/services/cartesia/tts.py index 0d8936fdd..a16aa2b39 100644 --- a/src/pipecat/services/cartesia/tts.py +++ b/src/pipecat/services/cartesia/tts.py @@ -313,6 +313,7 @@ class CartesiaTTSService(AudioContextWordTTSService): pause_frame_processing=True, sample_rate=sample_rate, text_aggregator=text_aggregator, + voice=voice_id, **kwargs, ) @@ -340,9 +341,9 @@ class CartesiaTTSService(AudioContextWordTTSService): emotion=params.emotion, generation_config=params.generation_config, pronunciation_dict_id=params.pronunciation_dict_id, + voice=voice_id, ) self.set_model_name(model) - self._voice_id = voice_id self._context_id = None self._receive_task = None @@ -440,7 +441,7 @@ class CartesiaTTSService(AudioContextWordTTSService): ): voice_config = {} voice_config["mode"] = "id" - voice_config["id"] = self._voice_id + voice_config["id"] = self._settings.voice if is_given(self._settings.emotion) and self._settings.emotion: with warnings.catch_warnings(): @@ -720,7 +721,7 @@ class CartesiaHttpTTSService(TTSService): params: Additional input parameters for voice customization. **kwargs: Additional arguments passed to the parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or CartesiaHttpTTSService.InputParams() @@ -741,7 +742,6 @@ class CartesiaHttpTTSService(TTSService): generation_config=params.generation_config, pronunciation_dict_id=params.pronunciation_dict_id, ) - self._voice_id = voice_id self.set_model_name(model) self._client = AsyncCartesia( @@ -809,7 +809,7 @@ class CartesiaHttpTTSService(TTSService): logger.debug(f"{self}: Generating TTS [{text}]") try: - voice_config = {"mode": "id", "id": self._voice_id} + voice_config = {"mode": "id", "id": self._settings.voice} if is_given(self._settings.emotion) and self._settings.emotion: with warnings.catch_warnings(): diff --git a/src/pipecat/services/deepgram/tts.py b/src/pipecat/services/deepgram/tts.py index 3458a4529..a8b46ce7e 100644 --- a/src/pipecat/services/deepgram/tts.py +++ b/src/pipecat/services/deepgram/tts.py @@ -101,6 +101,7 @@ class DeepgramTTSService(WebsocketTTSService): pause_frame_processing=True, push_stop_frames=True, append_trailing_space=True, + voice=voice, **kwargs, ) @@ -111,7 +112,6 @@ class DeepgramTTSService(WebsocketTTSService): voice=voice, encoding=encoding, ) - self._voice_id = voice self._receive_task = None self._context_id: Optional[str] = None @@ -210,7 +210,7 @@ class DeepgramTTSService(WebsocketTTSService): # Build WebSocket URL with query parameters params = [] - params.append(f"model={self._voice_id}") + params.append(f"model={self._settings.voice}") params.append(f"encoding={self._settings.encoding}") params.append(f"sample_rate={self.sample_rate}") @@ -388,7 +388,7 @@ class DeepgramHttpTTSService(TTSService): encoding: Audio encoding format. Defaults to "linear16". **kwargs: Additional arguments passed to parent TTSService class. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice, **kwargs) self._api_key = api_key self._session = aiohttp_session @@ -398,7 +398,6 @@ class DeepgramHttpTTSService(TTSService): voice=voice, encoding=encoding, ) - self._voice_id = voice def can_generate_metrics(self) -> bool: """Check if the service can generate metrics. @@ -427,7 +426,7 @@ class DeepgramHttpTTSService(TTSService): headers = {"Authorization": f"Token {self._api_key}", "Content-Type": "application/json"} params = { - "model": self._voice_id, + "model": self._settings.voice, "encoding": self._settings.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 9503866a7..73e9027d3 100644 --- a/src/pipecat/services/elevenlabs/tts.py +++ b/src/pipecat/services/elevenlabs/tts.py @@ -400,6 +400,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService): push_stop_frames=True, pause_frame_processing=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -424,7 +425,6 @@ class ElevenLabsTTSService(AudioContextWordTTSService): apply_text_normalization=params.apply_text_normalization, ) self.set_model_name(model) - self._voice_id = voice_id self._output_format = "" # initialized in start() self._voice_settings = self._set_voice_settings() @@ -607,7 +607,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService): logger.debug("Connecting to ElevenLabs") - voice_id = self._voice_id + voice_id = self._settings.voice model = self.model_name 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}" @@ -906,6 +906,7 @@ class ElevenLabsHttpTTSService(WordTTSService): push_text_frames=False, push_stop_frames=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -931,7 +932,6 @@ class ElevenLabsHttpTTSService(WordTTSService): apply_text_normalization=params.apply_text_normalization, ) self.set_model_name(model) - self._voice_id = voice_id self._output_format = "" # initialized in start() self._voice_settings = self._set_voice_settings() self._pronunciation_dictionary_locators = params.pronunciation_dictionary_locators @@ -1098,7 +1098,7 @@ class ElevenLabsHttpTTSService(WordTTSService): logger.debug(f"{self}: Generating TTS [{text}]") # Use the with-timestamps endpoint - url = f"{self._base_url}/v1/text-to-speech/{self._voice_id}/stream/with-timestamps" + url = f"{self._base_url}/v1/text-to-speech/{self._settings.voice}/stream/with-timestamps" payload: Dict[str, Union[str, Dict[str, Union[float, bool]]]] = { "text": text, diff --git a/src/pipecat/services/google/tts.py b/src/pipecat/services/google/tts.py index 60bed9c6d..33fa1b2a8 100644 --- a/src/pipecat/services/google/tts.py +++ b/src/pipecat/services/google/tts.py @@ -602,7 +602,7 @@ class GoogleHttpTTSService(TTSService): params: Voice customization parameters including pitch, rate, volume, etc. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or GoogleHttpTTSService.InputParams() @@ -618,8 +618,8 @@ class GoogleHttpTTSService(TTSService): else "en-US", gender=params.gender, google_style=params.google_style, + voice=voice_id, ) - self._voice_id = voice_id self._client: texttospeech_v1.TextToSpeechAsyncClient = self._create_client( credentials, credentials_path ) @@ -707,7 +707,7 @@ class GoogleHttpTTSService(TTSService): ssml = "" # Voice tag - voice_attrs = [f"name='{self._voice_id}'"] + voice_attrs = [f"name='{self._settings.voice}'"] language = self._settings.language voice_attrs.append(f"language='{language}'") @@ -766,8 +766,8 @@ class GoogleHttpTTSService(TTSService): await self.start_ttfb_metrics() # Check if the voice is a Chirp voice (including Chirp 3) or Journey voice - is_chirp_voice = "chirp" in self._voice_id.lower() - is_journey_voice = "journey" in self._voice_id.lower() + is_chirp_voice = "chirp" in self._settings.voice.lower() + is_journey_voice = "journey" in self._settings.voice.lower() # Create synthesis input based on voice_id if is_chirp_voice or is_journey_voice: @@ -778,7 +778,7 @@ class GoogleHttpTTSService(TTSService): synthesis_input = texttospeech_v1.SynthesisInput(ssml=ssml) voice = texttospeech_v1.VoiceSelectionParams( - language_code=self._settings.language, name=self._voice_id + language_code=self._settings.language, name=self._settings.voice ) # Build audio config with conditional speaking_rate audio_config_params = { @@ -1015,7 +1015,7 @@ class GoogleTTSService(GoogleBaseTTSService): params: Language configuration parameters. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or GoogleTTSService.InputParams() @@ -1025,8 +1025,8 @@ class GoogleTTSService(GoogleBaseTTSService): if params.language else "en-US", speaking_rate=params.speaking_rate, + voice=voice_id, ) - self._voice_id = voice_id self._voice_cloning_key = voice_cloning_key self._client: texttospeech_v1.TextToSpeechAsyncClient = self._create_client( credentials, credentials_path @@ -1073,7 +1073,7 @@ class GoogleTTSService(GoogleBaseTTSService): ) else: voice = texttospeech_v1.VoiceSelectionParams( - language_code=self._settings.language, name=self._voice_id + language_code=self._settings.language, name=self._settings.voice ) # Create streaming config @@ -1220,7 +1220,7 @@ class GeminiTTSService(GoogleBaseTTSService): f"Google TTS only supports {self.GOOGLE_SAMPLE_RATE}Hz sample rate. " f"Current rate of {sample_rate}Hz may cause issues." ) - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or GeminiTTSService.InputParams() @@ -1229,7 +1229,6 @@ class GeminiTTSService(GoogleBaseTTSService): self._location = location self._model = model - self._voice_id = voice_id self._settings = GeminiTTSSettings( language=self.language_to_service_language(params.language) if params.language @@ -1237,6 +1236,7 @@ class GeminiTTSService(GoogleBaseTTSService): prompt=params.prompt, multi_speaker=params.multi_speaker, speaker_configs=params.speaker_configs, + voice=voice_id, ) self._client: texttospeech_v1.TextToSpeechAsyncClient = self._create_client( @@ -1306,7 +1306,7 @@ class GeminiTTSService(GoogleBaseTTSService): speaker_voice_configs.append( texttospeech_v1.MultispeakerPrebuiltVoice( speaker_alias=speaker_config["speaker_alias"], - speaker_id=speaker_config.get("speaker_id", self._voice_id), + speaker_id=speaker_config.get("speaker_id", self._settings.voice), ) ) @@ -1323,7 +1323,7 @@ class GeminiTTSService(GoogleBaseTTSService): # Single speaker mode voice = texttospeech_v1.VoiceSelectionParams( language_code=self._settings.language, - name=self._voice_id, + name=self._settings.voice, model_name=self._model, ) diff --git a/src/pipecat/services/gradium/tts.py b/src/pipecat/services/gradium/tts.py index 8c18c9208..e3d855c5c 100644 --- a/src/pipecat/services/gradium/tts.py +++ b/src/pipecat/services/gradium/tts.py @@ -91,6 +91,7 @@ class GradiumTTSService(InterruptibleWordTTSService): push_stop_frames=True, pause_frame_processing=True, sample_rate=SAMPLE_RATE, + voice=voice_id, **kwargs, ) @@ -99,7 +100,6 @@ class GradiumTTSService(InterruptibleWordTTSService): # Store service configuration self._api_key = api_key self._url = url - self._voice_id = voice_id self._json_config = json_config self._settings = GradiumTTSSettings( model=model, @@ -208,7 +208,7 @@ class GradiumTTSService(InterruptibleWordTTSService): setup_msg = { "type": "setup", "output_format": "pcm", - "voice_id": self._voice_id, + "voice_id": self._settings.voice, } if self._json_config is not None: setup_msg["json_config"] = self._json_config diff --git a/src/pipecat/services/groq/tts.py b/src/pipecat/services/groq/tts.py index b3b4c5f57..78d744461 100644 --- a/src/pipecat/services/groq/tts.py +++ b/src/pipecat/services/groq/tts.py @@ -102,6 +102,7 @@ class GroqTTSService(TTSService): super().__init__( pause_frame_processing=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -110,7 +111,6 @@ class GroqTTSService(TTSService): self._api_key = api_key self._model_name = model_name self._output_format = output_format - self._voice_id = voice_id self._params = params self._settings = GroqTTSSettings( @@ -151,7 +151,7 @@ class GroqTTSService(TTSService): try: response = await self._client.audio.speech.create( model=self._model_name, - voice=self._voice_id, + voice=self._settings.voice, response_format=self._output_format, input=text, ) diff --git a/src/pipecat/services/hathora/tts.py b/src/pipecat/services/hathora/tts.py index e15dfcc54..6e75feeca 100644 --- a/src/pipecat/services/hathora/tts.py +++ b/src/pipecat/services/hathora/tts.py @@ -109,6 +109,7 @@ class HathoraTTSService(TTSService): """ super().__init__( sample_rate=sample_rate, + voice=voice_id, **kwargs, ) self._model = model @@ -125,7 +126,6 @@ class HathoraTTSService(TTSService): ) self.set_model_name(model) - self._voice_id = voice_id def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -154,8 +154,8 @@ class HathoraTTSService(TTSService): payload = {"model": self._model, "text": text} - if self._voice_id is not None: - payload["voice"] = self._voice_id + if self._settings.voice is not None: + payload["voice"] = self._settings.voice if self._settings.speed is not None: payload["speed"] = self._settings.speed if self._settings.config is not None: diff --git a/src/pipecat/services/hume/tts.py b/src/pipecat/services/hume/tts.py index 27c4b417e..a52922787 100644 --- a/src/pipecat/services/hume/tts.py +++ b/src/pipecat/services/hume/tts.py @@ -126,6 +126,7 @@ class HumeTTSService(WordTTSService): sample_rate=sample_rate, push_text_frames=False, push_stop_frames=True, + voice=voice_id, **kwargs, ) @@ -142,7 +143,6 @@ class HumeTTSService(WordTTSService): speed=params.speed, trailing_silence=params.trailing_silence, ) - self._voice_id = voice_id self._audio_bytes = b"" @@ -263,7 +263,7 @@ class HumeTTSService(WordTTSService): # Build the request payload utterance_kwargs: dict[str, Any] = { "text": text, - "voice": PostedUtteranceVoiceWithId(id=self._voice_id), + "voice": PostedUtteranceVoiceWithId(id=self._settings.voice), } if self._settings.description is not None: utterance_kwargs["description"] = self._settings.description diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index bdbbb82d7..34dc34933 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -144,6 +144,7 @@ class InworldHttpTTSService(WordTTSService): push_text_frames=False, push_stop_frames=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -173,7 +174,6 @@ class InworldHttpTTSService(WordTTSService): self._cumulative_time = 0.0 - self._voice_id = voice_id self.set_model_name(model) def can_generate_metrics(self) -> bool: @@ -519,6 +519,7 @@ class InworldTTSService(AudioContextWordTTSService): sample_rate=sample_rate, aggregate_sentences=aggregate_sentences, append_trailing_space=append_trailing_space, + voice=voice_id, **kwargs, ) @@ -563,7 +564,6 @@ class InworldTTSService(AudioContextWordTTSService): # Track the end time of the last word in the current generation self._generation_end_time = 0.0 - self._voice_id = voice_id self.set_model_name(model) def can_generate_metrics(self) -> bool: diff --git a/src/pipecat/services/kokoro/tts.py b/src/pipecat/services/kokoro/tts.py index 735145da7..9f2aac368 100644 --- a/src/pipecat/services/kokoro/tts.py +++ b/src/pipecat/services/kokoro/tts.py @@ -137,11 +137,10 @@ class KokoroTTSService(TTSService): **kwargs: Additional arguments passed to parent `TTSService`. """ - super().__init__(**kwargs) + super().__init__(voice=voice_id, **kwargs) params = params or KokoroTTSService.InputParams() - self._voice_id = voice_id self._lang_code = language_to_kokoro_language(params.language) self._settings = KokoroTTSSettings( @@ -182,7 +181,7 @@ class KokoroTTSService(TTSService): yield TTSStartedFrame(context_id=context_id) stream = self._kokoro.create_stream( - text, voice=self._voice_id, lang=self._lang_code, speed=1.0 + text, voice=self._settings.voice, lang=self._lang_code, 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 94f4a1a9e..ab56f2296 100644 --- a/src/pipecat/services/lmnt/tts.py +++ b/src/pipecat/services/lmnt/tts.py @@ -118,11 +118,11 @@ class LmntTTSService(InterruptibleTTSService): push_stop_frames=True, pause_frame_processing=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) self._api_key = api_key - self._voice_id = voice_id self.set_model_name(model) self._settings = LmntTTSSettings( model=model, @@ -235,7 +235,7 @@ class LmntTTSService(InterruptibleTTSService): # Build initial connection message init_msg = { "X-API-Key": self._api_key, - "voice": self._voice_id, + "voice": self._settings.voice, "format": self._settings.format, "sample_rate": self.sample_rate, "language": self._settings.language, diff --git a/src/pipecat/services/minimax/tts.py b/src/pipecat/services/minimax/tts.py index 290439704..ca6cfd7bc 100644 --- a/src/pipecat/services/minimax/tts.py +++ b/src/pipecat/services/minimax/tts.py @@ -227,7 +227,7 @@ class MiniMaxHttpTTSService(TTSService): params: Additional configuration parameters. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or MiniMaxHttpTTSService.InputParams() @@ -236,7 +236,6 @@ class MiniMaxHttpTTSService(TTSService): self._base_url = f"{base_url}?GroupId={group_id}" self._session = aiohttp_session self._model_name = model - self._voice_id = voice_id # Create voice settings self._settings = MiniMaxTTSSettings( @@ -251,8 +250,7 @@ class MiniMaxHttpTTSService(TTSService): audio_channel=1, ) - # Set voice and model - self._voice_id = voice_id + # Set model self.set_model_name(model) # Add language boost if provided @@ -359,7 +357,7 @@ class MiniMaxHttpTTSService(TTSService): # Build voice_setting dict for API voice_setting = { - "voice_id": self._voice_id, + "voice_id": self._settings.voice, "speed": self._settings.speed, "vol": self._settings.volume, "pitch": self._settings.pitch, diff --git a/src/pipecat/services/neuphonic/tts.py b/src/pipecat/services/neuphonic/tts.py index 5f58eb3b4..ffcbdcd8c 100644 --- a/src/pipecat/services/neuphonic/tts.py +++ b/src/pipecat/services/neuphonic/tts.py @@ -139,6 +139,7 @@ class NeuphonicTTSService(InterruptibleTTSService): push_stop_frames=True, stop_frame_timeout_s=2.0, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -151,8 +152,8 @@ class NeuphonicTTSService(InterruptibleTTSService): speed=params.speed, encoding=encoding, sampling_rate=sample_rate, + voice=voice_id, ) - self._voice_id = voice_id self._cumulative_time = 0 @@ -288,7 +289,7 @@ class NeuphonicTTSService(InterruptibleTTSService): "speed": self._settings.speed, "encoding": self._settings.encoding, "sampling_rate": self._settings.sampling_rate, - "voice_id": self._voice_id, + "voice_id": self._settings.voice, } query_params = [] @@ -442,7 +443,7 @@ class NeuphonicHttpTTSService(TTSService): params: Additional input parameters for TTS configuration. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or NeuphonicHttpTTSService.InputParams() diff --git a/src/pipecat/services/openai/tts.py b/src/pipecat/services/openai/tts.py index 2253e369a..764688125 100644 --- a/src/pipecat/services/openai/tts.py +++ b/src/pipecat/services/openai/tts.py @@ -132,10 +132,9 @@ class OpenAITTSService(TTSService): f"OpenAI TTS only supports {self.OPENAI_SAMPLE_RATE}Hz sample rate. " f"Current rate of {sample_rate}Hz may cause issues." ) - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice, **kwargs) self.set_model_name(model) - self._voice_id = voice self._client = AsyncOpenAI(api_key=api_key, base_url=base_url) if instructions or speed: @@ -196,7 +195,7 @@ class OpenAITTSService(TTSService): create_params = { "input": text, "model": self.model_name, - "voice": VALID_VOICES[self._voice_id], + "voice": VALID_VOICES[self._settings.voice], "response_format": "pcm", } diff --git a/src/pipecat/services/playht/tts.py b/src/pipecat/services/playht/tts.py index b5c683fbe..0242eaac1 100644 --- a/src/pipecat/services/playht/tts.py +++ b/src/pipecat/services/playht/tts.py @@ -173,6 +173,7 @@ class PlayHTTTSService(InterruptibleTTSService): super().__init__( pause_frame_processing=True, sample_rate=sample_rate, + voice=voice_url, **kwargs, ) @@ -205,7 +206,6 @@ class PlayHTTTSService(InterruptibleTTSService): seed=params.seed, ) self.set_model_name(voice_engine) - self._voice_id = voice_url def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -425,7 +425,7 @@ class PlayHTTTSService(InterruptibleTTSService): tts_command = { "text": text, - "voice": self._voice_id, + "voice": self._settings.voice, "voice_engine": self._settings.voice_engine, "output_format": self._settings.output_format, "sample_rate": self.sample_rate, @@ -511,7 +511,7 @@ class PlayHTHttpTTSService(TTSService): params: Additional input parameters for voice customization. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_url, **kwargs) # Warn about deprecated protocol parameter if explicitly provided if protocol: @@ -556,7 +556,6 @@ class PlayHTHttpTTSService(TTSService): seed=params.seed, ) self.set_model_name(voice_engine) - self._voice_id = voice_url async def start(self, frame: StartFrame): """Start the PlayHT HTTP TTS service. @@ -605,7 +604,7 @@ class PlayHTHttpTTSService(TTSService): # Prepare the request payload payload = { "text": text, - "voice": self._voice_id, + "voice": self._settings.voice, "voice_engine": self._settings.voice_engine, "output_format": self._settings.output_format, "sample_rate": self.sample_rate, diff --git a/src/pipecat/services/resembleai/tts.py b/src/pipecat/services/resembleai/tts.py index f2873a8a1..79fdf54a9 100644 --- a/src/pipecat/services/resembleai/tts.py +++ b/src/pipecat/services/resembleai/tts.py @@ -94,11 +94,11 @@ class ResembleAITTSService(AudioContextWordTTSService): """ super().__init__( sample_rate=sample_rate, + voice=voice_id, **kwargs, ) self._api_key = api_key - self._voice_id = voice_id self._url = url self._settings = ResembleAITTSSettings( voice=voice_id, @@ -126,8 +126,6 @@ class ResembleAITTSService(AudioContextWordTTSService): self._jitter_buffer_bytes = 44100 # ~1000ms at 22050Hz to handle 400ms+ network gaps self._playback_started: dict[str, bool] = {} # Track if we've started playback per request - self._voice_id = voice_id - def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -146,7 +144,7 @@ class ResembleAITTSService(AudioContextWordTTSService): JSON string containing the request payload. """ msg = { - "voice_uuid": self._voice_id, + "voice_uuid": self._settings.voice, "data": text, "binary_response": False, # Use JSON frames to get timestamps "request_id": self._request_id_counter, # ResembleAI only accepts number diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index 87596cefd..99250bce0 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -191,6 +191,7 @@ class RimeTTSService(AudioContextWordTTSService): pause_frame_processing=True, append_trailing_space=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) @@ -207,7 +208,6 @@ class RimeTTSService(AudioContextWordTTSService): # Store service configuration self._api_key = api_key self._url = url - self._voice_id = voice_id self._model = model self._settings = RimeTTSSettings( voice=voice_id, @@ -582,7 +582,7 @@ class RimeHttpTTSService(TTSService): params: Additional configuration parameters. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) params = params or RimeHttpTTSService.InputParams() @@ -596,8 +596,8 @@ class RimeHttpTTSService(TTSService): pauseBetweenBrackets=params.pause_between_brackets, phonemizeBetweenBrackets=params.phonemize_between_brackets, inlineSpeedAlpha=params.inline_speed_alpha if params.inline_speed_alpha else NOT_GIVEN, + voice=voice_id, ) - self._voice_id = voice_id self.set_model_name(model) def can_generate_metrics(self) -> bool: @@ -648,7 +648,7 @@ class RimeHttpTTSService(TTSService): if is_given(self._settings.inlineSpeedAlpha): payload["inlineSpeedAlpha"] = self._settings.inlineSpeedAlpha payload["text"] = text - payload["speaker"] = self._voice_id + payload["speaker"] = self._settings.voice payload["modelId"] = self._model_name payload["samplingRate"] = self.sample_rate @@ -762,12 +762,12 @@ class RimeNonJsonTTSService(InterruptibleTTSService): aggregate_sentences=aggregate_sentences, push_stop_frames=True, pause_frame_processing=True, + voice=voice_id, **kwargs, ) params = params or RimeNonJsonTTSService.InputParams() self._api_key = api_key self._url = url - self._voice_id = voice_id self._model = model self._settings = RimeNonJsonTTSSettings( voice=voice_id, diff --git a/src/pipecat/services/sarvam/tts.py b/src/pipecat/services/sarvam/tts.py index 332643fc9..ba93c7c26 100644 --- a/src/pipecat/services/sarvam/tts.py +++ b/src/pipecat/services/sarvam/tts.py @@ -281,7 +281,6 @@ class SarvamTTSSettings(TTSSettings): Parameters: target_language_code: Sarvam language code. - speaker: Voice speaker ID. speech_sample_rate: Audio sample rate as string. enable_preprocessing: Enable text preprocessing. Defaults to False. **Note:** Always enabled for bulbul:v3-beta. @@ -306,7 +305,6 @@ class SarvamTTSSettings(TTSSettings): """ target_language_code: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - speaker: str | _NotGiven = field(default_factory=lambda: NOT_GIVEN) 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) @@ -460,14 +458,14 @@ class SarvamHttpTTSService(TTSService): if sample_rate is None: sample_rate = self._config.default_sample_rate - super().__init__(sample_rate=sample_rate, **kwargs) - params = params or SarvamHttpTTSService.InputParams() # Set default voice based on model if not specified if voice_id is None: voice_id = self._config.default_speaker + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) + self._api_key = api_key self._base_url = base_url self._session = aiohttp_session @@ -489,6 +487,7 @@ class SarvamHttpTTSService(TTSService): ), pace=pace, model=model, + voice=voice_id, ) # Add parameters based on model support @@ -508,7 +507,6 @@ class SarvamHttpTTSService(TTSService): logger.warning(f"temperature parameter is ignored for {model}") self.set_model_name(model) - self._voice_id = voice_id def can_generate_metrics(self) -> bool: """Check if this service can generate processing metrics. @@ -558,7 +556,7 @@ class SarvamHttpTTSService(TTSService): payload = { "text": text, "target_language_code": self._settings.language, - "speaker": self._voice_id, + "speaker": self._settings.voice, "sample_rate": self.sample_rate, "enable_preprocessing": self._settings.enable_preprocessing, "model": self._model_name, @@ -812,6 +810,10 @@ class SarvamTTSService(InterruptibleTTSService): if sample_rate is None: sample_rate = self._config.default_sample_rate + # Set default voice based on model if not specified + if voice_id is None: + voice_id = self._config.default_speaker + # Initialize parent class first super().__init__( aggregate_sentences=aggregate_sentences, @@ -819,19 +821,15 @@ class SarvamTTSService(InterruptibleTTSService): pause_frame_processing=True, push_stop_frames=True, sample_rate=sample_rate, + voice=voice_id, **kwargs, ) params = params or SarvamTTSService.InputParams() - # Set default voice based on model if not specified - if voice_id is None: - voice_id = self._config.default_speaker - # WebSocket endpoint URL with model query parameter self._websocket_url = f"{url}?model={model}" self._api_key = api_key self.set_model_name(model) - self._voice_id = voice_id # Validate and clamp pace to model's valid range pace = params.pace @@ -845,7 +843,6 @@ class SarvamTTSService(InterruptibleTTSService): target_language_code=( self.language_to_service_language(params.language) if params.language else "en-IN" ), - speaker=voice_id, speech_sample_rate=str(sample_rate), enable_preprocessing=( True if self._config.preprocessing_always_enabled else params.enable_preprocessing @@ -856,6 +853,7 @@ class SarvamTTSService(InterruptibleTTSService): output_audio_bitrate=params.output_audio_bitrate, pace=pace, model=model, + voice=voice_id, ) # Add parameters based on model support @@ -1018,11 +1016,10 @@ class SarvamTTSService(InterruptibleTTSService): """Send initial configuration message.""" if not self._websocket: raise Exception("WebSocket not connected") - self._settings.speaker = self._voice_id # Build config dict for the API config_data = { "target_language_code": self._settings.target_language_code, - "speaker": self._settings.speaker, + "speaker": self._settings.voice, "speech_sample_rate": self._settings.speech_sample_rate, "enable_preprocessing": self._settings.enable_preprocessing, "min_buffer_size": self._settings.min_buffer_size, diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index ebf0e602a..4567e2db3 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -145,6 +145,11 @@ class TTSService(AIService): text_filter: Optional[BaseTextFilter] = None, # Audio transport destination of the generated frames. transport_destination: Optional[str] = None, + # Voice identifier or name to use for speech synthesis + voice: Optional[str] = None, + # Language to use for speech synthesis. This will be translated to a + # service-specific language identifier before being applied + language: Optional[Language] = None, **kwargs, ): """Initialize the TTS service. @@ -178,6 +183,10 @@ class TTSService(AIService): Use `text_filters` instead, which allows multiple filters. transport_destination: Destination for generated audio frames. + voice: Voice identifier or name to use for speech synthesis. + language: Language to use for speech synthesis. This will be + translated to a service-specific language identifier before + being applied. **kwargs: Additional arguments passed to the parent AIService. """ super().__init__(**kwargs) @@ -191,8 +200,9 @@ class TTSService(AIService): self._append_trailing_space: bool = append_trailing_space self._init_sample_rate = sample_rate self._sample_rate = 0 - self._voice_id: str = "" - self._settings = TTSSettings() # Here in case subclass doesn't implement more specific settings (hopefully shouldn't happen) + self._settings = TTSSettings( + voice=voice, language=language + ) # Here in case subclass doesn't implement more specific settings (hopefully shouldn't happen) self._text_aggregator: BaseTextAggregator = text_aggregator or SimpleTextAggregator() if text_aggregator: import warnings @@ -427,11 +437,7 @@ class TTSService(AIService): 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 dict. + Translates language to service-specific value before applying. Args: update: A TTS settings delta. @@ -447,10 +453,6 @@ class TTSService(AIService): changed = await super()._update_settings(update) - # Keep _voice_id in sync for code that reads it directly - if "voice" in changed: - self._voice_id = self._settings.voice - return changed async def say(self, text: str): diff --git a/src/pipecat/services/xtts/tts.py b/src/pipecat/services/xtts/tts.py index 65aa25e36..5cc91709f 100644 --- a/src/pipecat/services/xtts/tts.py +++ b/src/pipecat/services/xtts/tts.py @@ -111,14 +111,13 @@ class XTTSService(TTSService): sample_rate: Audio sample rate. If None, uses default. **kwargs: Additional arguments passed to parent TTSService. """ - super().__init__(sample_rate=sample_rate, **kwargs) + super().__init__(sample_rate=sample_rate, voice=voice_id, **kwargs) self._settings = XTTSTTSSettings( voice=voice_id, language=self.language_to_service_language(language), base_url=base_url, ) - self._voice_id = voice_id self._studio_speakers: Optional[Dict[str, Any]] = None self._aiohttp_session = aiohttp_session @@ -180,7 +179,7 @@ class XTTSService(TTSService): logger.error(f"{self} no studio speakers available") return - embeddings = self._studio_speakers[self._voice_id] + embeddings = self._studio_speakers[self._settings.voice] url = self._settings.base_url + "/tts_stream" diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 42babd5cd..3b23f337a 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -190,13 +190,14 @@ def traced_tts(func: Optional[Callable] = None, *, name: Optional[str] = None) - tracer = trace.get_tracer("pipecat") with tracer.start_as_current_span(span_name, context=parent_context) as span: try: + settings = getattr(self, "_settings", {}) add_tts_span_attributes( span=span, service_name=service_class_name, model=getattr(self, "model_name") or "unknown", - voice_id=getattr(self, "_voice_id", "unknown"), + voice_id=getattr(settings, "voice", "unknown"), text=text, - settings=getattr(self, "_settings", {}), + settings=settings, character_count=len(text), operation_name="tts", cartesia_version=getattr(self, "_cartesia_version", None),