From 2b9777b812e8cdb77d13760aebc32726f75db2f2 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 12 Feb 2026 14:01:41 -0500 Subject: [PATCH] Update RimeTTSService InputParams for arcana and mistv2 model support Add model-specific params (arcana: repetition_penalty, temperature, top_p; mistv2: no_text_normalization, save_oovs, segment) with dynamic query param building via _build_settings(). Model/voice/param changes now trigger WebSocket reconnection since all settings are URL query params. Co-Authored-By: Claude Opus 4.6 --- .../foundational/07q-interruptible-rime.py | 9 +- src/pipecat/services/rime/tts.py | 162 ++++++++++++++---- 2 files changed, 141 insertions(+), 30 deletions(-) diff --git a/examples/foundational/07q-interruptible-rime.py b/examples/foundational/07q-interruptible-rime.py index 1042d05ee..d381c33dc 100644 --- a/examples/foundational/07q-interruptible-rime.py +++ b/examples/foundational/07q-interruptible-rime.py @@ -25,6 +25,7 @@ from pipecat.runner.utils import create_transport from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.rime.tts import RimeTTSService +from pipecat.transcriptions.language import Language from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams @@ -56,7 +57,13 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): tts = RimeTTSService( api_key=os.getenv("RIME_API_KEY", ""), - voice_id="rex", + voice_id="luna", + params=RimeTTSService.InputParams( + language=Language.EN, + repetition_penalty=1.0, + temperature=0.5, + top_p=0.9, + ), ) llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index a1b9f8c58..d8574c75f 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -81,17 +81,31 @@ class RimeTTSService(AudioContextWordTTSService): Parameters: language: Language for synthesis. Defaults to English. - speed_alpha: Speech speed multiplier. Defaults to 1.0. - reduce_latency: Whether to reduce latency at potential quality cost. - pause_between_brackets: Whether to add pauses between bracketed content. - phonemize_between_brackets: Whether to phonemize bracketed content. + segment: Text segmentation mode ("immediate", "bySentence", "never"). + repetition_penalty: Token repetition penalty (arcana only). + temperature: Sampling temperature (arcana only). + top_p: Cumulative probability threshold (arcana only). + speed_alpha: Speech speed multiplier (mistv2 only). + reduce_latency: Whether to reduce latency at potential quality cost (mistv2 only). + pause_between_brackets: Whether to add pauses between bracketed content (mistv2 only). + phonemize_between_brackets: Whether to phonemize bracketed content (mistv2 only). + no_text_normalization: Whether to disable text normalization (mistv2 only). + save_oovs: Whether to save out-of-vocabulary words (mistv2 only). """ language: Optional[Language] = Language.EN - speed_alpha: Optional[float] = 1.0 - reduce_latency: Optional[bool] = False - pause_between_brackets: Optional[bool] = False - phonemize_between_brackets: Optional[bool] = False + segment: Optional[str] = None + # Arcana params + repetition_penalty: Optional[float] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + # Mistv2 params + speed_alpha: Optional[float] = None + reduce_latency: Optional[bool] = None + pause_between_brackets: Optional[bool] = None + phonemize_between_brackets: Optional[bool] = None + no_text_normalization: Optional[bool] = None + save_oovs: Optional[bool] = None def __init__( self, @@ -142,26 +156,14 @@ class RimeTTSService(AudioContextWordTTSService): # and insert these tags for the purpose of the TTS service alone. self._text_aggregator = SkipTagsAggregator([("spell(", ")")]) - params = params or RimeTTSService.InputParams() + self._params = params or RimeTTSService.InputParams() # Store service configuration self._api_key = api_key self._url = url self._voice_id = voice_id self._model = model - self._settings = { - "speaker": voice_id, - "modelId": model, - "audioFormat": "pcm", - "samplingRate": 0, - "lang": self.language_to_service_language(params.language) - if params.language - else "eng", - "speedAlpha": params.speed_alpha, - "reduceLatency": params.reduce_latency, - "pauseBetweenBrackets": json.dumps(params.pause_between_brackets), - "phonemizeBetweenBrackets": json.dumps(params.phonemize_between_brackets), - } + self._settings = self._build_settings() # State tracking self._context_id = None # Tracks current turn @@ -188,14 +190,60 @@ class RimeTTSService(AudioContextWordTTSService): """ return language_to_rime_language(language) + def _build_settings(self) -> dict: + """Build query params for the WebSocket URL based on the current model and params. + + Returns: + Dictionary of query parameters. Only explicitly-set values are included. + """ + settings = { + "speaker": self._voice_id, + "modelId": self._model, + "audioFormat": "pcm", + "samplingRate": self.sample_rate or 0, + } + if self._params.language: + settings["lang"] = self.language_to_service_language(self._params.language) or "eng" + if self._params.segment is not None: + settings["segment"] = self._params.segment + + if self._model == "arcana": + if self._params.repetition_penalty is not None: + settings["repetition_penalty"] = self._params.repetition_penalty + if self._params.temperature is not None: + settings["temperature"] = self._params.temperature + if self._params.top_p is not None: + settings["top_p"] = self._params.top_p + else: # mistv2/mist + if self._params.speed_alpha is not None: + settings["speedAlpha"] = self._params.speed_alpha + if self._params.reduce_latency is not None: + settings["reduceLatency"] = self._params.reduce_latency + if self._params.pause_between_brackets is not None: + settings["pauseBetweenBrackets"] = json.dumps(self._params.pause_between_brackets) + if self._params.phonemize_between_brackets is not None: + settings["phonemizeBetweenBrackets"] = json.dumps( + self._params.phonemize_between_brackets + ) + if self._params.no_text_normalization is not None: + settings["noTextNormalization"] = json.dumps(self._params.no_text_normalization) + if self._params.save_oovs is not None: + settings["saveOovs"] = json.dumps(self._params.save_oovs) + + return settings + async def set_model(self, model: str): - """Update the TTS model. + """Update the TTS model and reconnect. Args: model: The model name to use for synthesis. """ self._model = model + self._settings = self._build_settings() await super().set_model(model) + if self._websocket: + await self._disconnect() + await self._connect() # A set of Rime-specific helpers for text transformations def SPELL(text: str) -> str: @@ -223,12 +271,68 @@ class RimeTTSService(AudioContextWordTTSService): return f"[{text}]" async def _update_settings(self, settings: Mapping[str, Any]): - """Update service settings and reconnect if voice changed.""" - prev_voice = self._voice_id + """Update service settings and reconnect if necessary. + + Since all settings are WebSocket URL query parameters, + any setting change requires reconnecting to apply the new values. + """ + prev_settings = self._settings.copy() await super()._update_settings(settings) - if not prev_voice == self._voice_id: + + needs_reconnect = False + + if "voice" in settings or "voice_id" in settings: self._settings["speaker"] = self._voice_id - logger.info(f"Switching TTS voice to: [{self._voice_id}]") + if prev_settings.get("speaker") != self._voice_id: + logger.info(f"Switching TTS voice to: [{self._voice_id}]") + needs_reconnect = True + + if "model" in settings: + self._settings = self._build_settings() + needs_reconnect = True + + if "language" in settings: + new_lang = self.language_to_service_language(settings["language"]) + if new_lang and new_lang != prev_settings.get("lang"): + logger.info(f"Updating language to: [{new_lang}]") + self._settings["lang"] = new_lang + needs_reconnect = True + + # Arcana params + for key, settings_key in [ + ("repetition_penalty", "repetition_penalty"), + ("temperature", "temperature"), + ("top_p", "top_p"), + ]: + if key in settings and settings[key] != prev_settings.get(settings_key): + self._settings[settings_key] = settings[key] + needs_reconnect = True + + # Mistv2 params + for key, settings_key in [ + ("speed_alpha", "speedAlpha"), + ("reduce_latency", "reduceLatency"), + ]: + if key in settings and settings[key] != prev_settings.get(settings_key): + self._settings[settings_key] = settings[key] + needs_reconnect = True + + # Mistv2 boolean params (need json.dumps) + for key, settings_key in [ + ("pause_between_brackets", "pauseBetweenBrackets"), + ("phonemize_between_brackets", "phonemizeBetweenBrackets"), + ("no_text_normalization", "noTextNormalization"), + ("save_oovs", "saveOovs"), + ]: + if key in settings and json.dumps(settings[key]) != prev_settings.get(settings_key): + self._settings[settings_key] = json.dumps(settings[key]) + needs_reconnect = True + + if "segment" in settings and settings["segment"] != prev_settings.get("segment"): + self._settings["segment"] = settings["segment"] + needs_reconnect = True + + if needs_reconnect and self._websocket: await self._disconnect() await self._connect() @@ -255,7 +359,7 @@ class RimeTTSService(AudioContextWordTTSService): frame: The start frame containing initialization parameters. """ await super().start(frame) - self._settings["samplingRate"] = self.sample_rate + self._settings = self._build_settings() await self._connect() async def stop(self, frame: EndFrame): @@ -301,7 +405,7 @@ class RimeTTSService(AudioContextWordTTSService): if self._websocket and self._websocket.state is State.OPEN: return - params = "&".join(f"{k}={v}" for k, v in self._settings.items()) + params = "&".join(f"{k}={v}" for k, v in self._settings.items() if v is not None) url = f"{self._url}?{params}" headers = {"Authorization": f"Bearer {self._api_key}"} self._websocket = await websocket_connect(url, additional_headers=headers)