Fixing ElevenLabs TTS word timestamp interleaving across sentences.

This commit is contained in:
filipi87
2026-02-12 12:54:47 -03:00
parent d99a256715
commit 6d95a2425c

View File

@@ -13,6 +13,7 @@ with support for streaming audio, word timestamps, and voice customization.
import asyncio import asyncio
import base64 import base64
import json import json
import uuid
from typing import Any, AsyncGenerator, Dict, List, Literal, Mapping, Optional, Tuple, Union from typing import Any, AsyncGenerator, Dict, List, Literal, Mapping, Optional, Tuple, Union
import aiohttp import aiohttp
@@ -680,6 +681,20 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
msg = {"text": text, "context_id": self._context_id} msg = {"text": text, "context_id": self._context_id}
await self._websocket.send(json.dumps(msg)) await self._websocket.send(json.dumps(msg))
def create_context_id(self) -> str:
"""Generate a unique context ID for a TTS request in case we don't have one already in progress.
Returns:
A unique string identifier for the TTS context.
"""
# If a context ID does not exist, create a new one.
# If an ID exists, continue using the current ID.
# When interruptions happens, user speech results in
# an interruption, which resets the context ID.
if not self._context_id:
return str(uuid.uuid4())
return self._context_id
@traced_tts @traced_tts
async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]:
"""Generate speech from text using ElevenLabs' streaming WebSocket API. """Generate speech from text using ElevenLabs' streaming WebSocket API.
@@ -698,18 +713,14 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
await self._connect() await self._connect()
try: try:
if not self._context_id:
await self.start_ttfb_metrics() await self.start_ttfb_metrics()
yield TTSStartedFrame(context_id=context_id) yield TTSStartedFrame(context_id=context_id)
self._context_id = context_id
self._cumulative_time = 0 self._cumulative_time = 0
self._partial_word = "" self._partial_word = ""
self._partial_word_start_time = 0.0 self._partial_word_start_time = 0.0
# If a context ID does not exist, use the provided one.
# If an ID exists, that means the Pipeline doesn't allow
# user interruptions, so continue using the current ID.
# When interruptions are allowed, user speech results in
# an interruption, which resets the context ID.
if not self._context_id:
self._context_id = context_id
if not self.audio_context_available(self._context_id): if not self.audio_context_available(self._context_id):
await self.create_audio_context(self._context_id) await self.create_audio_context(self._context_id)
@@ -719,7 +730,8 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
msg["voice_settings"] = self._voice_settings msg["voice_settings"] = self._voice_settings
if self._pronunciation_dictionary_locators: if self._pronunciation_dictionary_locators:
msg["pronunciation_dictionary_locators"] = [ msg["pronunciation_dictionary_locators"] = [
locator.model_dump() for locator in self._pronunciation_dictionary_locators locator.model_dump()
for locator in self._pronunciation_dictionary_locators
] ]
await self._websocket.send(json.dumps(msg)) await self._websocket.send(json.dumps(msg))
logger.trace(f"Created new context {self._context_id}") logger.trace(f"Created new context {self._context_id}")