diff --git a/CHANGELOG.md b/CHANGELOG.md index b816d4ecd..dc732c569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `RimeHttpTTSService` needs an `aiohttp.ClientSession` to be passed to the + constructor as all the other HTTP-based services. + +- `RimeHttpTTSService` doesn't use a default voice anymore. + - `DeepgramSTTService` now uses the new `nova-3` model by default. If you want to use the previous model you can pass `LiveOptions(model="nova-2-general")`. (see https://deepgram.com/learn/introducing-nova-3-speech-to-text-api) @@ -27,6 +32,9 @@ stt = DeepgramSTTService(..., live_options=LiveOptions(model="nova-2-general")) ### Fixed +- Fixed an issue that was causing HTTP TTS services to push `TTSStoppedFrame` + more than once. + - Fixed a `FishAudioTTSService` issue where `TTSStoppedFrame` was not being pushed. diff --git a/src/pipecat/services/cartesia.py b/src/pipecat/services/cartesia.py index 67fc67ebf..f9f2bdbe6 100644 --- a/src/pipecat/services/cartesia.py +++ b/src/pipecat/services/cartesia.py @@ -364,9 +364,6 @@ class CartesiaHttpTTSService(TTSService): async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: logger.debug(f"Generating TTS: [{text}]") - await self.start_ttfb_metrics() - yield TTSStartedFrame() - try: voice_controls = None if self._settings["speed"] or self._settings["emotion"]: @@ -376,6 +373,8 @@ class CartesiaHttpTTSService(TTSService): if self._settings["emotion"]: voice_controls["emotion"] = self._settings["emotion"] + await self.start_ttfb_metrics() + output = await self._client.tts.sse( model_id=self._model_name, transcript=text, @@ -386,14 +385,17 @@ class CartesiaHttpTTSService(TTSService): _experimental_voice_controls=voice_controls, ) + await self.start_tts_usage_metrics(text) + + yield TTSStartedFrame() + frame = TTSAudioRawFrame( audio=output["audio"], sample_rate=self.sample_rate, num_channels=1 ) + yield frame except Exception as e: logger.error(f"{self} exception: {e}") - - await self.start_tts_usage_metrics(text) - - await self.stop_ttfb_metrics() - yield TTSStoppedFrame() + finally: + await self.stop_ttfb_metrics() + yield TTSStoppedFrame() diff --git a/src/pipecat/services/elevenlabs.py b/src/pipecat/services/elevenlabs.py index 5c312e7fe..58ec91342 100644 --- a/src/pipecat/services/elevenlabs.py +++ b/src/pipecat/services/elevenlabs.py @@ -566,18 +566,16 @@ class ElevenLabsHttpTTSService(TTSService): return await self.start_tts_usage_metrics(text) + yield TTSStartedFrame() async for chunk in response.content: if chunk: await self.stop_ttfb_metrics() yield TTSAudioRawFrame(chunk, self.sample_rate, 1) - - yield TTSStoppedFrame() - except Exception as e: logger.error(f"Error in run_tts: {e}") yield ErrorFrame(error=str(e)) - finally: + await self.stop_ttfb_metrics() yield TTSStoppedFrame() diff --git a/src/pipecat/services/playht.py b/src/pipecat/services/playht.py index 297e802d3..7f0b29c87 100644 --- a/src/pipecat/services/playht.py +++ b/src/pipecat/services/playht.py @@ -397,6 +397,7 @@ class PlayHTHttpTTSService(TTSService): await self.start_tts_usage_metrics(text) yield TTSStartedFrame() + async for chunk in playht_gen: # skip the RIFF header. if in_header: @@ -416,6 +417,8 @@ class PlayHTHttpTTSService(TTSService): await self.stop_ttfb_metrics() frame = TTSAudioRawFrame(chunk, self.sample_rate, 1) yield frame - yield TTSStoppedFrame() except Exception as e: logger.error(f"{self} error generating TTS: {e}") + finally: + await self.stop_ttfb_metrics() + yield TTSStoppedFrame() diff --git a/src/pipecat/services/rime.py b/src/pipecat/services/rime.py index e12844a96..cc59b132d 100644 --- a/src/pipecat/services/rime.py +++ b/src/pipecat/services/rime.py @@ -345,7 +345,8 @@ class RimeHttpTTSService(TTSService): self, *, api_key: str, - voice_id: str = "eva", + voice_id: str, + aiohttp_session: aiohttp.ClientSession, model: str = "mistv2", sample_rate: Optional[int] = None, params: InputParams = InputParams(), @@ -354,6 +355,7 @@ class RimeHttpTTSService(TTSService): super().__init__(sample_rate=sample_rate, **kwargs) self._api_key = api_key + self._session = aiohttp_session self._base_url = "https://users.rime.ai/v1/rime-tts" self._settings = { "speedAlpha": params.speed_alpha, @@ -387,36 +389,31 @@ class RimeHttpTTSService(TTSService): try: await self.start_ttfb_metrics() - await self.start_tts_usage_metrics(text) - yield TTSStartedFrame() + async with self._session.post( + self._base_url, json=payload, headers=headers + ) as response: + if response.status != 200: + error_message = f"Rime TTS error: HTTP {response.status}" + logger.error(error_message) + yield ErrorFrame(error=error_message) + return - async with aiohttp.ClientSession() as session: - async with session.post(self._base_url, json=payload, headers=headers) as response: - if response.status != 200: - error_message = f"Rime TTS error: HTTP {response.status}" - logger.error(error_message) - yield ErrorFrame(error=error_message) - return + await self.start_tts_usage_metrics(text) - # Process the streaming response - chunk_size = 8192 - first_chunk = True + yield TTSStartedFrame() - async for chunk in response.content.iter_chunked(chunk_size): - if first_chunk: - await self.stop_ttfb_metrics() - first_chunk = False - - if chunk: - frame = TTSAudioRawFrame(chunk, self.sample_rate, 1) - yield frame - - yield TTSStoppedFrame() + # Process the streaming response + chunk_size = 8192 + async for chunk in response.content.iter_chunked(chunk_size): + if chunk: + await self.stop_ttfb_metrics() + frame = TTSAudioRawFrame(chunk, self.sample_rate, 1) + yield frame except Exception as e: logger.exception(f"Error generating TTS: {e}") yield ErrorFrame(error=f"Rime TTS error: {str(e)}") - finally: + await self.stop_ttfb_metrics() yield TTSStoppedFrame()