Merge pull request #355 from pipecat-ai/aleix/usage-metrics-update

processors(base): add start_llm_usage_metrics and start_tts_usage_met…
This commit is contained in:
Aleix Conchillo Flaqué
2024-08-09 09:35:36 -07:00
committed by GitHub
12 changed files with 65 additions and 60 deletions

View File

@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added `DailyRESTHelper.delete_room_by_name()`.
- Added LLM and TTS usage metrics. Those will be enabled by when
`enable_usage_metrics` is True.
- `AudioRawFrame`s are not pushed downstream from the base output - `AudioRawFrame`s are not pushed downstream from the base output
transport. This allows capturing the exact words the bot says by adding an STT transport. This allows capturing the exact words the bot says by adding an STT
service at the end of the pipeline. service at the end of the pipeline.

View File

@@ -79,6 +79,7 @@ async def main():
task = PipelineTask(pipeline, PipelineParams( task = PipelineTask(pipeline, PipelineParams(
allow_interruptions=True, allow_interruptions=True,
enable_metrics=True, enable_metrics=True,
enable_usage_metrics=True,
report_only_initial_ttfb=True, report_only_initial_ttfb=True,
)) ))

View File

@@ -294,6 +294,7 @@ class StartFrame(ControlFrame):
"""This is the first frame that should be pushed down a pipeline.""" """This is the first frame that should be pushed down a pipeline."""
allow_interruptions: bool = False allow_interruptions: bool = False
enable_metrics: bool = False enable_metrics: bool = False
enable_usage_metrics: bool = False
report_only_initial_ttfb: bool = False report_only_initial_ttfb: bool = False

View File

@@ -21,6 +21,7 @@ from loguru import logger
class PipelineParams(BaseModel): class PipelineParams(BaseModel):
allow_interruptions: bool = False allow_interruptions: bool = False
enable_metrics: bool = False enable_metrics: bool = False
enable_usage_metrics: bool = False
send_initial_empty_metrics: bool = True send_initial_empty_metrics: bool = True
report_only_initial_ttfb: bool = False report_only_initial_ttfb: bool = False
@@ -104,6 +105,7 @@ class PipelineTask:
start_frame = StartFrame( start_frame = StartFrame(
allow_interruptions=self._params.allow_interruptions, allow_interruptions=self._params.allow_interruptions,
enable_metrics=self._params.enable_metrics, enable_metrics=self._params.enable_metrics,
enable_usage_metrics=self._params.enable_metrics,
report_only_initial_ttfb=self._params.report_only_initial_ttfb report_only_initial_ttfb=self._params.report_only_initial_ttfb
) )
await self._source.process_frame(start_frame, FrameDirection.DOWNSTREAM) await self._source.process_frame(start_frame, FrameDirection.DOWNSTREAM)

View File

@@ -61,6 +61,19 @@ class FrameProcessorMetrics:
self._start_processing_time = 0 self._start_processing_time = 0
return MetricsFrame(processing=[processing]) return MetricsFrame(processing=[processing])
async def start_llm_usage_metrics(self, tokens: dict):
logger.debug(
f"{self._name} prompt tokens: {tokens['prompt_tokens']}, completion tokens: {tokens['completion_tokens']}")
return MetricsFrame(tokens=[tokens])
async def start_tts_usage_metrics(self, text: str):
characters = {
"processor": self._name,
"value": len(text),
}
logger.debug(f"{self._name} usage characters: {characters['value']}")
return MetricsFrame(characters=[characters])
class FrameProcessor: class FrameProcessor:
@@ -80,6 +93,7 @@ class FrameProcessor:
# Properties # Properties
self._allow_interruptions = False self._allow_interruptions = False
self._enable_metrics = False self._enable_metrics = False
self._enable_usage_metrics = False
self._report_only_initial_ttfb = False self._report_only_initial_ttfb = False
# Metrics # Metrics
@@ -93,6 +107,10 @@ class FrameProcessor:
def metrics_enabled(self): def metrics_enabled(self):
return self._enable_metrics return self._enable_metrics
@property
def usage_metrics_enabled(self):
return self._enable_usage_metrics
@property @property
def report_only_initial_ttfb(self): def report_only_initial_ttfb(self):
return self._report_only_initial_ttfb return self._report_only_initial_ttfb
@@ -120,6 +138,18 @@ class FrameProcessor:
if frame: if frame:
await self.push_frame(frame) await self.push_frame(frame)
async def start_llm_usage_metrics(self, tokens: dict):
if self.can_generate_metrics() and self.usage_metrics_enabled:
frame = await self._metrics.start_llm_usage_metrics(tokens)
if frame:
await self.push_frame(frame)
async def start_tts_usage_metrics(self, text: str):
if self.can_generate_metrics() and self.usage_metrics_enabled:
frame = await self._metrics.start_tts_usage_metrics(text)
if frame:
await self.push_frame(frame)
async def stop_all_metrics(self): async def stop_all_metrics(self):
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
await self.stop_processing_metrics() await self.stop_processing_metrics()
@@ -145,6 +175,7 @@ class FrameProcessor:
if isinstance(frame, StartFrame): if isinstance(frame, StartFrame):
self._allow_interruptions = frame.allow_interruptions self._allow_interruptions = frame.allow_interruptions
self._enable_metrics = frame.enable_metrics self._enable_metrics = frame.enable_metrics
self._enable_usage_metrics = frame.enable_usage_metrics
self._report_only_initial_ttfb = frame.report_only_initial_ttfb self._report_only_initial_ttfb = frame.report_only_initial_ttfb
elif isinstance(frame, StartInterruptionFrame): elif isinstance(frame, StartInterruptionFrame):
await self.stop_all_metrics() await self.stop_all_metrics()

View File

@@ -88,13 +88,7 @@ class AzureTTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
await self.start_ttfb_metrics() await self.start_ttfb_metrics()
ssml = ( ssml = (
@@ -110,6 +104,7 @@ class AzureTTSService(TTSService):
result = await asyncio.to_thread(self._speech_synthesizer.speak_ssml, (ssml)) result = await asyncio.to_thread(self._speech_synthesizer.speak_ssml, (ssml))
if result.reason == ResultReason.SynthesizingAudioCompleted: if result.reason == ResultReason.SynthesizingAudioCompleted:
await self.start_tts_usage_metrics(text)
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
# Azure always sends a 44-byte header. Strip it off. # Azure always sends a 44-byte header. Strip it off.
yield AudioRawFrame(audio=result.audio_data[44:], sample_rate=16000, num_channels=1) yield AudioRawFrame(audio=result.audio_data[44:], sample_rate=16000, num_channels=1)

View File

@@ -201,13 +201,6 @@ class CartesiaTTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
try: try:
if not self._websocket: if not self._websocket:
@@ -232,6 +225,7 @@ class CartesiaTTSService(TTSService):
} }
try: try:
await self._websocket.send(json.dumps(msg)) await self._websocket.send(json.dumps(msg))
await self.start_tts_usage_metrics(text)
except Exception as e: except Exception as e:
logger.exception(f"{self} error sending message: {e}") logger.exception(f"{self} error sending message: {e}")
await self._disconnect() await self._disconnect()

View File

@@ -71,13 +71,7 @@ class DeepgramTTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
base_url = self._base_url base_url = self._base_url
request_url = f"{base_url}?model={self._voice}&encoding={self._encoding}&container=none&sample_rate={self._sample_rate}" request_url = f"{base_url}?model={self._voice}&encoding={self._encoding}&container=none&sample_rate={self._sample_rate}"
headers = {"authorization": f"token {self._api_key}"} headers = {"authorization": f"token {self._api_key}"}
@@ -100,6 +94,8 @@ class DeepgramTTSService(TTSService):
yield ErrorFrame(f"Error getting audio (status: {r.status}, error: {response_text})") yield ErrorFrame(f"Error getting audio (status: {r.status}, error: {response_text})")
return return
await self.start_tts_usage_metrics(text)
async for data in r.content: async for data in r.content:
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
frame = AudioRawFrame(audio=data, sample_rate=self._sample_rate, num_channels=1) frame = AudioRawFrame(audio=data, sample_rate=self._sample_rate, num_channels=1)

View File

@@ -40,13 +40,7 @@ class ElevenLabsTTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
url = f"https://api.elevenlabs.io/v1/text-to-speech/{self._voice_id}/stream" url = f"https://api.elevenlabs.io/v1/text-to-speech/{self._voice_id}/stream"
payload = {"text": text, "model_id": self._model} payload = {"text": text, "model_id": self._model}
@@ -69,6 +63,8 @@ class ElevenLabsTTSService(TTSService):
yield ErrorFrame(f"Error getting audio (status: {r.status}, error: {text})") yield ErrorFrame(f"Error getting audio (status: {r.status}, error: {text})")
return return
await self.start_tts_usage_metrics(text)
async for chunk in r.content: async for chunk in r.content:
if len(chunk) > 0: if len(chunk) > 0:
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()

View File

@@ -135,17 +135,13 @@ class BaseOpenAILLMService(LLMService):
async for chunk in chunk_stream: async for chunk in chunk_stream:
if chunk.usage: if chunk.usage:
if self.can_generate_metrics() and self.metrics_enabled: tokens = {
tokens = { "processor": self.name,
"processor": self.name, "prompt_tokens": chunk.usage.prompt_tokens,
"prompt_tokens": chunk.usage.prompt_tokens, "completion_tokens": chunk.usage.completion_tokens,
"completion_tokens": chunk.usage.completion_tokens, "total_tokens": chunk.usage.total_tokens
"total_tokens": chunk.usage.total_tokens }
} await self.start_llm_usage_metrics(tokens)
logger.debug(
f"{self.name} prompt tokens: {tokens['prompt_tokens']}, completion tokens: {tokens['completion_tokens']}")
await self.push_frame(MetricsFrame(tokens=[tokens]))
if len(chunk.choices) == 0: if len(chunk.choices) == 0:
continue continue
@@ -338,13 +334,6 @@ class OpenAITTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
try: try:
await self.start_ttfb_metrics() await self.start_ttfb_metrics()
@@ -360,6 +349,9 @@ class OpenAITTSService(TTSService):
f"{self} error getting audio (status: {r.status_code}, error: {error})") f"{self} error getting audio (status: {r.status_code}, error: {error})")
yield ErrorFrame(f"Error getting audio (status: {r.status_code}, error: {error})") yield ErrorFrame(f"Error getting audio (status: {r.status_code}, error: {error})")
return return
await self.start_tts_usage_metrics(text)
async for chunk in r.iter_bytes(8192): async for chunk in r.iter_bytes(8192):
if len(chunk) > 0: if len(chunk) > 0:
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()

View File

@@ -48,13 +48,7 @@ class PlayHTTTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
try: try:
b = bytearray() b = bytearray()
in_header = True in_header = True
@@ -66,6 +60,8 @@ class PlayHTTTSService(TTSService):
voice_engine="PlayHT2.0-turbo", voice_engine="PlayHT2.0-turbo",
options=self._options) options=self._options)
await self.start_tts_usage_metrics(text)
async for chunk in playht_gen: async for chunk in playht_gen:
# skip the RIFF header. # skip the RIFF header.
if in_header: if in_header:

View File

@@ -70,13 +70,7 @@ class XTTSService(TTSService):
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
logger.debug(f"Generating TTS: [{text}]") logger.debug(f"Generating TTS: [{text}]")
if self.can_generate_metrics() and self.metrics_enabled:
characters = {
"processor": self.name,
"value": len(text),
}
logger.debug(f"{self.name} Characters: {characters['value']}")
await self.push_frame(MetricsFrame(characters=[characters]))
if not self._studio_speakers: if not self._studio_speakers:
logger.error(f"{self} no studio speakers available") logger.error(f"{self} no studio speakers available")
return return
@@ -103,6 +97,8 @@ class XTTSService(TTSService):
yield ErrorFrame(f"Error getting audio (status: {r.status}, error: {text})") yield ErrorFrame(f"Error getting audio (status: {r.status}, error: {text})")
return return
await self.start_tts_usage_metrics(text)
buffer = bytearray() buffer = bytearray()
async for chunk in r.content.iter_chunked(1024): async for chunk in r.content.iter_chunked(1024):