Merge pull request #1600 from pipecat-ai/mb/11labs-previous-text

Add previous_text context to ElevenLabsHttpTTSService
This commit is contained in:
Mark Backman
2025-04-16 22:33:38 -04:00
committed by GitHub
2 changed files with 36 additions and 4 deletions

View File

@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
you to control aggregator settings. You can now pass these arguments when you to control aggregator settings. You can now pass these arguments when
creating aggregator pairs with `create_context_aggregator()`. creating aggregator pairs with `create_context_aggregator()`.
- Added `previous_text` context support to ElevenLabsHttpTTSService, improving
speech consistency across sentences within an LLM response.
- Added word/timestamp pairs to `ElevenLabsHttpTTSService`. - Added word/timestamp pairs to `ElevenLabsHttpTTSService`.
- It is now possible to disable `SoundfileMixer` when created. You can then use - It is now possible to disable `SoundfileMixer` when created. You can then use

View File

@@ -18,6 +18,7 @@ from pipecat.frames.frames import (
EndFrame, EndFrame,
ErrorFrame, ErrorFrame,
Frame, Frame,
LLMFullResponseEndFrame,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
@@ -508,6 +509,9 @@ class ElevenLabsHttpTTSService(WordTTSService):
self._cumulative_time = 0 self._cumulative_time = 0
self._started = False self._started = False
# Store previous text for context within a turn
self._previous_text = ""
def language_to_service_language(self, language: Language) -> Optional[str]: def language_to_service_language(self, language: Language) -> Optional[str]:
"""Convert pipecat Language to ElevenLabs language code.""" """Convert pipecat Language to ElevenLabs language code."""
return language_to_elevenlabs_language(language) return language_to_elevenlabs_language(language)
@@ -519,22 +523,32 @@ class ElevenLabsHttpTTSService(WordTTSService):
def _set_voice_settings(self): def _set_voice_settings(self):
return build_elevenlabs_voice_settings(self._settings) return build_elevenlabs_voice_settings(self._settings)
def _reset_state(self):
"""Reset internal state variables."""
self._cumulative_time = 0
self._started = False
self._previous_text = ""
logger.debug(f"{self}: Reset internal state")
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):
"""Initialize the service upon receiving a StartFrame.""" """Initialize the service upon receiving a StartFrame."""
await super().start(frame) await super().start(frame)
self._output_format = output_format_from_sample_rate(self.sample_rate) self._output_format = output_format_from_sample_rate(self.sample_rate)
self._cumulative_time = 0 self._reset_state()
self._started = False
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM): async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
await super().push_frame(frame, direction) await super().push_frame(frame, direction)
if isinstance(frame, (StartInterruptionFrame, TTSStoppedFrame)): if isinstance(frame, (StartInterruptionFrame, TTSStoppedFrame)):
# Reset timing on interruption or stop # Reset timing on interruption or stop
self._started = False self._reset_state()
self._cumulative_time = 0
if isinstance(frame, TTSStoppedFrame): if isinstance(frame, TTSStoppedFrame):
await self.add_word_timestamps([("LLMFullResponseEndFrame", 0), ("Reset", 0)]) await self.add_word_timestamps([("LLMFullResponseEndFrame", 0), ("Reset", 0)])
elif isinstance(frame, LLMFullResponseEndFrame):
# End of turn - reset previous text
self._previous_text = ""
def calculate_word_times(self, alignment_info: Mapping[str, Any]) -> List[Tuple[str, float]]: def calculate_word_times(self, alignment_info: Mapping[str, Any]) -> List[Tuple[str, float]]:
"""Calculate word timing from character alignment data. """Calculate word timing from character alignment data.
@@ -597,6 +611,10 @@ class ElevenLabsHttpTTSService(WordTTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
"""Generate speech from text using ElevenLabs streaming API with timestamps. """Generate speech from text using ElevenLabs streaming API with timestamps.
Makes a request to the ElevenLabs API to generate audio and timing data.
Tracks the duration of each utterance to ensure correct sequencing.
Includes previous text as context for better prosody continuity.
Args: Args:
text: Text to convert to speech text: Text to convert to speech
@@ -613,6 +631,10 @@ class ElevenLabsHttpTTSService(WordTTSService):
"model_id": self._model_name, "model_id": self._model_name,
} }
# Include previous text as context if available
if self._previous_text:
payload["previous_text"] = self._previous_text
if self._voice_settings: if self._voice_settings:
payload["voice_settings"] = self._voice_settings payload["voice_settings"] = self._voice_settings
@@ -701,6 +723,13 @@ class ElevenLabsHttpTTSService(WordTTSService):
if utterance_duration > 0: if utterance_duration > 0:
self._cumulative_time += utterance_duration self._cumulative_time += utterance_duration
# Append the current text to previous_text for context continuity
# Only add a space if there's already text
if self._previous_text:
self._previous_text += " " + text
else:
self._previous_text = text
except Exception as e: except Exception as e:
logger.error(f"Error in run_tts: {e}") logger.error(f"Error in run_tts: {e}")
yield ErrorFrame(error=str(e)) yield ErrorFrame(error=str(e))