Add TTFB metrics

This commit is contained in:
Mark Backman
2025-01-16 22:56:25 -05:00
parent 225b65c3d2
commit 740d2743df
3 changed files with 28 additions and 10 deletions

View File

@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added `ElevenLabsHttpTTSService` and the
`07d-interruptible-elevenlabs-http.py` foundational example.
- Introduced pipeline frame observers. Observers can view all the frames that go - Introduced pipeline frame observers. Observers can view all the frames that go
through the pipeline without the need to inject processors in the through the pipeline without the need to inject processors in the
pipeline. This can be useful, for example, to implement frame loggers or pipeline. This can be useful, for example, to implement frame loggers or

View File

@@ -51,7 +51,7 @@ cerebras = [ "openai~=1.59.6" ]
deepseek = [ "openai~=1.59.6" ] deepseek = [ "openai~=1.59.6" ]
daily = [ "daily-python~=0.14.2" ] daily = [ "daily-python~=0.14.2" ]
deepgram = [ "deepgram-sdk~=3.8.0" ] deepgram = [ "deepgram-sdk~=3.8.0" ]
elevenlabs = [ "websockets~=13.1" ] elevenlabs = [ "elevenlabs~=1.50.3","websockets~=13.1" ]
fal = [ "fal-client~=0.5.6" ] fal = [ "fal-client~=0.5.6" ]
fish = [ "ormsgpack~=1.7.0", "websockets~=13.1" ] fish = [ "ormsgpack~=1.7.0", "websockets~=13.1" ]
gladia = [ "websockets~=13.1" ] gladia = [ "websockets~=13.1" ]

View File

@@ -6,11 +6,9 @@
import asyncio import asyncio
import base64 import base64
import io
import json import json
from typing import Any, AsyncGenerator, Dict, List, Literal, Mapping, Optional, Tuple from typing import Any, AsyncGenerator, Dict, List, Literal, Mapping, Optional, Tuple
import aiohttp
from loguru import logger from loguru import logger
from pydantic import BaseModel, model_validator from pydantic import BaseModel, model_validator
@@ -18,6 +16,7 @@ from pipecat.frames.frames import (
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,
EndFrame, EndFrame,
ErrorFrame,
Frame, Frame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
StartFrame, StartFrame,
@@ -28,14 +27,14 @@ from pipecat.frames.frames import (
TTSStoppedFrame, TTSStoppedFrame,
) )
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_services import WordTTSService from pipecat.services.ai_services import TTSService, WordTTSService
from pipecat.services.websocket_service import WebsocketService from pipecat.services.websocket_service import WebsocketService
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
# See .env.example for ElevenLabs configuration needed # See .env.example for ElevenLabs configuration needed
try: try:
import websockets import websockets
from elevenlabs import Voice, VoiceSettings from elevenlabs import VoiceSettings
from elevenlabs.client import ElevenLabs from elevenlabs.client import ElevenLabs
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
logger.error(f"Exception: {e}") logger.error(f"Exception: {e}")
@@ -424,7 +423,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
logger.error(f"{self} exception: {e}") logger.error(f"{self} exception: {e}")
class ElevenLabsHttpTTSService(WordTTSService): class ElevenLabsHttpTTSService(TTSService):
class InputParams(BaseModel): class InputParams(BaseModel):
language: Optional[Language] = Language.EN language: Optional[Language] = Language.EN
optimize_streaming_latency: Optional[str] = None optimize_streaming_latency: Optional[str] = None
@@ -479,6 +478,9 @@ class ElevenLabsHttpTTSService(WordTTSService):
"pcm_44100": 44100, "pcm_44100": 44100,
}[output_format] }[output_format]
def can_generate_metrics(self) -> bool:
return True
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):
await super().start(frame) await super().start(frame)
@@ -490,7 +492,6 @@ class ElevenLabsHttpTTSService(WordTTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
def read_audio_stream(**kwargs): def read_audio_stream(**kwargs):
# Run the streaming in a separate thread
audio_chunks = [] audio_chunks = []
stream = self._client.text_to_speech.convert_as_stream(**kwargs) stream = self._client.text_to_speech.convert_as_stream(**kwargs)
for chunk in stream: for chunk in stream:
@@ -498,8 +499,11 @@ class ElevenLabsHttpTTSService(WordTTSService):
audio_chunks.append(chunk) audio_chunks.append(chunk)
return b"".join(audio_chunks) return b"".join(audio_chunks)
logger.debug(f"Generating TTS: [{text}]")
try: try:
yield TTSStartedFrame() # Start TTFB metrics before any processing
await self.start_ttfb_metrics()
# Prepare parameters # Prepare parameters
params = { params = {
@@ -508,7 +512,7 @@ class ElevenLabsHttpTTSService(WordTTSService):
"model_id": self._model, "model_id": self._model,
"output_format": self._output_format, "output_format": self._output_format,
"voice_settings": self._voice_settings, "voice_settings": self._voice_settings,
"optimize_streaming_latency": 4, # Maximum optimization + disabled text normalizer "optimize_streaming_latency": 4,
} }
# Get audio data in a separate thread # Get audio data in a separate thread
@@ -519,11 +523,19 @@ class ElevenLabsHttpTTSService(WordTTSService):
yield None yield None
return return
# Start usage metrics before sending any frames
await self.start_tts_usage_metrics(text)
yield TTSStartedFrame()
# Stream the audio data in chunks # Stream the audio data in chunks
chunk_size = 4096 # Adjust this value as needed chunk_size = 4096
for i in range(0, len(audio_data), chunk_size): for i in range(0, len(audio_data), chunk_size):
chunk = audio_data[i : i + chunk_size] chunk = audio_data[i : i + chunk_size]
if len(chunk) > 0: if len(chunk) > 0:
# Stop TTFB metrics on first chunk
await self.stop_ttfb_metrics()
yield TTSAudioRawFrame( yield TTSAudioRawFrame(
chunk, self._sample_rate_from_output_format(self._output_format), 1 chunk, self._sample_rate_from_output_format(self._output_format), 1
) )
@@ -532,4 +544,7 @@ class ElevenLabsHttpTTSService(WordTTSService):
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))
finally:
yield TTSStoppedFrame() yield TTSStoppedFrame()