Merge pull request #1370 from pipecat-ai/aleix/minor-ultravox-updates

services(ultravox): CHANGELOG, formatting and minor changes
This commit is contained in:
Aleix Conchillo Flaqué
2025-03-13 12:05:13 -07:00
committed by GitHub
5 changed files with 120 additions and 111 deletions

View File

@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added new `UltravoxSTTService`.
(see https://github.com/fixie-ai/ultravox)
- Added `on_frame_reached_upstream` and `on_frame_reached_downstream` event - Added `on_frame_reached_upstream` and `on_frame_reached_downstream` event
handlers to `PipelineTask`. Those events will be called when a frame reaches handlers to `PipelineTask`. Those events will be called when a frame reaches
the beginning or end of the pipeline respectively. the beginning or end of the pipeline respectively.

View File

@@ -56,8 +56,8 @@ pip install "pipecat-ai[option,...]"
### Available services ### Available services
| Category | Services | Install Command Example | | Category | Services | Install Command Example |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | |---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------|
| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | `pip install "pipecat-ai[deepgram]"` | | Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | `pip install "pipecat-ai[deepgram]"` |
| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Together AI](https://docs.pipecat.ai/server/services/llm/together) | `pip install "pipecat-ai[openai]"` | | LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Together AI](https://docs.pipecat.ai/server/services/llm/together) | `pip install "pipecat-ai[openai]"` |
| Text-to-Speech | [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) | `pip install "pipecat-ai[cartesia]"` | | Text-to-Speech | [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) | `pip install "pipecat-ai[cartesia]"` |
| Speech-to-Speech | [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) | `pip install "pipecat-ai[google]"` | | Speech-to-Speech | [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) | `pip install "pipecat-ai[google]"` |

View File

@@ -17,9 +17,9 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.transports.services.daily import DailyParams, DailyTransport
from pipecat.services.ultravox import UltravoxSTTService
from pipecat.services.cartesia import CartesiaTTSService from pipecat.services.cartesia import CartesiaTTSService
from pipecat.services.ultravox import UltravoxSTTService
from pipecat.transports.services.daily import DailyParams, DailyTransport
load_dotenv(override=True) load_dotenv(override=True)
@@ -30,13 +30,14 @@ load_dotenv(override=True)
logger.remove(0) logger.remove(0)
logger.add(sys.stderr, level="DEBUG") logger.add(sys.stderr, level="DEBUG")
#Want to initialize the ultravox processor since it takes time to load the model and dont # Want to initialize the ultravox processor since it takes time to load the model and dont
#want to load it every time the pipeline is run # want to load it every time the pipeline is run
ultravox_processor = UltravoxSTTService( ultravox_processor = UltravoxSTTService(
model_size="fixie-ai/ultravox-v0_4_1-llama-3_1-8b", model_size="fixie-ai/ultravox-v0_4_1-llama-3_1-8b",
hf_token=os.getenv("HF_TOKEN"), hf_token=os.getenv("HF_TOKEN"),
) )
async def main(): async def main():
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
(room_url, token) = await configure(session) (room_url, token) = await configure(session)
@@ -56,11 +57,9 @@ async def main():
tts = CartesiaTTSService( tts = CartesiaTTSService(
api_key=os.environ.get("CARTESIA_API_KEY"), api_key=os.environ.get("CARTESIA_API_KEY"),
voice_id='97f4b8fb-f2fe-444b-bb9a-c109783a857a', voice_id="97f4b8fb-f2fe-444b-bb9a-c109783a857a",
) )
pipeline = Pipeline( pipeline = Pipeline(
[ [
transport.input(), # Transport user input transport.input(), # Transport user input
@@ -89,5 +88,3 @@ async def main():
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@@ -78,6 +78,7 @@ silero = [ "onnxruntime~=1.20.1" ]
simli = [ "simli-ai~=0.1.10"] simli = [ "simli-ai~=0.1.10"]
soundfile = [ "soundfile~=0.13.0" ] soundfile = [ "soundfile~=0.13.0" ]
together = [] together = []
ultravox = [ "transformers~=4.48.0", "vllm~=0.7.3" ]
websocket = [ "websockets~=13.1", "fastapi~=0.115.6" ] websocket = [ "websockets~=13.1", "fastapi~=0.115.6" ]
whisper = [ "faster-whisper~=1.1.1" ] whisper = [ "faster-whisper~=1.1.1" ]
openrouter = [] openrouter = []

View File

@@ -1,132 +1,140 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""This module implements Ultravox speech-to-text with a locally-loaded model.""" """This module implements Ultravox speech-to-text with a locally-loaded model."""
import json import json
import time
import os import os
import time
from typing import AsyncGenerator, List, Optional
import numpy as np import numpy as np
from enum import Enum
from typing import AsyncGenerator, Optional, List
from loguru import logger
from pydantic import BaseModel
from huggingface_hub import login from huggingface_hub import login
from loguru import logger
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame,
AudioRawFrame, AudioRawFrame,
TranscriptionFrame,
TextFrame,
StartFrame,
EndFrame,
CancelFrame, CancelFrame,
EndFrame,
ErrorFrame,
Frame,
StartFrame,
TranscriptionFrame,
UserStartedSpeakingFrame, UserStartedSpeakingFrame,
UserStoppedSpeakingFrame, UserStoppedSpeakingFrame,
ErrorFrame
) )
from pipecat.services.ai_services import AIService
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_services import AIService
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
try: try:
from vllm import SamplingParams, AsyncLLMEngine
from vllm.engine.arg_utils import AsyncEngineArgs
from transformers import AutoTokenizer from transformers import AutoTokenizer
from vllm import AsyncLLMEngine, SamplingParams
from vllm.engine.arg_utils import AsyncEngineArgs
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
logger.error(f"Exception: {e}") logger.error(f"Exception: {e}")
logger.error("In order to use Ultravox, you need to `pip install pipecat-ai[ultravox]`.") logger.error("In order to use Ultravox, you need to `pip install pipecat-ai[ultravox]`.")
raise Exception(f"Missing module: {e}") raise Exception(f"Missing module: {e}")
class AudioBuffer: class AudioBuffer:
"""Buffer to collect audio frames before processing. """Buffer to collect audio frames before processing.
Attributes: Attributes:
frames: List of AudioRawFrames to process frames: List of AudioRawFrames to process
started_at: Timestamp when speech started started_at: Timestamp when speech started
is_processing: Flag to prevent concurrent processing is_processing: Flag to prevent concurrent processing
""" """
def __init__(self): def __init__(self):
self.frames: List[AudioRawFrame] = [] self.frames: List[AudioRawFrame] = []
self.started_at: Optional[float] = None self.started_at: Optional[float] = None
self.is_processing: bool = False self.is_processing: bool = False
class UltravoxModel: class UltravoxModel:
"""Model wrapper for the Ultravox multimodal model. """Model wrapper for the Ultravox multimodal model.
This class handles loading and running the Ultravox model for speech-to-text. This class handles loading and running the Ultravox model for speech-to-text.
Args: Args:
model_name: The name or path of the Ultravox model to load model_name: The name or path of the Ultravox model to load
Attributes: Attributes:
model_name: The name of the loaded model model_name: The name of the loaded model
engine: The vLLM engine for model inference engine: The vLLM engine for model inference
tokenizer: The tokenizer for the model tokenizer: The tokenizer for the model
stop_token_ids: Optional token IDs to stop generation stop_token_ids: Optional token IDs to stop generation
""" """
def __init__(self, model_name: str = "fixie-ai/ultravox-v0_4_1-llama-3_1-8b"): def __init__(self, model_name: str = "fixie-ai/ultravox-v0_4_1-llama-3_1-8b"):
self.model_name = model_name self.model_name = model_name
self._initialize_engine() self._initialize_engine()
self._initialize_tokenizer() self._initialize_tokenizer()
self.stop_token_ids = None self.stop_token_ids = None
def _initialize_engine(self): def _initialize_engine(self):
"""Initialize the vLLM engine for inference.""" """Initialize the vLLM engine for inference."""
engine_args = AsyncEngineArgs( engine_args = AsyncEngineArgs(
model=self.model_name, model=self.model_name,
gpu_memory_utilization=0.9, gpu_memory_utilization=0.9,
max_model_len=8192, max_model_len=8192,
trust_remote_code=True trust_remote_code=True,
) )
self.engine = AsyncLLMEngine.from_engine_args(engine_args) self.engine = AsyncLLMEngine.from_engine_args(engine_args)
def _initialize_tokenizer(self): def _initialize_tokenizer(self):
"""Initialize the tokenizer for the model.""" """Initialize the tokenizer for the model."""
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
def format_prompt(self, messages: list): def format_prompt(self, messages: list):
"""Format chat messages into a prompt for the model. """Format chat messages into a prompt for the model.
Args: Args:
messages: List of message dictionaries with 'role' and 'content' messages: List of message dictionaries with 'role' and 'content'
Returns: Returns:
str: Formatted prompt string str: Formatted prompt string
""" """
return self.tokenizer.apply_chat_template( return self.tokenizer.apply_chat_template(
messages, messages, tokenize=False, add_generation_prompt=True
tokenize=False,
add_generation_prompt=True
) )
async def generate(self, messages: list, temperature: float = 0.7, max_tokens: int = 100, audio: np.ndarray = None): async def generate(
self,
messages: list,
temperature: float = 0.7,
max_tokens: int = 100,
audio: np.ndarray = None,
):
"""Generate text from audio input using the model. """Generate text from audio input using the model.
Args: Args:
messages: List of message dictionaries messages: List of message dictionaries
temperature: Sampling temperature temperature: Sampling temperature
max_tokens: Maximum tokens to generate max_tokens: Maximum tokens to generate
audio: Audio data as numpy array audio: Audio data as numpy array
Yields: Yields:
str: JSON chunks of the generated response str: JSON chunks of the generated response
""" """
sampling_params = SamplingParams( sampling_params = SamplingParams(
temperature=temperature, temperature=temperature, max_tokens=max_tokens, stop_token_ids=self.stop_token_ids
max_tokens=max_tokens,
stop_token_ids=self.stop_token_ids
) )
mm_data = { mm_data = {"audio": audio}
"audio": audio
}
inputs = {"prompt": self.format_prompt(messages), "multi_modal_data": mm_data} inputs = {"prompt": self.format_prompt(messages), "multi_modal_data": mm_data}
results_generator = self.engine.generate(inputs, sampling_params, str(time.time())) results_generator = self.engine.generate(inputs, sampling_params, str(time.time()))
previous_text = "" previous_text = ""
first_chunk = True first_chunk = True
async for output in results_generator: async for output in results_generator:
prompt_output = output.outputs prompt_output = output.outputs
new_text = prompt_output[0].text[len(previous_text):] new_text = prompt_output[0].text[len(previous_text) :]
previous_text = prompt_output[0].text previous_text = prompt_output[0].text
# Construct OpenAI-compatible chunk # Construct OpenAI-compatible chunk
@@ -160,19 +168,20 @@ class UltravoxModel:
yield json.dumps(chunk) yield json.dumps(chunk)
class UltravoxSTTService(AIService): class UltravoxSTTService(AIService):
"""Service to transcribe audio using the Ultravox multimodal model. """Service to transcribe audio using the Ultravox multimodal model.
This service collects audio frames and processes them with Ultravox This service collects audio frames and processes them with Ultravox
to generate text transcriptions. to generate text transcriptions.
Args: Args:
model_size: The Ultravox model to use (ModelSize enum or string) model_size: The Ultravox model to use (ModelSize enum or string)
hf_token: Hugging Face token for model access hf_token: Hugging Face token for model access
temperature: Sampling temperature for generation temperature: Sampling temperature for generation
max_tokens: Maximum tokens to generate max_tokens: Maximum tokens to generate
**kwargs: Additional arguments passed to AIService **kwargs: Additional arguments passed to AIService
Attributes: Attributes:
model: The UltravoxModel instance model: The UltravoxModel instance
buffer: Buffer to collect audio frames buffer: Buffer to collect audio frames
@@ -180,17 +189,18 @@ class UltravoxSTTService(AIService):
max_tokens: Maximum tokens to generate max_tokens: Maximum tokens to generate
_connection_active: Flag indicating if service is active _connection_active: Flag indicating if service is active
""" """
def __init__( def __init__(
self, self,
*, *,
model_size: str = "fixie-ai/ultravox-v0_4_1-llama-3_1-8b", model_size: str = "fixie-ai/ultravox-v0_4_1-llama-3_1-8b",
hf_token: Optional[str] = None, hf_token: Optional[str] = None,
temperature: float = 0.7, temperature: float = 0.7,
max_tokens: int = 100, max_tokens: int = 100,
**kwargs, **kwargs,
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
# Authenticate with Hugging Face if token provided # Authenticate with Hugging Face if token provided
if hf_token: if hf_token:
login(token=hf_token) login(token=hf_token)
@@ -198,22 +208,22 @@ class UltravoxSTTService(AIService):
login(token=os.environ.get("HF_TOKEN")) login(token=os.environ.get("HF_TOKEN"))
else: else:
logger.warning("No Hugging Face token provided. Model may not load correctly.") logger.warning("No Hugging Face token provided. Model may not load correctly.")
# Initialize model # Initialize model
model_name = model_size if isinstance(model_size, str) else model_size.value model_name = model_size if isinstance(model_size, str) else model_size.value
self.model = UltravoxModel(model_name=model_name) self._model = UltravoxModel(model_name=model_name)
# Initialize service state # Initialize service state
self.buffer = AudioBuffer() self._buffer = AudioBuffer()
self.temperature = temperature self._temperature = temperature
self.max_tokens = max_tokens self._max_tokens = max_tokens
self._connection_active = False self._connection_active = False
logger.info(f"Initialized UltravoxSTTService with model: {model_name}") logger.info(f"Initialized UltravoxSTTService with model: {model_name}")
def can_generate_metrics(self) -> bool: def can_generate_metrics(self) -> bool:
"""Indicates whether this service can generate metrics. """Indicates whether this service can generate metrics.
Returns: Returns:
bool: True, as this service supports metric generation. bool: True, as this service supports metric generation.
""" """
@@ -221,7 +231,7 @@ class UltravoxSTTService(AIService):
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):
"""Handle service start. """Handle service start.
Args: Args:
frame: StartFrame that triggered this method frame: StartFrame that triggered this method
""" """
@@ -231,7 +241,7 @@ class UltravoxSTTService(AIService):
async def stop(self, frame: EndFrame): async def stop(self, frame: EndFrame):
"""Handle service stop. """Handle service stop.
Args: Args:
frame: EndFrame that triggered this method frame: EndFrame that triggered this method
""" """
@@ -241,20 +251,20 @@ class UltravoxSTTService(AIService):
async def cancel(self, frame: CancelFrame): async def cancel(self, frame: CancelFrame):
"""Handle service cancellation. """Handle service cancellation.
Args: Args:
frame: CancelFrame that triggered this method frame: CancelFrame that triggered this method
""" """
await super().cancel(frame) await super().cancel(frame)
self._connection_active = False self._connection_active = False
self.buffer = AudioBuffer() self._buffer = AudioBuffer()
logger.info("UltravoxSTTService cancelled") logger.info("UltravoxSTTService cancelled")
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process incoming frames. """Process incoming frames.
This method collects audio frames and processes them when speech ends. This method collects audio frames and processes them when speech ends.
Args: Args:
frame: The frame to process frame: The frame to process
direction: Direction of the frame (input/output) direction: Direction of the frame (input/output)
@@ -263,44 +273,44 @@ class UltravoxSTTService(AIService):
if isinstance(frame, UserStartedSpeakingFrame): if isinstance(frame, UserStartedSpeakingFrame):
logger.info("Speech started") logger.info("Speech started")
self.buffer = AudioBuffer() self._buffer = AudioBuffer()
self.buffer.started_at = time.time() self._buffer.started_at = time.time()
elif isinstance(frame, AudioRawFrame) and self.buffer.started_at is not None: elif isinstance(frame, AudioRawFrame) and self._buffer.started_at is not None:
self.buffer.frames.append(frame) self._buffer.frames.append(frame)
elif isinstance(frame, UserStoppedSpeakingFrame): elif isinstance(frame, UserStoppedSpeakingFrame):
if self.buffer.frames and not self.buffer.is_processing: if self._buffer.frames and not self._buffer.is_processing:
logger.info("Speech ended, processing buffer...") logger.info("Speech ended, processing buffer...")
await self.process_generator(self._process_audio_buffer()) await self.process_generator(self._process_audio_buffer())
return # Return early to avoid pushing None frame return # Return early to avoid pushing None frame
# Only push the original frame if we haven't processed audio # Only push the original frame if we haven't processed audio
if frame is not None: if frame is not None:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
async def _process_audio_buffer(self) -> AsyncGenerator[Frame, None]: async def _process_audio_buffer(self) -> AsyncGenerator[Frame, None]:
"""Process collected audio frames with Ultravox. """Process collected audio frames with Ultravox.
This method concatenates audio frames, processes them with the model, This method concatenates audio frames, processes them with the model,
and yields the resulting text frames. and yields the resulting text frames.
Yields: Yields:
Frame: TextFrame containing the transcribed text Frame: TextFrame containing the transcribed text
""" """
try: try:
self.buffer.is_processing = True self._buffer.is_processing = True
# Check if we have valid frames before processing # Check if we have valid frames before processing
if not self.buffer.frames: if not self._buffer.frames:
logger.warning("No audio frames to process") logger.warning("No audio frames to process")
yield ErrorFrame("No audio frames to process") yield ErrorFrame("No audio frames to process")
return return
# Process audio frames # Process audio frames
audio_arrays = [] audio_arrays = []
for f in self.buffer.frames: for f in self._buffer.frames:
if hasattr(f, 'audio') and f.audio: if hasattr(f, "audio") and f.audio:
# Handle bytes data - these are int16 PCM samples # Handle bytes data - these are int16 PCM samples
if isinstance(f.audio, bytes): if isinstance(f.audio, bytes):
try: try:
@@ -319,75 +329,73 @@ class UltravoxSTTService(AIService):
audio_arrays.append(f.audio.astype(np.int16)) audio_arrays.append(f.audio.astype(np.int16))
else: else:
audio_arrays.append(f.audio) audio_arrays.append(f.audio)
# Only proceed if we have valid audio arrays # Only proceed if we have valid audio arrays
if not audio_arrays: if not audio_arrays:
logger.warning("No valid audio data found in frames") logger.warning("No valid audio data found in frames")
yield ErrorFrame("No valid audio data found in frames") yield ErrorFrame("No valid audio data found in frames")
return return
# Concatenate audio frames - all should be int16 now # Concatenate audio frames - all should be int16 now
audio_data = np.concatenate(audio_arrays) audio_data = np.concatenate(audio_arrays)
# Generate text using the model # Generate text using the model
if self.model: if self._model:
try: try:
logger.info("Generating text from audio using model...") logger.info("Generating text from audio using model...")
full_response = "" full_response = ""
# Start metrics tracking # Start metrics tracking
await self.start_ttfb_metrics() await self.start_ttfb_metrics()
await self.start_processing_metrics() await self.start_processing_metrics()
async for response in self.model.generate( async for response in self._model.generate(
messages=[{ messages=[{"role": "user", "content": "<|audio|>\n"}],
'role': 'user', temperature=self._temperature,
'content': "<|audio|>\n" max_tokens=self._max_tokens,
}], audio=audio_data,
temperature=self.temperature,
max_tokens=self.max_tokens,
audio=audio_data
): ):
# Stop TTFB metrics after first response # Stop TTFB metrics after first response
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
chunk = json.loads(response) chunk = json.loads(response)
if "choices" in chunk and len(chunk["choices"]) > 0: if "choices" in chunk and len(chunk["choices"]) > 0:
delta = chunk["choices"][0]["delta"] delta = chunk["choices"][0]["delta"]
if "content" in delta: if "content" in delta:
new_text = delta["content"] new_text = delta["content"]
full_response += new_text full_response += new_text
# Stop processing metrics after completion # Stop processing metrics after completion
await self.stop_processing_metrics() await self.stop_processing_metrics()
logger.info(f"Generated text: {full_response}") logger.info(f"Generated text: {full_response}")
# Create a transcription frame with the generated text # Create a transcription frame with the generated text
transcription = full_response.strip() transcription = full_response.strip()
if transcription: if transcription:
yield TranscriptionFrame( yield TranscriptionFrame(
user_id="",
text=transcription, text=transcription,
interim_text="", timestamp=time_now_iso8601(),
timestamp=time_now_iso8601()
) )
else: else:
logger.warning("Empty transcription result") logger.warning("Empty transcription result")
yield ErrorFrame("Empty transcription result") yield ErrorFrame("Empty transcription result")
except Exception as e: except Exception as e:
logger.error(f"Error generating text from model: {e}") logger.error(f"Error generating text from model: {e}")
yield ErrorFrame(f"Error generating text: {str(e)}") yield ErrorFrame(f"Error generating text: {str(e)}")
else: else:
logger.warning("No model available for text generation") logger.warning("No model available for text generation")
yield ErrorFrame("No model available for text generation") yield ErrorFrame("No model available for text generation")
except Exception as e: except Exception as e:
logger.error(f"Error processing audio buffer: {e}") logger.error(f"Error processing audio buffer: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
yield ErrorFrame(f"Error processing audio: {str(e)}") yield ErrorFrame(f"Error processing audio: {str(e)}")
finally: finally:
self.buffer.is_processing = False self._buffer.is_processing = False
self.buffer.frames = [] self._buffer.frames = []
self.buffer.started_at = None self._buffer.started_at = None