Merge pull request #3926 from pipecat-ai/mb/update-deps-2026-03-05

Update dependency version ranges for flexibility
This commit is contained in:
Mark Backman
2026-03-05 18:09:04 -05:00
committed by GitHub
9 changed files with 397 additions and 381 deletions

View File

@@ -27,7 +27,7 @@ try:
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use Silero VAD, you need to `pip install pipecat-ai[silero]`.")
logger.error("In order to use Silero VAD, you need to `pip install pipecat-ai`.")
raise Exception(f"Missing module(s): {e}")

View File

@@ -13,6 +13,7 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import Any, AsyncGenerator, List, Literal, Mapping, Optional
import aiohttp
from loguru import logger
from pydantic import BaseModel, Field
@@ -33,13 +34,8 @@ from pipecat.utils.text.base_text_aggregator import BaseTextAggregator
from pipecat.utils.text.skip_tags_aggregator import SkipTagsAggregator
from pipecat.utils.tracing.service_decorators import traced_tts
# Suppress regex warnings from pydub (used by cartesia)
warnings.filterwarnings("ignore", message="invalid escape sequence", category=SyntaxWarning)
# See .env.example for Cartesia configuration needed
try:
from cartesia import AsyncCartesia
from websockets.asyncio.client import connect as websocket_connect
from websockets.protocol import State
except ModuleNotFoundError as e:
@@ -721,6 +717,7 @@ class CartesiaHttpTTSService(TTSService):
model: str = "sonic-3",
base_url: str = "https://api.cartesia.ai",
cartesia_version: str = "2024-11-13",
aiohttp_session: Optional[aiohttp.ClientSession] = None,
sample_rate: Optional[int] = None,
encoding: str = "pcm_s16le",
container: str = "raw",
@@ -735,6 +732,8 @@ class CartesiaHttpTTSService(TTSService):
model: TTS model to use (e.g., "sonic-3").
base_url: Base URL for Cartesia HTTP API.
cartesia_version: API version string for Cartesia service.
aiohttp_session: Optional aiohttp ClientSession for HTTP requests.
If not provided, a session will be created and managed internally.
sample_rate: Audio sample rate. If None, uses default.
encoding: Audio encoding format.
container: Audio container format.
@@ -766,10 +765,8 @@ class CartesiaHttpTTSService(TTSService):
self._base_url = base_url
self._cartesia_version = cartesia_version
self._client = AsyncCartesia(
api_key=api_key,
base_url=base_url,
)
self._session: aiohttp.ClientSession | None = aiohttp_session
self._owns_session = aiohttp_session is None
def can_generate_metrics(self) -> bool:
"""Check if this service can generate processing metrics.
@@ -798,6 +795,14 @@ class CartesiaHttpTTSService(TTSService):
"""
await super().start(frame)
self._settings.output_sample_rate = self.sample_rate
if self._owns_session:
self._session = aiohttp.ClientSession()
async def _close_session(self):
"""Close the HTTP session if we own it."""
if self._owns_session and self._session:
await self._session.close()
self._session = None
async def stop(self, frame: EndFrame):
"""Stop the Cartesia HTTP TTS service.
@@ -806,7 +811,7 @@ class CartesiaHttpTTSService(TTSService):
frame: The end frame.
"""
await super().stop(frame)
await self._client.close()
await self._close_session()
async def cancel(self, frame: CancelFrame):
"""Cancel the Cartesia HTTP TTS service.
@@ -815,7 +820,7 @@ class CartesiaHttpTTSService(TTSService):
frame: The cancel frame.
"""
await super().cancel(frame)
await self._client.close()
await self._close_session()
@traced_tts
async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]:
@@ -874,8 +879,6 @@ class CartesiaHttpTTSService(TTSService):
yield TTSStartedFrame(context_id=context_id)
session = await self._client._get_session()
headers = {
"Cartesia-Version": self._cartesia_version,
"X-API-Key": self._api_key,
@@ -884,7 +887,7 @@ class CartesiaHttpTTSService(TTSService):
url = f"{self._base_url}/tts/bytes"
async with session.post(url, json=payload, headers=headers) as response:
async with self._session.post(url, json=payload, headers=headers) as response:
if response.status != 200:
error_text = await response.text()
yield ErrorFrame(error=f"Cartesia API error: {error_text}")

View File

@@ -25,13 +25,6 @@ from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame
from pipecat.services.image_service import ImageGenService
from pipecat.services.settings import ImageGenSettings
try:
import fal_client
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use Fal, you need to `pip install pipecat-ai[fal]`.")
raise Exception(f"Missing module: {e}")
@dataclass
class FalImageGenSettings(ImageGenSettings):
@@ -91,6 +84,7 @@ class FalImageGenService(ImageGenService):
super().__init__(settings=FalImageGenSettings(model=model), **kwargs)
self._params = params
self._aiohttp_session = aiohttp_session
self._api_key = key or os.getenv("FAL_KEY", "")
if key:
os.environ["FAL_KEY"] = key
@@ -112,10 +106,22 @@ class FalImageGenService(ImageGenService):
logger.debug(f"Generating image from prompt: {prompt}")
response = await fal_client.run_async(
self._settings.model,
arguments={"prompt": prompt, **self._params.model_dump(exclude_none=True)},
)
headers = {
"Authorization": f"Key {self._api_key}",
"Content-Type": "application/json",
}
payload = {"prompt": prompt, **self._params.model_dump(exclude_none=True)}
async with self._aiohttp_session.post(
f"https://fal.run/{self._settings.model}",
json=payload,
headers=headers,
) as resp:
if resp.status != 200:
error_text = await resp.text()
yield ErrorFrame(error=f"Fal API error ({resp.status}): {error_text}")
return
response = await resp.json()
image_url = response["images"][0]["url"] if response else None

View File

@@ -10,10 +10,12 @@ This module provides integration with Fal's Wizper API for speech-to-text
transcription using segmented audio processing.
"""
import base64
import os
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, Optional
from typing import AsyncGenerator, Optional
import aiohttp
from loguru import logger
from pydantic import BaseModel
@@ -25,15 +27,6 @@ from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt
try:
import fal_client
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error(
"In order to use Fal, you need to `pip install pipecat-ai[fal]`. Also, set `FAL_KEY` environment variable."
)
raise Exception(f"Missing module: {e}")
def language_to_fal_language(language: Language) -> Optional[str]:
"""Convert a Language enum to Fal's Wizper language code.
@@ -192,6 +185,7 @@ class FalSTTService(SegmentedSTTService):
self,
*,
api_key: Optional[str] = None,
aiohttp_session: Optional[aiohttp.ClientSession] = None,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
ttfs_p99_latency: Optional[float] = FAL_TTFS_P99,
@@ -201,6 +195,8 @@ class FalSTTService(SegmentedSTTService):
Args:
api_key: Fal API key. If not provided, will check FAL_KEY environment variable.
aiohttp_session: Optional aiohttp ClientSession for HTTP requests.
If not provided, a session will be created and managed internally.
sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate.
params: Configuration parameters for the Wizper API.
ttfs_p99_latency: P99 latency from speech end to final transcript in seconds.
@@ -224,14 +220,14 @@ class FalSTTService(SegmentedSTTService):
**kwargs,
)
if api_key:
os.environ["FAL_KEY"] = api_key
elif "FAL_KEY" not in os.environ:
self._api_key = api_key or os.getenv("FAL_KEY", "")
if not self._api_key:
raise ValueError(
"FAL_KEY must be provided either through api_key parameter or environment variable"
)
self._fal_client = fal_client.AsyncClient(key=api_key or os.getenv("FAL_KEY"))
self._session: aiohttp.ClientSession | None = aiohttp_session
self._owns_session = aiohttp_session is None
def can_generate_metrics(self) -> bool:
"""Check if the service can generate processing metrics.
@@ -275,12 +271,26 @@ class FalSTTService(SegmentedSTTService):
try:
await self.start_processing_metrics()
# Send to Fal directly (audio is already in WAV format from base class)
data_uri = fal_client.encode(audio, "audio/x-wav")
response = await self._fal_client.run(
"fal-ai/wizper",
arguments={"audio_url": data_uri, **self._settings.given_fields()},
)
if not self._session:
self._session = aiohttp.ClientSession()
data_uri = f"data:audio/x-wav;base64,{base64.b64encode(audio).decode()}"
payload = {"audio_url": data_uri, **self._settings.given_fields()}
headers = {
"Authorization": f"Key {self._api_key}",
"Content-Type": "application/json",
}
async with self._session.post(
"https://fal.run/fal-ai/wizper",
json=payload,
headers=headers,
) as resp:
if resp.status != 200:
error_text = await resp.text()
yield ErrorFrame(error=f"Fal API error ({resp.status}): {error_text}")
return
response = await resp.json()
if response and "text" in response:
text = response["text"].strip()