Merge branch 'main' into groundingMetadata
This commit is contained in:
6
.github/workflows/format.yaml
vendored
6
.github/workflows/format.yaml
vendored
@@ -17,7 +17,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ruff-format:
|
ruff-format:
|
||||||
name: "Formatting checker"
|
name: "Code quality checks"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -39,8 +39,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
ruff format --diff
|
ruff format --diff
|
||||||
- name: Ruff import linter
|
- name: Ruff linter (all rules)
|
||||||
id: ruff-check
|
id: ruff-check
|
||||||
run: |
|
run: |
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
ruff check --select I
|
ruff check
|
||||||
|
|||||||
2
.github/workflows/publish.yaml
vendored
2
.github/workflows/publish.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
gitref:
|
gitref:
|
||||||
type: string
|
type: string
|
||||||
description: "what git ref to build"
|
description: "what git tag to build (e.g. v0.0.74)"
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -7,17 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Remove unncessary push task in each `FrameProcessor`.
|
||||||
|
|
||||||
|
## [0.0.74] - 2025-07-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added a new STT service, `SpeechmaticsSTTService`. This service provides
|
||||||
|
real-time speech-to-text transcription using the Speechmatics API. It supports
|
||||||
|
partial and final transcriptions, multiple languages, various audio formats,
|
||||||
|
and speaker diarization.
|
||||||
|
|
||||||
|
- Added `normalize` and `model_id` to `FishAudioTTSService`.
|
||||||
|
|
||||||
|
- Added `http_options` argument to `GoogleLLMService`.
|
||||||
|
|
||||||
|
- Added `run_llm` field to `LLMMessagesAppendFrame` and `LLMMessagesUpdateFrame`
|
||||||
|
frames. If true, a context frame will be pushed triggering the LLM to respond.
|
||||||
|
|
||||||
|
- Added a new `SOXRStreamAudioResampler` for processing audio in chunks or
|
||||||
|
streams. If you write your own processor and need to use an audio resampler,
|
||||||
|
use the new `create_stream_resampler()`.
|
||||||
|
|
||||||
|
- Added new `DailyParams.audio_in_user_tracks` to allow receiving one track per
|
||||||
|
user (default) or a single track from the room (all participants mixed).
|
||||||
|
|
||||||
|
- Added support for providing "direct" functions, which don't need an
|
||||||
|
accompanying `FunctionSchema` or function definition dict. Instead, metadata
|
||||||
|
(i.e. `name`, `description`, `properties`, and `required`) are automatically
|
||||||
|
extracted from a combination of the function signature and docstring.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# "Direct" function
|
||||||
|
# `params` must be the first parameter
|
||||||
|
async def do_something(params: FunctionCallParams, foo: int, bar: str = ""):
|
||||||
|
"""
|
||||||
|
Do something interesting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
foo (int): The foo to do something interesting with.
|
||||||
|
bar (string): The bar to do something interesting with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = await process(foo, bar)
|
||||||
|
await params.result_callback({"result": result})
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
llm.register_direct_function(do_something)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
tools = ToolsSchema(standard_tools=[do_something])
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user_id` is now populated in the `TranscriptionFrame` and
|
||||||
|
`InterimTranscriptionFrame` when using a transport that provides a `user_id`,
|
||||||
|
like `DailyTransport` or `LiveKitTransport`.
|
||||||
|
|
||||||
- Added `watchdog_coroutine()`. This is a watchdog helper for couroutines. So,
|
- Added `watchdog_coroutine()`. This is a watchdog helper for couroutines. So,
|
||||||
if you have a coroutine that is waiting for a result and that takes a long
|
if you have a coroutine that is waiting for a result and that takes a long
|
||||||
time, you will need to wrap it with `watchdog_coroutine()` so the watchdog
|
time, you will need to wrap it with `watchdog_coroutine()` so the watchdog
|
||||||
timers are reset regularly.
|
timers are reset regularly.
|
||||||
|
|
||||||
|
- Added `session_token` parameter to `AWSNovaSonicLLMService`.
|
||||||
|
|
||||||
|
- Added Gemini Multimodal Live File API for uploading, fetching, listing, and
|
||||||
|
deleting files. See `26f-gemini-multimodal-live-files-api.py` for example usage.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated all the services to use the new `SOXRStreamAudioResampler`, ensuring smooth
|
||||||
|
transitions and eliminating clicks.
|
||||||
|
|
||||||
|
- Upgraded `daily-python` to 0.19.4.
|
||||||
|
|
||||||
|
- Updated `google` optional dependency to use `google-genai` version `1.24.0`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue where audio would get stuck in the queue when an interrupt occurs
|
||||||
|
during Azure TTS synthesis.
|
||||||
|
|
||||||
|
- Fixed a race condition that occurs in Python 3.10+ where the task could miss
|
||||||
|
the `CancelledError` and continue running indefinitely, freezing the pipeline.
|
||||||
|
|
||||||
- Fixed a `AWSNovaSonicLLMService` issue introduced in 0.0.72.
|
- Fixed a `AWSNovaSonicLLMService` issue introduced in 0.0.72.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- In `FishAudioTTSService`, deprecated `model` and replaced with
|
||||||
|
`reference_id`. This change is to better align with Fish Audio's variable
|
||||||
|
naming and to reduce confusion about what functionality the variable
|
||||||
|
controls.
|
||||||
|
|
||||||
## [0.0.73] - 2025-06-26
|
## [0.0.73] - 2025-06-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -71,6 +71,28 @@ We follow Google-style docstrings with these specific conventions:
|
|||||||
- Use `Parameters:` section to document each enum value and its meaning
|
- Use `Parameters:` section to document each enum value and its meaning
|
||||||
- No `__init__` docstring (Enums don't have custom constructors)
|
- No `__init__` docstring (Enums don't have custom constructors)
|
||||||
|
|
||||||
|
**Code Examples in Docstrings:**
|
||||||
|
|
||||||
|
- Use `Examples:` as a section header for multiple examples
|
||||||
|
- Use descriptive text followed by double colons (`::`) for each example
|
||||||
|
- **Always include a blank line after the `::"`**
|
||||||
|
- Indent all code consistently within each block
|
||||||
|
- Separate multiple examples with blank lines for readability
|
||||||
|
|
||||||
|
**Lists and Bullets in Docstrings:**
|
||||||
|
|
||||||
|
- Use dashes (`-`) for bullet points, not asterisks (`*`)
|
||||||
|
- **Add a blank line before bullet lists** when they follow a colon
|
||||||
|
- Use section headers like "Supported features:" or "Behavior:" before lists
|
||||||
|
- For complex nested information, consider using paragraph format instead
|
||||||
|
|
||||||
|
**Deprecations:**
|
||||||
|
|
||||||
|
- Use `warnings.warn()` in code for runtime deprecation warnings
|
||||||
|
- Add `.. deprecated::` directive in docstrings for documentation visibility
|
||||||
|
- Include version information and describe current status
|
||||||
|
- Describe parameters in present tense, use directive to indicate deprecation status
|
||||||
|
|
||||||
#### Examples:
|
#### Examples:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -80,16 +102,32 @@ class MyService(BaseService):
|
|||||||
|
|
||||||
Provides detailed explanation of the service's functionality,
|
Provides detailed explanation of the service's functionality,
|
||||||
key features, and usage patterns.
|
key features, and usage patterns.
|
||||||
|
|
||||||
|
Supported features:
|
||||||
|
|
||||||
|
- Feature one with detailed explanation
|
||||||
|
- Feature two with additional context
|
||||||
|
- Feature three for advanced use cases
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, param1: str, param2: bool = True, **kwargs):
|
def __init__(self, param1: str, old_param: str = None, **kwargs):
|
||||||
"""Initialize the service.
|
"""Initialize the service.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
param1: Description of param1.
|
param1: Description of param1.
|
||||||
param2: Description of param2. Defaults to True.
|
old_param: Controls legacy behavior.
|
||||||
|
|
||||||
|
.. deprecated:: 1.2.0
|
||||||
|
This parameter no longer has any effect and will be removed in version 2.0.
|
||||||
|
|
||||||
**kwargs: Additional arguments passed to parent.
|
**kwargs: Additional arguments passed to parent.
|
||||||
"""
|
"""
|
||||||
|
if old_param is not None:
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter 'old_param' is deprecated and will be removed in version 2.0.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -112,20 +150,25 @@ class MyService(BaseService):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Dataclass
|
# Dataclass with code examples
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConfigParams:
|
class MessageFrame:
|
||||||
"""Configuration parameters for the service.
|
"""Frame containing messages in OpenAI format.
|
||||||
|
|
||||||
|
Supports both simple and content list message formats.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
[
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
{"role": "assistant", "content": "Hi there!"}
|
||||||
|
]
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
host: The host address.
|
messages: List of messages in OpenAI format.
|
||||||
port: The port number. Defaults to 8080.
|
|
||||||
timeout: Connection timeout in seconds.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
host: str
|
messages: List[dict]
|
||||||
port: int = 8080
|
|
||||||
timeout: float = 30.0
|
|
||||||
|
|
||||||
# Enum class
|
# Enum class
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -51,19 +51,19 @@ You can connect to Pipecat from any platform using our official SDKs:
|
|||||||
|
|
||||||
## 🧩 Available services
|
## 🧩 Available services
|
||||||
|
|
||||||
| Category | Services |
|
| Category | Services |
|
||||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [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), [Parakeet (NVIDIA)](https://docs.pipecat.ai/server/services/stt/parakeet), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova) [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) |
|
| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [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), [Parakeet (NVIDIA)](https://docs.pipecat.ai/server/services/stt/parakeet), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) |
|
||||||
| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [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), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) |
|
| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [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), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) |
|
||||||
| 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), [FastPitch (NVIDIA)](https://docs.pipecat.ai/server/services/tts/fastpitch), [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), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) |
|
| 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), [FastPitch (NVIDIA)](https://docs.pipecat.ai/server/services/tts/fastpitch), [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), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) |
|
||||||
| Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) |
|
| Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) |
|
||||||
| Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local |
|
| Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local |
|
||||||
| Serializers | [Plivo](https://docs.pipecat.ai/server/utilities/serializers/plivo), [Twilio](https://docs.pipecat.ai/server/utilities/serializers/twilio), [Telnyx](https://docs.pipecat.ai/server/utilities/serializers/telnyx) |
|
| Serializers | [Plivo](https://docs.pipecat.ai/server/utilities/serializers/plivo), [Twilio](https://docs.pipecat.ai/server/utilities/serializers/twilio), [Telnyx](https://docs.pipecat.ai/server/utilities/serializers/telnyx) |
|
||||||
| Video | [Tavus](https://docs.pipecat.ai/server/services/video/tavus), [Simli](https://docs.pipecat.ai/server/services/video/simli) |
|
| Video | [Tavus](https://docs.pipecat.ai/server/services/video/tavus), [Simli](https://docs.pipecat.ai/server/services/video/simli) |
|
||||||
| Memory | [mem0](https://docs.pipecat.ai/server/services/memory/mem0) |
|
| Memory | [mem0](https://docs.pipecat.ai/server/services/memory/mem0) |
|
||||||
| Vision & Image | [fal](https://docs.pipecat.ai/server/services/image-generation/fal), [Google Imagen](https://docs.pipecat.ai/server/services/image-generation/fal), [Moondream](https://docs.pipecat.ai/server/services/vision/moondream) |
|
| Vision & Image | [fal](https://docs.pipecat.ai/server/services/image-generation/fal), [Google Imagen](https://docs.pipecat.ai/server/services/image-generation/fal), [Moondream](https://docs.pipecat.ai/server/services/vision/moondream) |
|
||||||
| Audio Processing | [Silero VAD](https://docs.pipecat.ai/server/utilities/audio/silero-vad-analyzer), [Krisp](https://docs.pipecat.ai/server/utilities/audio/krisp-filter), [Koala](https://docs.pipecat.ai/server/utilities/audio/koala-filter), [Noisereduce](https://docs.pipecat.ai/server/utilities/audio/noisereduce-filter) |
|
| Audio Processing | [Silero VAD](https://docs.pipecat.ai/server/utilities/audio/silero-vad-analyzer), [Krisp](https://docs.pipecat.ai/server/utilities/audio/krisp-filter), [Koala](https://docs.pipecat.ai/server/utilities/audio/koala-filter), [Noisereduce](https://docs.pipecat.ai/server/utilities/audio/noisereduce-filter) |
|
||||||
| Analytics & Metrics | [OpenTelemetry](https://docs.pipecat.ai/server/utilities/opentelemetry), [Sentry](https://docs.pipecat.ai/server/services/analytics/sentry) |
|
| Analytics & Metrics | [OpenTelemetry](https://docs.pipecat.ai/server/utilities/opentelemetry), [Sentry](https://docs.pipecat.ai/server/services/analytics/sentry) |
|
||||||
|
|
||||||
📚 [View full services documentation →](https://docs.pipecat.ai/server/services/supported-services)
|
📚 [View full services documentation →](https://docs.pipecat.ai/server/services/supported-services)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
build~=1.2.2
|
build~=1.2.2
|
||||||
coverage~=7.6.12
|
coverage~=7.9.1
|
||||||
grpcio-tools~=1.67.1
|
grpcio-tools~=1.67.1
|
||||||
pip-tools~=7.4.1
|
pip-tools~=7.4.1
|
||||||
pre-commit~=4.0.1
|
pre-commit~=4.2.0
|
||||||
pyright~=1.1.400
|
pyright~=1.1.402
|
||||||
pytest~=8.3.4
|
pytest~=8.4.1
|
||||||
pytest-asyncio~=0.25.3
|
pytest-asyncio~=1.0.0
|
||||||
pytest-aiohttp==1.1.0
|
pytest-aiohttp==1.1.0
|
||||||
ruff~=0.11.13
|
ruff~=0.12.1
|
||||||
setuptools~=70.0.0
|
setuptools~=78.1.1
|
||||||
setuptools_scm~=8.1.0
|
setuptools_scm~=8.3.1
|
||||||
python-dotenv~=1.0.1
|
python-dotenv~=1.1.1
|
||||||
|
|||||||
162
docs/api/conf.py
162
docs/api/conf.py
@@ -26,6 +26,10 @@ extensions = [
|
|||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
suppress_warnings = [
|
||||||
|
"autodoc.mocked_object",
|
||||||
|
]
|
||||||
|
|
||||||
# Napoleon settings
|
# Napoleon settings
|
||||||
napoleon_google_docstring = True
|
napoleon_google_docstring = True
|
||||||
napoleon_include_init_with_doc = True
|
napoleon_include_init_with_doc = True
|
||||||
@@ -34,9 +38,8 @@ napoleon_include_init_with_doc = True
|
|||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
"members": True,
|
"members": True,
|
||||||
"member-order": "bysource",
|
"member-order": "bysource",
|
||||||
"undoc-members": True,
|
"undoc-members": False,
|
||||||
"exclude-members": "__weakref__,model_config",
|
"exclude-members": "__weakref__,model_config",
|
||||||
"no-index": True,
|
|
||||||
"show-inheritance": True,
|
"show-inheritance": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +74,6 @@ autodoc_mock_imports = [
|
|||||||
"langchain",
|
"langchain",
|
||||||
"lmnt",
|
"lmnt",
|
||||||
"noisereduce",
|
"noisereduce",
|
||||||
"openai",
|
|
||||||
"openpipe",
|
"openpipe",
|
||||||
"simli",
|
"simli",
|
||||||
"soundfile",
|
"soundfile",
|
||||||
@@ -81,10 +83,6 @@ autodoc_mock_imports = [
|
|||||||
"tkinter",
|
"tkinter",
|
||||||
"daily",
|
"daily",
|
||||||
"daily_python",
|
"daily_python",
|
||||||
"pydantic.BaseModel",
|
|
||||||
"pydantic.Field",
|
|
||||||
"pydantic._internal._model_construction",
|
|
||||||
"pydantic._internal._fields",
|
|
||||||
# Moondream dependencies
|
# Moondream dependencies
|
||||||
"torch",
|
"torch",
|
||||||
"transformers",
|
"transformers",
|
||||||
@@ -167,6 +165,19 @@ autodoc_mock_imports = [
|
|||||||
"mcp.client.stdio",
|
"mcp.client.stdio",
|
||||||
"mcp.ClientSession",
|
"mcp.ClientSession",
|
||||||
"mcp.StdioServerParameters",
|
"mcp.StdioServerParameters",
|
||||||
|
# gstreamer
|
||||||
|
"gi",
|
||||||
|
"gi.require_version",
|
||||||
|
"gi.repository",
|
||||||
|
# Protobuf mocks
|
||||||
|
"pipecat.frames.protobufs.frames_pb2",
|
||||||
|
"pipecat.serializers.protobuf",
|
||||||
|
"google.protobuf",
|
||||||
|
"google.protobuf.descriptor",
|
||||||
|
"google.protobuf.descriptor_pool",
|
||||||
|
"google.protobuf.runtime_version",
|
||||||
|
"google.protobuf.symbol_database",
|
||||||
|
"google.protobuf.internal.builder",
|
||||||
]
|
]
|
||||||
|
|
||||||
# HTML output settings
|
# HTML output settings
|
||||||
@@ -176,76 +187,32 @@ autodoc_typehints = "signature" # Show type hints in the signature only, not in
|
|||||||
html_show_sphinx = False
|
html_show_sphinx = False
|
||||||
|
|
||||||
|
|
||||||
def verify_modules():
|
def import_core_modules():
|
||||||
"""Verify that required modules are available."""
|
"""Import core pipecat modules for autodoc to discover."""
|
||||||
required_modules = {
|
core_modules = [
|
||||||
"services": [
|
"pipecat",
|
||||||
"assemblyai",
|
"pipecat.frames",
|
||||||
"aws",
|
"pipecat.pipeline",
|
||||||
"cartesia",
|
"pipecat.processors",
|
||||||
"deepgram",
|
"pipecat.services",
|
||||||
"google",
|
"pipecat.transports",
|
||||||
"lmnt",
|
"pipecat.audio",
|
||||||
"riva",
|
"pipecat.adapters",
|
||||||
"simli",
|
"pipecat.clocks",
|
||||||
],
|
"pipecat.metrics",
|
||||||
"serializers": ["livekit"],
|
"pipecat.observers",
|
||||||
"vad": ["silero", "vad_analyzer"],
|
"pipecat.serializers",
|
||||||
"transports": {
|
"pipecat.sync",
|
||||||
"services": ["daily", "livekit"],
|
"pipecat.transcriptions",
|
||||||
"local": ["audio", "tk"],
|
"pipecat.utils",
|
||||||
"network": ["fastapi_websocket", "websocket_server"],
|
]
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Skip importing modules that are in autodoc_mock_imports
|
for module_name in core_modules:
|
||||||
skipped_modules = set(autodoc_mock_imports)
|
try:
|
||||||
|
__import__(module_name)
|
||||||
missing = []
|
logger.info(f"Successfully imported {module_name}")
|
||||||
for category, modules in required_modules.items():
|
except ImportError as e:
|
||||||
if isinstance(modules, dict):
|
logger.warning(f"Failed to import {module_name}: {e}")
|
||||||
# Handle nested structure
|
|
||||||
for subcategory, submodules in modules.items():
|
|
||||||
for module in submodules:
|
|
||||||
# Check if module is in autodoc_mock_imports
|
|
||||||
if (
|
|
||||||
f"pipecat.{category}.{subcategory}.{module}" in skipped_modules
|
|
||||||
or module in skipped_modules
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
f"Skipping import of mocked module: pipecat.{category}.{subcategory}.{module}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
__import__(f"pipecat.{category}.{subcategory}.{module}")
|
|
||||||
logger.info(
|
|
||||||
f"Successfully imported pipecat.{category}.{subcategory}.{module}"
|
|
||||||
)
|
|
||||||
except (ImportError, TypeError, NameError) as e:
|
|
||||||
missing.append(f"pipecat.{category}.{subcategory}.{module}")
|
|
||||||
logger.warning(
|
|
||||||
f"Optional module not available: pipecat.{category}.{subcategory}.{module} - {str(e)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Handle flat structure
|
|
||||||
for module in modules:
|
|
||||||
# Check if module is in autodoc_mock_imports
|
|
||||||
if f"pipecat.{category}.{module}" in skipped_modules or module in skipped_modules:
|
|
||||||
logger.info(f"Skipping import of mocked module: pipecat.{category}.{module}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
__import__(f"pipecat.{category}.{module}")
|
|
||||||
logger.info(f"Successfully imported pipecat.{category}.{module}")
|
|
||||||
except (ImportError, TypeError, NameError) as e:
|
|
||||||
missing.append(f"pipecat.{category}.{module}")
|
|
||||||
logger.warning(
|
|
||||||
f"Optional module not available: pipecat.{category}.{module} - {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if missing:
|
|
||||||
logger.warning(f"Some optional modules are not available: {missing}")
|
|
||||||
|
|
||||||
|
|
||||||
def clean_title(title: str) -> str:
|
def clean_title(title: str) -> str:
|
||||||
@@ -257,40 +224,7 @@ def clean_title(title: str) -> str:
|
|||||||
parts = title.split(".")
|
parts = title.split(".")
|
||||||
title = parts[-1]
|
title = parts[-1]
|
||||||
|
|
||||||
# Special cases for service names and common acronyms
|
return title
|
||||||
special_cases = {
|
|
||||||
"ai": "AI",
|
|
||||||
"aws": "AWS",
|
|
||||||
"api": "API",
|
|
||||||
"vad": "VAD",
|
|
||||||
"assemblyai": "AssemblyAI",
|
|
||||||
"deepgram": "Deepgram",
|
|
||||||
"elevenlabs": "ElevenLabs",
|
|
||||||
"openai": "OpenAI",
|
|
||||||
"openpipe": "OpenPipe",
|
|
||||||
"playht": "PlayHT",
|
|
||||||
"xtts": "XTTS",
|
|
||||||
"lmnt": "LMNT",
|
|
||||||
"stt": "STT",
|
|
||||||
"tts": "TTS",
|
|
||||||
"llm": "LLM",
|
|
||||||
"rtvi": "RTVI",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if the entire title is a special case
|
|
||||||
if title.lower() in special_cases:
|
|
||||||
return special_cases[title.lower()]
|
|
||||||
|
|
||||||
# Otherwise, capitalize each word
|
|
||||||
words = title.split("_")
|
|
||||||
cleaned_words = []
|
|
||||||
for word in words:
|
|
||||||
if word.lower() in special_cases:
|
|
||||||
cleaned_words.append(special_cases[word.lower()])
|
|
||||||
else:
|
|
||||||
cleaned_words.append(word.capitalize())
|
|
||||||
|
|
||||||
return " ".join(cleaned_words)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
@@ -315,9 +249,8 @@ def setup(app):
|
|||||||
|
|
||||||
excludes = [
|
excludes = [
|
||||||
str(project_root / "src/pipecat/pipeline/to_be_updated"),
|
str(project_root / "src/pipecat/pipeline/to_be_updated"),
|
||||||
str(project_root / "src/pipecat/processors/gstreamer"),
|
str(project_root / "src/pipecat/examples"),
|
||||||
str(project_root / "src/pipecat/services/to_be_updated"),
|
str(project_root / "src/pipecat/tests"),
|
||||||
str(project_root / "src/pipecat/vad"), # deprecated
|
|
||||||
"**/test_*.py",
|
"**/test_*.py",
|
||||||
"**/tests/*.py",
|
"**/tests/*.py",
|
||||||
]
|
]
|
||||||
@@ -358,5 +291,4 @@ def setup(app):
|
|||||||
logger.error(f"Error generating API documentation: {e}", exc_info=True)
|
logger.error(f"Error generating API documentation: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
# Run module verification
|
import_core_modules()
|
||||||
verify_modules()
|
|
||||||
|
|||||||
@@ -1,57 +1,17 @@
|
|||||||
Pipecat API Reference Docs
|
Pipecat API Reference
|
||||||
==========================
|
=====================
|
||||||
|
|
||||||
Welcome to Pipecat's API reference documentation!
|
Welcome to the Pipecat API reference.
|
||||||
|
|
||||||
Pipecat is an open source framework for building voice and multimodal assistants.
|
Use the navigation on the left to browse modules, or search using the search box.
|
||||||
It provides a flexible pipeline architecture for connecting various AI services,
|
|
||||||
audio processing, and transport layers.
|
**New to Pipecat?** Check out the `main documentation <https://docs.pipecat.ai>`_ for tutorials, guides, and client SDK information.
|
||||||
|
|
||||||
Quick Links
|
Quick Links
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
* `GitHub Repository <https://github.com/pipecat-ai/pipecat>`_
|
* `GitHub Repository <https://github.com/pipecat-ai/pipecat>`_
|
||||||
* `Website <https://pipecat.ai>`_
|
* `Join our Community <https://discord.gg/pipecat>`_
|
||||||
|
|
||||||
API Reference
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Core Components
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Frames <pipecat.frames>`
|
|
||||||
* :mod:`Processors <pipecat.processors>`
|
|
||||||
* :mod:`Pipeline <pipecat.pipeline>`
|
|
||||||
|
|
||||||
Audio Processing
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Audio <pipecat.audio>`
|
|
||||||
|
|
||||||
Services
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Services <pipecat.services>`
|
|
||||||
|
|
||||||
Transport & Serialization
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Transports <pipecat.transports>`
|
|
||||||
* :mod:`Local <pipecat.transports.local>`
|
|
||||||
* :mod:`Network <pipecat.transports.network>`
|
|
||||||
* :mod:`Services <pipecat.transports.services>`
|
|
||||||
* :mod:`Serializers <pipecat.serializers>`
|
|
||||||
|
|
||||||
Utilities
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
* :mod:`Adapters <pipecat.adapters>`
|
|
||||||
* :mod:`Clocks <pipecat.clocks>`
|
|
||||||
* :mod:`Metrics <pipecat.metrics>`
|
|
||||||
* :mod:`Observers <pipecat.observers>`
|
|
||||||
* :mod:`Sync <pipecat.sync>`
|
|
||||||
* :mod:`Transcriptions <pipecat.transcriptions>`
|
|
||||||
* :mod:`Utils <pipecat.utils>`
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 3
|
:maxdepth: 3
|
||||||
@@ -72,10 +32,3 @@ Utilities
|
|||||||
Transcriptions <api/pipecat.transcriptions>
|
Transcriptions <api/pipecat.transcriptions>
|
||||||
Transports <api/pipecat.transports>
|
Transports <api/pipecat.transports>
|
||||||
Utils <api/pipecat.utils>
|
Utils <api/pipecat.utils>
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
@@ -46,6 +46,7 @@ pipecat-ai[sambanova]
|
|||||||
pipecat-ai[silero]
|
pipecat-ai[silero]
|
||||||
pipecat-ai[simli]
|
pipecat-ai[simli]
|
||||||
pipecat-ai[soundfile]
|
pipecat-ai[soundfile]
|
||||||
|
pipecat-ai[speechmatics]
|
||||||
pipecat-ai[tavus]
|
pipecat-ai[tavus]
|
||||||
pipecat-ai[together]
|
pipecat-ai[together]
|
||||||
# pipecat-ai[ultravox] # Mocked
|
# pipecat-ai[ultravox] # Mocked
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ MINIMAX_GROUP_ID=...
|
|||||||
# Sarvam AI
|
# Sarvam AI
|
||||||
SARVAM_API_KEY=...
|
SARVAM_API_KEY=...
|
||||||
|
|
||||||
|
# Speechmatics
|
||||||
|
SPEECHMATICS_API_KEY=...
|
||||||
|
|
||||||
|
|
||||||
# SambaNova
|
# SambaNova
|
||||||
SAMBANOVA_API_KEY=...
|
SAMBANOVA_API_KEY=...
|
||||||
|
|
||||||
|
|||||||
@@ -4364,9 +4364,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -6081,9 +6081,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ aiofiles
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
fastapi[all]
|
fastapi[all]
|
||||||
uvicorn
|
uvicorn
|
||||||
pipecat-ai[daily,deepgram,openai,silero,cartesia]
|
pipecat-ai[daily,deepgram,openai,silero,cartesia,soundfile]
|
||||||
|
|||||||
@@ -215,10 +215,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz",
|
||||||
"integrity": "sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA==",
|
"integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
"version": "14.2.25",
|
"version": "14.2.25",
|
||||||
@@ -231,13 +230,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz",
|
||||||
"integrity": "sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ==",
|
"integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -247,13 +245,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz",
|
||||||
"integrity": "sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg==",
|
"integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -263,13 +260,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz",
|
||||||
"integrity": "sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg==",
|
"integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -279,13 +275,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz",
|
||||||
"integrity": "sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA==",
|
"integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -295,13 +290,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz",
|
||||||
"integrity": "sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg==",
|
"integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -311,13 +305,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz",
|
||||||
"integrity": "sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg==",
|
"integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -327,13 +320,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw==",
|
"integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -343,13 +335,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg==",
|
"integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -359,13 +350,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w==",
|
"integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -620,11 +610,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -1224,11 +1213,10 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -2614,11 +2602,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -3613,12 +3600,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "14.2.26",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.26.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz",
|
||||||
"integrity": "sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw==",
|
"integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "14.2.26",
|
"@next/env": "14.2.30",
|
||||||
"@swc/helpers": "0.5.5",
|
"@swc/helpers": "0.5.5",
|
||||||
"busboy": "1.6.0",
|
"busboy": "1.6.0",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -3633,15 +3619,15 @@
|
|||||||
"node": ">=18.17.0"
|
"node": ">=18.17.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "14.2.26",
|
"@next/swc-darwin-arm64": "14.2.30",
|
||||||
"@next/swc-darwin-x64": "14.2.26",
|
"@next/swc-darwin-x64": "14.2.30",
|
||||||
"@next/swc-linux-arm64-gnu": "14.2.26",
|
"@next/swc-linux-arm64-gnu": "14.2.30",
|
||||||
"@next/swc-linux-arm64-musl": "14.2.26",
|
"@next/swc-linux-arm64-musl": "14.2.30",
|
||||||
"@next/swc-linux-x64-gnu": "14.2.26",
|
"@next/swc-linux-x64-gnu": "14.2.30",
|
||||||
"@next/swc-linux-x64-musl": "14.2.26",
|
"@next/swc-linux-x64-musl": "14.2.30",
|
||||||
"@next/swc-win32-arm64-msvc": "14.2.26",
|
"@next/swc-win32-arm64-msvc": "14.2.30",
|
||||||
"@next/swc-win32-ia32-msvc": "14.2.26",
|
"@next/swc-win32-ia32-msvc": "14.2.30",
|
||||||
"@next/swc-win32-x64-msvc": "14.2.26"
|
"@next/swc-win32-x64-msvc": "14.2.30"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
|||||||
153
examples/foundational/07a-interruptible-speechmatics.py
Normal file
153
examples/foundational/07a-interruptible-speechmatics.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.llm_response import (
|
||||||
|
LLMUserAggregatorParams,
|
||||||
|
)
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
|
||||||
|
from pipecat.services.openai.base_llm import BaseOpenAILLMService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.services.speechmatics.stt import SpeechmaticsSTTService
|
||||||
|
from pipecat.transcriptions.language import Language
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
|
from pipecat.transports.services.daily import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"webrtc": lambda: TransportParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
|
||||||
|
"""Run example using Speechmatics STT.
|
||||||
|
|
||||||
|
This example will use diarization within our STT service and output the words spoken by
|
||||||
|
each individual speaker and wrap them with XML tags for the LLM to process. Note the
|
||||||
|
instructions in the system context for the LLM. This greatly improves the conversation
|
||||||
|
experience by allowing the LLM to understand who is speaking in a multi-party call.
|
||||||
|
|
||||||
|
If you do not wish to use diarization, then set the `enable_speaker_diarization` parameter
|
||||||
|
to `False` or omit it altogether. The `text_format` will only be used if diarization is enabled.
|
||||||
|
|
||||||
|
By default, this example will use our ENHANCED operating point, which is optimized for
|
||||||
|
high accuracy. You can change this by setting the `operating_point` parameter to a different
|
||||||
|
value.
|
||||||
|
|
||||||
|
For more information on operating points, see the Speechmatics documentation:
|
||||||
|
https://docs.speechmatics.com/rt-api-ref
|
||||||
|
"""
|
||||||
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
|
stt = SpeechmaticsSTTService(
|
||||||
|
api_key=os.getenv("SPEECHMATICS_API_KEY"),
|
||||||
|
language=Language.EN,
|
||||||
|
enable_speaker_diarization=True,
|
||||||
|
text_format="<{speaker_id}>{text}</{speaker_id}>",
|
||||||
|
)
|
||||||
|
|
||||||
|
tts = ElevenLabsTTSService(
|
||||||
|
api_key=os.getenv("ELEVENLABS_API_KEY", ""),
|
||||||
|
voice_id=os.getenv("ELEVENLABS_VOICE_ID", ""),
|
||||||
|
model="eleven_turbo_v2_5",
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(
|
||||||
|
api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
|
params=BaseOpenAILLMService.InputParams(temperature=0.75),
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
"You are a helpful British assistant called Alfred. "
|
||||||
|
"Your goal is to demonstrate your capabilities in a succinct way. "
|
||||||
|
"Your output will be converted to audio so don't include special characters in your answers. "
|
||||||
|
"Always include punctuation in your responses. "
|
||||||
|
"Give very short replies - do not give longer replies unless strictly necessary. "
|
||||||
|
"Respond to what the user said in a concise, funny, creative and helpful way. "
|
||||||
|
"Use `<Sn/>` tags to identify different speakers - do not use tags in your replies."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(
|
||||||
|
context,
|
||||||
|
user_params=LLMUserAggregatorParams(aggregation_timeout=0.005),
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # Transport user input
|
||||||
|
stt, # STT
|
||||||
|
context_aggregator.user(), # User responses
|
||||||
|
llm, # LLM
|
||||||
|
tts, # TTS
|
||||||
|
transport.output(), # Transport bot output
|
||||||
|
context_aggregator.assistant(), # Assistant spoken responses
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
logger.info(f"Client connected")
|
||||||
|
# Kick off the conversation.
|
||||||
|
messages.append({"role": "system", "content": "Say a short hello to the user."})
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner(handle_sigint=handle_sigint)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.examples.run import main
|
||||||
|
|
||||||
|
main(run_example, transport_params=transport_params)
|
||||||
@@ -35,7 +35,7 @@ transport_params = {
|
|||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
"twilio": lambda: TransportParams(
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
89
examples/foundational/13h-speechmatics-transcription.py
Normal file
89
examples/foundational/13h-speechmatics-transcription.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.frames.frames import Frame, TranscriptionFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineTask
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.services.speechmatics.stt import SpeechmaticsSTTService
|
||||||
|
from pipecat.transcriptions.language import Language
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
|
from pipecat.transports.services.daily import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TranscriptionLogger(FrameProcessor):
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
if isinstance(frame, TranscriptionFrame):
|
||||||
|
print(f"Transcription: {frame.text}")
|
||||||
|
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(audio_in_enabled=True),
|
||||||
|
"twilio": lambda: FastAPIWebsocketParams(audio_in_enabled=True),
|
||||||
|
"webrtc": lambda: TransportParams(audio_in_enabled=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
|
||||||
|
"""Run example using Speechmatics STT.
|
||||||
|
|
||||||
|
This example will use diarization within our STT service and output the words spoken by
|
||||||
|
each individual speaker and wrap them with XML tags.
|
||||||
|
|
||||||
|
If you do not wish to use diarization, then set the `enable_speaker_diarization` parameter
|
||||||
|
to `False` or omit it altogether. The `text_format` will only be used if diarization is enabled.
|
||||||
|
|
||||||
|
By default, this example will use our ENHANCED operating point, which is optimized for
|
||||||
|
high accuracy. You can change this by setting the `operating_point` parameter to a different
|
||||||
|
value.
|
||||||
|
|
||||||
|
For more information on operating points, see the Speechmatics documentation:
|
||||||
|
https://docs.speechmatics.com/rt-api-ref
|
||||||
|
"""
|
||||||
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
|
stt = SpeechmaticsSTTService(
|
||||||
|
api_key=os.getenv("SPEECHMATICS_API_KEY"),
|
||||||
|
language=Language.EN,
|
||||||
|
enable_speaker_diarization=True,
|
||||||
|
text_format="<{speaker_id}>{text}</{speaker_id}>",
|
||||||
|
)
|
||||||
|
|
||||||
|
tl = TranscriptionLogger()
|
||||||
|
|
||||||
|
pipeline = Pipeline([transport.input(), stt, tl])
|
||||||
|
|
||||||
|
task = PipelineTask(pipeline)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner(handle_sigint=handle_sigint)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.examples.run import main
|
||||||
|
|
||||||
|
main(run_example, transport_params=transport_params)
|
||||||
@@ -42,7 +42,7 @@ transport_params = {
|
|||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
"twilio": lambda: TransportParams(
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
146
examples/foundational/14t-function-calling-direct.py
Normal file
146
examples/foundational/14t-function-calling-direct.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import TTSSpeakFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
|
from pipecat.services.llm_service import FunctionCallParams
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
|
from pipecat.transports.services.daily import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_weather(params: FunctionCallParams, location: str, format: str):
|
||||||
|
"""
|
||||||
|
Get the current weather.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (str): The city and state, e.g. "San Francisco, CA".
|
||||||
|
format (str): The temperature unit to use. Must be either "celsius" or "fahrenheit". Infer this from the user's location.
|
||||||
|
"""
|
||||||
|
await params.result_callback({"conditions": "nice", "temperature": "75"})
|
||||||
|
|
||||||
|
|
||||||
|
async def get_restaurant_recommendation(params: FunctionCallParams, location: str):
|
||||||
|
"""
|
||||||
|
Get a restaurant recommendation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (str): The city and state, e.g. "San Francisco, CA".
|
||||||
|
"""
|
||||||
|
await params.result_callback({"name": "The Golden Dragon"})
|
||||||
|
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
"webrtc": lambda: TransportParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
|
||||||
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
# You can also register a function_name of None to get all functions
|
||||||
|
# sent to the same callback with an additional function_name parameter.
|
||||||
|
llm.register_direct_function(get_current_weather)
|
||||||
|
llm.register_direct_function(get_restaurant_recommendation)
|
||||||
|
|
||||||
|
@llm.event_handler("on_function_calls_started")
|
||||||
|
async def on_function_calls_started(service, function_calls):
|
||||||
|
await tts.queue_frame(TTSSpeakFrame("Let me check on that."))
|
||||||
|
|
||||||
|
tools = ToolsSchema(standard_tools=[get_current_weather, get_restaurant_recommendation])
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages, tools)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
stt,
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
logger.info(f"Client connected")
|
||||||
|
# Kick off the conversation.
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner(handle_sigint=handle_sigint)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.examples.run import main
|
||||||
|
|
||||||
|
main(run_example, transport_params=transport_params)
|
||||||
@@ -33,7 +33,7 @@ transport_params = {
|
|||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
"twilio": lambda: TransportParams(
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ transport_params = {
|
|||||||
# endpointing, for now.
|
# endpointing, for now.
|
||||||
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
||||||
),
|
),
|
||||||
"twilio": lambda: TransportParams(
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
# set stop_secs to something roughly similar to the internal setting
|
# set stop_secs to something roughly similar to the internal setting
|
||||||
|
|||||||
242
examples/foundational/26f-gemini-multimodal-live-files-api.py
Normal file
242
examples/foundational/26f-gemini-multimodal-live-files-api.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.audio.vad.vad_analyzer import VADParams
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.gemini_multimodal_live.gemini import (
|
||||||
|
GeminiMultimodalLiveLLMService,
|
||||||
|
)
|
||||||
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
|
from pipecat.transports.services.daily import DailyParams
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
# We store functions so objects (e.g. SileroVADAnalyzer) don't get
|
||||||
|
# instantiated. The function will be called when the desired transport gets
|
||||||
|
# selected.
|
||||||
|
transport_params = {
|
||||||
|
"daily": lambda: DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
video_in_enabled=False,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
||||||
|
),
|
||||||
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
video_in_enabled=False,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
||||||
|
),
|
||||||
|
"webrtc": lambda: TransportParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
video_in_enabled=False,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sample_file_path = ""
|
||||||
|
|
||||||
|
|
||||||
|
async def create_sample_file():
|
||||||
|
if sample_file_path:
|
||||||
|
return sample_file_path
|
||||||
|
else:
|
||||||
|
"""Create a sample text file for testing the File API."""
|
||||||
|
content = """# Sample Document for Gemini File API Test
|
||||||
|
|
||||||
|
This is a test document to demonstrate the Gemini File API functionality.
|
||||||
|
|
||||||
|
## Key Information:
|
||||||
|
- This document was created for testing purposes
|
||||||
|
- It contains information about AI assistants
|
||||||
|
- The document should be analyzed by Gemini
|
||||||
|
- The secret phrase for the test is "Pineapple Pizza"
|
||||||
|
|
||||||
|
## AI Assistant Capabilities:
|
||||||
|
1. Natural language processing
|
||||||
|
2. File analysis and understanding
|
||||||
|
3. Context-aware conversations
|
||||||
|
4. Multi-modal interactions
|
||||||
|
|
||||||
|
## Conclusion:
|
||||||
|
This document serves as a test case for the Gemini File API integration with Pipecat.
|
||||||
|
The AI should be able to reference and discuss the contents of this file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
||||||
|
f.write(content)
|
||||||
|
return f.name
|
||||||
|
|
||||||
|
|
||||||
|
async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool):
|
||||||
|
logger.info(f"Starting File API bot")
|
||||||
|
|
||||||
|
# Create a sample file to upload
|
||||||
|
sample_file_path = await create_sample_file()
|
||||||
|
logger.info(f"Created sample file: {sample_file_path}")
|
||||||
|
|
||||||
|
system_instruction = """
|
||||||
|
You are a helpful AI assistant with access to a document that has been uploaded for analysis.
|
||||||
|
|
||||||
|
The document contains test information.
|
||||||
|
You should be able to:
|
||||||
|
- Reference and discuss the contents of the uploaded document
|
||||||
|
- Answer questions about what's in the document
|
||||||
|
- Use the information from the document in our conversation
|
||||||
|
|
||||||
|
Your output will be converted to audio so don't include special characters in your answers.
|
||||||
|
Be friendly and demonstrate your ability to work with the uploaded file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize Gemini service with File API support
|
||||||
|
llm = GeminiMultimodalLiveLLMService(
|
||||||
|
api_key=os.getenv("GOOGLE_API_KEY"),
|
||||||
|
system_instruction=system_instruction,
|
||||||
|
voice_id="Charon", # Aoede, Charon, Fenrir, Kore, Puck
|
||||||
|
transcribe_user_audio=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upload the sample file to Gemini File API
|
||||||
|
logger.info("Uploading file to Gemini File API...")
|
||||||
|
file_info = None
|
||||||
|
try:
|
||||||
|
file_info = await llm.file_api.upload_file(
|
||||||
|
sample_file_path, display_name="Sample Test Document"
|
||||||
|
)
|
||||||
|
logger.info(f"File uploaded successfully: {file_info['file']['name']}")
|
||||||
|
|
||||||
|
# Get file URI and mime type
|
||||||
|
file_uri = file_info["file"]["uri"]
|
||||||
|
mime_type = "text/plain"
|
||||||
|
|
||||||
|
# Create context with file reference
|
||||||
|
context = OpenAILLMContext(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Greet the user and let them know you have access to a document they can ask you about. Mention that you can discuss its contents.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file_data",
|
||||||
|
"file_data": {"mime_type": mime_type, "file_uri": file_uri},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("File reference added to conversation context")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading file: {e}")
|
||||||
|
# Continue with a basic context if file upload fails
|
||||||
|
context = OpenAILLMContext(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Greet the user and explain that there was an issue with file upload, but you're ready to help with other tasks.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create context aggregator
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
# Build the pipeline
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure the pipeline task
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
allow_interruptions=True,
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle client connection event
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
logger.info(f"Client connected")
|
||||||
|
# Kick off the conversation using standard context frame
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
# Handle client disconnection events
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
logger.info(f"Client disconnected")
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_closed")
|
||||||
|
async def on_client_closed(transport, client):
|
||||||
|
logger.info(f"Client closed connection")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
# Run the pipeline
|
||||||
|
runner = PipelineRunner(handle_sigint=False)
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
# Clean up: delete the uploaded file and temporary file
|
||||||
|
if file_info:
|
||||||
|
try:
|
||||||
|
await llm.file_api.delete_file(file_info["file"]["name"])
|
||||||
|
logger.info("Cleaned up uploaded file from Gemini")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error cleaning up file: {e}")
|
||||||
|
|
||||||
|
# Remove temporary file
|
||||||
|
try:
|
||||||
|
os.unlink(sample_file_path)
|
||||||
|
logger.info("Cleaned up temporary file")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing temporary file: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pipecat.examples.run import main
|
||||||
|
|
||||||
|
upload_example_file = input("""
|
||||||
|
|
||||||
|
Please pass in a TEXT filepath to test upload.
|
||||||
|
NOTE: Files are stored on Google's servers for 48 hours.
|
||||||
|
|
||||||
|
Press Enter to use a default test file.
|
||||||
|
|
||||||
|
text filepath : """)
|
||||||
|
if upload_example_file:
|
||||||
|
print(f"Uploading file: {upload_example_file}")
|
||||||
|
sample_file_path = upload_example_file.strip()
|
||||||
|
else:
|
||||||
|
print(f"Using default file")
|
||||||
|
|
||||||
|
main(run_example, transport_params=transport_params)
|
||||||
@@ -18,8 +18,8 @@ from pipecat.pipeline.task import PipelineParams, PipelineTask
|
|||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
from pipecat.services.cartesia.tts import CartesiaTTSService
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
from pipecat.services.deepgram.stt import DeepgramSTTService
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
|
from pipecat.services.google.llm import GoogleLLMService
|
||||||
from pipecat.services.mcp_service import MCPClient
|
from pipecat.services.mcp_service import MCPClient
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
|
||||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
from pipecat.transports.services.daily import DailyParams
|
from pipecat.transports.services.daily import DailyParams
|
||||||
@@ -58,7 +58,7 @@ async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_si
|
|||||||
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
)
|
)
|
||||||
|
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini")
|
llm = GoogleLLMService(api_key=os.getenv("GOOGLE_API_KEY"), model="gemini-2.0-flash")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Github MCP docs: https://github.com/github/github-mcp-server
|
# Github MCP docs: https://github.com/github/github-mcp-server
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_si
|
|||||||
secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
||||||
access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
|
access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
|
||||||
region=os.getenv("AWS_REGION"), # as of 2025-05-06, us-east-1 is the only supported region
|
region=os.getenv("AWS_REGION"), # as of 2025-05-06, us-east-1 is the only supported region
|
||||||
|
session_token=os.getenv("AWS_SESSION_TOKEN"),
|
||||||
voice_id="tiffany", # matthew, tiffany, amy
|
voice_id="tiffany", # matthew, tiffany, amy
|
||||||
# you could choose to pass instruction here rather than via context
|
# you could choose to pass instruction here rather than via context
|
||||||
# system_instruction=system_instruction
|
# system_instruction=system_instruction
|
||||||
|
|||||||
@@ -191,7 +191,17 @@ class WebsocketClientApp {
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
this.recordingSerializer = new RecordingSerializer()
|
this.recordingSerializer = new RecordingSerializer()
|
||||||
const transport = this.ENABLE_RECORDING_MODE ? new WebSocketTransport({serializer: this.recordingSerializer}) : new WebSocketTransport();
|
const transport = this.ENABLE_RECORDING_MODE ?
|
||||||
|
new WebSocketTransport({
|
||||||
|
serializer: this.recordingSerializer,
|
||||||
|
recorderSampleRate: 8000,
|
||||||
|
playerSampleRate:8000
|
||||||
|
}) :
|
||||||
|
new WebSocketTransport({
|
||||||
|
serializer: new ProtobufFrameSerializer(),
|
||||||
|
recorderSampleRate: 8000,
|
||||||
|
playerSampleRate:8000
|
||||||
|
});
|
||||||
this.websocketTransport = transport
|
this.websocketTransport = transport
|
||||||
|
|
||||||
const RTVIConfig: RTVIClientOptions = {
|
const RTVIConfig: RTVIClientOptions = {
|
||||||
|
|||||||
4
examples/freeze-test/env.example
Normal file
4
examples/freeze-test/env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
SENTRY_DSN=
|
||||||
|
DEEPGRAM_API_KEY=
|
||||||
|
CARTESIA_API_KEY=
|
||||||
|
OPENAI_API_KEY=
|
||||||
@@ -18,7 +18,6 @@ from fastapi import FastAPI, Request, WebSocket
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
@@ -27,11 +26,13 @@ from pipecat.frames.frames import (
|
|||||||
Frame,
|
Frame,
|
||||||
InterimTranscriptionFrame,
|
InterimTranscriptionFrame,
|
||||||
LLMFullResponseEndFrame,
|
LLMFullResponseEndFrame,
|
||||||
|
LLMMessagesFrame,
|
||||||
StartFrame,
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
StopFrame,
|
StopFrame,
|
||||||
StopInterruptionFrame,
|
StopInterruptionFrame,
|
||||||
TranscriptionFrame,
|
TranscriptionFrame,
|
||||||
|
TTSSpeakFrame,
|
||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
UserStoppedSpeakingFrame,
|
UserStoppedSpeakingFrame,
|
||||||
)
|
)
|
||||||
@@ -47,6 +48,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
|
|||||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
from pipecat.processors.frameworks.rtvi import RTVIConfig, RTVIProcessor
|
from pipecat.processors.frameworks.rtvi import RTVIConfig, RTVIProcessor
|
||||||
from pipecat.processors.metrics.sentry import SentryMetrics
|
from pipecat.processors.metrics.sentry import SentryMetrics
|
||||||
|
from pipecat.processors.user_idle_processor import UserIdleProcessor
|
||||||
from pipecat.serializers.protobuf import ProtobufFrameSerializer
|
from pipecat.serializers.protobuf import ProtobufFrameSerializer
|
||||||
from pipecat.services.cartesia.tts import CartesiaTTSService
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
from pipecat.services.deepgram.stt import DeepgramSTTService
|
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||||
@@ -78,9 +80,6 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mount the frontend at /
|
|
||||||
app.mount("/client", SmallWebRTCPrebuiltUI)
|
|
||||||
|
|
||||||
|
|
||||||
class SimulateFreezeInput(FrameProcessor):
|
class SimulateFreezeInput(FrameProcessor):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -188,6 +187,37 @@ async def run_example(websocket_client):
|
|||||||
|
|
||||||
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
async def handle_user_idle(user_idle: UserIdleProcessor, retry_count: int) -> bool:
|
||||||
|
if retry_count == 1:
|
||||||
|
# First attempt: Add a gentle prompt to the conversation
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "The user has been quiet. Politely and briefly ask if they're still there.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await user_idle.push_frame(LLMMessagesFrame(messages))
|
||||||
|
return True
|
||||||
|
elif retry_count == 2:
|
||||||
|
# Second attempt: More direct prompt
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "The user is still inactive. Ask if they'd like to continue our conversation.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await user_idle.push_frame(LLMMessagesFrame(messages))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Third attempt: End the conversation
|
||||||
|
await user_idle.push_frame(
|
||||||
|
TTSSpeakFrame("It seems like you're busy right now. Have a nice day!")
|
||||||
|
)
|
||||||
|
await task.queue_frame(EndFrame())
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_idle = UserIdleProcessor(callback=handle_user_idle, timeout=10.0)
|
||||||
|
|
||||||
tts = CartesiaTTSService(
|
tts = CartesiaTTSService(
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
@@ -222,6 +252,7 @@ async def run_example(websocket_client):
|
|||||||
stt,
|
stt,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
user_idle,
|
||||||
rtvi,
|
rtvi,
|
||||||
context_aggregator.user(), # User responses
|
context_aggregator.user(), # User responses
|
||||||
llm, # LLM
|
llm, # LLM
|
||||||
@@ -238,6 +269,8 @@ async def run_example(websocket_client):
|
|||||||
enable_metrics=True,
|
enable_metrics=True,
|
||||||
enable_usage_metrics=True,
|
enable_usage_metrics=True,
|
||||||
report_only_initial_ttfb=True,
|
report_only_initial_ttfb=True,
|
||||||
|
audio_in_sample_rate=8000,
|
||||||
|
audio_out_sample_rate=8000,
|
||||||
),
|
),
|
||||||
idle_timeout_secs=120,
|
idle_timeout_secs=120,
|
||||||
observers=[
|
observers=[
|
||||||
@@ -249,6 +282,10 @@ async def run_example(websocket_client):
|
|||||||
# LLMTextFrame: None,
|
# LLMTextFrame: None,
|
||||||
OpenAILLMContextFrame: None,
|
OpenAILLMContextFrame: None,
|
||||||
LLMFullResponseEndFrame: None,
|
LLMFullResponseEndFrame: None,
|
||||||
|
UserStartedSpeakingFrame: None,
|
||||||
|
UserStoppedSpeakingFrame: None,
|
||||||
|
StartInterruptionFrame: None,
|
||||||
|
StopInterruptionFrame: None,
|
||||||
},
|
},
|
||||||
exclude_fields={
|
exclude_fields={
|
||||||
"result",
|
"result",
|
||||||
|
|||||||
4
examples/freeze-test/requirements.txt
Normal file
4
examples/freeze-test/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
python-dotenv
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn
|
||||||
|
pipecat-ai[silero,websocket,openai, deepgram, cartesia, sentry]
|
||||||
@@ -24,6 +24,7 @@ from pipecat.services.deepgram.stt import DeepgramSTTService
|
|||||||
from pipecat.services.llm_service import FunctionCallParams
|
from pipecat.services.llm_service import FunctionCallParams
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
from pipecat.transports.services.daily import DailyParams
|
from pipecat.transports.services.daily import DailyParams
|
||||||
from pipecat.utils.tracing.setup import setup_tracing
|
from pipecat.utils.tracing.setup import setup_tracing
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ transport_params = {
|
|||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
"twilio": lambda: TransportParams(
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pipecat-ai[webrtc,silero,cartesia,deepgram,openai,tracing]
|
pipecat-ai[daily,webrtc,silero,cartesia,deepgram,openai,tracing]
|
||||||
pipecat-ai-small-webrtc-prebuilt
|
pipecat-ai-small-webrtc-prebuilt
|
||||||
opentelemetry-exporter-otlp-proto-grpc
|
opentelemetry-exporter-otlp-proto-grpc
|
||||||
@@ -26,7 +26,7 @@ Create a `.env` file with your API keys to enable tracing:
|
|||||||
```
|
```
|
||||||
ENABLE_TRACING=true
|
ENABLE_TRACING=true
|
||||||
# OTLP endpoint for Langfuse
|
# OTLP endpoint for Langfuse
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://cloud.langfuse.com/api/public/otel
|
OTEL_EXPORTER_OTLP_ENDPOINT=https://cloud.langfuse.com/api/public/otel
|
||||||
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20<base64_encoded_api_key>
|
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20<base64_encoded_api_key>
|
||||||
# Set to any value to enable console output for debugging
|
# Set to any value to enable console output for debugging
|
||||||
# OTEL_CONSOLE_EXPORT=true
|
# OTEL_CONSOLE_EXPORT=true
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from pipecat.services.deepgram.stt import DeepgramSTTService
|
|||||||
from pipecat.services.llm_service import FunctionCallParams
|
from pipecat.services.llm_service import FunctionCallParams
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
|
from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams
|
||||||
from pipecat.transports.services.daily import DailyParams
|
from pipecat.transports.services.daily import DailyParams
|
||||||
from pipecat.utils.tracing.setup import setup_tracing
|
from pipecat.utils.tracing.setup import setup_tracing
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ transport_params = {
|
|||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
"twilio": lambda: TransportParams(
|
"twilio": lambda: FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pipecat-ai[webrtc,silero,cartesia,deepgram,openai,tracing]
|
pipecat-ai[daily,webrtc,silero,cartesia,deepgram,openai,tracing]
|
||||||
pipecat-ai-small-webrtc-prebuilt
|
pipecat-ai-small-webrtc-prebuilt
|
||||||
opentelemetry-exporter-otlp-proto-http
|
opentelemetry-exporter-otlp-proto-http
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
pipecat-ai[daily,elevenlabs,openai,silero]
|
pipecat-ai[daily,cartesia,openai,silero]
|
||||||
fastapi==0.115.6
|
fastapi==0.115.6
|
||||||
uvicorn
|
uvicorn
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
90031FC22C616EE900408370 /* SimpleChatbotUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FC12C616EE900408370 /* SimpleChatbotUITests.swift */; };
|
90031FC22C616EE900408370 /* SimpleChatbotUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FC12C616EE900408370 /* SimpleChatbotUITests.swift */; };
|
||||||
90031FC42C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FC32C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift */; };
|
90031FC42C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FC32C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift */; };
|
||||||
90031FDC2C6D5DD700408370 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FDB2C6D5DD700408370 /* ToastModifier.swift */; };
|
90031FDC2C6D5DD700408370 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FDB2C6D5DD700408370 /* ToastModifier.swift */; };
|
||||||
907C98842D37E6AF0079441F /* PipecatClientIOSDaily in Frameworks */ = {isa = PBXBuildFile; productRef = 907C98832D37E6AF0079441F /* PipecatClientIOSDaily */; };
|
|
||||||
90ABB98E2C735ED6000D9CC7 /* MeetingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB98D2C735ED6000D9CC7 /* MeetingView.swift */; };
|
90ABB98E2C735ED6000D9CC7 /* MeetingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB98D2C735ED6000D9CC7 /* MeetingView.swift */; };
|
||||||
90ABB9902C736A8B000D9CC7 /* WaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB98F2C736A8B000D9CC7 /* WaveformView.swift */; };
|
90ABB9902C736A8B000D9CC7 /* WaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB98F2C736A8B000D9CC7 /* WaveformView.swift */; };
|
||||||
90ABB9932C73820D000D9CC7 /* MicrophoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9922C73820D000D9CC7 /* MicrophoneView.swift */; };
|
90ABB9932C73820D000D9CC7 /* MicrophoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9922C73820D000D9CC7 /* MicrophoneView.swift */; };
|
||||||
@@ -25,6 +24,8 @@
|
|||||||
90ABB9A32C74E1CE000D9CC7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A22C74E1CE000D9CC7 /* SettingsView.swift */; };
|
90ABB9A32C74E1CE000D9CC7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A22C74E1CE000D9CC7 /* SettingsView.swift */; };
|
||||||
90ABB9A62C74EA8A000D9CC7 /* SettingsPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A52C74EA8A000D9CC7 /* SettingsPreference.swift */; };
|
90ABB9A62C74EA8A000D9CC7 /* SettingsPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A52C74EA8A000D9CC7 /* SettingsPreference.swift */; };
|
||||||
90ABB9A82C74EAB1000D9CC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A72C74EAB1000D9CC7 /* SettingsManager.swift */; };
|
90ABB9A82C74EAB1000D9CC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A72C74EAB1000D9CC7 /* SettingsManager.swift */; };
|
||||||
|
90CC98B02E158093003C2706 /* PipecatClientIOSDaily in Frameworks */ = {isa = PBXBuildFile; productRef = 90CC98AF2E158093003C2706 /* PipecatClientIOSDaily */; };
|
||||||
|
90CC98B62E15820B003C2706 /* PipecatClientIOSDaily in Frameworks */ = {isa = PBXBuildFile; productRef = 90CC98B52E15820B003C2706 /* PipecatClientIOSDaily */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -73,7 +74,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
907C98842D37E6AF0079441F /* PipecatClientIOSDaily in Frameworks */,
|
90CC98B62E15820B003C2706 /* PipecatClientIOSDaily in Frameworks */,
|
||||||
|
90CC98B02E158093003C2706 /* PipecatClientIOSDaily in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -218,7 +220,8 @@
|
|||||||
);
|
);
|
||||||
name = SimpleChatbot;
|
name = SimpleChatbot;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
907C98832D37E6AF0079441F /* PipecatClientIOSDaily */,
|
90CC98AF2E158093003C2706 /* PipecatClientIOSDaily */,
|
||||||
|
90CC98B52E15820B003C2706 /* PipecatClientIOSDaily */,
|
||||||
);
|
);
|
||||||
productName = SimpleChatbot;
|
productName = SimpleChatbot;
|
||||||
productReference = 90031FA32C616EE700408370 /* SimpleChatbot.app */;
|
productReference = 90031FA32C616EE700408370 /* SimpleChatbot.app */;
|
||||||
@@ -293,7 +296,7 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 90031F9A2C616EE700408370;
|
mainGroup = 90031F9A2C616EE700408370;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
907C98822D37E6AF0079441F /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */,
|
90CC98B42E15820B003C2706 /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 90031FA42C616EE700408370 /* Products */;
|
productRefGroup = 90031FA42C616EE700408370 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -682,20 +685,24 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
907C98822D37E6AF0079441F /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */ = {
|
90CC98B42E15820B003C2706 /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/pipecat-ai/pipecat-client-ios-daily/";
|
repositoryURL = "https://github.com/pipecat-ai/pipecat-client-ios-daily/";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 0.3.2;
|
minimumVersion = 0.3.6;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
907C98832D37E6AF0079441F /* PipecatClientIOSDaily */ = {
|
90CC98AF2E158093003C2706 /* PipecatClientIOSDaily */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 907C98822D37E6AF0079441F /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */;
|
productName = PipecatClientIOSDaily;
|
||||||
|
};
|
||||||
|
90CC98B52E15820B003C2706 /* PipecatClientIOSDaily */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 90CC98B42E15820B003C2706 /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */;
|
||||||
productName = PipecatClientIOSDaily;
|
productName = PipecatClientIOSDaily;
|
||||||
};
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"originHash" : "cc17f08b06def9570d775e9c6f7a8dc10d1588b98127e977c47d052abac659b7",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "daily-client-ios",
|
"identity" : "daily-client-ios",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/daily-co/daily-client-ios.git",
|
"location" : "https://github.com/daily-co/daily-client-ios.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "15804ce495780da3ec2d05ab99736315f7bfbd24",
|
"revision" : "431938db25e5807120e89e2dc5bab1c076729f59",
|
||||||
"version" : "0.28.0"
|
"version" : "0.31.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -14,8 +15,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pipecat-ai/pipecat-client-ios.git",
|
"location" : "https://github.com/pipecat-ai/pipecat-client-ios.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c679512e367002a1a67da85d503fec72d9b17191",
|
"revision" : "f92b5e68e56a8311f7d8ead68a7a5674843cbc40",
|
||||||
"version" : "0.3.2"
|
"version" : "0.3.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23,10 +24,10 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pipecat-ai/pipecat-client-ios-daily/",
|
"location" : "https://github.com/pipecat-ai/pipecat-client-ios-daily/",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "a337fe6642c52376d2f90eafcb965f5be772ce72",
|
"revision" : "8f494da903192c22c367ecf9e51248c9b651fbc6",
|
||||||
"version" : "0.3.2"
|
"version" : "0.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,10 +78,11 @@ class CallContainerModel: ObservableObject {
|
|||||||
self.saveCredentials(backendURL: baseUrl)
|
self.saveCredentials(backendURL: baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
self.rtviClientIOS?.disconnect(completion: nil)
|
Task { @MainActor in
|
||||||
self.rtviClientIOS?.release()
|
try await self.rtviClientIOS?.disconnect()
|
||||||
|
self.rtviClientIOS?.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showError(message: String) {
|
func showError(message: String) {
|
||||||
|
|||||||
104
examples/storytelling-chatbot/client/package-lock.json
generated
104
examples/storytelling-chatbot/client/package-lock.json
generated
@@ -345,9 +345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.30.tgz",
|
||||||
"integrity": "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g=="
|
"integrity": "sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug=="
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
"version": "14.1.4",
|
"version": "14.1.4",
|
||||||
@@ -359,9 +359,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz",
|
||||||
"integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==",
|
"integrity": "sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -374,9 +374,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz",
|
||||||
"integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==",
|
"integrity": "sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -389,9 +389,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz",
|
||||||
"integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==",
|
"integrity": "sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -404,9 +404,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz",
|
||||||
"integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==",
|
"integrity": "sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -419,9 +419,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz",
|
||||||
"integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==",
|
"integrity": "sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -434,9 +434,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz",
|
||||||
"integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==",
|
"integrity": "sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -449,9 +449,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==",
|
"integrity": "sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -464,9 +464,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==",
|
"integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -479,9 +479,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz",
|
||||||
"integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==",
|
"integrity": "sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1317,9 +1317,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
@@ -1960,9 +1960,9 @@
|
|||||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@@ -3391,9 +3391,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -4389,11 +4389,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "14.2.28",
|
"version": "14.2.30",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-14.2.30.tgz",
|
||||||
"integrity": "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==",
|
"integrity": "sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "14.2.28",
|
"@next/env": "14.2.30",
|
||||||
"@swc/helpers": "0.5.5",
|
"@swc/helpers": "0.5.5",
|
||||||
"busboy": "1.6.0",
|
"busboy": "1.6.0",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -4408,15 +4408,15 @@
|
|||||||
"node": ">=18.17.0"
|
"node": ">=18.17.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "14.2.28",
|
"@next/swc-darwin-arm64": "14.2.30",
|
||||||
"@next/swc-darwin-x64": "14.2.28",
|
"@next/swc-darwin-x64": "14.2.30",
|
||||||
"@next/swc-linux-arm64-gnu": "14.2.28",
|
"@next/swc-linux-arm64-gnu": "14.2.30",
|
||||||
"@next/swc-linux-arm64-musl": "14.2.28",
|
"@next/swc-linux-arm64-musl": "14.2.30",
|
||||||
"@next/swc-linux-x64-gnu": "14.2.28",
|
"@next/swc-linux-x64-gnu": "14.2.30",
|
||||||
"@next/swc-linux-x64-musl": "14.2.28",
|
"@next/swc-linux-x64-musl": "14.2.30",
|
||||||
"@next/swc-win32-arm64-msvc": "14.2.28",
|
"@next/swc-win32-arm64-msvc": "14.2.30",
|
||||||
"@next/swc-win32-ia32-msvc": "14.2.28",
|
"@next/swc-win32-ia32-msvc": "14.2.30",
|
||||||
"@next/swc-win32-x64-msvc": "14.2.28"
|
"@next/swc-win32-x64-msvc": "14.2.30"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ Basic implementation using the [Pipecat JavaScript SDK](https://docs.pipecat.ai/
|
|||||||
|
|
||||||
1. Run the bot server. See the [server README](../README).
|
1. Run the bot server. See the [server README](../README).
|
||||||
|
|
||||||
2. Navigate to the `client/javascript` directory:
|
2. Navigate to the `client` directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd client/javascript
|
cd client
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Install dependencies:
|
3. Install dependencies:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp~=3.11.12",
|
"aiohttp~=3.11.12",
|
||||||
"audioop-lts~=0.2.1; python_version>='3.13'",
|
"audioop-lts~=0.2.1; python_version>='3.13'",
|
||||||
|
"docstring_parser~=0.16",
|
||||||
"loguru~=0.7.3",
|
"loguru~=0.7.3",
|
||||||
"Markdown~=3.7",
|
"Markdown~=3.7",
|
||||||
"numpy~=1.26.4",
|
"numpy~=1.26.4",
|
||||||
@@ -31,7 +32,7 @@ dependencies = [
|
|||||||
"pyloudnorm~=0.1.1",
|
"pyloudnorm~=0.1.1",
|
||||||
"resampy~=0.4.3",
|
"resampy~=0.4.3",
|
||||||
"soxr~=0.5.0",
|
"soxr~=0.5.0",
|
||||||
"openai~=1.70.0"
|
"openai~=1.70.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -47,14 +48,14 @@ azure = [ "azure-cognitiveservices-speech~=1.42.0"]
|
|||||||
cartesia = [ "cartesia~=2.0.3", "websockets~=13.1" ]
|
cartesia = [ "cartesia~=2.0.3", "websockets~=13.1" ]
|
||||||
cerebras = []
|
cerebras = []
|
||||||
deepseek = []
|
deepseek = []
|
||||||
daily = [ "daily-python~=0.19.3" ]
|
daily = [ "daily-python~=0.19.4" ]
|
||||||
deepgram = [ "deepgram-sdk~=4.1.0" ]
|
deepgram = [ "deepgram-sdk~=4.1.0" ]
|
||||||
elevenlabs = [ "websockets~=13.1" ]
|
elevenlabs = [ "websockets~=13.1" ]
|
||||||
fal = [ "fal-client~=0.5.9" ]
|
fal = [ "fal-client~=0.5.9" ]
|
||||||
fireworks = []
|
fireworks = []
|
||||||
fish = [ "ormsgpack~=1.7.0", "websockets~=13.1" ]
|
fish = [ "ormsgpack~=1.7.0", "websockets~=13.1" ]
|
||||||
gladia = [ "websockets~=13.1" ]
|
gladia = [ "websockets~=13.1" ]
|
||||||
google = [ "google-cloud-speech~=2.32.0", "google-cloud-texttospeech~=2.26.0", "google-genai~=1.14.0", "websockets~=13.1" ]
|
google = [ "google-cloud-speech~=2.32.0", "google-cloud-texttospeech~=2.26.0", "google-genai~=1.24.0", "websockets~=13.1" ]
|
||||||
grok = []
|
grok = []
|
||||||
groq = [ "groq~=0.23.0" ]
|
groq = [ "groq~=0.23.0" ]
|
||||||
gstreamer = [ "pygobject~=3.50.0" ]
|
gstreamer = [ "pygobject~=3.50.0" ]
|
||||||
@@ -86,6 +87,7 @@ remote-smart-turn = []
|
|||||||
silero = [ "onnxruntime~=1.20.1" ]
|
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" ]
|
||||||
|
speechmatics = [ "speechmatics-rt>=0.3.1" ]
|
||||||
tavus=[]
|
tavus=[]
|
||||||
together = []
|
together = []
|
||||||
tracing = [ "opentelemetry-sdk>=1.33.0", "opentelemetry-api>=1.33.0", "opentelemetry-instrumentation>=0.54b0" ]
|
tracing = [ "opentelemetry-sdk>=1.33.0", "opentelemetry-api>=1.33.0", "opentelemetry-instrumentation>=0.54b0" ]
|
||||||
@@ -123,9 +125,21 @@ select = [
|
|||||||
"D", # Docstring rules
|
"D", # Docstring rules
|
||||||
"I", # Import rules
|
"I", # Import rules
|
||||||
]
|
]
|
||||||
|
ignore = [
|
||||||
|
"D105", # Missing docstring in magic methods (__str__, __repr__, etc.)
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# Skip docstring checks for non-source code
|
||||||
|
"examples/**/*.py" = ["D"]
|
||||||
|
"tests/**/*.py" = ["D"]
|
||||||
|
"scripts/**/*.py" = ["D"]
|
||||||
|
"docs/**/*.py" = ["D"]
|
||||||
|
# Skip D104 (missing docstring in public package) for __init__.py files
|
||||||
"**/__init__.py" = ["D104"]
|
"**/__init__.py" = ["D104"]
|
||||||
|
# Skip specific rules for generated protobuf files
|
||||||
|
"**/*_pb2.py" = ["D"]
|
||||||
|
"src/pipecat/services/__init__.py" = ["D"]
|
||||||
|
|
||||||
[tool.ruff.lint.pydocstyle]
|
[tool.ruff.lint.pydocstyle]
|
||||||
convention = "google"
|
convention = "google"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ python run-release-evals.py -p 07 -a -v
|
|||||||
You can also run evals for a single example (not part of the release set):
|
You can also run evals for a single example (not part of the release set):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python run-eval.py YOUR_EXAMPLE_SCRIPT -a -v
|
python run-eval.py -p "A simple math addition" -a -v YOUR_EXAMPLE_SCRIPT
|
||||||
```
|
```
|
||||||
|
|
||||||
Your script needs to follow any of the foundation examples pattern.
|
Your script needs to follow any of the foundation examples pattern.
|
||||||
|
|||||||
@@ -100,17 +100,18 @@ class EvalRunner:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.wait(
|
tasks = [
|
||||||
[
|
asyncio.create_task(run_example_pipeline(script_path)),
|
||||||
asyncio.create_task(run_example_pipeline(script_path)),
|
asyncio.create_task(run_eval_pipeline(self, example_file, prompt, eval)),
|
||||||
asyncio.create_task(run_eval_pipeline(self, example_file, prompt, eval)),
|
]
|
||||||
],
|
_, pending = await asyncio.wait(tasks, timeout=90)
|
||||||
timeout=90,
|
if pending:
|
||||||
)
|
logger.error(f"ERROR: Eval timeout expired, cancelling pending tasks...")
|
||||||
except asyncio.CancelledError:
|
for task in pending:
|
||||||
pass
|
task.cancel()
|
||||||
|
await asyncio.gather(*pending, return_exceptions=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Unable to run {example_file}: {e}")
|
logger.error(f"ERROR: Unable to run {example_file}: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await asyncio.wait_for(self._queue.get(), timeout=1.0)
|
result = await asyncio.wait_for(self._queue.get(), timeout=1.0)
|
||||||
@@ -134,6 +135,7 @@ class EvalRunner:
|
|||||||
async def save_audio(self, name: str, audio: bytes, sample_rate: int, num_channels: int):
|
async def save_audio(self, name: str, audio: bytes, sample_rate: int, num_channels: int):
|
||||||
if len(audio) > 0:
|
if len(audio) > 0:
|
||||||
filename = self._recording_file_name(name)
|
filename = self._recording_file_name(name)
|
||||||
|
logger.debug(f"Saving {name} audio to {filename}")
|
||||||
with io.BytesIO() as buffer:
|
with io.BytesIO() as buffer:
|
||||||
with wave.open(buffer, "wb") as wf:
|
with wave.open(buffer, "wb") as wf:
|
||||||
wf.setsampwidth(2)
|
wf.setsampwidth(2)
|
||||||
@@ -142,7 +144,6 @@ class EvalRunner:
|
|||||||
wf.writeframes(audio)
|
wf.writeframes(audio)
|
||||||
async with aiofiles.open(filename, "wb") as file:
|
async with aiofiles.open(filename, "wb") as file:
|
||||||
await file.write(buffer.getvalue())
|
await file.write(buffer.getvalue())
|
||||||
logger.debug(f"Saving {name} audio to {filename}")
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"There's no audio to save for {name}")
|
logger.warning(f"There's no audio to save for {name}")
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ TESTS_07 = [
|
|||||||
# 07 series
|
# 07 series
|
||||||
("07-interruptible.py", PROMPT_SIMPLE_MATH, None),
|
("07-interruptible.py", PROMPT_SIMPLE_MATH, None),
|
||||||
("07-interruptible-cartesia-http.py", PROMPT_SIMPLE_MATH, None),
|
("07-interruptible-cartesia-http.py", PROMPT_SIMPLE_MATH, None),
|
||||||
|
("07a-interruptible-speechmatics.py", PROMPT_SIMPLE_MATH, None),
|
||||||
("07b-interruptible-langchain.py", PROMPT_SIMPLE_MATH, None),
|
("07b-interruptible-langchain.py", PROMPT_SIMPLE_MATH, None),
|
||||||
("07c-interruptible-deepgram.py", PROMPT_SIMPLE_MATH, None),
|
("07c-interruptible-deepgram.py", PROMPT_SIMPLE_MATH, None),
|
||||||
("07d-interruptible-elevenlabs.py", PROMPT_SIMPLE_MATH, None),
|
("07d-interruptible-elevenlabs.py", PROMPT_SIMPLE_MATH, None),
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ ruff format src
|
|||||||
ruff format examples
|
ruff format examples
|
||||||
ruff format tests
|
ruff format tests
|
||||||
ruff format scripts
|
ruff format scripts
|
||||||
ruff check --select I --fix
|
ruff check --select I,D --fix
|
||||||
@@ -1,3 +1,27 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
NO_COLOR=1 ruff format --diff
|
# Color codes for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo "🔍 Running pre-commit checks..."
|
||||||
|
|
||||||
|
# Change to project root (one level up from scripts/)
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Format check
|
||||||
|
echo "📝 Checking code formatting..."
|
||||||
|
if ! NO_COLOR=1 ruff format --diff --check; then
|
||||||
|
echo -e "${RED}❌ Code formatting issues found. Run 'ruff format' to fix.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lint check
|
||||||
|
echo "🔍 Running linter..."
|
||||||
|
if ! ruff check; then
|
||||||
|
echo -e "${RED}❌ Linting issues found.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ All pre-commit checks passed!${NC}"
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Base adapter for LLM provider integration.
|
||||||
|
|
||||||
|
This module provides the abstract base class for implementing LLM provider-specific
|
||||||
|
adapters that handle tool format conversion and standardization.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, List, Union, cast
|
from typing import Any, List, Union, cast
|
||||||
|
|
||||||
@@ -7,12 +19,35 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class BaseLLMAdapter(ABC):
|
class BaseLLMAdapter(ABC):
|
||||||
|
"""Abstract base class for LLM provider adapters.
|
||||||
|
|
||||||
|
Provides a standard interface for converting between Pipecat's standardized
|
||||||
|
tool schemas and provider-specific tool formats. Subclasses must implement
|
||||||
|
provider-specific conversion logic.
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Any]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Any]:
|
||||||
"""Converts tools to the provider's format."""
|
"""Convert tools schema to the provider's specific format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tools_schema: The standardized tools schema to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tools in the provider's expected format.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def from_standard_tools(self, tools: Any) -> List[Any]:
|
def from_standard_tools(self, tools: Any) -> List[Any]:
|
||||||
|
"""Convert tools from standard format to provider format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tools: Tools in standard format or provider-specific format.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tools converted to provider format, or original tools
|
||||||
|
if not in standard format.
|
||||||
|
"""
|
||||||
if isinstance(tools, ToolsSchema):
|
if isinstance(tools, ToolsSchema):
|
||||||
logger.debug(f"Retrieving the tools using the adapter: {type(self)}")
|
logger.debug(f"Retrieving the tools using the adapter: {type(self)}")
|
||||||
return self.to_provider_tools_format(tools)
|
return self.to_provider_tools_format(tools)
|
||||||
|
|||||||
296
src/pipecat/adapters/schemas/direct_function.py
Normal file
296
src/pipecat/adapters/schemas/direct_function.py
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Direct function wrapper utilities for LLM function calling.
|
||||||
|
|
||||||
|
This module provides utilities for wrapping "direct" functions that handle LLM
|
||||||
|
function calls. Direct functions have their metadata automatically extracted
|
||||||
|
from function signatures and docstrings, allowing them to be used without
|
||||||
|
accompanying configurations (as FunctionSchemas or in provider-specific
|
||||||
|
formats).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import types
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
Protocol,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
|
get_type_hints,
|
||||||
|
)
|
||||||
|
|
||||||
|
import docstring_parser
|
||||||
|
|
||||||
|
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pipecat.services.llm_service import FunctionCallParams
|
||||||
|
|
||||||
|
|
||||||
|
class DirectFunction(Protocol):
|
||||||
|
"""Protocol for a "direct" function that handles LLM function calls.
|
||||||
|
|
||||||
|
"Direct" functions' metadata is automatically extracted from their function signature and
|
||||||
|
docstrings, allowing them to be used without accompanying function configurations (as
|
||||||
|
`FunctionSchema`s or in provider-specific formats).
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def __call__(self, params: "FunctionCallParams", **kwargs: Any) -> None:
|
||||||
|
"""Execute the direct function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: Function call parameters from the LLM service.
|
||||||
|
**kwargs: Additional keyword arguments passed to the function.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDirectFunctionWrapper:
|
||||||
|
"""Base class for a wrapper around a DirectFunction.
|
||||||
|
|
||||||
|
Provides functionality to:
|
||||||
|
|
||||||
|
- extract metadata from the function signature and docstring
|
||||||
|
- use that metadata to generate a corresponding FunctionSchema
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, function: Callable):
|
||||||
|
"""Initialize the direct function wrapper.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The function to wrap and extract metadata from.
|
||||||
|
"""
|
||||||
|
self.__class__.validate_function(function)
|
||||||
|
self.function = function
|
||||||
|
self._initialize_metadata()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def special_first_param_name(cls) -> str:
|
||||||
|
"""Get the name of the special first function parameter.
|
||||||
|
|
||||||
|
The special first parameter is ignored by metadata extraction as it's
|
||||||
|
not relevant to the LLM (e.g., 'params' for FunctionCallParams).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The name of the special first parameter.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Subclasses must define the special first parameter name.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_function(cls, function: Callable) -> None:
|
||||||
|
"""Validate that the function meets direct function requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The function to validate.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If function doesn't meet requirements (not async, missing
|
||||||
|
parameters, incorrect first parameter name).
|
||||||
|
"""
|
||||||
|
if not inspect.iscoroutinefunction(function):
|
||||||
|
raise Exception(f"Direct function {function.__name__} must be async")
|
||||||
|
params = list(inspect.signature(function).parameters.items())
|
||||||
|
special_first_param_name = cls.special_first_param_name()
|
||||||
|
if len(params) == 0:
|
||||||
|
raise Exception(
|
||||||
|
f"Direct function {function.__name__} must have at least one parameter ({special_first_param_name})"
|
||||||
|
)
|
||||||
|
first_param_name = params[0][0]
|
||||||
|
if first_param_name != special_first_param_name:
|
||||||
|
raise Exception(
|
||||||
|
f"Direct function {function.__name__} first parameter must be named '{special_first_param_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_function_schema(self) -> FunctionSchema:
|
||||||
|
"""Convert the wrapped function to a FunctionSchema.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A FunctionSchema instance with extracted metadata.
|
||||||
|
"""
|
||||||
|
return FunctionSchema(
|
||||||
|
name=self.name,
|
||||||
|
description=self.description,
|
||||||
|
properties=self.properties,
|
||||||
|
required=self.required,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _initialize_metadata(self):
|
||||||
|
"""Initialize metadata from function signature and docstring."""
|
||||||
|
# Get function name
|
||||||
|
self.name = self.function.__name__
|
||||||
|
|
||||||
|
# Parse docstring for description and parameters
|
||||||
|
docstring = docstring_parser.parse(inspect.getdoc(self.function))
|
||||||
|
|
||||||
|
# Get function description
|
||||||
|
self.description = (docstring.description or "").strip()
|
||||||
|
|
||||||
|
# Get function parameters as JSON schemas, and the list of required parameters
|
||||||
|
self.properties, self.required = self._get_parameters_as_jsonschema(
|
||||||
|
self.function, docstring.params
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: maybe to better support things like enums, check if each type is a pydantic type and use its convert-to-jsonschema function
|
||||||
|
def _get_parameters_as_jsonschema(
|
||||||
|
self, func: Callable, docstring_params: List[docstring_parser.DocstringParam]
|
||||||
|
) -> Tuple[Dict[str, Any], List[str]]:
|
||||||
|
"""Get function parameters as a dictionary of JSON schemas and a list of required parameters.
|
||||||
|
|
||||||
|
Ignore the first parameter, as it's expected to be the "special" one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: Function to get parameters from.
|
||||||
|
docstring_params: List of parameters extracted from the function's docstring.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple containing:
|
||||||
|
|
||||||
|
- A dictionary mapping each function parameter to its JSON schema
|
||||||
|
- A list of required parameter names
|
||||||
|
"""
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
hints = get_type_hints(func)
|
||||||
|
properties = {}
|
||||||
|
required = []
|
||||||
|
|
||||||
|
for name, param in sig.parameters.items():
|
||||||
|
# Ignore 'self' parameter
|
||||||
|
if name == "self":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ignore the first parameter, which is expected to be the "special" one
|
||||||
|
# (We have already validated that this is the case in validate_function())
|
||||||
|
is_first_param = name == next(iter(sig.parameters))
|
||||||
|
if is_first_param:
|
||||||
|
continue
|
||||||
|
|
||||||
|
type_hint = hints.get(name)
|
||||||
|
|
||||||
|
# Convert type hint to JSON schema
|
||||||
|
properties[name] = self._typehint_to_jsonschema(type_hint)
|
||||||
|
|
||||||
|
# Add whether the parameter is required
|
||||||
|
# If the parameter has no default value, it's required
|
||||||
|
if param.default is inspect.Parameter.empty:
|
||||||
|
required.append(name)
|
||||||
|
|
||||||
|
# Add parameter description from docstring
|
||||||
|
for doc_param in docstring_params:
|
||||||
|
if doc_param.arg_name == name:
|
||||||
|
properties[name]["description"] = doc_param.description or ""
|
||||||
|
|
||||||
|
return properties, required
|
||||||
|
|
||||||
|
def _typehint_to_jsonschema(self, type_hint: Any) -> Dict[str, Any]:
|
||||||
|
"""Convert a Python type hint to a JSON Schema.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_hint: A Python type hint
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary representing the JSON Schema
|
||||||
|
"""
|
||||||
|
if type_hint is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Handle basic types
|
||||||
|
if type_hint is type(None):
|
||||||
|
return {"type": "null"}
|
||||||
|
if type_hint is str:
|
||||||
|
return {"type": "string"}
|
||||||
|
elif type_hint is int:
|
||||||
|
return {"type": "integer"}
|
||||||
|
elif type_hint is float:
|
||||||
|
return {"type": "number"}
|
||||||
|
elif type_hint is bool:
|
||||||
|
return {"type": "boolean"}
|
||||||
|
elif type_hint is dict or type_hint is Dict:
|
||||||
|
return {"type": "object"}
|
||||||
|
elif type_hint is list or type_hint is List:
|
||||||
|
return {"type": "array"}
|
||||||
|
|
||||||
|
# Get origin and arguments for complex types
|
||||||
|
origin = get_origin(type_hint)
|
||||||
|
args = get_args(type_hint)
|
||||||
|
|
||||||
|
# Handle Optional/Union types
|
||||||
|
if origin is Union or origin is types.UnionType:
|
||||||
|
return {"anyOf": [self._typehint_to_jsonschema(arg) for arg in args]}
|
||||||
|
|
||||||
|
# Handle List, Tuple, Set with specific item types
|
||||||
|
if origin in (list, List, tuple, Tuple, set, Set) and args:
|
||||||
|
return {"type": "array", "items": self._typehint_to_jsonschema(args[0])}
|
||||||
|
|
||||||
|
# Handle Dict with specific key/value types
|
||||||
|
if origin in (dict, Dict) and len(args) == 2:
|
||||||
|
# For JSON Schema, keys must be strings
|
||||||
|
return {"type": "object", "additionalProperties": self._typehint_to_jsonschema(args[1])}
|
||||||
|
|
||||||
|
# Handle TypedDict
|
||||||
|
if hasattr(type_hint, "__annotations__"):
|
||||||
|
properties = {}
|
||||||
|
required = []
|
||||||
|
|
||||||
|
# NOTE: this does not yet support some fields being required and others not, which could happen when:
|
||||||
|
# - the base class is a TypedDict with required fields (total=True or not specified) and the derived class has optional fields (total=False)
|
||||||
|
# - Python 3.11+ NotRequired is used
|
||||||
|
all_fields_required = getattr(type_hint, "__total__", True)
|
||||||
|
|
||||||
|
for field_name, field_type in get_type_hints(type_hint).items():
|
||||||
|
properties[field_name] = self._typehint_to_jsonschema(field_type)
|
||||||
|
if all_fields_required:
|
||||||
|
required.append(field_name)
|
||||||
|
|
||||||
|
schema = {"type": "object", "properties": properties}
|
||||||
|
|
||||||
|
if required:
|
||||||
|
schema["required"] = required
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
# Default to any type if we can't determine the specific schema
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class DirectFunctionWrapper(BaseDirectFunctionWrapper):
|
||||||
|
"""Wrapper around a DirectFunction for LLM function calling.
|
||||||
|
|
||||||
|
This class:
|
||||||
|
|
||||||
|
- Extracts metadata from the function signature and docstring
|
||||||
|
- Generates a corresponding FunctionSchema
|
||||||
|
- Helps with function invocation
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def special_first_param_name(cls) -> str:
|
||||||
|
"""Get the special first parameter name for direct functions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The string "params" which is expected as the first parameter.
|
||||||
|
"""
|
||||||
|
return "params"
|
||||||
|
|
||||||
|
async def invoke(self, args: Mapping[str, Any], params: "FunctionCallParams"):
|
||||||
|
"""Invoke the wrapped function with the provided arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Arguments to pass to the function.
|
||||||
|
params: Function call parameters from the LLM service.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of the function call.
|
||||||
|
"""
|
||||||
|
return await self.function(params=params, **args)
|
||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Function schema utilities for AI tool definitions.
|
||||||
|
|
||||||
|
This module provides standardized function schema representation for defining
|
||||||
|
tools and functions used with AI models, ensuring consistent formatting
|
||||||
|
across different AI service providers.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
@@ -13,17 +20,19 @@ class FunctionSchema:
|
|||||||
Provides a structured way to define function tools used with AI models like OpenAI.
|
Provides a structured way to define function tools used with AI models like OpenAI.
|
||||||
This schema defines the function's name, description, parameter properties, and
|
This schema defines the function's name, description, parameter properties, and
|
||||||
required parameters, following specifications required by AI service providers.
|
required parameters, following specifications required by AI service providers.
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Name of the function to be called.
|
|
||||||
description: Description of what the function does.
|
|
||||||
properties: Dictionary defining parameter types, descriptions, and constraints.
|
|
||||||
required: List of property names that are required parameters.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, description: str, properties: Dict[str, Any], required: List[str]
|
self, name: str, description: str, properties: Dict[str, Any], required: List[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize the function schema.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name of the function to be called.
|
||||||
|
description: Description of what the function does.
|
||||||
|
properties: Dictionary defining parameter types, descriptions, and constraints.
|
||||||
|
required: List of property names that are required parameters.
|
||||||
|
"""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._description = description
|
self._description = description
|
||||||
self._properties = properties
|
self._properties = properties
|
||||||
|
|||||||
@@ -4,40 +4,88 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Tools schema definitions for function calling adapters.
|
||||||
|
|
||||||
|
This module provides schemas for managing both standardized function tools
|
||||||
|
and custom adapter-specific tools in the Pipecat framework.
|
||||||
|
"""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pipecat.adapters.schemas.direct_function import DirectFunction, DirectFunctionWrapper
|
||||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||||
|
|
||||||
|
|
||||||
class AdapterType(Enum):
|
class AdapterType(Enum):
|
||||||
|
"""Supported adapter types for custom tools.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
GEMINI: Google Gemini adapter - currently the only service supporting custom tools.
|
||||||
|
"""
|
||||||
|
|
||||||
GEMINI = "gemini" # that is the only service where we are able to add custom tools for now
|
GEMINI = "gemini" # that is the only service where we are able to add custom tools for now
|
||||||
|
|
||||||
|
|
||||||
class ToolsSchema:
|
class ToolsSchema:
|
||||||
|
"""Schema for managing both standard and custom function calling tools.
|
||||||
|
|
||||||
|
This class provides a unified interface for handling standardized function
|
||||||
|
schemas alongside custom tools that may not follow the standard format,
|
||||||
|
such as adapter-specific search tools.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
standard_tools: List[FunctionSchema],
|
standard_tools: List[FunctionSchema | DirectFunction],
|
||||||
custom_tools: Optional[Dict[AdapterType, List[Dict[str, Any]]]] = None,
|
custom_tools: Optional[Dict[AdapterType, List[Dict[str, Any]]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Initialize the tools schema.
|
||||||
A schema for tools that includes both standardized function schemas
|
|
||||||
and custom tools that do not follow the FunctionSchema format.
|
|
||||||
|
|
||||||
:param standard_tools: List of tools following FunctionSchema.
|
Args:
|
||||||
:param custom_tools: List of tools in a custom format (e.g., search_tool).
|
standard_tools: List of tools following the standardized FunctionSchema format.
|
||||||
|
custom_tools: Dictionary mapping adapter types to their custom tool definitions.
|
||||||
|
These tools may not follow the FunctionSchema format (e.g., search_tool).
|
||||||
"""
|
"""
|
||||||
self._standard_tools = standard_tools
|
|
||||||
|
def _map_standard_tools(tools):
|
||||||
|
schemas = []
|
||||||
|
for tool in tools:
|
||||||
|
if isinstance(tool, FunctionSchema):
|
||||||
|
schemas.append(tool)
|
||||||
|
elif callable(tool):
|
||||||
|
wrapper = DirectFunctionWrapper(tool)
|
||||||
|
schemas.append(wrapper.to_function_schema())
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unsupported tool type: {type(tool)}")
|
||||||
|
return schemas
|
||||||
|
|
||||||
|
self._standard_tools = _map_standard_tools(standard_tools)
|
||||||
self._custom_tools = custom_tools
|
self._custom_tools = custom_tools
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def standard_tools(self) -> List[FunctionSchema]:
|
def standard_tools(self) -> List[FunctionSchema]:
|
||||||
|
"""Get the list of standard function schema tools.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tools following the FunctionSchema format.
|
||||||
|
"""
|
||||||
return self._standard_tools
|
return self._standard_tools
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def custom_tools(self) -> Dict[AdapterType, List[Dict[str, Any]]]:
|
def custom_tools(self) -> Dict[AdapterType, List[Dict[str, Any]]]:
|
||||||
|
"""Get the custom tools dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping adapter types to their custom tool definitions.
|
||||||
|
"""
|
||||||
return self._custom_tools
|
return self._custom_tools
|
||||||
|
|
||||||
@custom_tools.setter
|
@custom_tools.setter
|
||||||
def custom_tools(self, value: Dict[AdapterType, List[Dict[str, Any]]]) -> None:
|
def custom_tools(self, value: Dict[AdapterType, List[Dict[str, Any]]]) -> None:
|
||||||
|
"""Set the custom tools dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Dictionary mapping adapter types to their custom tool definitions.
|
||||||
|
"""
|
||||||
self._custom_tools = value
|
self._custom_tools = value
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Anthropic LLM adapter for Pipecat."""
|
||||||
|
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
||||||
@@ -12,8 +14,22 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class AnthropicLLMAdapter(BaseLLMAdapter):
|
class AnthropicLLMAdapter(BaseLLMAdapter):
|
||||||
|
"""Adapter for converting tool schemas to Anthropic's function-calling format.
|
||||||
|
|
||||||
|
This adapter handles the conversion of Pipecat's standard function schemas
|
||||||
|
to the specific format required by Anthropic's Claude models for function calling.
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_anthropic_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
def _to_anthropic_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
||||||
|
"""Convert a single function schema to Anthropic's format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The function schema to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing the function definition in Anthropic's format.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"name": function.name,
|
"name": function.name,
|
||||||
"description": function.description,
|
"description": function.description,
|
||||||
@@ -25,10 +41,13 @@ class AnthropicLLMAdapter(BaseLLMAdapter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
||||||
"""Converts function schemas to Anthropic's function-calling format.
|
"""Convert function schemas to Anthropic's function-calling format.
|
||||||
|
|
||||||
:return: Anthropic formatted function call definition.
|
Args:
|
||||||
|
tools_schema: The tools schema containing functions to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of function definitions formatted for Anthropic's API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
return [self._to_anthropic_function_format(func) for func in functions_schema]
|
return [self._to_anthropic_function_format(func) for func in functions_schema]
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""AWS Nova Sonic LLM adapter for Pipecat."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
@@ -12,8 +15,22 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class AWSNovaSonicLLMAdapter(BaseLLMAdapter):
|
class AWSNovaSonicLLMAdapter(BaseLLMAdapter):
|
||||||
|
"""Adapter for AWS Nova Sonic language models.
|
||||||
|
|
||||||
|
Converts Pipecat's standard function schemas into AWS Nova Sonic's
|
||||||
|
specific function-calling format, enabling tool use with Nova Sonic models.
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_aws_nova_sonic_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
def _to_aws_nova_sonic_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
||||||
|
"""Convert a function schema to AWS Nova Sonic format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The function schema to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary in AWS Nova Sonic function format with toolSpec structure.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"toolSpec": {
|
"toolSpec": {
|
||||||
"name": function.name,
|
"name": function.name,
|
||||||
@@ -31,10 +48,13 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
||||||
"""Converts function schemas to AWS Nova Sonic function-calling format.
|
"""Convert tools schema to AWS Nova Sonic function-calling format.
|
||||||
|
|
||||||
:return: AWS Nova Sonic formatted function call definition.
|
Args:
|
||||||
|
tools_schema: The tools schema containing function definitions to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries in AWS Nova Sonic function format.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
return [self._to_aws_nova_sonic_function_format(func) for func in functions_schema]
|
return [self._to_aws_nova_sonic_function_format(func) for func in functions_schema]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""AWS Bedrock LLM adapter for Pipecat."""
|
||||||
|
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
||||||
@@ -12,8 +14,22 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class AWSBedrockLLMAdapter(BaseLLMAdapter):
|
class AWSBedrockLLMAdapter(BaseLLMAdapter):
|
||||||
|
"""Adapter for AWS Bedrock LLM integration with Pipecat.
|
||||||
|
|
||||||
|
Provides conversion utilities for transforming Pipecat function schemas
|
||||||
|
into AWS Bedrock's expected tool format for function calling capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_bedrock_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
def _to_bedrock_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
||||||
|
"""Convert a function schema to Bedrock's tool format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The function schema to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary formatted for Bedrock's tool specification.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"toolSpec": {
|
"toolSpec": {
|
||||||
"name": function.name,
|
"name": function.name,
|
||||||
@@ -29,10 +45,13 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
||||||
"""Converts function schemas to Bedrock's function-calling format.
|
"""Convert function schemas to Bedrock's function-calling format.
|
||||||
|
|
||||||
:return: Bedrock formatted function call definition.
|
Args:
|
||||||
|
tools_schema: The tools schema containing functions to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Bedrock formatted function call definitions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
return [self._to_bedrock_function_format(func) for func in functions_schema]
|
return [self._to_bedrock_function_format(func) for func in functions_schema]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Gemini LLM adapter for Pipecat."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
||||||
@@ -11,12 +13,23 @@ from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class GeminiLLMAdapter(BaseLLMAdapter):
|
class GeminiLLMAdapter(BaseLLMAdapter):
|
||||||
|
"""LLM adapter for Google's Gemini service.
|
||||||
|
|
||||||
|
Provides tool schema conversion functionality to transform standard tool
|
||||||
|
definitions into Gemini's specific function-calling format for use with
|
||||||
|
Gemini LLM models.
|
||||||
|
"""
|
||||||
|
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
||||||
"""Converts function schemas to Gemini's function-calling format.
|
"""Convert tool schemas to Gemini's function-calling format.
|
||||||
|
|
||||||
:return: Gemini formatted function call definition.
|
Args:
|
||||||
|
tools_schema: The tools schema containing standard and custom tool definitions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tool definitions formatted for Gemini's function-calling API.
|
||||||
|
Includes both converted standard tools and any custom Gemini-specific tools.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
formatted_standard_tools = [
|
formatted_standard_tools = [
|
||||||
{"function_declarations": [func.to_default_dict() for func in functions_schema]}
|
{"function_declarations": [func.to_default_dict() for func in functions_schema]}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""OpenAI LLM adapter for Pipecat."""
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from openai.types.chat import ChatCompletionToolParam
|
from openai.types.chat import ChatCompletionToolParam
|
||||||
@@ -12,10 +15,22 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class OpenAILLMAdapter(BaseLLMAdapter):
|
class OpenAILLMAdapter(BaseLLMAdapter):
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[ChatCompletionToolParam]:
|
"""Adapter for converting tool schemas to OpenAI's format.
|
||||||
"""Converts function schemas to OpenAI's function-calling format.
|
|
||||||
|
|
||||||
:return: OpenAI formatted function call definition.
|
Provides conversion utilities for transforming Pipecat's standard tool
|
||||||
|
schemas into the format expected by OpenAI's ChatCompletion API for
|
||||||
|
function calling capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[ChatCompletionToolParam]:
|
||||||
|
"""Convert function schemas to OpenAI's function-calling format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tools_schema: The Pipecat tools schema to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of OpenAI formatted function call definitions ready for use
|
||||||
|
with ChatCompletion API.
|
||||||
"""
|
"""
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""OpenAI Realtime LLM adapter for Pipecat."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
||||||
@@ -11,8 +14,22 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||||||
|
|
||||||
|
|
||||||
class OpenAIRealtimeLLMAdapter(BaseLLMAdapter):
|
class OpenAIRealtimeLLMAdapter(BaseLLMAdapter):
|
||||||
|
"""LLM adapter for OpenAI Realtime API function calling.
|
||||||
|
|
||||||
|
Converts Pipecat's tool schemas into the specific format required by
|
||||||
|
OpenAI's Realtime API for function calling capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_openai_realtime_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
def _to_openai_realtime_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
||||||
|
"""Convert a function schema to OpenAI Realtime format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The function schema to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary in OpenAI Realtime function format.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"name": function.name,
|
"name": function.name,
|
||||||
@@ -25,10 +42,13 @@ class OpenAIRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Dict[str, Any]]:
|
||||||
"""Converts function schemas to Openai Realtime function-calling format.
|
"""Convert tool schemas to OpenAI Realtime function-calling format.
|
||||||
|
|
||||||
:return: Openai Realtime formatted function call definition.
|
Args:
|
||||||
|
tools_schema: The tools schema containing functions to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of function definitions in OpenAI Realtime format.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
return [self._to_openai_realtime_function_format(func) for func in functions_schema]
|
return [self._to_openai_realtime_function_format(func) for func in functions_schema]
|
||||||
|
|||||||
@@ -4,44 +4,68 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base audio filter interface for input transport audio processing.
|
||||||
|
|
||||||
|
This module provides the abstract base class for implementing audio filters
|
||||||
|
that process audio data before VAD and downstream processing in input transports.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from pipecat.frames.frames import FilterControlFrame
|
from pipecat.frames.frames import FilterControlFrame
|
||||||
|
|
||||||
|
|
||||||
class BaseAudioFilter(ABC):
|
class BaseAudioFilter(ABC):
|
||||||
"""This is a base class for input transport audio filters. If an audio
|
"""Base class for input transport audio filters.
|
||||||
|
|
||||||
|
This is a base class for input transport audio filters. If an audio
|
||||||
filter is provided to the input transport it will be used to process audio
|
filter is provided to the input transport it will be used to process audio
|
||||||
before VAD and before pushing it downstream. There are control frames to
|
before VAD and before pushing it downstream. There are control frames to
|
||||||
update filter settings or to enable or disable the filter at runtime.
|
update filter settings or to enable or disable the filter at runtime.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def start(self, sample_rate: int):
|
async def start(self, sample_rate: int):
|
||||||
"""This will be called from the input transport when the transport is
|
"""Initialize the filter when the input transport starts.
|
||||||
|
|
||||||
|
This will be called from the input transport when the transport is
|
||||||
started. It can be used to initialize the filter. The input transport
|
started. It can be used to initialize the filter. The input transport
|
||||||
sample rate is provided so the filter can adjust to that sample rate.
|
sample rate is provided so the filter can adjust to that sample rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: The sample rate of the input transport in Hz.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""This will be called from the input transport when the transport is
|
"""Clean up the filter when the input transport stops.
|
||||||
stopping.
|
|
||||||
|
|
||||||
|
This will be called from the input transport when the transport is
|
||||||
|
stopping.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def process_frame(self, frame: FilterControlFrame):
|
async def process_frame(self, frame: FilterControlFrame):
|
||||||
"""This will be called when the input transport receives a
|
"""Process control frames for runtime filter configuration.
|
||||||
|
|
||||||
|
This will be called when the input transport receives a
|
||||||
FilterControlFrame.
|
FilterControlFrame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The control frame containing filter commands or settings.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def filter(self, audio: bytes) -> bytes:
|
async def filter(self, audio: bytes) -> bytes:
|
||||||
|
"""Apply the audio filter to the provided audio data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio data as bytes to be filtered.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered audio data as bytes.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Koala noise suppression audio filter for Pipecat.
|
||||||
|
|
||||||
|
This module provides an audio filter implementation using PicoVoice's Koala
|
||||||
|
Noise Suppression engine to reduce background noise in audio streams.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -21,12 +27,19 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class KoalaFilter(BaseAudioFilter):
|
class KoalaFilter(BaseAudioFilter):
|
||||||
"""This is an audio filter that uses Koala Noise Suppression (from
|
"""Audio filter using Koala Noise Suppression from PicoVoice.
|
||||||
PicoVoice).
|
|
||||||
|
|
||||||
|
Provides real-time noise suppression for audio streams using PicoVoice's
|
||||||
|
Koala engine. The filter buffers audio data to match Koala's required
|
||||||
|
frame length and processes it in chunks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, access_key: str) -> None:
|
def __init__(self, *, access_key: str) -> None:
|
||||||
|
"""Initialize the Koala noise suppression filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
access_key: PicoVoice access key for Koala engine authentication.
|
||||||
|
"""
|
||||||
self._access_key = access_key
|
self._access_key = access_key
|
||||||
|
|
||||||
self._filtering = True
|
self._filtering = True
|
||||||
@@ -36,6 +49,11 @@ class KoalaFilter(BaseAudioFilter):
|
|||||||
self._audio_buffer = bytearray()
|
self._audio_buffer = bytearray()
|
||||||
|
|
||||||
async def start(self, sample_rate: int):
|
async def start(self, sample_rate: int):
|
||||||
|
"""Initialize the filter with the transport's sample rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: The sample rate of the input transport in Hz.
|
||||||
|
"""
|
||||||
self._sample_rate = sample_rate
|
self._sample_rate = sample_rate
|
||||||
if self._sample_rate != self._koala.sample_rate:
|
if self._sample_rate != self._koala.sample_rate:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -44,13 +62,30 @@ class KoalaFilter(BaseAudioFilter):
|
|||||||
self._koala_ready = False
|
self._koala_ready = False
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
|
"""Clean up the Koala engine when stopping."""
|
||||||
self._koala.reset()
|
self._koala.reset()
|
||||||
|
|
||||||
async def process_frame(self, frame: FilterControlFrame):
|
async def process_frame(self, frame: FilterControlFrame):
|
||||||
|
"""Process control frames to enable/disable filtering.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The control frame containing filter commands.
|
||||||
|
"""
|
||||||
if isinstance(frame, FilterEnableFrame):
|
if isinstance(frame, FilterEnableFrame):
|
||||||
self._filtering = frame.enable
|
self._filtering = frame.enable
|
||||||
|
|
||||||
async def filter(self, audio: bytes) -> bytes:
|
async def filter(self, audio: bytes) -> bytes:
|
||||||
|
"""Apply Koala noise suppression to audio data.
|
||||||
|
|
||||||
|
Buffers incoming audio and processes it in chunks that match Koala's
|
||||||
|
required frame length. Returns filtered audio data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio data as bytes to be filtered.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Noise-suppressed audio data as bytes.
|
||||||
|
"""
|
||||||
if not self._koala_ready or not self._filtering:
|
if not self._koala_ready or not self._filtering:
|
||||||
return audio
|
return audio
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Krisp noise reduction audio filter for Pipecat.
|
||||||
|
|
||||||
|
This module provides an audio filter implementation using Krisp's noise
|
||||||
|
reduction technology to suppress background noise in audio streams.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -21,14 +27,27 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class KrispProcessorManager:
|
class KrispProcessorManager:
|
||||||
"""
|
"""Singleton manager for KrispAudioProcessor instances.
|
||||||
Ensures that only one KrispAudioProcessor instance exists for the entire program.
|
|
||||||
|
Ensures that only one KrispAudioProcessor instance exists for the entire
|
||||||
|
program.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_krisp_instance = None
|
_krisp_instance = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_processor(cls, sample_rate: int, sample_type: str, channels: int, model_path: str):
|
def get_processor(cls, sample_rate: int, sample_type: str, channels: int, model_path: str):
|
||||||
|
"""Get or create a KrispAudioProcessor instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Audio sample rate in Hz.
|
||||||
|
sample_type: Audio sample type (e.g., "PCM_16").
|
||||||
|
channels: Number of audio channels.
|
||||||
|
model_path: Path to the Krisp model file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Shared KrispAudioProcessor instance.
|
||||||
|
"""
|
||||||
if cls._krisp_instance is None:
|
if cls._krisp_instance is None:
|
||||||
cls._krisp_instance = KrispAudioProcessor(
|
cls._krisp_instance = KrispAudioProcessor(
|
||||||
sample_rate, sample_type, channels, model_path
|
sample_rate, sample_type, channels, model_path
|
||||||
@@ -37,14 +56,26 @@ class KrispProcessorManager:
|
|||||||
|
|
||||||
|
|
||||||
class KrispFilter(BaseAudioFilter):
|
class KrispFilter(BaseAudioFilter):
|
||||||
|
"""Audio filter using Krisp noise reduction technology.
|
||||||
|
|
||||||
|
Provides real-time noise reduction for audio streams using Krisp's
|
||||||
|
proprietary noise suppression algorithms. Requires a Krisp model file
|
||||||
|
for operation.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, sample_type: str = "PCM_16", channels: int = 1, model_path: str = None
|
self, sample_type: str = "PCM_16", channels: int = 1, model_path: str = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initializes the KrispAudioProcessor with customizable audio processing settings.
|
"""Initialize the Krisp noise reduction filter.
|
||||||
|
|
||||||
:param sample_type: The type of audio sample, default is 'PCM_16'.
|
Args:
|
||||||
:param channels: Number of audio channels, default is 1.
|
sample_type: The audio sample format. Defaults to "PCM_16".
|
||||||
:param model_path: Path to the Krisp model; defaults to environment variable KRISP_MODEL_PATH if not provided.
|
channels: Number of audio channels. Defaults to 1.
|
||||||
|
model_path: Path to the Krisp model file. If None, uses KRISP_MODEL_PATH
|
||||||
|
environment variable.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If model_path is not provided and KRISP_MODEL_PATH is not set.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -63,19 +94,41 @@ class KrispFilter(BaseAudioFilter):
|
|||||||
self._krisp_processor = None
|
self._krisp_processor = None
|
||||||
|
|
||||||
async def start(self, sample_rate: int):
|
async def start(self, sample_rate: int):
|
||||||
|
"""Initialize the Krisp processor with the transport's sample rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: The sample rate of the input transport in Hz.
|
||||||
|
"""
|
||||||
self._sample_rate = sample_rate
|
self._sample_rate = sample_rate
|
||||||
self._krisp_processor = KrispProcessorManager.get_processor(
|
self._krisp_processor = KrispProcessorManager.get_processor(
|
||||||
self._sample_rate, self._sample_type, self._channels, self._model_path
|
self._sample_rate, self._sample_type, self._channels, self._model_path
|
||||||
)
|
)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
|
"""Clean up the Krisp processor when stopping."""
|
||||||
self._krisp_processor = None
|
self._krisp_processor = None
|
||||||
|
|
||||||
async def process_frame(self, frame: FilterControlFrame):
|
async def process_frame(self, frame: FilterControlFrame):
|
||||||
|
"""Process control frames to enable/disable filtering.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The control frame containing filter commands.
|
||||||
|
"""
|
||||||
if isinstance(frame, FilterEnableFrame):
|
if isinstance(frame, FilterEnableFrame):
|
||||||
self._filtering = frame.enable
|
self._filtering = frame.enable
|
||||||
|
|
||||||
async def filter(self, audio: bytes) -> bytes:
|
async def filter(self, audio: bytes) -> bytes:
|
||||||
|
"""Apply Krisp noise reduction to audio data.
|
||||||
|
|
||||||
|
Converts audio to float32, applies Krisp noise reduction processing,
|
||||||
|
and returns the filtered audio clipped to int16 range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio data as bytes to be filtered.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Noise-reduced audio data as bytes.
|
||||||
|
"""
|
||||||
if not self._filtering:
|
if not self._filtering:
|
||||||
return audio
|
return audio
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Noisereduce audio filter for Pipecat.
|
||||||
|
|
||||||
|
This module provides an audio filter implementation using the noisereduce
|
||||||
|
library to reduce background noise in audio streams through spectral
|
||||||
|
gating algorithms.
|
||||||
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -21,21 +28,51 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class NoisereduceFilter(BaseAudioFilter):
|
class NoisereduceFilter(BaseAudioFilter):
|
||||||
|
"""Audio filter using the noisereduce library for noise suppression.
|
||||||
|
|
||||||
|
Applies spectral gating noise reduction algorithms to suppress background
|
||||||
|
noise in audio streams. Uses the noisereduce library's default noise
|
||||||
|
reduction parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the noisereduce filter."""
|
||||||
self._filtering = True
|
self._filtering = True
|
||||||
self._sample_rate = 0
|
self._sample_rate = 0
|
||||||
|
|
||||||
async def start(self, sample_rate: int):
|
async def start(self, sample_rate: int):
|
||||||
|
"""Initialize the filter with the transport's sample rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: The sample rate of the input transport in Hz.
|
||||||
|
"""
|
||||||
self._sample_rate = sample_rate
|
self._sample_rate = sample_rate
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
|
"""Clean up the filter when stopping."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def process_frame(self, frame: FilterControlFrame):
|
async def process_frame(self, frame: FilterControlFrame):
|
||||||
|
"""Process control frames to enable/disable filtering.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The control frame containing filter commands.
|
||||||
|
"""
|
||||||
if isinstance(frame, FilterEnableFrame):
|
if isinstance(frame, FilterEnableFrame):
|
||||||
self._filtering = frame.enable
|
self._filtering = frame.enable
|
||||||
|
|
||||||
async def filter(self, audio: bytes) -> bytes:
|
async def filter(self, audio: bytes) -> bytes:
|
||||||
|
"""Apply noise reduction to audio data using spectral gating.
|
||||||
|
|
||||||
|
Converts audio to float32, applies noisereduce processing, and returns
|
||||||
|
the filtered audio clipped to int16 range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio data as bytes to be filtered.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Noise-reduced audio data as bytes.
|
||||||
|
"""
|
||||||
if not self._filtering:
|
if not self._filtering:
|
||||||
return audio
|
return audio
|
||||||
|
|
||||||
|
|||||||
@@ -4,31 +4,51 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base interruption strategy for determining when users can interrupt bot speech."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class BaseInterruptionStrategy(ABC):
|
class BaseInterruptionStrategy(ABC):
|
||||||
"""This is a base class for interruption strategies. Interruption strategies
|
"""Base class for interruption strategies.
|
||||||
|
|
||||||
|
This is a base class for interruption strategies. Interruption strategies
|
||||||
decide when the user can interrupt the bot while the bot is speaking. For
|
decide when the user can interrupt the bot while the bot is speaking. For
|
||||||
example, there could be strategies based on audio volume or strategies based
|
example, there could be strategies based on audio volume or strategies based
|
||||||
on the number of words the user spoke.
|
on the number of words the user spoke.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def append_audio(self, audio: bytes, sample_rate: int):
|
async def append_audio(self, audio: bytes, sample_rate: int):
|
||||||
"""Appends audio to the strategy. Not all strategies handle audio."""
|
"""Append audio data to the strategy for analysis.
|
||||||
|
|
||||||
|
Not all strategies handle audio. Default implementation does nothing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio bytes to append.
|
||||||
|
sample_rate: Sample rate of the audio data in Hz.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def append_text(self, text: str):
|
async def append_text(self, text: str):
|
||||||
"""Appends text to the strategy. Not all strategies handle text."""
|
"""Append text data to the strategy for analysis.
|
||||||
|
|
||||||
|
Not all strategies handle text. Default implementation does nothing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text string to append for analysis.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def should_interrupt(self) -> bool:
|
async def should_interrupt(self) -> bool:
|
||||||
"""This is called when the user stops speaking and it's time to decide
|
"""Determine if the user should interrupt the bot.
|
||||||
|
|
||||||
|
This is called when the user stops speaking and it's time to decide
|
||||||
whether the user should interrupt the bot. The decision will be based on
|
whether the user should interrupt the bot. The decision will be based on
|
||||||
the aggregated audio and/or text.
|
the aggregated audio and/or text.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the user should interrupt the bot, False otherwise.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -4,31 +4,47 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Minimum words interruption strategy for word count-based interruptions."""
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.interruptions.base_interruption_strategy import BaseInterruptionStrategy
|
from pipecat.audio.interruptions.base_interruption_strategy import BaseInterruptionStrategy
|
||||||
|
|
||||||
|
|
||||||
class MinWordsInterruptionStrategy(BaseInterruptionStrategy):
|
class MinWordsInterruptionStrategy(BaseInterruptionStrategy):
|
||||||
"""This is an interruption strategy based on a minimum number of words said
|
"""Interruption strategy based on minimum number of words spoken.
|
||||||
|
|
||||||
|
This is an interruption strategy based on a minimum number of words said
|
||||||
by the user. That is, the strategy will be true if the user has said at
|
by the user. That is, the strategy will be true if the user has said at
|
||||||
least that amount of words.
|
least that amount of words.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, min_words: int):
|
def __init__(self, *, min_words: int):
|
||||||
|
"""Initialize the minimum words interruption strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
min_words: Minimum number of words required to trigger an interruption.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._min_words = min_words
|
self._min_words = min_words
|
||||||
self._text = ""
|
self._text = ""
|
||||||
|
|
||||||
async def append_text(self, text: str):
|
async def append_text(self, text: str):
|
||||||
"""Appends text for later analysis. Not all strategies need to handle
|
"""Append text for word count analysis.
|
||||||
text.
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text string to append to the accumulated text.
|
||||||
|
|
||||||
|
Note: Not all strategies need to handle text.
|
||||||
"""
|
"""
|
||||||
self._text += text
|
self._text += text
|
||||||
|
|
||||||
async def should_interrupt(self) -> bool:
|
async def should_interrupt(self) -> bool:
|
||||||
|
"""Check if the minimum word count has been reached.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the user has spoken at least the minimum number of words.
|
||||||
|
"""
|
||||||
word_count = len(self._text.split())
|
word_count = len(self._text.split())
|
||||||
interrupt = word_count >= self._min_words
|
interrupt = word_count >= self._min_words
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -37,4 +53,5 @@ class MinWordsInterruptionStrategy(BaseInterruptionStrategy):
|
|||||||
return interrupt
|
return interrupt
|
||||||
|
|
||||||
async def reset(self):
|
async def reset(self):
|
||||||
|
"""Reset the accumulated text for the next analysis cycle."""
|
||||||
self._text = ""
|
self._text = ""
|
||||||
|
|||||||
@@ -4,50 +4,73 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base audio mixer for output transport integration.
|
||||||
|
|
||||||
|
Provides the abstract base class for audio mixers that can be integrated with
|
||||||
|
output transports to mix incoming audio with generated audio from the mixer.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from pipecat.frames.frames import MixerControlFrame
|
from pipecat.frames.frames import MixerControlFrame
|
||||||
|
|
||||||
|
|
||||||
class BaseAudioMixer(ABC):
|
class BaseAudioMixer(ABC):
|
||||||
"""This is a base class for output transport audio mixers. If an audio mixer
|
"""Base class for output transport audio mixers.
|
||||||
|
|
||||||
|
This is a base class for output transport audio mixers. If an audio mixer
|
||||||
is provided to the output transport it will be used to mix the audio frames
|
is provided to the output transport it will be used to mix the audio frames
|
||||||
coming into to the transport with the audio generated from the mixer. There
|
coming into to the transport with the audio generated from the mixer. There
|
||||||
are control frames to update mixer settings or to enable or disable the
|
are control frames to update mixer settings or to enable or disable the
|
||||||
mixer at runtime.
|
mixer at runtime.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def start(self, sample_rate: int):
|
async def start(self, sample_rate: int):
|
||||||
"""This will be called from the output transport when the transport is
|
"""Initialize the mixer when the output transport starts.
|
||||||
|
|
||||||
|
This will be called from the output transport when the transport is
|
||||||
started. It can be used to initialize the mixer. The output transport
|
started. It can be used to initialize the mixer. The output transport
|
||||||
sample rate is provided so the mixer can adjust to that sample rate.
|
sample rate is provided so the mixer can adjust to that sample rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: The sample rate of the output transport in Hz.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""This will be called from the output transport when the transport is
|
"""Clean up the mixer when the output transport stops.
|
||||||
stopping.
|
|
||||||
|
|
||||||
|
This will be called from the output transport when the transport is
|
||||||
|
stopping.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def process_frame(self, frame: MixerControlFrame):
|
async def process_frame(self, frame: MixerControlFrame):
|
||||||
"""This will be called when the output transport receives a
|
"""Process mixer control frames from the transport.
|
||||||
|
|
||||||
|
This will be called when the output transport receives a
|
||||||
MixerControlFrame.
|
MixerControlFrame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The mixer control frame to process.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def mix(self, audio: bytes) -> bytes:
|
async def mix(self, audio: bytes) -> bytes:
|
||||||
"""This is called with the audio that is about to be sent from the
|
"""Mix transport audio with mixer-generated audio.
|
||||||
|
|
||||||
|
This is called with the audio that is about to be sent from the
|
||||||
output transport and that should be mixed with the mixer audio if the
|
output transport and that should be mixed with the mixer audio if the
|
||||||
mixer is enabled.
|
mixer is enabled.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio bytes from the transport to mix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mixed audio bytes combining transport and mixer audio.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Soundfile-based audio mixer for file playback integration.
|
||||||
|
|
||||||
|
Provides an audio mixer that combines incoming audio with audio loaded from
|
||||||
|
files using the soundfile library. Supports multiple audio formats and
|
||||||
|
runtime configuration changes.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any, Dict, Mapping
|
from typing import Any, Dict, Mapping
|
||||||
|
|
||||||
@@ -24,7 +31,9 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class SoundfileMixer(BaseAudioMixer):
|
class SoundfileMixer(BaseAudioMixer):
|
||||||
"""This is an audio mixer that mixes incoming audio with audio from a
|
"""Audio mixer that combines incoming audio with file-based audio.
|
||||||
|
|
||||||
|
This is an audio mixer that mixes incoming audio with audio from a
|
||||||
file. It uses the soundfile library to load files so it supports multiple
|
file. It uses the soundfile library to load files so it supports multiple
|
||||||
formats. The audio files need to only have one channel (mono) and it needs
|
formats. The audio files need to only have one channel (mono) and it needs
|
||||||
to match the sample rate of the output transport.
|
to match the sample rate of the output transport.
|
||||||
@@ -33,7 +42,6 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
`MixerUpdateSettingsFrame` has the following settings available: `sound`
|
`MixerUpdateSettingsFrame` has the following settings available: `sound`
|
||||||
(str) and `volume` (float) to be able to update to a different sound file or
|
(str) and `volume` (float) to be able to update to a different sound file or
|
||||||
to change the volume at runtime.
|
to change the volume at runtime.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -46,6 +54,16 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
loop: bool = True,
|
loop: bool = True,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
"""Initialize the soundfile mixer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sound_files: Mapping of sound names to file paths for loading.
|
||||||
|
default_sound: Name of the default sound to play initially.
|
||||||
|
volume: Mixing volume level (0.0 to 1.0). Defaults to 0.4.
|
||||||
|
mixing: Whether mixing is initially enabled. Defaults to True.
|
||||||
|
loop: Whether to loop audio files when they end. Defaults to True.
|
||||||
|
**kwargs: Additional arguments passed to parent class.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._sound_files = sound_files
|
self._sound_files = sound_files
|
||||||
self._volume = volume
|
self._volume = volume
|
||||||
@@ -58,14 +76,28 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
self._loop = loop
|
self._loop = loop
|
||||||
|
|
||||||
async def start(self, sample_rate: int):
|
async def start(self, sample_rate: int):
|
||||||
|
"""Initialize the mixer and load all sound files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: The sample rate of the output transport in Hz.
|
||||||
|
"""
|
||||||
self._sample_rate = sample_rate
|
self._sample_rate = sample_rate
|
||||||
for sound_name, file_name in self._sound_files.items():
|
for sound_name, file_name in self._sound_files.items():
|
||||||
await asyncio.to_thread(self._load_sound_file, sound_name, file_name)
|
await asyncio.to_thread(self._load_sound_file, sound_name, file_name)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
|
"""Clean up mixer resources.
|
||||||
|
|
||||||
|
Currently performs no cleanup as sound data is managed by garbage collection.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def process_frame(self, frame: MixerControlFrame):
|
async def process_frame(self, frame: MixerControlFrame):
|
||||||
|
"""Process mixer control frames to update settings or enable/disable mixing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The mixer control frame to process.
|
||||||
|
"""
|
||||||
if isinstance(frame, MixerUpdateSettingsFrame):
|
if isinstance(frame, MixerUpdateSettingsFrame):
|
||||||
await self._update_settings(frame)
|
await self._update_settings(frame)
|
||||||
elif isinstance(frame, MixerEnableFrame):
|
elif isinstance(frame, MixerEnableFrame):
|
||||||
@@ -73,12 +105,22 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def mix(self, audio: bytes) -> bytes:
|
async def mix(self, audio: bytes) -> bytes:
|
||||||
|
"""Mix transport audio with the current sound file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Raw audio bytes from the transport to mix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mixed audio bytes combining transport and file audio.
|
||||||
|
"""
|
||||||
return self._mix_with_sound(audio)
|
return self._mix_with_sound(audio)
|
||||||
|
|
||||||
async def _enable_mixing(self, enable: bool):
|
async def _enable_mixing(self, enable: bool):
|
||||||
|
"""Enable or disable audio mixing."""
|
||||||
self._mixing = enable
|
self._mixing = enable
|
||||||
|
|
||||||
async def _update_settings(self, frame: MixerUpdateSettingsFrame):
|
async def _update_settings(self, frame: MixerUpdateSettingsFrame):
|
||||||
|
"""Update mixer settings from a control frame."""
|
||||||
for setting, value in frame.settings.items():
|
for setting, value in frame.settings.items():
|
||||||
match setting:
|
match setting:
|
||||||
case "sound":
|
case "sound":
|
||||||
@@ -89,6 +131,11 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
await self._update_loop(value)
|
await self._update_loop(value)
|
||||||
|
|
||||||
async def _change_sound(self, sound: str):
|
async def _change_sound(self, sound: str):
|
||||||
|
"""Change the currently playing sound file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sound: Name of the sound file to switch to.
|
||||||
|
"""
|
||||||
if sound in self._sound_files:
|
if sound in self._sound_files:
|
||||||
self._current_sound = sound
|
self._current_sound = sound
|
||||||
self._sound_pos = 0
|
self._sound_pos = 0
|
||||||
@@ -96,12 +143,15 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
logger.error(f"Sound {sound} is not available")
|
logger.error(f"Sound {sound} is not available")
|
||||||
|
|
||||||
async def _update_volume(self, volume: float):
|
async def _update_volume(self, volume: float):
|
||||||
|
"""Update the mixing volume level."""
|
||||||
self._volume = volume
|
self._volume = volume
|
||||||
|
|
||||||
async def _update_loop(self, loop: bool):
|
async def _update_loop(self, loop: bool):
|
||||||
|
"""Update the looping behavior."""
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
|
|
||||||
def _load_sound_file(self, sound_name: str, file_name: str):
|
def _load_sound_file(self, sound_name: str, file_name: str):
|
||||||
|
"""Load an audio file into memory for mixing."""
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Loading mixer sound from {file_name}")
|
logger.debug(f"Loading mixer sound from {file_name}")
|
||||||
sound, sample_rate = sf.read(file_name, dtype="int16")
|
sound, sample_rate = sf.read(file_name, dtype="int16")
|
||||||
@@ -118,10 +168,7 @@ class SoundfileMixer(BaseAudioMixer):
|
|||||||
logger.error(f"Unable to open file {file_name}: {e}")
|
logger.error(f"Unable to open file {file_name}: {e}")
|
||||||
|
|
||||||
def _mix_with_sound(self, audio: bytes):
|
def _mix_with_sound(self, audio: bytes):
|
||||||
"""Mixes raw audio frames with chunks of the same length from the sound
|
"""Mix raw audio frames with chunks of the same length from the sound file."""
|
||||||
file.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not self._mixing or not self._current_sound in self._sounds:
|
if not self._mixing or not self._current_sound in self._sounds:
|
||||||
return audio
|
return audio
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,35 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base audio resampler interface for Pipecat.
|
||||||
|
|
||||||
|
This module defines the abstract base class for audio resampling implementations,
|
||||||
|
providing a common interface for converting audio between different sample rates.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class BaseAudioResampler(ABC):
|
class BaseAudioResampler(ABC):
|
||||||
"""Abstract base class for audio resampling. This class defines an
|
"""Abstract base class for audio resampling implementations.
|
||||||
interface for audio resampling implementations.
|
|
||||||
|
This class defines the interface that all audio resampling implementations
|
||||||
|
must follow, providing a standardized way to convert audio data between
|
||||||
|
different sample rates.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
||||||
"""
|
"""Resamples the given audio data to a different sample rate.
|
||||||
Resamples the given audio data to a different sample rate.
|
|
||||||
|
|
||||||
This is an abstract method that must be implemented in subclasses.
|
This is an abstract method that must be implemented in subclasses.
|
||||||
|
|
||||||
Parameters:
|
Args:
|
||||||
audio (bytes): The audio data to be resampled, represented as a byte string.
|
audio: The audio data to be resampled, as raw bytes.
|
||||||
in_rate (int): The original sample rate of the audio data (in Hz).
|
in_rate: The original sample rate of the audio data in Hz.
|
||||||
out_rate (int): The desired sample rate for the resampled audio data (in Hz).
|
out_rate: The desired sample rate for the output audio in Hz.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bytes: The resampled audio data as a byte string.
|
The resampled audio data as raw bytes.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Resampy-based audio resampler implementation.
|
||||||
|
|
||||||
|
This module provides an audio resampler that uses the resampy library
|
||||||
|
for high-quality audio sample rate conversion.
|
||||||
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import resampy
|
import resampy
|
||||||
|
|
||||||
@@ -11,12 +17,31 @@ from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler
|
|||||||
|
|
||||||
|
|
||||||
class ResampyResampler(BaseAudioResampler):
|
class ResampyResampler(BaseAudioResampler):
|
||||||
"""Audio resampler implementation using the resampy library."""
|
"""Audio resampler implementation using the resampy library.
|
||||||
|
|
||||||
|
This resampler uses the resampy library's Kaiser windowing filter
|
||||||
|
for high-quality audio resampling with good performance characteristics.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize the resampy resampler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Additional keyword arguments (currently unused).
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
||||||
|
"""Resample audio data using resampy library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Input audio data as raw bytes (16-bit signed integers).
|
||||||
|
in_rate: Original sample rate in Hz.
|
||||||
|
out_rate: Target sample rate in Hz.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resampled audio data as raw bytes (16-bit signed integers).
|
||||||
|
"""
|
||||||
if in_rate == out_rate:
|
if in_rate == out_rate:
|
||||||
return audio
|
return audio
|
||||||
audio_data = np.frombuffer(audio, dtype=np.int16)
|
audio_data = np.frombuffer(audio, dtype=np.int16)
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""SoX-based audio resampler implementation.
|
||||||
|
|
||||||
|
This module provides an audio resampler that uses the SoX resampler library
|
||||||
|
for very high-quality audio sample rate conversion.
|
||||||
|
|
||||||
|
When to use the SOXRAudioResampler:
|
||||||
|
1. For batch processing of complete audio files
|
||||||
|
2. When you have all the audio data available at once
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import soxr
|
import soxr
|
||||||
|
|
||||||
@@ -11,12 +22,32 @@ from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler
|
|||||||
|
|
||||||
|
|
||||||
class SOXRAudioResampler(BaseAudioResampler):
|
class SOXRAudioResampler(BaseAudioResampler):
|
||||||
"""Audio resampler implementation using the SoX resampler library."""
|
"""Audio resampler implementation using the SoX resampler library.
|
||||||
|
|
||||||
|
This resampler uses the SoX resampler library configured for very high
|
||||||
|
quality (VHQ) resampling, providing excellent audio quality at the cost
|
||||||
|
of additional computational overhead.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize the SoX audio resampler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Additional keyword arguments (currently unused).
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
||||||
|
"""Resample audio data using SoX resampler library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Input audio data as raw bytes (16-bit signed integers).
|
||||||
|
in_rate: Original sample rate in Hz.
|
||||||
|
out_rate: Target sample rate in Hz.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resampled audio data as raw bytes (16-bit signed integers).
|
||||||
|
"""
|
||||||
if in_rate == out_rate:
|
if in_rate == out_rate:
|
||||||
return audio
|
return audio
|
||||||
audio_data = np.frombuffer(audio, dtype=np.int16)
|
audio_data = np.frombuffer(audio, dtype=np.int16)
|
||||||
|
|||||||
101
src/pipecat/audio/resamplers/soxr_stream_resampler.py
Normal file
101
src/pipecat/audio/resamplers/soxr_stream_resampler.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""SoX-based audio resampler stream implementation.
|
||||||
|
|
||||||
|
This module provides an audio resampler that uses the SoX ResampleStream library
|
||||||
|
for very high quality audio sample rate conversion.
|
||||||
|
|
||||||
|
When to use the SOXRStreamAudioResampler:
|
||||||
|
1. For real-time processing scenarios
|
||||||
|
2. When dealing with very long audio signals
|
||||||
|
3. When processing audio in chunks or streams
|
||||||
|
4. When you need to reuse the same resampler configuration multiple times, as it saves initialization overhead
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import soxr
|
||||||
|
|
||||||
|
from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler
|
||||||
|
|
||||||
|
CLEAR_STREAM_AFTER_SECS = 0.2
|
||||||
|
|
||||||
|
|
||||||
|
class SOXRStreamAudioResampler(BaseAudioResampler):
|
||||||
|
"""Audio resampler implementation using the SoX ResampleStream library.
|
||||||
|
|
||||||
|
This resampler uses the SoX ResampleStream library configured for very high
|
||||||
|
quality (VHQ) resampling, providing excellent audio quality at the cost
|
||||||
|
of additional computational overhead.
|
||||||
|
It keeps an internal history which avoids clicks at chunk boundaries.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Only supports mono audio (1 channel).
|
||||||
|
- Input must be 16-bit signed PCM audio as raw bytes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize the resampler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Additional keyword arguments (currently unused).
|
||||||
|
"""
|
||||||
|
self._in_rate: float | None = None
|
||||||
|
self._out_rate: float | None = None
|
||||||
|
self._last_resample_time: float = 0
|
||||||
|
self._soxr_stream: soxr.ResampleStream | None = None
|
||||||
|
|
||||||
|
def _initialize(self, in_rate: float, out_rate: float):
|
||||||
|
self._in_rate = in_rate
|
||||||
|
self._out_rate = out_rate
|
||||||
|
self._last_resample_time = time.time()
|
||||||
|
self._soxr_stream = soxr.ResampleStream(
|
||||||
|
in_rate=in_rate, out_rate=out_rate, num_channels=1, quality="VHQ", dtype="int16"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _maybe_clear_internal_state(self):
|
||||||
|
current_time = time.time()
|
||||||
|
time_since_last_resample = current_time - self._last_resample_time
|
||||||
|
# If more than CLEAR_STREAM_AFTER_SECS seconds have passed, clear the resampler state
|
||||||
|
if time_since_last_resample > CLEAR_STREAM_AFTER_SECS:
|
||||||
|
if self._soxr_stream:
|
||||||
|
self._soxr_stream.clear()
|
||||||
|
self._last_resample_time = current_time
|
||||||
|
|
||||||
|
def _maybe_initialize_sox_stream(self, in_rate: int, out_rate: int):
|
||||||
|
if self._soxr_stream is None:
|
||||||
|
self._initialize(in_rate, out_rate)
|
||||||
|
else:
|
||||||
|
self._maybe_clear_internal_state()
|
||||||
|
|
||||||
|
if self._in_rate != in_rate or self._out_rate != out_rate:
|
||||||
|
raise ValueError(
|
||||||
|
f"SOXRStreamAudioResampler cannot be reused with different sample rates: "
|
||||||
|
f"expected {self._in_rate}->{self._out_rate}, got {in_rate}->{out_rate}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
||||||
|
"""Resample audio data using soxr.ResampleStream resampler library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Input audio data as raw bytes (16-bit signed integers).
|
||||||
|
in_rate: Original sample rate in Hz.
|
||||||
|
out_rate: Target sample rate in Hz.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resampled audio data as raw bytes (16-bit signed integers).
|
||||||
|
"""
|
||||||
|
if in_rate == out_rate:
|
||||||
|
return audio
|
||||||
|
|
||||||
|
self._maybe_initialize_sox_stream(in_rate, out_rate)
|
||||||
|
audio_data = np.frombuffer(audio, dtype=np.int16)
|
||||||
|
resampled_audio = self._soxr_stream.resample_chunk(audio_data)
|
||||||
|
result = resampled_audio.astype(np.int16).tobytes()
|
||||||
|
return result
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base turn analyzer for determining end-of-turn in audio conversations.
|
||||||
|
|
||||||
|
This module provides the abstract base class and enumeration for analyzing
|
||||||
|
when a user has finished speaking in a conversation.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
@@ -12,6 +18,13 @@ from pipecat.metrics.metrics import MetricsData
|
|||||||
|
|
||||||
|
|
||||||
class EndOfTurnState(Enum):
|
class EndOfTurnState(Enum):
|
||||||
|
"""State enumeration for end-of-turn analysis results.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
COMPLETE: The user has finished their turn and stopped speaking.
|
||||||
|
INCOMPLETE: The user is still speaking or may continue speaking.
|
||||||
|
"""
|
||||||
|
|
||||||
COMPLETE = 1
|
COMPLETE = 1
|
||||||
INCOMPLETE = 2
|
INCOMPLETE = 2
|
||||||
|
|
||||||
@@ -24,6 +37,12 @@ class BaseTurnAnalyzer(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, sample_rate: Optional[int] = None):
|
def __init__(self, *, sample_rate: Optional[int] = None):
|
||||||
|
"""Initialize the turn analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Optional initial sample rate for audio processing.
|
||||||
|
If provided, this will be used as the fixed sample rate.
|
||||||
|
"""
|
||||||
self._init_sample_rate = sample_rate
|
self._init_sample_rate = sample_rate
|
||||||
self._sample_rate = 0
|
self._sample_rate = 0
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Smart turn analyzer base class using ML models for end-of-turn detection.
|
||||||
|
|
||||||
|
This module provides the base implementation for smart turn analyzers that use
|
||||||
|
machine learning models to determine when a user has finished speaking, going
|
||||||
|
beyond simple silence-based detection.
|
||||||
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
@@ -23,6 +30,14 @@ USE_ONLY_LAST_VAD_SEGMENT = True
|
|||||||
|
|
||||||
|
|
||||||
class SmartTurnParams(BaseModel):
|
class SmartTurnParams(BaseModel):
|
||||||
|
"""Configuration parameters for smart turn analysis.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
stop_secs: Maximum silence duration in seconds before ending turn.
|
||||||
|
pre_speech_ms: Milliseconds of audio to include before speech starts.
|
||||||
|
max_duration_secs: Maximum duration in seconds for audio segments.
|
||||||
|
"""
|
||||||
|
|
||||||
stop_secs: float = STOP_SECS
|
stop_secs: float = STOP_SECS
|
||||||
pre_speech_ms: float = PRE_SPEECH_MS
|
pre_speech_ms: float = PRE_SPEECH_MS
|
||||||
max_duration_secs: float = MAX_DURATION_SECONDS
|
max_duration_secs: float = MAX_DURATION_SECONDS
|
||||||
@@ -31,13 +46,28 @@ class SmartTurnParams(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SmartTurnTimeoutException(Exception):
|
class SmartTurnTimeoutException(Exception):
|
||||||
|
"""Exception raised when smart turn analysis times out."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseSmartTurn(BaseTurnAnalyzer):
|
class BaseSmartTurn(BaseTurnAnalyzer):
|
||||||
|
"""Base class for smart turn analyzers using ML models.
|
||||||
|
|
||||||
|
Provides common functionality for smart turn detection including audio
|
||||||
|
buffering, speech tracking, and ML model integration. Subclasses must
|
||||||
|
implement the specific model prediction logic.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, sample_rate: Optional[int] = None, params: Optional[SmartTurnParams] = None
|
self, *, sample_rate: Optional[int] = None, params: Optional[SmartTurnParams] = None
|
||||||
):
|
):
|
||||||
|
"""Initialize the smart turn analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Optional sample rate for audio processing.
|
||||||
|
params: Configuration parameters for turn analysis behavior.
|
||||||
|
"""
|
||||||
super().__init__(sample_rate=sample_rate)
|
super().__init__(sample_rate=sample_rate)
|
||||||
self._params = params or SmartTurnParams()
|
self._params = params or SmartTurnParams()
|
||||||
# Configuration
|
# Configuration
|
||||||
@@ -50,9 +80,23 @@ class BaseSmartTurn(BaseTurnAnalyzer):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def speech_triggered(self) -> bool:
|
def speech_triggered(self) -> bool:
|
||||||
|
"""Check if speech has been detected and triggered analysis.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if speech has been detected and turn analysis is active.
|
||||||
|
"""
|
||||||
return self._speech_triggered
|
return self._speech_triggered
|
||||||
|
|
||||||
def append_audio(self, buffer: bytes, is_speech: bool) -> EndOfTurnState:
|
def append_audio(self, buffer: bytes, is_speech: bool) -> EndOfTurnState:
|
||||||
|
"""Append audio data for turn analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: Raw audio data bytes to append for analysis.
|
||||||
|
is_speech: Whether the audio buffer contains detected speech.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current end-of-turn state after processing the audio.
|
||||||
|
"""
|
||||||
# Convert raw audio to float32 format and append to the buffer
|
# Convert raw audio to float32 format and append to the buffer
|
||||||
audio_int16 = np.frombuffer(buffer, dtype=np.int16)
|
audio_int16 = np.frombuffer(buffer, dtype=np.int16)
|
||||||
audio_float32 = np.frombuffer(audio_int16, dtype=np.int16).astype(np.float32) / 32768.0
|
audio_float32 = np.frombuffer(audio_int16, dtype=np.int16).astype(np.float32) / 32768.0
|
||||||
@@ -92,6 +136,12 @@ class BaseSmartTurn(BaseTurnAnalyzer):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
async def analyze_end_of_turn(self) -> Tuple[EndOfTurnState, Optional[MetricsData]]:
|
async def analyze_end_of_turn(self) -> Tuple[EndOfTurnState, Optional[MetricsData]]:
|
||||||
|
"""Analyze the current audio state to determine if turn has ended.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing the end-of-turn state and optional metrics data
|
||||||
|
from the ML model analysis.
|
||||||
|
"""
|
||||||
state, result = await self._process_speech_segment(self._audio_buffer)
|
state, result = await self._process_speech_segment(self._audio_buffer)
|
||||||
if state == EndOfTurnState.COMPLETE or USE_ONLY_LAST_VAD_SEGMENT:
|
if state == EndOfTurnState.COMPLETE or USE_ONLY_LAST_VAD_SEGMENT:
|
||||||
self._clear(state)
|
self._clear(state)
|
||||||
@@ -99,9 +149,11 @@ class BaseSmartTurn(BaseTurnAnalyzer):
|
|||||||
return state, result
|
return state, result
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Reset the turn analyzer to its initial state."""
|
||||||
self._clear(EndOfTurnState.COMPLETE)
|
self._clear(EndOfTurnState.COMPLETE)
|
||||||
|
|
||||||
def _clear(self, turn_state: EndOfTurnState):
|
def _clear(self, turn_state: EndOfTurnState):
|
||||||
|
"""Clear internal state based on turn completion status."""
|
||||||
# If the state is still incomplete, keep the _speech_triggered as True
|
# If the state is still incomplete, keep the _speech_triggered as True
|
||||||
self._speech_triggered = turn_state == EndOfTurnState.INCOMPLETE
|
self._speech_triggered = turn_state == EndOfTurnState.INCOMPLETE
|
||||||
self._audio_buffer = []
|
self._audio_buffer = []
|
||||||
@@ -111,6 +163,7 @@ class BaseSmartTurn(BaseTurnAnalyzer):
|
|||||||
async def _process_speech_segment(
|
async def _process_speech_segment(
|
||||||
self, audio_buffer
|
self, audio_buffer
|
||||||
) -> Tuple[EndOfTurnState, Optional[MetricsData]]:
|
) -> Tuple[EndOfTurnState, Optional[MetricsData]]:
|
||||||
|
"""Process accumulated audio segment using ML model."""
|
||||||
state = EndOfTurnState.INCOMPLETE
|
state = EndOfTurnState.INCOMPLETE
|
||||||
|
|
||||||
if not audio_buffer:
|
if not audio_buffer:
|
||||||
@@ -188,14 +241,5 @@ class BaseSmartTurn(BaseTurnAnalyzer):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
||||||
"""Abstract method to predict if a turn has ended based on audio.
|
"""Predict end-of-turn using ML model from audio data."""
|
||||||
|
|
||||||
Args:
|
|
||||||
audio_array: Float32 numpy array of audio samples at 16kHz.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with:
|
|
||||||
- prediction: 1 if turn is complete, else 0
|
|
||||||
- probability: Confidence of the prediction
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Fal.ai smart turn analyzer implementation.
|
||||||
|
|
||||||
|
This module provides a smart turn analyzer that uses Fal.ai's hosted smart-turn model
|
||||||
|
for end-of-turn detection in conversations.
|
||||||
|
|
||||||
|
Note: To learn more about the smart-turn model, visit:
|
||||||
|
- https://fal.ai/models/fal-ai/smart-turn/playground
|
||||||
|
- https://github.com/pipecat-ai/smart-turn
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -12,6 +22,12 @@ from pipecat.audio.turn.smart_turn.http_smart_turn import HttpSmartTurnAnalyzer
|
|||||||
|
|
||||||
|
|
||||||
class FalSmartTurnAnalyzer(HttpSmartTurnAnalyzer):
|
class FalSmartTurnAnalyzer(HttpSmartTurnAnalyzer):
|
||||||
|
"""Smart turn analyzer using Fal.ai's hosted smart-turn model.
|
||||||
|
|
||||||
|
Extends HttpSmartTurnAnalyzer to provide integration with Fal.ai's
|
||||||
|
smart turn detection API endpoint with proper authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -20,6 +36,14 @@ class FalSmartTurnAnalyzer(HttpSmartTurnAnalyzer):
|
|||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
"""Initialize the Fal.ai smart turn analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aiohttp_session: HTTP client session for making API requests.
|
||||||
|
url: Fal.ai API endpoint URL for smart turn detection.
|
||||||
|
api_key: API key for authenticating with Fal.ai service.
|
||||||
|
**kwargs: Additional arguments passed to parent HttpSmartTurnAnalyzer.
|
||||||
|
"""
|
||||||
headers = {}
|
headers = {}
|
||||||
if api_key:
|
if api_key:
|
||||||
headers = {"Authorization": f"Key {api_key}"}
|
headers = {"Authorization": f"Key {api_key}"}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""HTTP-based smart turn analyzer for remote ML inference.
|
||||||
|
|
||||||
|
This module provides a smart turn analyzer that sends audio data to remote
|
||||||
|
HTTP endpoints for ML-based end-of-turn detection.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import io
|
import io
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
@@ -16,6 +22,12 @@ from pipecat.audio.turn.smart_turn.base_smart_turn import BaseSmartTurn, SmartTu
|
|||||||
|
|
||||||
|
|
||||||
class HttpSmartTurnAnalyzer(BaseSmartTurn):
|
class HttpSmartTurnAnalyzer(BaseSmartTurn):
|
||||||
|
"""Smart turn analyzer using HTTP-based ML inference.
|
||||||
|
|
||||||
|
Sends audio data to remote HTTP endpoints for ML-based end-of-turn
|
||||||
|
prediction. Handles serialization, HTTP communication, and error recovery.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -24,12 +36,21 @@ class HttpSmartTurnAnalyzer(BaseSmartTurn):
|
|||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
"""Initialize the HTTP smart turn analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: HTTP endpoint URL for the smart turn ML service.
|
||||||
|
aiohttp_session: HTTP client session for making requests.
|
||||||
|
headers: Optional HTTP headers to include in requests.
|
||||||
|
**kwargs: Additional arguments passed to BaseSmartTurn.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._url = url
|
self._url = url
|
||||||
self._headers = headers or {}
|
self._headers = headers or {}
|
||||||
self._aiohttp_session = aiohttp_session
|
self._aiohttp_session = aiohttp_session
|
||||||
|
|
||||||
def _serialize_array(self, audio_array: np.ndarray) -> bytes:
|
def _serialize_array(self, audio_array: np.ndarray) -> bytes:
|
||||||
|
"""Serialize NumPy audio array to bytes for HTTP transmission."""
|
||||||
logger.trace("Serializing NumPy array to bytes...")
|
logger.trace("Serializing NumPy array to bytes...")
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
np.save(buffer, audio_array)
|
np.save(buffer, audio_array)
|
||||||
@@ -38,6 +59,7 @@ class HttpSmartTurnAnalyzer(BaseSmartTurn):
|
|||||||
return serialized_bytes
|
return serialized_bytes
|
||||||
|
|
||||||
async def _send_raw_request(self, data_bytes: bytes) -> Dict[str, Any]:
|
async def _send_raw_request(self, data_bytes: bytes) -> Dict[str, Any]:
|
||||||
|
"""Send raw audio data to the HTTP endpoint for prediction."""
|
||||||
headers = {"Content-Type": "application/octet-stream"}
|
headers = {"Content-Type": "application/octet-stream"}
|
||||||
headers.update(self._headers)
|
headers.update(self._headers)
|
||||||
|
|
||||||
@@ -83,6 +105,7 @@ class HttpSmartTurnAnalyzer(BaseSmartTurn):
|
|||||||
raise Exception("Failed to send raw request to Daily Smart Turn.")
|
raise Exception("Failed to send raw request to Daily Smart Turn.")
|
||||||
|
|
||||||
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""Predict end-of-turn using remote HTTP ML service."""
|
||||||
try:
|
try:
|
||||||
serialized_array = self._serialize_array(audio_array)
|
serialized_array = self._serialize_array(audio_array)
|
||||||
return await self._send_raw_request(serialized_array)
|
return await self._send_raw_request(serialized_array)
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Local CoreML smart turn analyzer for on-device ML inference.
|
||||||
|
|
||||||
|
This module provides a smart turn analyzer that uses CoreML models for
|
||||||
|
local end-of-turn detection without requiring network connectivity.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
@@ -25,7 +30,24 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn):
|
class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn):
|
||||||
|
"""Local smart turn analyzer using CoreML models.
|
||||||
|
|
||||||
|
Provides end-of-turn detection using locally-stored CoreML models,
|
||||||
|
enabling offline operation without network dependencies. Optimized
|
||||||
|
for Apple Silicon and other CoreML-compatible hardware.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *, smart_turn_model_path: str, **kwargs):
|
def __init__(self, *, smart_turn_model_path: str, **kwargs):
|
||||||
|
"""Initialize the local CoreML smart turn analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
smart_turn_model_path: Path to directory containing the CoreML model
|
||||||
|
and feature extractor files.
|
||||||
|
**kwargs: Additional arguments passed to BaseSmartTurn.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If smart_turn_model_path is not provided or model loading fails.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if not smart_turn_model_path:
|
if not smart_turn_model_path:
|
||||||
@@ -41,6 +63,7 @@ class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn):
|
|||||||
logger.debug("Loaded Local Smart Turn")
|
logger.debug("Loaded Local Smart Turn")
|
||||||
|
|
||||||
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""Predict end-of-turn using local CoreML model."""
|
||||||
inputs = self._turn_processor(
|
inputs = self._turn_processor(
|
||||||
audio_array,
|
audio_array,
|
||||||
sampling_rate=16000,
|
sampling_rate=16000,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Local PyTorch smart turn analyzer for on-device ML inference.
|
||||||
|
|
||||||
|
This module provides a smart turn analyzer that uses PyTorch models for
|
||||||
|
local end-of-turn detection without requiring network connectivity.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
@@ -24,7 +29,21 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class LocalSmartTurnAnalyzer(BaseSmartTurn):
|
class LocalSmartTurnAnalyzer(BaseSmartTurn):
|
||||||
|
"""Local smart turn analyzer using PyTorch models.
|
||||||
|
|
||||||
|
Provides end-of-turn detection using locally-stored PyTorch models,
|
||||||
|
enabling offline operation without network dependencies. Uses
|
||||||
|
Wav2Vec2-BERT architecture for audio sequence classification.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *, smart_turn_model_path: str, **kwargs):
|
def __init__(self, *, smart_turn_model_path: str, **kwargs):
|
||||||
|
"""Initialize the local PyTorch smart turn analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
smart_turn_model_path: Path to directory containing the PyTorch model
|
||||||
|
and feature extractor files. If empty, uses default HuggingFace model.
|
||||||
|
**kwargs: Additional arguments passed to BaseSmartTurn.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if not smart_turn_model_path:
|
if not smart_turn_model_path:
|
||||||
@@ -46,6 +65,7 @@ class LocalSmartTurnAnalyzer(BaseSmartTurn):
|
|||||||
logger.debug("Loaded Local Smart Turn")
|
logger.debug("Loaded Local Smart Turn")
|
||||||
|
|
||||||
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
|
||||||
|
"""Predict end-of-turn using local PyTorch model."""
|
||||||
inputs = self._turn_processor(
|
inputs = self._turn_processor(
|
||||||
audio_array,
|
audio_array,
|
||||||
sampling_rate=16000,
|
sampling_rate=16000,
|
||||||
|
|||||||
@@ -4,21 +4,87 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Audio utility functions for Pipecat.
|
||||||
|
|
||||||
|
This module provides common audio processing utilities including mixing,
|
||||||
|
format conversion, volume calculation, and codec transformations for
|
||||||
|
various audio formats used in Pipecat pipelines.
|
||||||
|
"""
|
||||||
|
|
||||||
import audioop
|
import audioop
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyloudnorm as pyln
|
import pyloudnorm as pyln
|
||||||
import soxr
|
|
||||||
|
|
||||||
from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler
|
from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler
|
||||||
from pipecat.audio.resamplers.soxr_resampler import SOXRAudioResampler
|
from pipecat.audio.resamplers.soxr_resampler import SOXRAudioResampler
|
||||||
|
from pipecat.audio.resamplers.soxr_stream_resampler import SOXRStreamAudioResampler
|
||||||
|
|
||||||
|
|
||||||
def create_default_resampler(**kwargs) -> BaseAudioResampler:
|
def create_default_resampler(**kwargs) -> BaseAudioResampler:
|
||||||
|
"""Create a default audio resampler instance.
|
||||||
|
|
||||||
|
. deprecated:: 0.0.74
|
||||||
|
This function is deprecated and will be removed in a future version.
|
||||||
|
Use `create_stream_resampler` for real-time processing scenarios or
|
||||||
|
`create_file_resampler` for batch processing of complete audio files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Additional keyword arguments passed to the resampler constructor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A configured SOXRAudioResampler instance.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"`create_default_resampler` is deprecated. "
|
||||||
|
"Use `create_stream_resampler` for real-time processing scenarios or "
|
||||||
|
"`create_file_resampler` for batch processing of complete audio files.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
return SOXRAudioResampler(**kwargs)
|
return SOXRAudioResampler(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_file_resampler(**kwargs) -> BaseAudioResampler:
|
||||||
|
"""Create an audio resampler instance for batch processing of complete audio files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Additional keyword arguments passed to the resampler constructor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A configured SOXRAudioResampler instance.
|
||||||
|
"""
|
||||||
|
return SOXRAudioResampler(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_stream_resampler(**kwargs) -> BaseAudioResampler:
|
||||||
|
"""Create a stream audio resampler instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Additional keyword arguments passed to the resampler constructor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A configured SOXRStreamAudioResampler instance.
|
||||||
|
"""
|
||||||
|
return SOXRStreamAudioResampler(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def mix_audio(audio1: bytes, audio2: bytes) -> bytes:
|
def mix_audio(audio1: bytes, audio2: bytes) -> bytes:
|
||||||
|
"""Mix two audio streams together by adding their samples.
|
||||||
|
|
||||||
|
Both audio streams are assumed to be 16-bit signed integer PCM data.
|
||||||
|
If the streams have different lengths, the shorter one is zero-padded
|
||||||
|
to match the longer stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio1: First audio stream as raw bytes (16-bit signed integers).
|
||||||
|
audio2: Second audio stream as raw bytes (16-bit signed integers).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mixed audio data as raw bytes with samples clipped to 16-bit range.
|
||||||
|
"""
|
||||||
data1 = np.frombuffer(audio1, dtype=np.int16)
|
data1 = np.frombuffer(audio1, dtype=np.int16)
|
||||||
data2 = np.frombuffer(audio2, dtype=np.int16)
|
data2 = np.frombuffer(audio2, dtype=np.int16)
|
||||||
|
|
||||||
@@ -37,6 +103,19 @@ def mix_audio(audio1: bytes, audio2: bytes) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def interleave_stereo_audio(left_audio: bytes, right_audio: bytes) -> bytes:
|
def interleave_stereo_audio(left_audio: bytes, right_audio: bytes) -> bytes:
|
||||||
|
"""Interleave left and right mono audio channels into stereo audio.
|
||||||
|
|
||||||
|
Takes two mono audio streams and combines them into a single stereo
|
||||||
|
stream by interleaving the samples (L, R, L, R, ...). If the channels
|
||||||
|
have different lengths, both are truncated to the shorter length.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
left_audio: Left channel audio as raw bytes (16-bit signed integers).
|
||||||
|
right_audio: Right channel audio as raw bytes (16-bit signed integers).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Interleaved stereo audio data as raw bytes.
|
||||||
|
"""
|
||||||
left = np.frombuffer(left_audio, dtype=np.int16)
|
left = np.frombuffer(left_audio, dtype=np.int16)
|
||||||
right = np.frombuffer(right_audio, dtype=np.int16)
|
right = np.frombuffer(right_audio, dtype=np.int16)
|
||||||
|
|
||||||
@@ -50,12 +129,34 @@ def interleave_stereo_audio(left_audio: bytes, right_audio: bytes) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def normalize_value(value, min_value, max_value):
|
def normalize_value(value, min_value, max_value):
|
||||||
|
"""Normalize a value to the range [0, 1] and clamp it to bounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The value to normalize.
|
||||||
|
min_value: The minimum value of the input range.
|
||||||
|
max_value: The maximum value of the input range.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalized value clamped to the range [0, 1].
|
||||||
|
"""
|
||||||
normalized = (value - min_value) / (max_value - min_value)
|
normalized = (value - min_value) / (max_value - min_value)
|
||||||
normalized_clamped = max(0, min(1, normalized))
|
normalized_clamped = max(0, min(1, normalized))
|
||||||
return normalized_clamped
|
return normalized_clamped
|
||||||
|
|
||||||
|
|
||||||
def calculate_audio_volume(audio: bytes, sample_rate: int) -> float:
|
def calculate_audio_volume(audio: bytes, sample_rate: int) -> float:
|
||||||
|
"""Calculate the loudness level of audio data using EBU R128 standard.
|
||||||
|
|
||||||
|
Uses the pyloudnorm library to calculate integrated loudness according
|
||||||
|
to the EBU R128 recommendation, then normalizes the result to [0, 1].
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio: Audio data as raw bytes (16-bit signed integers).
|
||||||
|
sample_rate: Sample rate of the audio in Hz.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalized loudness value between 0 (quiet) and 1 (loud).
|
||||||
|
"""
|
||||||
audio_np = np.frombuffer(audio, dtype=np.int16)
|
audio_np = np.frombuffer(audio, dtype=np.int16)
|
||||||
audio_float = audio_np.astype(np.float64)
|
audio_float = audio_np.astype(np.float64)
|
||||||
|
|
||||||
@@ -71,12 +172,37 @@ def calculate_audio_volume(audio: bytes, sample_rate: int) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def exp_smoothing(value: float, prev_value: float, factor: float) -> float:
|
def exp_smoothing(value: float, prev_value: float, factor: float) -> float:
|
||||||
|
"""Apply exponential smoothing to a value.
|
||||||
|
|
||||||
|
Exponential smoothing is used to reduce noise in time-series data by
|
||||||
|
giving more weight to recent values while still considering historical data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The new value to incorporate.
|
||||||
|
prev_value: The previous smoothed value.
|
||||||
|
factor: Smoothing factor between 0 and 1. Higher values give more
|
||||||
|
weight to the new value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The exponentially smoothed value.
|
||||||
|
"""
|
||||||
return prev_value + factor * (value - prev_value)
|
return prev_value + factor * (value - prev_value)
|
||||||
|
|
||||||
|
|
||||||
async def ulaw_to_pcm(
|
async def ulaw_to_pcm(
|
||||||
ulaw_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler
|
ulaw_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler
|
||||||
):
|
):
|
||||||
|
"""Convert μ-law encoded audio to PCM and optionally resample.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ulaw_bytes: μ-law encoded audio data as raw bytes.
|
||||||
|
in_rate: Original sample rate of the μ-law audio in Hz.
|
||||||
|
out_rate: Desired output sample rate in Hz.
|
||||||
|
resampler: Audio resampler instance for rate conversion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PCM audio data as raw bytes at the specified output rate.
|
||||||
|
"""
|
||||||
# Convert μ-law to PCM
|
# Convert μ-law to PCM
|
||||||
in_pcm_bytes = audioop.ulaw2lin(ulaw_bytes, 2)
|
in_pcm_bytes = audioop.ulaw2lin(ulaw_bytes, 2)
|
||||||
|
|
||||||
@@ -87,6 +213,17 @@ async def ulaw_to_pcm(
|
|||||||
|
|
||||||
|
|
||||||
async def pcm_to_ulaw(pcm_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler):
|
async def pcm_to_ulaw(pcm_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler):
|
||||||
|
"""Convert PCM audio to μ-law encoding and optionally resample.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pcm_bytes: PCM audio data as raw bytes (16-bit signed integers).
|
||||||
|
in_rate: Original sample rate of the PCM audio in Hz.
|
||||||
|
out_rate: Desired output sample rate in Hz.
|
||||||
|
resampler: Audio resampler instance for rate conversion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
μ-law encoded audio data as raw bytes at the specified output rate.
|
||||||
|
"""
|
||||||
# Resample
|
# Resample
|
||||||
in_pcm_bytes = await resampler.resample(pcm_bytes, in_rate, out_rate)
|
in_pcm_bytes = await resampler.resample(pcm_bytes, in_rate, out_rate)
|
||||||
|
|
||||||
@@ -99,6 +236,17 @@ async def pcm_to_ulaw(pcm_bytes: bytes, in_rate: int, out_rate: int, resampler:
|
|||||||
async def alaw_to_pcm(
|
async def alaw_to_pcm(
|
||||||
alaw_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler
|
alaw_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
|
"""Convert A-law encoded audio to PCM and optionally resample.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alaw_bytes: A-law encoded audio data as raw bytes.
|
||||||
|
in_rate: Original sample rate of the A-law audio in Hz.
|
||||||
|
out_rate: Desired output sample rate in Hz.
|
||||||
|
resampler: Audio resampler instance for rate conversion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PCM audio data as raw bytes at the specified output rate.
|
||||||
|
"""
|
||||||
# Convert a-law to PCM
|
# Convert a-law to PCM
|
||||||
in_pcm_bytes = audioop.alaw2lin(alaw_bytes, 2)
|
in_pcm_bytes = audioop.alaw2lin(alaw_bytes, 2)
|
||||||
|
|
||||||
@@ -109,6 +257,17 @@ async def alaw_to_pcm(
|
|||||||
|
|
||||||
|
|
||||||
async def pcm_to_alaw(pcm_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler):
|
async def pcm_to_alaw(pcm_bytes: bytes, in_rate: int, out_rate: int, resampler: BaseAudioResampler):
|
||||||
|
"""Convert PCM audio to A-law encoding and optionally resample.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pcm_bytes: PCM audio data as raw bytes (16-bit signed integers).
|
||||||
|
in_rate: Original sample rate of the PCM audio in Hz.
|
||||||
|
out_rate: Desired output sample rate in Hz.
|
||||||
|
resampler: Audio resampler instance for rate conversion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A-law encoded audio data as raw bytes at the specified output rate.
|
||||||
|
"""
|
||||||
# Resample
|
# Resample
|
||||||
in_pcm_bytes = await resampler.resample(pcm_bytes, in_rate, out_rate)
|
in_pcm_bytes = await resampler.resample(pcm_bytes, in_rate, out_rate)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Silero Voice Activity Detection (VAD) implementation for Pipecat.
|
||||||
|
|
||||||
|
This module provides a VAD analyzer based on the Silero VAD ONNX model,
|
||||||
|
which can detect voice activity in audio streams with high accuracy.
|
||||||
|
Supports 8kHz and 16kHz sample rates.
|
||||||
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -25,11 +32,20 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
|
|
||||||
class SileroOnnxModel:
|
class SileroOnnxModel:
|
||||||
|
"""ONNX runtime wrapper for the Silero VAD model.
|
||||||
|
|
||||||
|
Provides voice activity detection using the pre-trained Silero VAD model
|
||||||
|
with ONNX runtime for efficient inference. Handles model state management
|
||||||
|
and input validation for audio processing.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, path, force_onnx_cpu=True):
|
def __init__(self, path, force_onnx_cpu=True):
|
||||||
import numpy as np
|
"""Initialize the Silero ONNX model.
|
||||||
|
|
||||||
global np
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to the ONNX model file.
|
||||||
|
force_onnx_cpu: Whether to force CPU execution provider.
|
||||||
|
"""
|
||||||
opts = onnxruntime.SessionOptions()
|
opts = onnxruntime.SessionOptions()
|
||||||
opts.inter_op_num_threads = 1
|
opts.inter_op_num_threads = 1
|
||||||
opts.intra_op_num_threads = 1
|
opts.intra_op_num_threads = 1
|
||||||
@@ -45,6 +61,7 @@ class SileroOnnxModel:
|
|||||||
self.sample_rates = [8000, 16000]
|
self.sample_rates = [8000, 16000]
|
||||||
|
|
||||||
def _validate_input(self, x, sr: int):
|
def _validate_input(self, x, sr: int):
|
||||||
|
"""Validate and preprocess input audio data."""
|
||||||
if np.ndim(x) == 1:
|
if np.ndim(x) == 1:
|
||||||
x = np.expand_dims(x, 0)
|
x = np.expand_dims(x, 0)
|
||||||
if np.ndim(x) > 2:
|
if np.ndim(x) > 2:
|
||||||
@@ -60,12 +77,18 @@ class SileroOnnxModel:
|
|||||||
return x, sr
|
return x, sr
|
||||||
|
|
||||||
def reset_states(self, batch_size=1):
|
def reset_states(self, batch_size=1):
|
||||||
|
"""Reset the internal model states.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
batch_size: Batch size for state initialization. Defaults to 1.
|
||||||
|
"""
|
||||||
self._state = np.zeros((2, batch_size, 128), dtype="float32")
|
self._state = np.zeros((2, batch_size, 128), dtype="float32")
|
||||||
self._context = np.zeros((batch_size, 0), dtype="float32")
|
self._context = np.zeros((batch_size, 0), dtype="float32")
|
||||||
self._last_sr = 0
|
self._last_sr = 0
|
||||||
self._last_batch_size = 0
|
self._last_batch_size = 0
|
||||||
|
|
||||||
def __call__(self, x, sr: int):
|
def __call__(self, x, sr: int):
|
||||||
|
"""Process audio input through the VAD model."""
|
||||||
x, sr = self._validate_input(x, sr)
|
x, sr = self._validate_input(x, sr)
|
||||||
num_samples = 512 if sr == 16000 else 256
|
num_samples = 512 if sr == 16000 else 256
|
||||||
|
|
||||||
@@ -105,7 +128,20 @@ class SileroOnnxModel:
|
|||||||
|
|
||||||
|
|
||||||
class SileroVADAnalyzer(VADAnalyzer):
|
class SileroVADAnalyzer(VADAnalyzer):
|
||||||
|
"""Voice Activity Detection analyzer using the Silero VAD model.
|
||||||
|
|
||||||
|
Implements VAD analysis using the pre-trained Silero ONNX model for
|
||||||
|
accurate voice activity detection. Supports 8kHz and 16kHz sample rates
|
||||||
|
with automatic model state management and periodic resets.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *, sample_rate: Optional[int] = None, params: Optional[VADParams] = None):
|
def __init__(self, *, sample_rate: Optional[int] = None, params: Optional[VADParams] = None):
|
||||||
|
"""Initialize the Silero VAD analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Audio sample rate (8000 or 16000 Hz). If None, will be set later.
|
||||||
|
params: VAD parameters for detection thresholds and timing.
|
||||||
|
"""
|
||||||
super().__init__(sample_rate=sample_rate, params=params)
|
super().__init__(sample_rate=sample_rate, params=params)
|
||||||
|
|
||||||
logger.debug("Loading Silero VAD model...")
|
logger.debug("Loading Silero VAD model...")
|
||||||
@@ -137,6 +173,14 @@ class SileroVADAnalyzer(VADAnalyzer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def set_sample_rate(self, sample_rate: int):
|
def set_sample_rate(self, sample_rate: int):
|
||||||
|
"""Set the sample rate for audio processing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Audio sample rate (must be 8000 or 16000 Hz).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If sample rate is not 8000 or 16000 Hz.
|
||||||
|
"""
|
||||||
if sample_rate != 16000 and sample_rate != 8000:
|
if sample_rate != 16000 and sample_rate != 8000:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Silero VAD sample rate needs to be 16000 or 8000 (sample rate: {sample_rate})"
|
f"Silero VAD sample rate needs to be 16000 or 8000 (sample rate: {sample_rate})"
|
||||||
@@ -145,9 +189,22 @@ class SileroVADAnalyzer(VADAnalyzer):
|
|||||||
super().set_sample_rate(sample_rate)
|
super().set_sample_rate(sample_rate)
|
||||||
|
|
||||||
def num_frames_required(self) -> int:
|
def num_frames_required(self) -> int:
|
||||||
|
"""Get the number of audio frames required for VAD analysis.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of frames required (512 for 16kHz, 256 for 8kHz).
|
||||||
|
"""
|
||||||
return 512 if self.sample_rate == 16000 else 256
|
return 512 if self.sample_rate == 16000 else 256
|
||||||
|
|
||||||
def voice_confidence(self, buffer) -> float:
|
def voice_confidence(self, buffer) -> float:
|
||||||
|
"""Calculate voice activity confidence for the given audio buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: Audio buffer to analyze.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Voice confidence score between 0.0 and 1.0.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
audio_int16 = np.frombuffer(buffer, np.int16)
|
audio_int16 = np.frombuffer(buffer, np.int16)
|
||||||
# Divide by 32768 because we have signed 16-bit data.
|
# Divide by 32768 because we have signed 16-bit data.
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Voice Activity Detection (VAD) analyzer base classes and utilities.
|
||||||
|
|
||||||
|
This module provides the abstract base class for VAD analyzers and associated
|
||||||
|
data structures for voice activity detection in audio streams. Includes state
|
||||||
|
management, parameter configuration, and audio analysis framework.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -20,6 +27,15 @@ VAD_MIN_VOLUME = 0.6
|
|||||||
|
|
||||||
|
|
||||||
class VADState(Enum):
|
class VADState(Enum):
|
||||||
|
"""Voice Activity Detection states.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
QUIET: No voice activity detected.
|
||||||
|
STARTING: Voice activity beginning, transitioning from quiet.
|
||||||
|
SPEAKING: Active voice detected and confirmed.
|
||||||
|
STOPPING: Voice activity ending, transitioning to quiet.
|
||||||
|
"""
|
||||||
|
|
||||||
QUIET = 1
|
QUIET = 1
|
||||||
STARTING = 2
|
STARTING = 2
|
||||||
SPEAKING = 3
|
SPEAKING = 3
|
||||||
@@ -27,6 +43,15 @@ class VADState(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class VADParams(BaseModel):
|
class VADParams(BaseModel):
|
||||||
|
"""Configuration parameters for Voice Activity Detection.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
confidence: Minimum confidence threshold for voice detection.
|
||||||
|
start_secs: Duration to wait before confirming voice start.
|
||||||
|
stop_secs: Duration to wait before confirming voice stop.
|
||||||
|
min_volume: Minimum audio volume threshold for voice detection.
|
||||||
|
"""
|
||||||
|
|
||||||
confidence: float = VAD_CONFIDENCE
|
confidence: float = VAD_CONFIDENCE
|
||||||
start_secs: float = VAD_START_SECS
|
start_secs: float = VAD_START_SECS
|
||||||
stop_secs: float = VAD_STOP_SECS
|
stop_secs: float = VAD_STOP_SECS
|
||||||
@@ -34,7 +59,20 @@ class VADParams(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class VADAnalyzer(ABC):
|
class VADAnalyzer(ABC):
|
||||||
|
"""Abstract base class for Voice Activity Detection analyzers.
|
||||||
|
|
||||||
|
Provides the framework for implementing VAD analysis with configurable
|
||||||
|
parameters, state management, and audio processing capabilities.
|
||||||
|
Subclasses must implement the core voice confidence calculation.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *, sample_rate: Optional[int] = None, params: Optional[VADParams] = None):
|
def __init__(self, *, sample_rate: Optional[int] = None, params: Optional[VADParams] = None):
|
||||||
|
"""Initialize the VAD analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Audio sample rate in Hz. If None, will be set later.
|
||||||
|
params: VAD parameters for detection configuration.
|
||||||
|
"""
|
||||||
self._init_sample_rate = sample_rate
|
self._init_sample_rate = sample_rate
|
||||||
self._sample_rate = 0
|
self._sample_rate = 0
|
||||||
self._params = params or VADParams()
|
self._params = params or VADParams()
|
||||||
@@ -48,29 +86,67 @@ class VADAnalyzer(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sample_rate(self) -> int:
|
def sample_rate(self) -> int:
|
||||||
|
"""Get the current sample rate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current audio sample rate in Hz.
|
||||||
|
"""
|
||||||
return self._sample_rate
|
return self._sample_rate
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def num_channels(self) -> int:
|
def num_channels(self) -> int:
|
||||||
|
"""Get the number of audio channels.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of audio channels (always 1 for mono).
|
||||||
|
"""
|
||||||
return self._num_channels
|
return self._num_channels
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def params(self) -> VADParams:
|
def params(self) -> VADParams:
|
||||||
|
"""Get the current VAD parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current VAD configuration parameters.
|
||||||
|
"""
|
||||||
return self._params
|
return self._params
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def num_frames_required(self) -> int:
|
def num_frames_required(self) -> int:
|
||||||
|
"""Get the number of audio frames required for analysis.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of frames needed for VAD processing.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def voice_confidence(self, buffer) -> float:
|
def voice_confidence(self, buffer) -> float:
|
||||||
|
"""Calculate voice activity confidence for the given audio buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: Audio buffer to analyze.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Voice confidence score between 0.0 and 1.0.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_sample_rate(self, sample_rate: int):
|
def set_sample_rate(self, sample_rate: int):
|
||||||
|
"""Set the sample rate for audio processing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Audio sample rate in Hz.
|
||||||
|
"""
|
||||||
self._sample_rate = self._init_sample_rate or sample_rate
|
self._sample_rate = self._init_sample_rate or sample_rate
|
||||||
self.set_params(self._params)
|
self.set_params(self._params)
|
||||||
|
|
||||||
def set_params(self, params: VADParams):
|
def set_params(self, params: VADParams):
|
||||||
|
"""Set VAD parameters and recalculate internal values.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: VAD parameters for detection configuration.
|
||||||
|
"""
|
||||||
logger.debug(f"Setting VAD params to: {params}")
|
logger.debug(f"Setting VAD params to: {params}")
|
||||||
self._params = params
|
self._params = params
|
||||||
self._vad_frames = self.num_frames_required()
|
self._vad_frames = self.num_frames_required()
|
||||||
@@ -85,10 +161,22 @@ class VADAnalyzer(ABC):
|
|||||||
self._vad_state: VADState = VADState.QUIET
|
self._vad_state: VADState = VADState.QUIET
|
||||||
|
|
||||||
def _get_smoothed_volume(self, audio: bytes) -> float:
|
def _get_smoothed_volume(self, audio: bytes) -> float:
|
||||||
|
"""Calculate smoothed audio volume using exponential smoothing."""
|
||||||
volume = calculate_audio_volume(audio, self.sample_rate)
|
volume = calculate_audio_volume(audio, self.sample_rate)
|
||||||
return exp_smoothing(volume, self._prev_volume, self._smoothing_factor)
|
return exp_smoothing(volume, self._prev_volume, self._smoothing_factor)
|
||||||
|
|
||||||
def analyze_audio(self, buffer) -> VADState:
|
def analyze_audio(self, buffer) -> VADState:
|
||||||
|
"""Analyze audio buffer and return current VAD state.
|
||||||
|
|
||||||
|
Processes incoming audio data, maintains internal state, and determines
|
||||||
|
voice activity status based on confidence and volume thresholds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: Audio buffer to analyze.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current VAD state after processing the buffer.
|
||||||
|
"""
|
||||||
self._vad_buffer += buffer
|
self._vad_buffer += buffer
|
||||||
|
|
||||||
num_required_bytes = self._vad_frames_num_bytes
|
num_required_bytes = self._vad_frames_num_bytes
|
||||||
|
|||||||
@@ -4,14 +4,33 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base clock interface for Pipecat timing operations."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class BaseClock(ABC):
|
class BaseClock(ABC):
|
||||||
|
"""Abstract base class for clock implementations.
|
||||||
|
|
||||||
|
Provides a common interface for timing operations used in Pipecat
|
||||||
|
for synchronization, scheduling, and time-based processing.
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_time(self) -> int:
|
def get_time(self) -> int:
|
||||||
|
"""Get the current time value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The current time as an integer value. The specific unit and
|
||||||
|
reference point depend on the concrete implementation.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Start or initialize the clock.
|
||||||
|
|
||||||
|
Performs any necessary initialization or starts the timing mechanism.
|
||||||
|
This method should be called before using get_time().
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,17 +4,42 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""System clock implementation for Pipecat."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from pipecat.clocks.base_clock import BaseClock
|
from pipecat.clocks.base_clock import BaseClock
|
||||||
|
|
||||||
|
|
||||||
class SystemClock(BaseClock):
|
class SystemClock(BaseClock):
|
||||||
|
"""A monotonic clock implementation using system time.
|
||||||
|
|
||||||
|
Provides high-precision timing using the system's monotonic clock,
|
||||||
|
which is not affected by system clock adjustments and is suitable
|
||||||
|
for measuring elapsed time in real-time applications.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Initialize the system clock.
|
||||||
|
|
||||||
|
The clock starts in an uninitialized state and must be started
|
||||||
|
explicitly using the start() method before time measurement begins.
|
||||||
|
"""
|
||||||
self._time = 0
|
self._time = 0
|
||||||
|
|
||||||
def get_time(self) -> int:
|
def get_time(self) -> int:
|
||||||
|
"""Get the elapsed time since the clock was started.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The elapsed time in nanoseconds since start() was called.
|
||||||
|
Returns 0 if the clock has not been started yet.
|
||||||
|
"""
|
||||||
return time.monotonic_ns() - self._time if self._time > 0 else 0
|
return time.monotonic_ns() - self._time if self._time > 0 else 0
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Start the clock and begin time measurement.
|
||||||
|
|
||||||
|
Records the current monotonic time as the reference point
|
||||||
|
for all subsequent get_time() calls.
|
||||||
|
"""
|
||||||
self._time = time.monotonic_ns()
|
self._time = time.monotonic_ns()
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Daily.co room configuration utilities for Pipecat examples."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -14,6 +16,17 @@ from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
|||||||
|
|
||||||
|
|
||||||
async def configure(aiohttp_session: aiohttp.ClientSession):
|
async def configure(aiohttp_session: aiohttp.ClientSession):
|
||||||
|
"""Configure Daily.co room URL and token from arguments or environment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aiohttp_session: HTTP session for making API requests.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing the room URL and authentication token.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If room URL or API key are not provided.
|
||||||
|
"""
|
||||||
(url, token, _) = await configure_with_args(aiohttp_session)
|
(url, token, _) = await configure_with_args(aiohttp_session)
|
||||||
return (url, token)
|
return (url, token)
|
||||||
|
|
||||||
@@ -21,6 +34,18 @@ async def configure(aiohttp_session: aiohttp.ClientSession):
|
|||||||
async def configure_with_args(
|
async def configure_with_args(
|
||||||
aiohttp_session: aiohttp.ClientSession, parser: Optional[argparse.ArgumentParser] = None
|
aiohttp_session: aiohttp.ClientSession, parser: Optional[argparse.ArgumentParser] = None
|
||||||
):
|
):
|
||||||
|
"""Configure Daily.co room with command-line argument parsing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aiohttp_session: HTTP session for making API requests.
|
||||||
|
parser: Optional argument parser. If None, creates a default one.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing room URL, authentication token, and parsed arguments.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If room URL or API key are not provided via arguments or environment.
|
||||||
|
"""
|
||||||
if not parser:
|
if not parser:
|
||||||
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@@ -4,10 +4,18 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Pipecat example runner with support for multiple transport types.
|
||||||
|
|
||||||
|
This module provides a unified interface for running Pipecat examples across
|
||||||
|
different transport types including Daily.co, WebRTC, and Twilio. It handles
|
||||||
|
setup, configuration, and lifecycle management for each transport type.
|
||||||
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Callable, Dict, Mapping, Optional
|
from typing import Any, Callable, Dict, Mapping, Optional
|
||||||
@@ -35,6 +43,15 @@ load_dotenv(override=True)
|
|||||||
|
|
||||||
|
|
||||||
def get_transport_client_id(transport: BaseTransport, client: Any) -> str:
|
def get_transport_client_id(transport: BaseTransport, client: Any) -> str:
|
||||||
|
"""Get client identifier from transport-specific client object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transport: The transport instance.
|
||||||
|
client: Transport-specific client object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Client identifier string, empty if transport not supported.
|
||||||
|
"""
|
||||||
if isinstance(transport, SmallWebRTCTransport):
|
if isinstance(transport, SmallWebRTCTransport):
|
||||||
return client.pc_id
|
return client.pc_id
|
||||||
elif isinstance(transport, DailyTransport):
|
elif isinstance(transport, DailyTransport):
|
||||||
@@ -46,6 +63,13 @@ def get_transport_client_id(transport: BaseTransport, client: Any) -> str:
|
|||||||
async def maybe_capture_participant_camera(
|
async def maybe_capture_participant_camera(
|
||||||
transport: BaseTransport, client: Any, framerate: int = 0
|
transport: BaseTransport, client: Any, framerate: int = 0
|
||||||
):
|
):
|
||||||
|
"""Capture participant camera video if transport supports it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transport: The transport instance.
|
||||||
|
client: Transport-specific client object.
|
||||||
|
framerate: Video capture framerate. Defaults to 0 (auto).
|
||||||
|
"""
|
||||||
if isinstance(transport, DailyTransport):
|
if isinstance(transport, DailyTransport):
|
||||||
await transport.capture_participant_video(
|
await transport.capture_participant_video(
|
||||||
client["id"], framerate=framerate, video_source="camera"
|
client["id"], framerate=framerate, video_source="camera"
|
||||||
@@ -55,17 +79,84 @@ async def maybe_capture_participant_camera(
|
|||||||
async def maybe_capture_participant_screen(
|
async def maybe_capture_participant_screen(
|
||||||
transport: BaseTransport, client: Any, framerate: int = 0
|
transport: BaseTransport, client: Any, framerate: int = 0
|
||||||
):
|
):
|
||||||
|
"""Capture participant screen video if transport supports it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transport: The transport instance.
|
||||||
|
client: Transport-specific client object.
|
||||||
|
framerate: Video capture framerate. Defaults to 0 (auto).
|
||||||
|
"""
|
||||||
if isinstance(transport, DailyTransport):
|
if isinstance(transport, DailyTransport):
|
||||||
await transport.capture_participant_video(
|
await transport.capture_participant_video(
|
||||||
client["id"], framerate=framerate, video_source="screenVideo"
|
client["id"], framerate=framerate, video_source="screenVideo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def smallwebrtc_sdp_cleanup_ice_candidates(text: str, pattern: str) -> str:
|
||||||
|
"""Clean up ICE candidates in SDP text for SmallWebRTC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: SDP text to clean up.
|
||||||
|
pattern: Pattern to match for candidate filtering.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned SDP text with filtered ICE candidates.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
lines = text.splitlines()
|
||||||
|
for line in lines:
|
||||||
|
if re.search("a=candidate", line):
|
||||||
|
if re.search(pattern, line) and not re.search("raddr", line):
|
||||||
|
result.append(line)
|
||||||
|
else:
|
||||||
|
result.append(line)
|
||||||
|
return "\r\n".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def smallwebrtc_sdp_cleanup_fingerprints(text: str) -> str:
|
||||||
|
"""Remove unsupported fingerprint algorithms from SDP text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: SDP text to clean up.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SDP text with sha-384 and sha-512 fingerprints removed.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
lines = text.splitlines()
|
||||||
|
for line in lines:
|
||||||
|
if not re.search("sha-384", line) and not re.search("sha-512", line):
|
||||||
|
result.append(line)
|
||||||
|
return "\r\n".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def smallwebrtc_sdp_munging(sdp: str, host: str) -> str:
|
||||||
|
"""Apply SDP modifications for SmallWebRTC compatibility.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sdp: Original SDP string.
|
||||||
|
host: Host address for ICE candidate filtering.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Modified SDP string with fingerprint and ICE candidate cleanup.
|
||||||
|
"""
|
||||||
|
sdp = smallwebrtc_sdp_cleanup_fingerprints(sdp)
|
||||||
|
sdp = smallwebrtc_sdp_cleanup_ice_candidates(sdp, host)
|
||||||
|
return sdp
|
||||||
|
|
||||||
|
|
||||||
def run_example_daily(
|
def run_example_daily(
|
||||||
run_example: Callable,
|
run_example: Callable,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
transport_params: Mapping[str, Callable] = {},
|
transport_params: Mapping[str, Callable] = {},
|
||||||
):
|
):
|
||||||
|
"""Run example using Daily.co transport.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_example: The example function to run.
|
||||||
|
args: Parsed command-line arguments.
|
||||||
|
transport_params: Mapping of transport names to parameter factory functions.
|
||||||
|
"""
|
||||||
logger.info("Running example with DailyTransport...")
|
logger.info("Running example with DailyTransport...")
|
||||||
|
|
||||||
from pipecat.examples.daily_runner import configure
|
from pipecat.examples.daily_runner import configure
|
||||||
@@ -87,6 +178,13 @@ def run_example_webrtc(
|
|||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
transport_params: Mapping[str, Callable] = {},
|
transport_params: Mapping[str, Callable] = {},
|
||||||
):
|
):
|
||||||
|
"""Run example using WebRTC transport with FastAPI server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_example: The example function to run.
|
||||||
|
args: Parsed command-line arguments.
|
||||||
|
transport_params: Mapping of transport names to parameter factory functions.
|
||||||
|
"""
|
||||||
logger.info("Running example with SmallWebRTCTransport...")
|
logger.info("Running example with SmallWebRTCTransport...")
|
||||||
|
|
||||||
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
||||||
@@ -96,21 +194,25 @@ def run_example_webrtc(
|
|||||||
# Store connections by pc_id
|
# Store connections by pc_id
|
||||||
pcs_map: Dict[str, SmallWebRTCConnection] = {}
|
pcs_map: Dict[str, SmallWebRTCConnection] = {}
|
||||||
|
|
||||||
ice_servers = [
|
|
||||||
IceServer(
|
|
||||||
urls="stun:stun.l.google.com:19302",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Mount the frontend at /
|
# Mount the frontend at /
|
||||||
app.mount("/client", SmallWebRTCPrebuiltUI)
|
app.mount("/client", SmallWebRTCPrebuiltUI)
|
||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
async def root_redirect():
|
async def root_redirect():
|
||||||
|
"""Redirect root requests to client interface."""
|
||||||
return RedirectResponse(url="/client/")
|
return RedirectResponse(url="/client/")
|
||||||
|
|
||||||
@app.post("/api/offer")
|
@app.post("/api/offer")
|
||||||
async def offer(request: dict, background_tasks: BackgroundTasks):
|
async def offer(request: dict, background_tasks: BackgroundTasks):
|
||||||
|
"""Handle WebRTC offer requests and manage peer connections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: WebRTC offer request containing SDP and connection details.
|
||||||
|
background_tasks: FastAPI background tasks for running examples.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
WebRTC answer with connection details.
|
||||||
|
"""
|
||||||
pc_id = request.get("pc_id")
|
pc_id = request.get("pc_id")
|
||||||
|
|
||||||
if pc_id and pc_id in pcs_map:
|
if pc_id and pc_id in pcs_map:
|
||||||
@@ -122,11 +224,16 @@ def run_example_webrtc(
|
|||||||
restart_pc=request.get("restart_pc", False),
|
restart_pc=request.get("restart_pc", False),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pipecat_connection = SmallWebRTCConnection(ice_servers)
|
pipecat_connection = SmallWebRTCConnection()
|
||||||
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
||||||
|
|
||||||
@pipecat_connection.event_handler("closed")
|
@pipecat_connection.event_handler("closed")
|
||||||
async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
|
async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
|
||||||
|
"""Handle WebRTC connection closure and cleanup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webrtc_connection: The closed WebRTC connection.
|
||||||
|
"""
|
||||||
logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
|
logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
|
||||||
pcs_map.pop(webrtc_connection.pc_id, None)
|
pcs_map.pop(webrtc_connection.pc_id, None)
|
||||||
|
|
||||||
@@ -136,6 +243,10 @@ def run_example_webrtc(
|
|||||||
background_tasks.add_task(run_example, transport, args, False)
|
background_tasks.add_task(run_example, transport, args, False)
|
||||||
|
|
||||||
answer = pipecat_connection.get_answer()
|
answer = pipecat_connection.get_answer()
|
||||||
|
|
||||||
|
if args.esp32 and args.host:
|
||||||
|
answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], args.host)
|
||||||
|
|
||||||
# Updating the peer connection inside the map
|
# Updating the peer connection inside the map
|
||||||
pcs_map[answer["pc_id"]] = pipecat_connection
|
pcs_map[answer["pc_id"]] = pipecat_connection
|
||||||
|
|
||||||
@@ -143,6 +254,14 @@ def run_example_webrtc(
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Manage FastAPI application lifecycle and cleanup connections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: The FastAPI application instance.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Control to the FastAPI application runtime.
|
||||||
|
"""
|
||||||
yield # Run app
|
yield # Run app
|
||||||
coros = [pc.disconnect() for pc in pcs_map.values()]
|
coros = [pc.disconnect() for pc in pcs_map.values()]
|
||||||
await asyncio.gather(*coros)
|
await asyncio.gather(*coros)
|
||||||
@@ -156,6 +275,13 @@ def run_example_twilio(
|
|||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
transport_params: Mapping[str, Callable] = {},
|
transport_params: Mapping[str, Callable] = {},
|
||||||
):
|
):
|
||||||
|
"""Run example using Twilio transport with FastAPI WebSocket server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_example: The example function to run.
|
||||||
|
args: Parsed command-line arguments.
|
||||||
|
transport_params: Mapping of transport names to parameter factory functions.
|
||||||
|
"""
|
||||||
logger.info("Running example with FastAPIWebsocketTransport (Twilio)...")
|
logger.info("Running example with FastAPIWebsocketTransport (Twilio)...")
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -170,6 +296,11 @@ def run_example_twilio(
|
|||||||
|
|
||||||
@app.post("/")
|
@app.post("/")
|
||||||
async def start_call():
|
async def start_call():
|
||||||
|
"""Handle Twilio webhook and return TwiML response.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TwiML XML response directing call to WebSocket stream.
|
||||||
|
"""
|
||||||
logger.debug("POST TwiML")
|
logger.debug("POST TwiML")
|
||||||
|
|
||||||
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -184,6 +315,11 @@ def run_example_twilio(
|
|||||||
|
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
"""Handle Twilio WebSocket connections for voice streaming.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
websocket: The WebSocket connection from Twilio.
|
||||||
|
"""
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
logger.debug("WebSocket connection accepted")
|
logger.debug("WebSocket connection accepted")
|
||||||
@@ -216,6 +352,13 @@ def run_main(
|
|||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
transport_params: Mapping[str, Callable] = {},
|
transport_params: Mapping[str, Callable] = {},
|
||||||
):
|
):
|
||||||
|
"""Run the example with the specified transport type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_example: The example function to run.
|
||||||
|
args: Parsed command-line arguments.
|
||||||
|
transport_params: Mapping of transport names to parameter factory functions.
|
||||||
|
"""
|
||||||
if args.transport not in transport_params:
|
if args.transport not in transport_params:
|
||||||
logger.error(f"Transport '{args.transport}' not supported by this example")
|
logger.error(f"Transport '{args.transport}' not supported by this example")
|
||||||
return
|
return
|
||||||
@@ -235,6 +378,13 @@ def main(
|
|||||||
parser: Optional[argparse.ArgumentParser] = None,
|
parser: Optional[argparse.ArgumentParser] = None,
|
||||||
transport_params: Mapping[str, Callable] = {},
|
transport_params: Mapping[str, Callable] = {},
|
||||||
):
|
):
|
||||||
|
"""Main entry point for running Pipecat examples with transport selection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_example: The example function to run.
|
||||||
|
parser: Optional argument parser. If None, creates a default one.
|
||||||
|
transport_params: Mapping of transport names to parameter factory functions.
|
||||||
|
"""
|
||||||
if not parser:
|
if not parser:
|
||||||
parser = argparse.ArgumentParser(description="Pipecat Bot Runner")
|
parser = argparse.ArgumentParser(description="Pipecat Bot Runner")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -254,9 +404,16 @@ def main(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--proxy", "-x", help="A public proxy host name (no protocol, e.g. proxy.example.com)"
|
"--proxy", "-x", help="A public proxy host name (no protocol, e.g. proxy.example.com)"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--esp32", action="store_true", default=False, help="Perform SDP munging for the ESP32"
|
||||||
|
)
|
||||||
parser.add_argument("--verbose", "-v", action="count", default=0)
|
parser.add_argument("--verbose", "-v", action="count", default=0)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.esp32 and args.host == "localhost":
|
||||||
|
logger.error("For ESP32, you need to specify `--host IP` so we can do SDP munging.")
|
||||||
|
return
|
||||||
|
|
||||||
# Log level
|
# Log level
|
||||||
logger.remove(0)
|
logger.remove(0)
|
||||||
logger.add(sys.stderr, level="TRACE" if args.verbose else "DEBUG")
|
logger.add(sys.stderr, level="TRACE" if args.verbose else "DEBUG")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,64 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Metrics data models for Pipecat framework.
|
||||||
|
|
||||||
|
This module defines Pydantic models for various types of metrics data
|
||||||
|
collected throughout the pipeline, including timing, token usage, and
|
||||||
|
processing statistics.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class MetricsData(BaseModel):
|
class MetricsData(BaseModel):
|
||||||
|
"""Base class for all metrics data.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
processor: Name of the processor generating the metrics.
|
||||||
|
model: Optional model name associated with the metrics.
|
||||||
|
"""
|
||||||
|
|
||||||
processor: str
|
processor: str
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TTFBMetricsData(MetricsData):
|
class TTFBMetricsData(MetricsData):
|
||||||
|
"""Time To First Byte (TTFB) metrics data.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
value: TTFB measurement in seconds.
|
||||||
|
"""
|
||||||
|
|
||||||
value: float
|
value: float
|
||||||
|
|
||||||
|
|
||||||
class ProcessingMetricsData(MetricsData):
|
class ProcessingMetricsData(MetricsData):
|
||||||
|
"""General processing time metrics data.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
value: Processing time measurement in seconds.
|
||||||
|
"""
|
||||||
|
|
||||||
value: float
|
value: float
|
||||||
|
|
||||||
|
|
||||||
class LLMTokenUsage(BaseModel):
|
class LLMTokenUsage(BaseModel):
|
||||||
|
"""Token usage statistics for LLM operations.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
prompt_tokens: Number of tokens in the input prompt.
|
||||||
|
completion_tokens: Number of tokens in the generated completion.
|
||||||
|
total_tokens: Total number of tokens used (prompt + completion).
|
||||||
|
cache_read_input_tokens: Number of tokens read from cache, if applicable.
|
||||||
|
cache_creation_input_tokens: Number of tokens used to create cache entries, if applicable.
|
||||||
|
"""
|
||||||
|
|
||||||
prompt_tokens: int
|
prompt_tokens: int
|
||||||
completion_tokens: int
|
completion_tokens: int
|
||||||
total_tokens: int
|
total_tokens: int
|
||||||
@@ -26,15 +68,35 @@ class LLMTokenUsage(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class LLMUsageMetricsData(MetricsData):
|
class LLMUsageMetricsData(MetricsData):
|
||||||
|
"""LLM token usage metrics data.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
value: Token usage statistics for the LLM operation.
|
||||||
|
"""
|
||||||
|
|
||||||
value: LLMTokenUsage
|
value: LLMTokenUsage
|
||||||
|
|
||||||
|
|
||||||
class TTSUsageMetricsData(MetricsData):
|
class TTSUsageMetricsData(MetricsData):
|
||||||
|
"""Text-to-Speech usage metrics data.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
value: Number of characters processed by TTS.
|
||||||
|
"""
|
||||||
|
|
||||||
value: int
|
value: int
|
||||||
|
|
||||||
|
|
||||||
class SmartTurnMetricsData(MetricsData):
|
class SmartTurnMetricsData(MetricsData):
|
||||||
"""Metrics data for smart turn predictions."""
|
"""Metrics data for smart turn predictions.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
is_complete: Whether the turn is predicted to be complete.
|
||||||
|
probability: Confidence probability of the turn completion prediction.
|
||||||
|
inference_time_ms: Time taken for inference in milliseconds.
|
||||||
|
server_total_time_ms: Total server processing time in milliseconds.
|
||||||
|
e2e_processing_time_ms: End-to-end processing time in milliseconds.
|
||||||
|
"""
|
||||||
|
|
||||||
is_complete: bool
|
is_complete: bool
|
||||||
probability: float
|
probability: float
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base observer classes for monitoring frame flow in the Pipecat pipeline.
|
||||||
|
|
||||||
|
This module provides the foundation for observing frame transfers between
|
||||||
|
processors without modifying the pipeline structure. Observers can be used
|
||||||
|
for logging, debugging, analytics, and monitoring pipeline behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -18,19 +25,19 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FramePushed:
|
class FramePushed:
|
||||||
"""Represents an event where a frame is pushed from one processor to another
|
"""Event data for frame transfers between processors in the pipeline.
|
||||||
within the pipeline.
|
|
||||||
|
|
||||||
This data structure is typically used by observers to track the flow of
|
Represents an event where a frame is pushed from one processor to another
|
||||||
frames through the pipeline for logging, debugging, or analytics purposes.
|
within the pipeline. This data structure is typically used by observers
|
||||||
|
to track the flow of frames through the pipeline for logging, debugging,
|
||||||
Attributes:
|
or analytics purposes.
|
||||||
source (FrameProcessor): The processor sending the frame.
|
|
||||||
destination (FrameProcessor): The processor receiving the frame.
|
|
||||||
frame (Frame): The frame being transferred.
|
|
||||||
direction (FrameDirection): The direction of the transfer (e.g., downstream or upstream).
|
|
||||||
timestamp (int): The time when the frame was pushed, based on the pipeline clock.
|
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
source: The processor sending the frame.
|
||||||
|
destination: The processor receiving the frame.
|
||||||
|
frame: The frame being transferred.
|
||||||
|
direction: The direction of the transfer (e.g., downstream or upstream).
|
||||||
|
timestamp: The time when the frame was pushed, based on the pipeline clock.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source: "FrameProcessor"
|
source: "FrameProcessor"
|
||||||
@@ -41,11 +48,12 @@ class FramePushed:
|
|||||||
|
|
||||||
|
|
||||||
class BaseObserver(BaseObject):
|
class BaseObserver(BaseObject):
|
||||||
"""This is the base class for pipeline frame observers. Observers can view
|
"""Base class for pipeline frame observers.
|
||||||
all the frames that go through the pipeline without the need to inject
|
|
||||||
processors in the pipeline. This can be useful, for example, to implement
|
|
||||||
frame loggers or debuggers among other things.
|
|
||||||
|
|
||||||
|
Observers can view all frames that flow through the pipeline without
|
||||||
|
needing to inject processors into the pipeline structure. This enables
|
||||||
|
non-intrusive monitoring capabilities such as frame logging, debugging,
|
||||||
|
performance analysis, and analytics collection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -57,7 +65,6 @@ class BaseObserver(BaseObject):
|
|||||||
transferred through the pipeline.
|
transferred through the pipeline.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (FramePushed): The event data containing details about the frame transfer.
|
data: The event data containing details about the frame transfer.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Debug logging observer for frame activity monitoring.
|
||||||
|
|
||||||
|
This module provides a debug observer that logs detailed frame activity
|
||||||
|
to the console, making it useful for debugging pipeline behavior and
|
||||||
|
understanding frame flow between processors.
|
||||||
|
"""
|
||||||
|
|
||||||
from dataclasses import fields, is_dataclass
|
from dataclasses import fields, is_dataclass
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Dict, Optional, Set, Tuple, Type, Union
|
from typing import Dict, Optional, Set, Tuple, Type, Union
|
||||||
@@ -16,7 +23,12 @@ from pipecat.processors.frame_processor import FrameDirection
|
|||||||
|
|
||||||
|
|
||||||
class FrameEndpoint(Enum):
|
class FrameEndpoint(Enum):
|
||||||
"""Specifies which endpoint (source or destination) to filter on."""
|
"""Specifies which endpoint (source or destination) to filter on.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
SOURCE: Filter on the source component that is pushing the frame.
|
||||||
|
DESTINATION: Filter on the destination component receiving the frame.
|
||||||
|
"""
|
||||||
|
|
||||||
SOURCE = auto()
|
SOURCE = auto()
|
||||||
DESTINATION = auto()
|
DESTINATION = auto()
|
||||||
@@ -28,44 +40,37 @@ class DebugLogObserver(BaseObserver):
|
|||||||
Automatically extracts and formats data from any frame type, making it useful
|
Automatically extracts and formats data from any frame type, making it useful
|
||||||
for debugging pipeline behavior without needing frame-specific observers.
|
for debugging pipeline behavior without needing frame-specific observers.
|
||||||
|
|
||||||
Args:
|
|
||||||
frame_types: Optional tuple of frame types to log, or a dict with frame type
|
|
||||||
filters. If None, logs all frame types.
|
|
||||||
exclude_fields: Optional set of field names to exclude from logging.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
Log all frames from all services:
|
Log all frames from all services::
|
||||||
```python
|
|
||||||
observers = DebugLogObserver()
|
|
||||||
```
|
|
||||||
|
|
||||||
Log specific frame types from any source/destination:
|
observers = DebugLogObserver()
|
||||||
```python
|
|
||||||
from pipecat.frames.frames import TranscriptionFrame, InterimTranscriptionFrame
|
|
||||||
observers=[
|
|
||||||
DebugLogObserver(frame_types=(LLMTextFrame,TranscriptionFrame,)),
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
Log frames with specific source/destination filters:
|
Log specific frame types from any source/destination::
|
||||||
```python
|
|
||||||
from pipecat.frames.frames import StartInterruptionFrame, UserStartedSpeakingFrame, LLMTextFrame
|
|
||||||
from pipecat.transports.base_output_transport import BaseOutputTransport
|
|
||||||
from pipecat.services.stt_service import STTService
|
|
||||||
|
|
||||||
observers=[
|
from pipecat.frames.frames import LLMTextFrame, TranscriptionFrame
|
||||||
DebugLogObserver(
|
observers=[
|
||||||
frame_types={
|
DebugLogObserver(frame_types=(LLMTextFrame,TranscriptionFrame,)),
|
||||||
# Only log StartInterruptionFrame when source is BaseOutputTransport
|
]
|
||||||
StartInterruptionFrame: (BaseOutputTransport, FrameEndpoint.SOURCE),
|
|
||||||
# Only log UserStartedSpeakingFrame when destination is STTService
|
Log frames with specific source/destination filters::
|
||||||
UserStartedSpeakingFrame: (STTService, FrameEndpoint.DESTINATION),
|
|
||||||
# Log LLMTextFrame regardless of source or destination type
|
from pipecat.frames.frames import StartInterruptionFrame, UserStartedSpeakingFrame, LLMTextFrame
|
||||||
LLMTextFrame: None,
|
from pipecat.observers.loggers.debug_log_observer import DebugLogObserver, FrameEndpoint
|
||||||
}
|
from pipecat.transports.base_output import BaseOutputTransport
|
||||||
),
|
from pipecat.services.stt_service import STTService
|
||||||
],
|
|
||||||
```
|
observers=[
|
||||||
|
DebugLogObserver(
|
||||||
|
frame_types={
|
||||||
|
# Only log StartInterruptionFrame when source is BaseOutputTransport
|
||||||
|
StartInterruptionFrame: (BaseOutputTransport, FrameEndpoint.SOURCE),
|
||||||
|
# Only log UserStartedSpeakingFrame when destination is STTService
|
||||||
|
UserStartedSpeakingFrame: (STTService, FrameEndpoint.DESTINATION),
|
||||||
|
# Log LLMTextFrame regardless of source or destination type
|
||||||
|
LLMTextFrame: None,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -79,14 +84,17 @@ class DebugLogObserver(BaseObserver):
|
|||||||
"""Initialize the debug log observer.
|
"""Initialize the debug log observer.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frame_types: Tuple of frame types to log, or a dict mapping frame types to
|
frame_types: Frame types to log. Can be:
|
||||||
filter configurations. Filter configs can be:
|
|
||||||
- None to log all instances of the frame type
|
- Tuple of frame types to log all instances
|
||||||
- A tuple of (service_type, endpoint) to filter on a specific service
|
- Dict mapping frame types to filter configurations
|
||||||
and endpoint (SOURCE or DESTINATION)
|
- None to log all frames
|
||||||
If None is provided instead of a tuple/dict, log all frames.
|
|
||||||
exclude_fields: Set of field names to exclude from logging. If None, only binary
|
Filter configurations can be None (log all instances) or a tuple
|
||||||
data fields are excluded.
|
of (service_type, endpoint) to filter on specific services.
|
||||||
|
exclude_fields: Field names to exclude from logging. Defaults to
|
||||||
|
excluding binary data fields like 'audio', 'image', 'images'.
|
||||||
|
**kwargs: Additional arguments passed to parent class.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@@ -113,14 +121,7 @@ class DebugLogObserver(BaseObserver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _format_value(self, value):
|
def _format_value(self, value):
|
||||||
"""Format a value for logging.
|
"""Format a value for logging."""
|
||||||
|
|
||||||
Args:
|
|
||||||
value: The value to format.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A string representation of the value suitable for logging.
|
|
||||||
"""
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return "None"
|
return "None"
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
@@ -143,16 +144,7 @@ class DebugLogObserver(BaseObserver):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
def _should_log_frame(self, frame, src, dst):
|
def _should_log_frame(self, frame, src, dst):
|
||||||
"""Determine if a frame should be logged based on filters.
|
"""Determine if a frame should be logged based on filters."""
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The frame being processed
|
|
||||||
src: The source component
|
|
||||||
dst: The destination component
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the frame should be logged, False otherwise
|
|
||||||
"""
|
|
||||||
# If no filters, log all frames
|
# If no filters, log all frames
|
||||||
if not self.frame_filters:
|
if not self.frame_filters:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""LLM logging observer for Pipecat."""
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
@@ -34,10 +36,15 @@ class LLMLogObserver(BaseObserver):
|
|||||||
|
|
||||||
This allows you to track when the LLM starts responding, what it generates,
|
This allows you to track when the LLM starts responding, what it generates,
|
||||||
and when it finishes.
|
and when it finishes.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def on_push_frame(self, data: FramePushed):
|
async def on_push_frame(self, data: FramePushed):
|
||||||
|
"""Handle frame push events and log LLM-related activities.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The frame push event data containing source, destination,
|
||||||
|
frame, direction, and timestamp information.
|
||||||
|
"""
|
||||||
src = data.source
|
src = data.source
|
||||||
dst = data.destination
|
dst = data.destination
|
||||||
frame = data.frame
|
frame = data.frame
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Transcription logging observer for Pipecat.
|
||||||
|
|
||||||
|
This module provides an observer that logs transcription frames to the console,
|
||||||
|
allowing developers to monitor speech-to-text activity in real-time.
|
||||||
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
@@ -17,17 +23,23 @@ from pipecat.services.stt_service import STTService
|
|||||||
class TranscriptionLogObserver(BaseObserver):
|
class TranscriptionLogObserver(BaseObserver):
|
||||||
"""Observer to log transcription activity to the console.
|
"""Observer to log transcription activity to the console.
|
||||||
|
|
||||||
Logs all frame instances (only from STT service) of:
|
Monitors and logs all transcription frames from STT services, including
|
||||||
|
both final transcriptions and interim results. This allows developers
|
||||||
- TranscriptionFrame
|
to track speech recognition activity and debug transcription issues.
|
||||||
- InterimTranscriptionFrame
|
|
||||||
|
|
||||||
This allows you to track when the LLM starts responding, what it generates,
|
|
||||||
and when it finishes.
|
|
||||||
|
|
||||||
|
Only processes frames from STTService instances to avoid logging
|
||||||
|
unrelated transcription frames from other sources.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def on_push_frame(self, data: FramePushed):
|
async def on_push_frame(self, data: FramePushed):
|
||||||
|
"""Handle frame push events and log transcription frames.
|
||||||
|
|
||||||
|
Logs TranscriptionFrame and InterimTranscriptionFrame instances
|
||||||
|
with timestamps and user information for debugging purposes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Frame push event data containing source, frame, and timestamp.
|
||||||
|
"""
|
||||||
src = data.source
|
src = data.source
|
||||||
frame = data.frame
|
frame = data.frame
|
||||||
timestamp = data.timestamp
|
timestamp = data.timestamp
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Observer for measuring user-to-bot response latency."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -18,19 +20,28 @@ from pipecat.processors.frame_processor import FrameDirection
|
|||||||
|
|
||||||
|
|
||||||
class UserBotLatencyLogObserver(BaseObserver):
|
class UserBotLatencyLogObserver(BaseObserver):
|
||||||
"""Observer that logs the latency between when the user stops speaking and
|
"""Observer that measures time between user stopping speech and bot starting speech.
|
||||||
when the bot starts speaking.
|
|
||||||
|
|
||||||
This helps measure how quickly the AI services respond.
|
|
||||||
|
|
||||||
|
This helps measure how quickly the AI services respond by tracking
|
||||||
|
conversation turn timing and logging latency metrics.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Initialize the latency observer.
|
||||||
|
|
||||||
|
Sets up tracking for processed frames and user speech timing
|
||||||
|
to calculate response latencies.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._processed_frames = set()
|
self._processed_frames = set()
|
||||||
self._user_stopped_time = 0
|
self._user_stopped_time = 0
|
||||||
|
|
||||||
async def on_push_frame(self, data: FramePushed):
|
async def on_push_frame(self, data: FramePushed):
|
||||||
|
"""Process frames to track speech timing and calculate latency.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Frame push event containing the frame and direction information.
|
||||||
|
"""
|
||||||
# Only process downstream frames
|
# Only process downstream frames
|
||||||
if data.direction != FrameDirection.DOWNSTREAM:
|
if data.direction != FrameDirection.DOWNSTREAM:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Turn tracking observer for conversation flow monitoring.
|
||||||
|
|
||||||
|
This module provides an observer that monitors conversation turns in a pipeline,
|
||||||
|
tracking when turns start and end based on user and bot speech patterns.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
@@ -23,15 +29,30 @@ from pipecat.observers.base_observer import BaseObserver, FramePushed
|
|||||||
class TurnTrackingObserver(BaseObserver):
|
class TurnTrackingObserver(BaseObserver):
|
||||||
"""Observer that tracks conversation turns in a pipeline.
|
"""Observer that tracks conversation turns in a pipeline.
|
||||||
|
|
||||||
|
This observer monitors the flow of conversation by tracking when turns
|
||||||
|
start and end based on user and bot speaking patterns. It handles
|
||||||
|
interruptions, timeouts, and maintains turn state throughout the pipeline.
|
||||||
|
|
||||||
Turn tracking logic:
|
Turn tracking logic:
|
||||||
|
|
||||||
- The first turn starts immediately when the pipeline starts (StartFrame)
|
- The first turn starts immediately when the pipeline starts (StartFrame)
|
||||||
- Subsequent turns start when the user starts speaking
|
- Subsequent turns start when the user starts speaking
|
||||||
- A turn ends when the bot stops speaking and either:
|
- A turn ends when the bot stops speaking and either:
|
||||||
|
|
||||||
- The user starts speaking again
|
- The user starts speaking again
|
||||||
- A timeout period elapses with no more bot speech
|
- A timeout period elapses with no more bot speech
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, max_frames=100, turn_end_timeout_secs=2.5, **kwargs):
|
def __init__(self, max_frames=100, turn_end_timeout_secs=2.5, **kwargs):
|
||||||
|
"""Initialize the turn tracking observer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_frames: Maximum number of frame IDs to keep in history for
|
||||||
|
duplicate detection. Defaults to 100.
|
||||||
|
turn_end_timeout_secs: Timeout in seconds after bot stops speaking
|
||||||
|
before automatically ending the turn. Defaults to 2.5.
|
||||||
|
**kwargs: Additional arguments passed to the parent observer.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._turn_count = 0
|
self._turn_count = 0
|
||||||
self._is_turn_active = False
|
self._is_turn_active = False
|
||||||
@@ -49,7 +70,11 @@ class TurnTrackingObserver(BaseObserver):
|
|||||||
self._register_event_handler("on_turn_ended")
|
self._register_event_handler("on_turn_ended")
|
||||||
|
|
||||||
async def on_push_frame(self, data: FramePushed):
|
async def on_push_frame(self, data: FramePushed):
|
||||||
"""Process frame events for turn tracking."""
|
"""Process frame events for turn tracking.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Frame push event data containing the frame and metadata.
|
||||||
|
"""
|
||||||
# Skip already processed frames
|
# Skip already processed frames
|
||||||
if data.frame.id in self._processed_frames:
|
if data.frame.id in self._processed_frames:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base pipeline implementation for frame processing."""
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -11,9 +13,24 @@ from pipecat.processors.frame_processor import FrameProcessor
|
|||||||
|
|
||||||
|
|
||||||
class BasePipeline(FrameProcessor):
|
class BasePipeline(FrameProcessor):
|
||||||
|
"""Base class for all pipeline implementations.
|
||||||
|
|
||||||
|
Provides the foundation for pipeline processors that need to support
|
||||||
|
metrics collection from their contained processors.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Initialize the base pipeline."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def processors_with_metrics(self) -> List[FrameProcessor]:
|
def processors_with_metrics(self) -> List[FrameProcessor]:
|
||||||
|
"""Return processors that can generate metrics.
|
||||||
|
|
||||||
|
Implementing classes should collect and return all processors within
|
||||||
|
their pipeline that support metrics generation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of frame processors that support metrics collection.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Base pipeline task implementation for managing pipeline execution.
|
||||||
|
|
||||||
|
This module provides the abstract base class and configuration for pipeline
|
||||||
|
tasks that manage the lifecycle and execution of frame processing pipelines.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -15,44 +21,81 @@ from pipecat.utils.base_object import BaseObject
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PipelineTaskParams:
|
class PipelineTaskParams:
|
||||||
"""Specific configuration for the pipeline task."""
|
"""Configuration parameters for pipeline task execution.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
loop: The asyncio event loop to use for task execution.
|
||||||
|
"""
|
||||||
|
|
||||||
loop: asyncio.AbstractEventLoop
|
loop: asyncio.AbstractEventLoop
|
||||||
|
|
||||||
|
|
||||||
class BasePipelineTask(BaseObject):
|
class BasePipelineTask(BaseObject):
|
||||||
|
"""Abstract base class for pipeline task implementations.
|
||||||
|
|
||||||
|
Defines the interface for managing pipeline execution lifecycle,
|
||||||
|
including starting, stopping, and frame queuing operations.
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def has_finished(self) -> bool:
|
def has_finished(self) -> bool:
|
||||||
"""Indicates whether the tasks has finished. That is, all processors
|
"""Check if the pipeline task has finished execution.
|
||||||
have stopped.
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all processors have stopped and the task is complete.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def stop_when_done(self):
|
async def stop_when_done(self):
|
||||||
"""This is a helper function that sends an EndFrame to the pipeline in
|
"""Schedule the pipeline to stop after processing all queued frames.
|
||||||
order to stop the task after everything in it has been processed.
|
|
||||||
|
|
||||||
|
Implementing classes should send an EndFrame or equivalent signal to
|
||||||
|
gracefully terminate the pipeline once all current processing is complete.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def cancel(self):
|
async def cancel(self):
|
||||||
"""Stops the running pipeline immediately."""
|
"""Immediately stop the running pipeline.
|
||||||
|
|
||||||
|
Implementing classes should cancel all running tasks and stop frame
|
||||||
|
processing without waiting for completion.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def run(self, params: PipelineTaskParams):
|
async def run(self, params: PipelineTaskParams):
|
||||||
"""Starts running the given pipeline."""
|
"""Start and run the pipeline with the given parameters.
|
||||||
|
|
||||||
|
Implementing classes should initialize and execute the pipeline using
|
||||||
|
the provided configuration parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: Configuration parameters for pipeline execution.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def queue_frame(self, frame: Frame):
|
async def queue_frame(self, frame: Frame):
|
||||||
"""Queue a frame to be pushed down the pipeline."""
|
"""Queue a single frame for processing by the pipeline.
|
||||||
|
|
||||||
|
Implementing classes should add the frame to their processing queue
|
||||||
|
for downstream handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to be processed.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def queue_frames(self, frames: Iterable[Frame] | AsyncIterable[Frame]):
|
async def queue_frames(self, frames: Iterable[Frame] | AsyncIterable[Frame]):
|
||||||
"""Queues multiple frames to be pushed down the pipeline."""
|
"""Queue multiple frames for processing by the pipeline.
|
||||||
|
|
||||||
|
Implementing classes should process the iterable/async iterable and
|
||||||
|
add all frames to their processing queue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frames: An iterable or async iterable of frames to be processed.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Parallel pipeline implementation for concurrent frame processing.
|
||||||
|
|
||||||
|
This module provides a parallel pipeline that processes frames through multiple
|
||||||
|
sub-pipelines concurrently, with coordination for system frames and proper
|
||||||
|
handling of pipeline lifecycle events.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Awaitable, Callable, Dict, List
|
from typing import Awaitable, Callable, Dict, List
|
||||||
@@ -25,16 +32,34 @@ from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
|
|||||||
|
|
||||||
|
|
||||||
class ParallelPipelineSource(FrameProcessor):
|
class ParallelPipelineSource(FrameProcessor):
|
||||||
|
"""Source processor for parallel pipeline branches.
|
||||||
|
|
||||||
|
Handles frame routing for parallel pipeline inputs, directing system frames
|
||||||
|
to the parent push function and other upstream frames to a queue for processing.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
upstream_queue: asyncio.Queue,
|
upstream_queue: asyncio.Queue,
|
||||||
push_frame_func: Callable[[Frame, FrameDirection], Awaitable[None]],
|
push_frame_func: Callable[[Frame, FrameDirection], Awaitable[None]],
|
||||||
):
|
):
|
||||||
|
"""Initialize the parallel pipeline source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
upstream_queue: Queue for collecting upstream frames from this branch.
|
||||||
|
push_frame_func: Function to push frames to the parent parallel pipeline.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._up_queue = upstream_queue
|
self._up_queue = upstream_queue
|
||||||
self._push_frame_func = push_frame_func
|
self._push_frame_func = push_frame_func
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames with special handling for system frames.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -48,16 +73,34 @@ class ParallelPipelineSource(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class ParallelPipelineSink(FrameProcessor):
|
class ParallelPipelineSink(FrameProcessor):
|
||||||
|
"""Sink processor for parallel pipeline branches.
|
||||||
|
|
||||||
|
Handles frame routing for parallel pipeline outputs, directing system frames
|
||||||
|
to the parent push function and other downstream frames to a queue for coordination.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
downstream_queue: asyncio.Queue,
|
downstream_queue: asyncio.Queue,
|
||||||
push_frame_func: Callable[[Frame, FrameDirection], Awaitable[None]],
|
push_frame_func: Callable[[Frame, FrameDirection], Awaitable[None]],
|
||||||
):
|
):
|
||||||
|
"""Initialize the parallel pipeline sink.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
downstream_queue: Queue for collecting downstream frames from this branch.
|
||||||
|
push_frame_func: Function to push frames to the parent parallel pipeline.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._down_queue = downstream_queue
|
self._down_queue = downstream_queue
|
||||||
self._push_frame_func = push_frame_func
|
self._push_frame_func = push_frame_func
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames with special handling for system frames.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -71,7 +114,24 @@ class ParallelPipelineSink(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class ParallelPipeline(BasePipeline):
|
class ParallelPipeline(BasePipeline):
|
||||||
|
"""Pipeline that processes frames through multiple sub-pipelines concurrently.
|
||||||
|
|
||||||
|
Creates multiple parallel processing branches from the provided processor lists,
|
||||||
|
coordinating frame flow and ensuring proper synchronization of lifecycle events
|
||||||
|
like EndFrames. Each branch runs independently while system frames are handled
|
||||||
|
specially to maintain pipeline coordination.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
|
"""Initialize the parallel pipeline with processor lists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args: Variable number of processor lists, each becoming a parallel branch.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If no processor lists are provided.
|
||||||
|
TypeError: If any argument is not a list of processors.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
if len(args) == 0:
|
if len(args) == 0:
|
||||||
@@ -93,6 +153,11 @@ class ParallelPipeline(BasePipeline):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def processors_with_metrics(self) -> List[FrameProcessor]:
|
def processors_with_metrics(self) -> List[FrameProcessor]:
|
||||||
|
"""Collect processors that can generate metrics from all parallel branches.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of frame processors that support metrics collection from all branches.
|
||||||
|
"""
|
||||||
return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
|
return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -100,6 +165,14 @@ class ParallelPipeline(BasePipeline):
|
|||||||
#
|
#
|
||||||
|
|
||||||
async def setup(self, setup: FrameProcessorSetup):
|
async def setup(self, setup: FrameProcessorSetup):
|
||||||
|
"""Set up the parallel pipeline and all its branches.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
setup: Configuration for frame processor setup.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If any processor list argument is not actually a list.
|
||||||
|
"""
|
||||||
await super().setup(setup)
|
await super().setup(setup)
|
||||||
|
|
||||||
self._up_queue = WatchdogQueue(setup.task_manager)
|
self._up_queue = WatchdogQueue(setup.task_manager)
|
||||||
@@ -129,12 +202,19 @@ class ParallelPipeline(BasePipeline):
|
|||||||
await asyncio.gather(*[s.setup(setup) for s in self._sinks])
|
await asyncio.gather(*[s.setup(setup) for s in self._sinks])
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
|
"""Clean up the parallel pipeline and all its branches."""
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
await asyncio.gather(*[s.cleanup() for s in self._sources])
|
await asyncio.gather(*[s.cleanup() for s in self._sources])
|
||||||
await asyncio.gather(*[p.cleanup() for p in self._pipelines])
|
await asyncio.gather(*[p.cleanup() for p in self._pipelines])
|
||||||
await asyncio.gather(*[s.cleanup() for s in self._sinks])
|
await asyncio.gather(*[s.cleanup() for s in self._sinks])
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames through all parallel branches with lifecycle coordination.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
if isinstance(frame, StartFrame):
|
if isinstance(frame, StartFrame):
|
||||||
@@ -159,9 +239,11 @@ class ParallelPipeline(BasePipeline):
|
|||||||
await self._stop()
|
await self._stop()
|
||||||
|
|
||||||
async def _start(self, frame: StartFrame):
|
async def _start(self, frame: StartFrame):
|
||||||
|
"""Start the parallel pipeline processing tasks."""
|
||||||
await self._create_tasks()
|
await self._create_tasks()
|
||||||
|
|
||||||
async def _stop(self):
|
async def _stop(self):
|
||||||
|
"""Stop all parallel pipeline processing tasks."""
|
||||||
if self._up_task:
|
if self._up_task:
|
||||||
# The up task doesn't receive an EndFrame, so we just cancel it.
|
# The up task doesn't receive an EndFrame, so we just cancel it.
|
||||||
await self.cancel_task(self._up_task)
|
await self.cancel_task(self._up_task)
|
||||||
@@ -174,42 +256,55 @@ class ParallelPipeline(BasePipeline):
|
|||||||
self._down_task = None
|
self._down_task = None
|
||||||
|
|
||||||
async def _cancel(self):
|
async def _cancel(self):
|
||||||
|
"""Cancel all parallel pipeline processing tasks."""
|
||||||
if self._up_task:
|
if self._up_task:
|
||||||
|
self._up_queue.cancel()
|
||||||
await self.cancel_task(self._up_task)
|
await self.cancel_task(self._up_task)
|
||||||
self._up_task = None
|
self._up_task = None
|
||||||
if self._down_task:
|
if self._down_task:
|
||||||
|
self._down_queue.cancel()
|
||||||
await self.cancel_task(self._down_task)
|
await self.cancel_task(self._down_task)
|
||||||
self._down_task = None
|
self._down_task = None
|
||||||
|
|
||||||
async def _create_tasks(self):
|
async def _create_tasks(self):
|
||||||
|
"""Create upstream and downstream processing tasks if not already running."""
|
||||||
if not self._up_task:
|
if not self._up_task:
|
||||||
self._up_task = self.create_task(self._process_up_queue())
|
self._up_task = self.create_task(self._process_up_queue())
|
||||||
if not self._down_task:
|
if not self._down_task:
|
||||||
self._down_task = self.create_task(self._process_down_queue())
|
self._down_task = self.create_task(self._process_down_queue())
|
||||||
|
|
||||||
async def _drain_queues(self):
|
async def _drain_queues(self):
|
||||||
|
"""Drain all frames from upstream and downstream queues."""
|
||||||
while not self._up_queue.empty:
|
while not self._up_queue.empty:
|
||||||
await self._up_queue.get()
|
await self._up_queue.get()
|
||||||
while not self._down_queue.empty:
|
while not self._down_queue.empty:
|
||||||
await self._down_queue.get()
|
await self._down_queue.get()
|
||||||
|
|
||||||
async def _handle_interruption(self):
|
async def _handle_interruption(self):
|
||||||
|
"""Handle interruption by cancelling tasks, draining queues, and restarting."""
|
||||||
await self._cancel()
|
await self._cancel()
|
||||||
await self._drain_queues()
|
await self._drain_queues()
|
||||||
await self._create_tasks()
|
await self._create_tasks()
|
||||||
|
|
||||||
async def _parallel_push_frame(self, frame: Frame, direction: FrameDirection):
|
async def _parallel_push_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Push frames while avoiding duplicates using frame ID tracking."""
|
||||||
if frame.id not in self._seen_ids:
|
if frame.id not in self._seen_ids:
|
||||||
self._seen_ids.add(frame.id)
|
self._seen_ids.add(frame.id)
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
async def _process_up_queue(self):
|
async def _process_up_queue(self):
|
||||||
|
"""Process upstream frames from all parallel branches."""
|
||||||
while True:
|
while True:
|
||||||
frame = await self._up_queue.get()
|
frame = await self._up_queue.get()
|
||||||
await self._parallel_push_frame(frame, FrameDirection.UPSTREAM)
|
await self._parallel_push_frame(frame, FrameDirection.UPSTREAM)
|
||||||
self._up_queue.task_done()
|
self._up_queue.task_done()
|
||||||
|
|
||||||
async def _process_down_queue(self):
|
async def _process_down_queue(self):
|
||||||
|
"""Process downstream frames with EndFrame coordination.
|
||||||
|
|
||||||
|
Coordinates EndFrames to ensure they are only pushed upstream once
|
||||||
|
all parallel branches have completed processing them.
|
||||||
|
"""
|
||||||
running = True
|
running = True
|
||||||
while running:
|
while running:
|
||||||
frame = await self._down_queue.get()
|
frame = await self._down_queue.get()
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Pipeline implementation for connecting and managing frame processors.
|
||||||
|
|
||||||
|
This module provides the main Pipeline class that connects frame processors
|
||||||
|
in sequence and manages frame flow between them, along with helper classes
|
||||||
|
for pipeline source and sink operations.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Callable, Coroutine, List
|
from typing import Callable, Coroutine, List
|
||||||
|
|
||||||
from pipecat.frames.frames import Frame
|
from pipecat.frames.frames import Frame
|
||||||
@@ -12,11 +19,29 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, F
|
|||||||
|
|
||||||
|
|
||||||
class PipelineSource(FrameProcessor):
|
class PipelineSource(FrameProcessor):
|
||||||
|
"""Source processor that forwards frames to an upstream handler.
|
||||||
|
|
||||||
|
This processor acts as the entry point for a pipeline, forwarding
|
||||||
|
downstream frames to the next processor and upstream frames to a
|
||||||
|
provided upstream handler function.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, upstream_push_frame: Callable[[Frame, FrameDirection], Coroutine]):
|
def __init__(self, upstream_push_frame: Callable[[Frame, FrameDirection], Coroutine]):
|
||||||
|
"""Initialize the pipeline source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
upstream_push_frame: Coroutine function to handle upstream frames.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._upstream_push_frame = upstream_push_frame
|
self._upstream_push_frame = upstream_push_frame
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames and route them based on direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -27,11 +52,29 @@ class PipelineSource(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class PipelineSink(FrameProcessor):
|
class PipelineSink(FrameProcessor):
|
||||||
|
"""Sink processor that forwards frames to a downstream handler.
|
||||||
|
|
||||||
|
This processor acts as the exit point for a pipeline, forwarding
|
||||||
|
upstream frames to the previous processor and downstream frames to a
|
||||||
|
provided downstream handler function.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, downstream_push_frame: Callable[[Frame, FrameDirection], Coroutine]):
|
def __init__(self, downstream_push_frame: Callable[[Frame, FrameDirection], Coroutine]):
|
||||||
|
"""Initialize the pipeline sink.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
downstream_push_frame: Coroutine function to handle downstream frames.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._downstream_push_frame = downstream_push_frame
|
self._downstream_push_frame = downstream_push_frame
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames and route them based on direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -42,7 +85,19 @@ class PipelineSink(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class Pipeline(BasePipeline):
|
class Pipeline(BasePipeline):
|
||||||
|
"""Main pipeline implementation that connects frame processors in sequence.
|
||||||
|
|
||||||
|
Creates a linear chain of frame processors with automatic source and sink
|
||||||
|
processors for external frame handling. Manages processor lifecycle and
|
||||||
|
provides metrics collection from contained processors.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, processors: List[FrameProcessor]):
|
def __init__(self, processors: List[FrameProcessor]):
|
||||||
|
"""Initialize the pipeline with a list of processors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
processors: List of frame processors to connect in sequence.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# Add a source and a sink queue so we can forward frames upstream and
|
# Add a source and a sink queue so we can forward frames upstream and
|
||||||
@@ -58,6 +113,14 @@ class Pipeline(BasePipeline):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def processors_with_metrics(self):
|
def processors_with_metrics(self):
|
||||||
|
"""Return processors that can generate metrics.
|
||||||
|
|
||||||
|
Recursively collects all processors that support metrics generation,
|
||||||
|
including those from nested pipelines.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of frame processors that can generate metrics.
|
||||||
|
"""
|
||||||
services = []
|
services = []
|
||||||
for p in self._processors:
|
for p in self._processors:
|
||||||
if isinstance(p, BasePipeline):
|
if isinstance(p, BasePipeline):
|
||||||
@@ -71,14 +134,26 @@ class Pipeline(BasePipeline):
|
|||||||
#
|
#
|
||||||
|
|
||||||
async def setup(self, setup: FrameProcessorSetup):
|
async def setup(self, setup: FrameProcessorSetup):
|
||||||
|
"""Set up the pipeline and all contained processors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
setup: Configuration for frame processor setup.
|
||||||
|
"""
|
||||||
await super().setup(setup)
|
await super().setup(setup)
|
||||||
await self._setup_processors(setup)
|
await self._setup_processors(setup)
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
|
"""Clean up the pipeline and all contained processors."""
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
await self._cleanup_processors()
|
await self._cleanup_processors()
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames by routing them through the pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
if direction == FrameDirection.DOWNSTREAM:
|
if direction == FrameDirection.DOWNSTREAM:
|
||||||
@@ -87,14 +162,17 @@ class Pipeline(BasePipeline):
|
|||||||
await self._sink.queue_frame(frame, FrameDirection.UPSTREAM)
|
await self._sink.queue_frame(frame, FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
async def _setup_processors(self, setup: FrameProcessorSetup):
|
async def _setup_processors(self, setup: FrameProcessorSetup):
|
||||||
|
"""Set up all processors in the pipeline."""
|
||||||
for p in self._processors:
|
for p in self._processors:
|
||||||
await p.setup(setup)
|
await p.setup(setup)
|
||||||
|
|
||||||
async def _cleanup_processors(self):
|
async def _cleanup_processors(self):
|
||||||
|
"""Clean up all processors in the pipeline."""
|
||||||
for p in self._processors:
|
for p in self._processors:
|
||||||
await p.cleanup()
|
await p.cleanup()
|
||||||
|
|
||||||
def _link_processors(self):
|
def _link_processors(self):
|
||||||
|
"""Link all processors in sequence and set their parent."""
|
||||||
prev = self._processors[0]
|
prev = self._processors[0]
|
||||||
for curr in self._processors[1:]:
|
for curr in self._processors[1:]:
|
||||||
prev.set_parent(self)
|
prev.set_parent(self)
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Pipeline runner for managing pipeline task execution.
|
||||||
|
|
||||||
|
This module provides the PipelineRunner class that handles the execution
|
||||||
|
of pipeline tasks with signal handling, garbage collection, and lifecycle
|
||||||
|
management.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import gc
|
import gc
|
||||||
import signal
|
import signal
|
||||||
@@ -17,6 +24,13 @@ from pipecat.utils.base_object import BaseObject
|
|||||||
|
|
||||||
|
|
||||||
class PipelineRunner(BaseObject):
|
class PipelineRunner(BaseObject):
|
||||||
|
"""Manages the execution of pipeline tasks with lifecycle and signal handling.
|
||||||
|
|
||||||
|
Provides a high-level interface for running pipeline tasks with automatic
|
||||||
|
signal handling (SIGINT/SIGTERM), optional garbage collection, and proper
|
||||||
|
cleanup of resources.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -25,6 +39,14 @@ class PipelineRunner(BaseObject):
|
|||||||
force_gc: bool = False,
|
force_gc: bool = False,
|
||||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||||
):
|
):
|
||||||
|
"""Initialize the pipeline runner.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Optional name for the runner instance.
|
||||||
|
handle_sigint: Whether to automatically handle SIGINT/SIGTERM signals.
|
||||||
|
force_gc: Whether to force garbage collection after task completion.
|
||||||
|
loop: Event loop to use. If None, uses the current running loop.
|
||||||
|
"""
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
|
|
||||||
self._tasks = {}
|
self._tasks = {}
|
||||||
@@ -36,6 +58,11 @@ class PipelineRunner(BaseObject):
|
|||||||
self._setup_sigint()
|
self._setup_sigint()
|
||||||
|
|
||||||
async def run(self, task: PipelineTask):
|
async def run(self, task: PipelineTask):
|
||||||
|
"""Run a pipeline task to completion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task: The pipeline task to execute.
|
||||||
|
"""
|
||||||
logger.debug(f"Runner {self} started running {task}")
|
logger.debug(f"Runner {self} started running {task}")
|
||||||
self._tasks[task.name] = task
|
self._tasks[task.name] = task
|
||||||
params = PipelineTaskParams(loop=self._loop)
|
params = PipelineTaskParams(loop=self._loop)
|
||||||
@@ -56,27 +83,33 @@ class PipelineRunner(BaseObject):
|
|||||||
logger.debug(f"Runner {self} finished running {task}")
|
logger.debug(f"Runner {self} finished running {task}")
|
||||||
|
|
||||||
async def stop_when_done(self):
|
async def stop_when_done(self):
|
||||||
|
"""Schedule all running tasks to stop when their current processing is complete."""
|
||||||
logger.debug(f"Runner {self} scheduled to stop when all tasks are done")
|
logger.debug(f"Runner {self} scheduled to stop when all tasks are done")
|
||||||
await asyncio.gather(*[t.stop_when_done() for t in self._tasks.values()])
|
await asyncio.gather(*[t.stop_when_done() for t in self._tasks.values()])
|
||||||
|
|
||||||
async def cancel(self):
|
async def cancel(self):
|
||||||
|
"""Cancel all running tasks immediately."""
|
||||||
logger.debug(f"Cancelling runner {self}")
|
logger.debug(f"Cancelling runner {self}")
|
||||||
await asyncio.gather(*[t.cancel() for t in self._tasks.values()])
|
await asyncio.gather(*[t.cancel() for t in self._tasks.values()])
|
||||||
|
|
||||||
def _setup_sigint(self):
|
def _setup_sigint(self):
|
||||||
|
"""Set up signal handlers for graceful shutdown."""
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
loop.add_signal_handler(signal.SIGINT, lambda *args: self._sig_handler())
|
loop.add_signal_handler(signal.SIGINT, lambda *args: self._sig_handler())
|
||||||
loop.add_signal_handler(signal.SIGTERM, lambda *args: self._sig_handler())
|
loop.add_signal_handler(signal.SIGTERM, lambda *args: self._sig_handler())
|
||||||
|
|
||||||
def _sig_handler(self):
|
def _sig_handler(self):
|
||||||
|
"""Handle interrupt signals by cancelling all tasks."""
|
||||||
if not self._sig_task:
|
if not self._sig_task:
|
||||||
self._sig_task = asyncio.create_task(self._sig_cancel())
|
self._sig_task = asyncio.create_task(self._sig_cancel())
|
||||||
|
|
||||||
async def _sig_cancel(self):
|
async def _sig_cancel(self):
|
||||||
|
"""Cancel all running tasks due to signal interruption."""
|
||||||
logger.warning(f"Interruption detected. Cancelling runner {self}")
|
logger.warning(f"Interruption detected. Cancelling runner {self}")
|
||||||
await self.cancel()
|
await self.cancel()
|
||||||
|
|
||||||
def _gc_collect(self):
|
def _gc_collect(self):
|
||||||
|
"""Force garbage collection and log results."""
|
||||||
collected = gc.collect()
|
collected = gc.collect()
|
||||||
logger.debug(f"Garbage collector: collected {collected} objects.")
|
logger.debug(f"Garbage collector: collected {collected} objects.")
|
||||||
logger.debug(f"Garbage collector: uncollectable objects {gc.garbage}")
|
logger.debug(f"Garbage collector: uncollectable objects {gc.garbage}")
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Synchronous parallel pipeline implementation for concurrent frame processing.
|
||||||
|
|
||||||
|
This module provides a pipeline that processes frames through multiple parallel
|
||||||
|
pipelines simultaneously, synchronizing their output to maintain frame ordering
|
||||||
|
and prevent duplicate processing.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -20,17 +27,38 @@ from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SyncFrame(ControlFrame):
|
class SyncFrame(ControlFrame):
|
||||||
"""This frame is used to know when the internal pipelines have finished."""
|
"""Control frame used to synchronize parallel pipeline processing.
|
||||||
|
|
||||||
|
This frame is sent through parallel pipelines to determine when the
|
||||||
|
internal pipelines have finished processing a batch of frames.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SyncParallelPipelineSource(FrameProcessor):
|
class SyncParallelPipelineSource(FrameProcessor):
|
||||||
|
"""Source processor for synchronous parallel pipeline processing.
|
||||||
|
|
||||||
|
Routes frames to parallel pipelines and collects upstream responses
|
||||||
|
for synchronization purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, upstream_queue: asyncio.Queue):
|
def __init__(self, upstream_queue: asyncio.Queue):
|
||||||
|
"""Initialize the sync parallel pipeline source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
upstream_queue: Queue for collecting upstream frames from the pipeline.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._up_queue = upstream_queue
|
self._up_queue = upstream_queue
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames and route them based on direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -41,11 +69,28 @@ class SyncParallelPipelineSource(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class SyncParallelPipelineSink(FrameProcessor):
|
class SyncParallelPipelineSink(FrameProcessor):
|
||||||
|
"""Sink processor for synchronous parallel pipeline processing.
|
||||||
|
|
||||||
|
Collects downstream frames from parallel pipelines and routes
|
||||||
|
upstream frames back through the pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, downstream_queue: asyncio.Queue):
|
def __init__(self, downstream_queue: asyncio.Queue):
|
||||||
|
"""Initialize the sync parallel pipeline sink.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
downstream_queue: Queue for collecting downstream frames from the pipeline.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._down_queue = downstream_queue
|
self._down_queue = downstream_queue
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames and route them based on direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -56,7 +101,28 @@ class SyncParallelPipelineSink(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class SyncParallelPipeline(BasePipeline):
|
class SyncParallelPipeline(BasePipeline):
|
||||||
|
"""Pipeline that processes frames through multiple parallel pipelines synchronously.
|
||||||
|
|
||||||
|
Creates multiple parallel processing paths that all receive the same input frames
|
||||||
|
and produces synchronized output. Each parallel path is a separate pipeline that
|
||||||
|
processes frames independently, with synchronization points to ensure consistent
|
||||||
|
ordering and prevent duplicate frame processing.
|
||||||
|
|
||||||
|
The pipeline uses SyncFrame control frames to coordinate between parallel paths
|
||||||
|
and ensure all paths have completed processing before moving to the next frame.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
|
"""Initialize the synchronous parallel pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args: Variable number of processor lists, each representing a parallel pipeline path.
|
||||||
|
Each argument should be a list of FrameProcessor instances.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If no arguments are provided.
|
||||||
|
TypeError: If any argument is not a list of processors.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
if len(args) == 0:
|
if len(args) == 0:
|
||||||
@@ -72,6 +138,11 @@ class SyncParallelPipeline(BasePipeline):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def processors_with_metrics(self) -> List[FrameProcessor]:
|
def processors_with_metrics(self) -> List[FrameProcessor]:
|
||||||
|
"""Collect processors that can generate metrics from all parallel pipelines.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of frame processors that support metrics collection from all parallel paths.
|
||||||
|
"""
|
||||||
return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
|
return list(chain.from_iterable(p.processors_with_metrics() for p in self._pipelines))
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -79,6 +150,11 @@ class SyncParallelPipeline(BasePipeline):
|
|||||||
#
|
#
|
||||||
|
|
||||||
async def setup(self, setup: FrameProcessorSetup):
|
async def setup(self, setup: FrameProcessorSetup):
|
||||||
|
"""Set up the parallel pipeline and all contained processors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
setup: Configuration for frame processor setup.
|
||||||
|
"""
|
||||||
await super().setup(setup)
|
await super().setup(setup)
|
||||||
|
|
||||||
self._up_queue = WatchdogQueue(setup.task_manager)
|
self._up_queue = WatchdogQueue(setup.task_manager)
|
||||||
@@ -113,12 +189,23 @@ class SyncParallelPipeline(BasePipeline):
|
|||||||
await asyncio.gather(*[s["processor"].setup(setup) for s in self._sinks])
|
await asyncio.gather(*[s["processor"].setup(setup) for s in self._sinks])
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
|
"""Clean up the parallel pipeline and all contained processors."""
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
await asyncio.gather(*[s["processor"].cleanup() for s in self._sources])
|
await asyncio.gather(*[s["processor"].cleanup() for s in self._sources])
|
||||||
await asyncio.gather(*[p.cleanup() for p in self._pipelines])
|
await asyncio.gather(*[p.cleanup() for p in self._pipelines])
|
||||||
await asyncio.gather(*[s["processor"].cleanup() for s in self._sinks])
|
await asyncio.gather(*[s["processor"].cleanup() for s in self._sinks])
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames through all parallel pipelines with synchronization.
|
||||||
|
|
||||||
|
Distributes frames to all parallel pipelines and synchronizes their output
|
||||||
|
to maintain proper ordering and prevent duplicate processing. Uses SyncFrame
|
||||||
|
control frames to coordinate between parallel paths.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
# The last processor of each pipeline needs to be synchronous otherwise
|
# The last processor of each pipeline needs to be synchronous otherwise
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Pipeline task implementation for managing frame processing pipelines.
|
||||||
|
|
||||||
|
This module provides the main PipelineTask class that orchestrates pipeline
|
||||||
|
execution, frame routing, lifecycle management, and monitoring capabilities
|
||||||
|
including heartbeats, idle detection, and observer integration.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@@ -53,12 +60,13 @@ HEARTBEAT_MONITOR_SECONDS = HEARTBEAT_SECONDS * 10
|
|||||||
|
|
||||||
|
|
||||||
class PipelineParams(BaseModel):
|
class PipelineParams(BaseModel):
|
||||||
"""Configuration parameters for pipeline execution. These parameters are
|
"""Configuration parameters for pipeline execution.
|
||||||
usually passed to all frame processors using through `StartFrame`. For other
|
|
||||||
generic pipeline task parameters use `PipelineTask` constructor arguments
|
|
||||||
instead.
|
|
||||||
|
|
||||||
Attributes:
|
These parameters are usually passed to all frame processors through
|
||||||
|
StartFrame. For other generic pipeline task parameters use PipelineTask
|
||||||
|
constructor arguments instead.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
allow_interruptions: Whether to allow pipeline interruptions.
|
allow_interruptions: Whether to allow pipeline interruptions.
|
||||||
audio_in_sample_rate: Input audio sample rate in Hz.
|
audio_in_sample_rate: Input audio sample rate in Hz.
|
||||||
audio_out_sample_rate: Output audio sample rate in Hz.
|
audio_out_sample_rate: Output audio sample rate in Hz.
|
||||||
@@ -66,12 +74,15 @@ class PipelineParams(BaseModel):
|
|||||||
enable_metrics: Whether to enable metrics collection.
|
enable_metrics: Whether to enable metrics collection.
|
||||||
enable_usage_metrics: Whether to enable usage metrics.
|
enable_usage_metrics: Whether to enable usage metrics.
|
||||||
heartbeats_period_secs: Period between heartbeats in seconds.
|
heartbeats_period_secs: Period between heartbeats in seconds.
|
||||||
|
interruption_strategies: Strategies for bot interruption behavior.
|
||||||
observers: [deprecated] Use `observers` arg in `PipelineTask` class.
|
observers: [deprecated] Use `observers` arg in `PipelineTask` class.
|
||||||
|
|
||||||
|
.. deprecated:: 0.0.58
|
||||||
|
Use the `observers` argument in the `PipelineTask` class instead.
|
||||||
|
|
||||||
report_only_initial_ttfb: Whether to report only initial time to first byte.
|
report_only_initial_ttfb: Whether to report only initial time to first byte.
|
||||||
send_initial_empty_metrics: Whether to send initial empty metrics.
|
send_initial_empty_metrics: Whether to send initial empty metrics.
|
||||||
start_metadata: Additional metadata for pipeline start.
|
start_metadata: Additional metadata for pipeline start.
|
||||||
interruption_strategies: Strategies for bot interruption behavior.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
@@ -97,17 +108,25 @@ class PipelineTaskSource(FrameProcessor):
|
|||||||
pipeline given to the pipeline task. It allows us to easily push frames
|
pipeline given to the pipeline task. It allows us to easily push frames
|
||||||
downstream to the pipeline and also receive upstream frames coming from the
|
downstream to the pipeline and also receive upstream frames coming from the
|
||||||
pipeline.
|
pipeline.
|
||||||
|
|
||||||
Args:
|
|
||||||
up_queue: Queue for upstream frame processing.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, up_queue: asyncio.Queue, **kwargs):
|
def __init__(self, up_queue: asyncio.Queue, **kwargs):
|
||||||
|
"""Initialize the pipeline task source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
up_queue: Queue for upstream frame processing.
|
||||||
|
**kwargs: Additional arguments passed to the parent class.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._up_queue = up_queue
|
self._up_queue = up_queue
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames and route them based on direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
match direction:
|
match direction:
|
||||||
@@ -123,16 +142,25 @@ class PipelineTaskSink(FrameProcessor):
|
|||||||
This is the sink processor that is linked at the end of the pipeline
|
This is the sink processor that is linked at the end of the pipeline
|
||||||
given to the pipeline task. It allows us to receive downstream frames and
|
given to the pipeline task. It allows us to receive downstream frames and
|
||||||
act on them, for example, waiting to receive an EndFrame.
|
act on them, for example, waiting to receive an EndFrame.
|
||||||
|
|
||||||
Args:
|
|
||||||
down_queue: Queue for downstream frame processing.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, down_queue: asyncio.Queue, **kwargs):
|
def __init__(self, down_queue: asyncio.Queue, **kwargs):
|
||||||
|
"""Initialize the pipeline task sink.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
down_queue: Queue for downstream frame processing.
|
||||||
|
**kwargs: Additional arguments passed to the parent class.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._down_queue = down_queue
|
self._down_queue = down_queue
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process frames and route them to the downstream queue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame flow.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
await self._down_queue.put(frame)
|
await self._down_queue.put(frame)
|
||||||
|
|
||||||
@@ -140,69 +168,30 @@ class PipelineTaskSink(FrameProcessor):
|
|||||||
class PipelineTask(BasePipelineTask):
|
class PipelineTask(BasePipelineTask):
|
||||||
"""Manages the execution of a pipeline, handling frame processing and task lifecycle.
|
"""Manages the execution of a pipeline, handling frame processing and task lifecycle.
|
||||||
|
|
||||||
It has a couple of event handlers `on_frame_reached_upstream` and
|
This class orchestrates pipeline execution with comprehensive monitoring,
|
||||||
`on_frame_reached_downstream` that are called when upstream frames or
|
event handling, and lifecycle management. It provides event handlers for
|
||||||
downstream frames reach both ends of pipeline. By default, the events
|
various pipeline states and frame types, idle detection, heartbeat monitoring,
|
||||||
handlers will not be called unless some filters are set using
|
and observer integration.
|
||||||
`set_reached_upstream_filter` and `set_reached_downstream_filter`.
|
|
||||||
|
|
||||||
@task.event_handler("on_frame_reached_upstream")
|
Event handlers available:
|
||||||
async def on_frame_reached_upstream(task, frame):
|
|
||||||
...
|
|
||||||
|
|
||||||
@task.event_handler("on_frame_reached_downstream")
|
- on_frame_reached_upstream: Called when upstream frames reach the source
|
||||||
async def on_frame_reached_downstream(task, frame):
|
- on_frame_reached_downstream: Called when downstream frames reach the sink
|
||||||
...
|
- on_idle_timeout: Called when pipeline is idle beyond timeout threshold
|
||||||
|
- on_pipeline_started: Called when pipeline starts with StartFrame
|
||||||
|
- on_pipeline_stopped: Called when pipeline stops with StopFrame
|
||||||
|
- on_pipeline_ended: Called when pipeline ends with EndFrame
|
||||||
|
- on_pipeline_cancelled: Called when pipeline is cancelled
|
||||||
|
|
||||||
It also has an event handler that detects when the pipeline is idle. By
|
Example::
|
||||||
default, a pipeline is idle if no `BotSpeakingFrame` or
|
|
||||||
`LLMFullResponseEndFrame` are received within `idle_timeout_secs`.
|
|
||||||
|
|
||||||
@task.event_handler("on_idle_timeout")
|
@task.event_handler("on_frame_reached_upstream")
|
||||||
async def on_pipeline_idle_timeout(task):
|
async def on_frame_reached_upstream(task, frame):
|
||||||
...
|
...
|
||||||
|
|
||||||
There are also events to know if a pipeline has been started, stopped, ended
|
@task.event_handler("on_idle_timeout")
|
||||||
or cancelled.
|
async def on_pipeline_idle_timeout(task):
|
||||||
|
...
|
||||||
@task.event_handler("on_pipeline_started")
|
|
||||||
async def on_pipeline_started(task, frame: StartFrame):
|
|
||||||
...
|
|
||||||
|
|
||||||
@task.event_handler("on_pipeline_stopped")
|
|
||||||
async def on_pipeline_stopped(task, frame: StopFrame):
|
|
||||||
...
|
|
||||||
|
|
||||||
@task.event_handler("on_pipeline_ended")
|
|
||||||
async def on_pipeline_ended(task, frame: EndFrame):
|
|
||||||
...
|
|
||||||
|
|
||||||
@task.event_handler("on_pipeline_cancelled")
|
|
||||||
async def on_pipeline_cancelled(task, frame: CancelFrame):
|
|
||||||
...
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pipeline: The pipeline to execute.
|
|
||||||
params: Configuration parameters for the pipeline.
|
|
||||||
additional_span_attributes: Optional dictionary of attributes to propagate as
|
|
||||||
OpenTelemetry conversation span attributes.
|
|
||||||
cancel_on_idle_timeout: Whether the pipeline task should be cancelled if
|
|
||||||
the idle timeout is reached.
|
|
||||||
check_dangling_tasks: Whether to check for processors' tasks finishing properly.
|
|
||||||
clock: Clock implementation for timing operations.
|
|
||||||
conversation_id: Optional custom ID for the conversation.
|
|
||||||
enable_tracing: Whether to enable tracing.
|
|
||||||
enable_turn_tracking: Whether to enable turn tracking.
|
|
||||||
enable_watchdog_logging: Whether to print task processing times.
|
|
||||||
enable_watchdog_timers: Whether to enable task watchdog timers.
|
|
||||||
idle_timeout_frames: A tuple with the frames that should trigger an idle
|
|
||||||
timeout if not received withing `idle_timeout_seconds`.
|
|
||||||
idle_timeout_secs: Timeout (in seconds) to consider pipeline idle or
|
|
||||||
None. If a pipeline is idle the pipeline task will be cancelled
|
|
||||||
automatically.
|
|
||||||
observers: List of observers for monitoring pipeline execution.
|
|
||||||
watchdog_timeout_secs: Watchdog timer timeout (in seconds). A warning
|
|
||||||
will be logged if the watchdog timer is not reset before this timeout.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -228,6 +217,32 @@ class PipelineTask(BasePipelineTask):
|
|||||||
task_manager: Optional[BaseTaskManager] = None,
|
task_manager: Optional[BaseTaskManager] = None,
|
||||||
watchdog_timeout_secs: float = WATCHDOG_TIMEOUT,
|
watchdog_timeout_secs: float = WATCHDOG_TIMEOUT,
|
||||||
):
|
):
|
||||||
|
"""Initialize the PipelineTask.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pipeline: The pipeline to execute.
|
||||||
|
params: Configuration parameters for the pipeline.
|
||||||
|
additional_span_attributes: Optional dictionary of attributes to propagate as
|
||||||
|
OpenTelemetry conversation span attributes.
|
||||||
|
cancel_on_idle_timeout: Whether the pipeline task should be cancelled if
|
||||||
|
the idle timeout is reached.
|
||||||
|
check_dangling_tasks: Whether to check for processors' tasks finishing properly.
|
||||||
|
clock: Clock implementation for timing operations.
|
||||||
|
conversation_id: Optional custom ID for the conversation.
|
||||||
|
enable_tracing: Whether to enable tracing.
|
||||||
|
enable_turn_tracking: Whether to enable turn tracking.
|
||||||
|
enable_watchdog_logging: Whether to print task processing times.
|
||||||
|
enable_watchdog_timers: Whether to enable task watchdog timers.
|
||||||
|
idle_timeout_frames: A tuple with the frames that should trigger an idle
|
||||||
|
timeout if not received within `idle_timeout_seconds`.
|
||||||
|
idle_timeout_secs: Timeout (in seconds) to consider pipeline idle or
|
||||||
|
None. If a pipeline is idle the pipeline task will be cancelled
|
||||||
|
automatically.
|
||||||
|
observers: List of observers for monitoring pipeline execution.
|
||||||
|
task_manager: Optional task manager for handling asyncio tasks.
|
||||||
|
watchdog_timeout_secs: Watchdog timer timeout (in seconds). A warning
|
||||||
|
will be logged if the watchdog timer is not reset before this timeout.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._pipeline = pipeline
|
self._pipeline = pipeline
|
||||||
self._params = params or PipelineParams()
|
self._params = params or PipelineParams()
|
||||||
@@ -331,60 +346,97 @@ class PipelineTask(BasePipelineTask):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def params(self) -> PipelineParams:
|
def params(self) -> PipelineParams:
|
||||||
"""Returns the pipeline parameters of this task."""
|
"""Get the pipeline parameters for this task.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The pipeline parameters configuration.
|
||||||
|
"""
|
||||||
return self._params
|
return self._params
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def turn_tracking_observer(self) -> Optional[TurnTrackingObserver]:
|
def turn_tracking_observer(self) -> Optional[TurnTrackingObserver]:
|
||||||
"""Return the turn tracking observer if enabled."""
|
"""Get the turn tracking observer if enabled.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The turn tracking observer instance or None if not enabled.
|
||||||
|
"""
|
||||||
return self._turn_tracking_observer
|
return self._turn_tracking_observer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def turn_trace_observer(self) -> Optional[TurnTraceObserver]:
|
def turn_trace_observer(self) -> Optional[TurnTraceObserver]:
|
||||||
"""Return the turn trace observer if enabled."""
|
"""Get the turn trace observer if enabled.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The turn trace observer instance or None if not enabled.
|
||||||
|
"""
|
||||||
return self._turn_trace_observer
|
return self._turn_trace_observer
|
||||||
|
|
||||||
def add_observer(self, observer: BaseObserver):
|
def add_observer(self, observer: BaseObserver):
|
||||||
|
"""Add an observer to monitor pipeline execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observer: The observer to add to the pipeline monitoring.
|
||||||
|
"""
|
||||||
self._observer.add_observer(observer)
|
self._observer.add_observer(observer)
|
||||||
|
|
||||||
async def remove_observer(self, observer: BaseObserver):
|
async def remove_observer(self, observer: BaseObserver):
|
||||||
|
"""Remove an observer from pipeline monitoring.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observer: The observer to remove from pipeline monitoring.
|
||||||
|
"""
|
||||||
await self._observer.remove_observer(observer)
|
await self._observer.remove_observer(observer)
|
||||||
|
|
||||||
def set_reached_upstream_filter(self, types: Tuple[Type[Frame], ...]):
|
def set_reached_upstream_filter(self, types: Tuple[Type[Frame], ...]):
|
||||||
"""Sets which frames will be checked before calling the
|
"""Set which frame types trigger the on_frame_reached_upstream event.
|
||||||
on_frame_reached_upstream event handler.
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
types: Tuple of frame types to monitor for upstream events.
|
||||||
"""
|
"""
|
||||||
self._reached_upstream_types = types
|
self._reached_upstream_types = types
|
||||||
|
|
||||||
def set_reached_downstream_filter(self, types: Tuple[Type[Frame], ...]):
|
def set_reached_downstream_filter(self, types: Tuple[Type[Frame], ...]):
|
||||||
"""Sets which frames will be checked before calling the
|
"""Set which frame types trigger the on_frame_reached_downstream event.
|
||||||
on_frame_reached_downstream event handler.
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
types: Tuple of frame types to monitor for downstream events.
|
||||||
"""
|
"""
|
||||||
self._reached_downstream_types = types
|
self._reached_downstream_types = types
|
||||||
|
|
||||||
def has_finished(self) -> bool:
|
def has_finished(self) -> bool:
|
||||||
"""Indicates whether the tasks has finished. That is, all processors
|
"""Check if the pipeline task has finished execution.
|
||||||
|
|
||||||
|
This indicates whether the tasks has finished, meaninig all processors
|
||||||
have stopped.
|
have stopped.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all processors have stopped and the task is complete.
|
||||||
"""
|
"""
|
||||||
return self._finished
|
return self._finished
|
||||||
|
|
||||||
async def stop_when_done(self):
|
async def stop_when_done(self):
|
||||||
"""This is a helper function that sends an EndFrame to the pipeline in
|
"""Schedule the pipeline to stop after processing all queued frames.
|
||||||
order to stop the task after everything in it has been processed.
|
|
||||||
|
|
||||||
|
Sends an EndFrame to gracefully terminate the pipeline once all
|
||||||
|
current processing is complete.
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Task {self} scheduled to stop when done")
|
logger.debug(f"Task {self} scheduled to stop when done")
|
||||||
await self.queue_frame(EndFrame())
|
await self.queue_frame(EndFrame())
|
||||||
|
|
||||||
async def cancel(self):
|
async def cancel(self):
|
||||||
"""Stops the running pipeline immediately."""
|
"""Immediately stop the running pipeline.
|
||||||
|
|
||||||
|
Cancels all running tasks and stops frame processing without
|
||||||
|
waiting for completion.
|
||||||
|
"""
|
||||||
await self._cancel()
|
await self._cancel()
|
||||||
|
|
||||||
async def run(self, params: PipelineTaskParams):
|
async def run(self, params: PipelineTaskParams):
|
||||||
"""Starts and manages the pipeline execution until completion or cancellation."""
|
"""Start and manage the pipeline execution until completion or cancellation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: Configuration parameters for pipeline execution.
|
||||||
|
"""
|
||||||
if self.has_finished():
|
if self.has_finished():
|
||||||
return
|
return
|
||||||
cleanup_pipeline = True
|
cleanup_pipeline = True
|
||||||
@@ -440,6 +492,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
await self.queue_frame(frame)
|
await self.queue_frame(frame)
|
||||||
|
|
||||||
async def _cancel(self):
|
async def _cancel(self):
|
||||||
|
"""Internal cancellation logic for the pipeline task."""
|
||||||
if not self._cancelled:
|
if not self._cancelled:
|
||||||
logger.debug(f"Canceling pipeline task {self}")
|
logger.debug(f"Canceling pipeline task {self}")
|
||||||
self._cancelled = True
|
self._cancelled = True
|
||||||
@@ -453,6 +506,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
self._process_push_task = None
|
self._process_push_task = None
|
||||||
|
|
||||||
async def _create_tasks(self):
|
async def _create_tasks(self):
|
||||||
|
"""Create and start all pipeline processing tasks."""
|
||||||
self._process_up_task = self._task_manager.create_task(
|
self._process_up_task = self._task_manager.create_task(
|
||||||
self._process_up_queue(), f"{self}::_process_up_queue"
|
self._process_up_queue(), f"{self}::_process_up_queue"
|
||||||
)
|
)
|
||||||
@@ -468,6 +522,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
return self._process_push_task
|
return self._process_push_task
|
||||||
|
|
||||||
def _maybe_start_heartbeat_tasks(self):
|
def _maybe_start_heartbeat_tasks(self):
|
||||||
|
"""Start heartbeat tasks if heartbeats are enabled and not already running."""
|
||||||
if self._params.enable_heartbeats and self._heartbeat_push_task is None:
|
if self._params.enable_heartbeats and self._heartbeat_push_task is None:
|
||||||
self._heartbeat_push_task = self._task_manager.create_task(
|
self._heartbeat_push_task = self._task_manager.create_task(
|
||||||
self._heartbeat_push_handler(), f"{self}::_heartbeat_push_handler"
|
self._heartbeat_push_handler(), f"{self}::_heartbeat_push_handler"
|
||||||
@@ -477,12 +532,14 @@ class PipelineTask(BasePipelineTask):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _maybe_start_idle_task(self):
|
def _maybe_start_idle_task(self):
|
||||||
|
"""Start idle monitoring task if idle timeout is configured."""
|
||||||
if self._idle_timeout_secs:
|
if self._idle_timeout_secs:
|
||||||
self._idle_monitor_task = self._task_manager.create_task(
|
self._idle_monitor_task = self._task_manager.create_task(
|
||||||
self._idle_monitor_handler(), f"{self}::_idle_monitor_handler"
|
self._idle_monitor_handler(), f"{self}::_idle_monitor_handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _cancel_tasks(self):
|
async def _cancel_tasks(self):
|
||||||
|
"""Cancel all running pipeline tasks."""
|
||||||
await self._observer.stop()
|
await self._observer.stop()
|
||||||
|
|
||||||
if self._process_up_task:
|
if self._process_up_task:
|
||||||
@@ -497,6 +554,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
await self._maybe_cancel_idle_task()
|
await self._maybe_cancel_idle_task()
|
||||||
|
|
||||||
async def _maybe_cancel_heartbeat_tasks(self):
|
async def _maybe_cancel_heartbeat_tasks(self):
|
||||||
|
"""Cancel heartbeat tasks if they are running."""
|
||||||
if not self._params.enable_heartbeats:
|
if not self._params.enable_heartbeats:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -509,11 +567,14 @@ class PipelineTask(BasePipelineTask):
|
|||||||
self._heartbeat_monitor_task = None
|
self._heartbeat_monitor_task = None
|
||||||
|
|
||||||
async def _maybe_cancel_idle_task(self):
|
async def _maybe_cancel_idle_task(self):
|
||||||
|
"""Cancel idle monitoring task if it is running."""
|
||||||
if self._idle_timeout_secs and self._idle_monitor_task:
|
if self._idle_timeout_secs and self._idle_monitor_task:
|
||||||
|
self._idle_queue.cancel()
|
||||||
await self._task_manager.cancel_task(self._idle_monitor_task)
|
await self._task_manager.cancel_task(self._idle_monitor_task)
|
||||||
self._idle_monitor_task = None
|
self._idle_monitor_task = None
|
||||||
|
|
||||||
def _initial_metrics_frame(self) -> MetricsFrame:
|
def _initial_metrics_frame(self) -> MetricsFrame:
|
||||||
|
"""Create an initial metrics frame with zero values for all processors."""
|
||||||
processors = self._pipeline.processors_with_metrics()
|
processors = self._pipeline.processors_with_metrics()
|
||||||
data = []
|
data = []
|
||||||
for p in processors:
|
for p in processors:
|
||||||
@@ -522,10 +583,12 @@ class PipelineTask(BasePipelineTask):
|
|||||||
return MetricsFrame(data=data)
|
return MetricsFrame(data=data)
|
||||||
|
|
||||||
async def _wait_for_pipeline_end(self):
|
async def _wait_for_pipeline_end(self):
|
||||||
|
"""Wait for the pipeline to signal completion."""
|
||||||
await self._pipeline_end_event.wait()
|
await self._pipeline_end_event.wait()
|
||||||
self._pipeline_end_event.clear()
|
self._pipeline_end_event.clear()
|
||||||
|
|
||||||
async def _setup(self, params: PipelineTaskParams):
|
async def _setup(self, params: PipelineTaskParams):
|
||||||
|
"""Set up the pipeline task and all processors."""
|
||||||
mgr_params = TaskManagerParams(
|
mgr_params = TaskManagerParams(
|
||||||
loop=params.loop,
|
loop=params.loop,
|
||||||
enable_watchdog_logging=self._enable_watchdog_logging,
|
enable_watchdog_logging=self._enable_watchdog_logging,
|
||||||
@@ -545,6 +608,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
await self._sink.setup(setup)
|
await self._sink.setup(setup)
|
||||||
|
|
||||||
async def _cleanup(self, cleanup_pipeline: bool):
|
async def _cleanup(self, cleanup_pipeline: bool):
|
||||||
|
"""Clean up the pipeline task and processors."""
|
||||||
# Cleanup base object.
|
# Cleanup base object.
|
||||||
await self.cleanup()
|
await self.cleanup()
|
||||||
|
|
||||||
@@ -559,10 +623,11 @@ class PipelineTask(BasePipelineTask):
|
|||||||
await self._sink.cleanup()
|
await self._sink.cleanup()
|
||||||
|
|
||||||
async def _process_push_queue(self):
|
async def _process_push_queue(self):
|
||||||
"""This is the task that runs the pipeline for the first time by sending
|
"""Process frames from the push queue and send them through the pipeline.
|
||||||
|
|
||||||
|
This is the task that runs the pipeline for the first time by sending
|
||||||
a StartFrame and by pushing any other frames queued by the user. It runs
|
a StartFrame and by pushing any other frames queued by the user. It runs
|
||||||
until the tasks is cancelled or stopped (e.g. with an EndFrame).
|
until the tasks is cancelled or stopped (e.g. with an EndFrame).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._clock.start()
|
self._clock.start()
|
||||||
|
|
||||||
@@ -596,11 +661,12 @@ class PipelineTask(BasePipelineTask):
|
|||||||
await self._cleanup(cleanup_pipeline)
|
await self._cleanup(cleanup_pipeline)
|
||||||
|
|
||||||
async def _process_up_queue(self):
|
async def _process_up_queue(self):
|
||||||
"""This is the task that processes frames coming upstream from the
|
"""Process frames coming upstream from the pipeline.
|
||||||
|
|
||||||
|
This is the task that processes frames coming upstream from the
|
||||||
pipeline. These frames might indicate, for example, that we want the
|
pipeline. These frames might indicate, for example, that we want the
|
||||||
pipeline to be stopped (e.g. EndTaskFrame) in which case we would send
|
pipeline to be stopped (e.g. EndTaskFrame) in which case we would send
|
||||||
an EndFrame down the pipeline.
|
an EndFrame down the pipeline.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
frame = await self._up_queue.get()
|
frame = await self._up_queue.get()
|
||||||
@@ -629,11 +695,12 @@ class PipelineTask(BasePipelineTask):
|
|||||||
self._up_queue.task_done()
|
self._up_queue.task_done()
|
||||||
|
|
||||||
async def _process_down_queue(self):
|
async def _process_down_queue(self):
|
||||||
"""This tasks process frames coming downstream from the pipeline. For
|
"""Process frames coming downstream from the pipeline.
|
||||||
|
|
||||||
|
This tasks process frames coming downstream from the pipeline. For
|
||||||
example, heartbeat frames or an EndFrame which would indicate all
|
example, heartbeat frames or an EndFrame which would indicate all
|
||||||
processors have handled the EndFrame and therefore we can exit the task
|
processors have handled the EndFrame and therefore we can exit the task
|
||||||
cleanly.
|
cleanly.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
frame = await self._down_queue.get()
|
frame = await self._down_queue.get()
|
||||||
@@ -664,7 +731,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
self._down_queue.task_done()
|
self._down_queue.task_done()
|
||||||
|
|
||||||
async def _heartbeat_push_handler(self):
|
async def _heartbeat_push_handler(self):
|
||||||
"""This tasks pushes a heartbeat frame every heartbeat period."""
|
"""Push heartbeat frames at regular intervals."""
|
||||||
while True:
|
while True:
|
||||||
# Don't use `queue_frame()` because if an EndFrame is queued the
|
# Don't use `queue_frame()` because if an EndFrame is queued the
|
||||||
# task will just stop waiting for the pipeline to finish not
|
# task will just stop waiting for the pipeline to finish not
|
||||||
@@ -673,11 +740,12 @@ class PipelineTask(BasePipelineTask):
|
|||||||
await asyncio.sleep(self._params.heartbeats_period_secs)
|
await asyncio.sleep(self._params.heartbeats_period_secs)
|
||||||
|
|
||||||
async def _heartbeat_monitor_handler(self):
|
async def _heartbeat_monitor_handler(self):
|
||||||
"""This tasks monitors heartbeat frames. If a heartbeat frame has not
|
"""Monitor heartbeat frames for processing time and timeout detection.
|
||||||
|
|
||||||
|
This task monitors heartbeat frames. If a heartbeat frame has not
|
||||||
been received for a long period a warning will be logged. It also logs
|
been received for a long period a warning will be logged. It also logs
|
||||||
the time that a heartbeat frame takes to processes, that is how long it
|
the time that a heartbeat frame takes to processes, that is how long it
|
||||||
takes for the heartbeat frame to traverse all the pipeline.
|
takes for the heartbeat frame to traverse all the pipeline.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
wait_time = HEARTBEAT_MONITOR_SECONDS
|
wait_time = HEARTBEAT_MONITOR_SECONDS
|
||||||
while True:
|
while True:
|
||||||
@@ -692,9 +760,12 @@ class PipelineTask(BasePipelineTask):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _idle_monitor_handler(self):
|
async def _idle_monitor_handler(self):
|
||||||
"""This tasks monitors activity in the pipeline. If no frames are
|
"""Monitor pipeline activity and detect idle conditions.
|
||||||
received (heartbeats don't count) the pipeline is considered idle.
|
|
||||||
|
|
||||||
|
Tracks frame activity and triggers idle timeout events when the
|
||||||
|
pipeline hasn't received relevant frames within the timeout period.
|
||||||
|
|
||||||
|
Note: Heartbeats are excluded from idle detection.
|
||||||
"""
|
"""
|
||||||
running = True
|
running = True
|
||||||
last_frame_time = 0
|
last_frame_time = 0
|
||||||
@@ -732,10 +803,13 @@ class PipelineTask(BasePipelineTask):
|
|||||||
running = await self._idle_timeout_detected(frame_buffer)
|
running = await self._idle_timeout_detected(frame_buffer)
|
||||||
|
|
||||||
async def _idle_timeout_detected(self, last_frames: Deque[Frame]) -> bool:
|
async def _idle_timeout_detected(self, last_frames: Deque[Frame]) -> bool:
|
||||||
"""Logic for when the pipeline is idle.
|
"""Handle idle timeout detection and optional cancellation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
last_frames: Recent frames received before timeout for debugging.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: Whther the pipeline task is being cancelled or not.
|
Whether the pipeline task should continue running.
|
||||||
"""
|
"""
|
||||||
logger.warning("Idle timeout detected. Last 10 frames received:")
|
logger.warning("Idle timeout detected. Last 10 frames received:")
|
||||||
for i, frame in enumerate(last_frames, 1):
|
for i, frame in enumerate(last_frames, 1):
|
||||||
@@ -749,6 +823,7 @@ class PipelineTask(BasePipelineTask):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _print_dangling_tasks(self):
|
def _print_dangling_tasks(self):
|
||||||
|
"""Log any dangling tasks that haven't been properly cleaned up."""
|
||||||
tasks = [t.get_name() for t in self._task_manager.current_tasks()]
|
tasks = [t.get_name() for t in self._task_manager.current_tasks()]
|
||||||
if tasks:
|
if tasks:
|
||||||
logger.warning(f"Dangling tasks detected: {tasks}")
|
logger.warning(f"Dangling tasks detected: {tasks}")
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Task observer for managing pipeline frame observers.
|
||||||
|
|
||||||
|
This module provides a proxy observer system that manages multiple observers
|
||||||
|
for pipeline frame events, ensuring that observer processing doesn't block
|
||||||
|
the main pipeline execution.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
@@ -17,9 +24,15 @@ from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Proxy:
|
class Proxy:
|
||||||
"""This is the data we receive from the main observer and that we put into
|
"""Proxy data for managing observer tasks and queues.
|
||||||
a queue for later processing.
|
|
||||||
|
|
||||||
|
This represents is the data received from the main observer that
|
||||||
|
is queued for later processing.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
queue: Queue for frame data awaiting observer processing.
|
||||||
|
task: Asyncio task running the observer's frame processing loop.
|
||||||
|
observer: The actual observer instance being proxied.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queue: asyncio.Queue
|
queue: asyncio.Queue
|
||||||
@@ -28,7 +41,9 @@ class Proxy:
|
|||||||
|
|
||||||
|
|
||||||
class TaskObserver(BaseObserver):
|
class TaskObserver(BaseObserver):
|
||||||
"""This is a pipeline frame observer that is meant to be used as a proxy to
|
"""Proxy observer that manages multiple observers without blocking the pipeline.
|
||||||
|
|
||||||
|
This is a pipeline frame observer that is meant to be used as a proxy to
|
||||||
the user provided observers. That is, this is the observer that should be
|
the user provided observers. That is, this is the observer that should be
|
||||||
passed to the frame processors. Then, every time a frame is pushed this
|
passed to the frame processors. Then, every time a frame is pushed this
|
||||||
observer will call all the observers registered to the pipeline task.
|
observer will call all the observers registered to the pipeline task.
|
||||||
@@ -37,7 +52,6 @@ class TaskObserver(BaseObserver):
|
|||||||
pipeline by creating a queue and a task for each user observer. When a frame
|
pipeline by creating a queue and a task for each user observer. When a frame
|
||||||
is received, it will be put in a queue for efficiency and later processed by
|
is received, it will be put in a queue for efficiency and later processed by
|
||||||
each task.
|
each task.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -47,6 +61,13 @@ class TaskObserver(BaseObserver):
|
|||||||
task_manager: BaseTaskManager,
|
task_manager: BaseTaskManager,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
"""Initialize the TaskObserver.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observers: List of observers to manage. Defaults to empty list.
|
||||||
|
task_manager: Task manager for creating and managing observer tasks.
|
||||||
|
**kwargs: Additional arguments passed to the base observer.
|
||||||
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._observers = observers or []
|
self._observers = observers or []
|
||||||
self._task_manager = task_manager
|
self._task_manager = task_manager
|
||||||
@@ -55,6 +76,11 @@ class TaskObserver(BaseObserver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def add_observer(self, observer: BaseObserver):
|
def add_observer(self, observer: BaseObserver):
|
||||||
|
"""Add a new observer to the managed list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observer: The observer to add.
|
||||||
|
"""
|
||||||
# Add the observer to the list.
|
# Add the observer to the list.
|
||||||
self._observers.append(observer)
|
self._observers.append(observer)
|
||||||
|
|
||||||
@@ -65,6 +91,11 @@ class TaskObserver(BaseObserver):
|
|||||||
self._proxies[observer] = proxy
|
self._proxies[observer] = proxy
|
||||||
|
|
||||||
async def remove_observer(self, observer: BaseObserver):
|
async def remove_observer(self, observer: BaseObserver):
|
||||||
|
"""Remove an observer and clean up its resources.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observer: The observer to remove.
|
||||||
|
"""
|
||||||
# If the observer has a proxy, remove it.
|
# If the observer has a proxy, remove it.
|
||||||
if observer in self._proxies:
|
if observer in self._proxies:
|
||||||
proxy = self._proxies[observer]
|
proxy = self._proxies[observer]
|
||||||
@@ -78,11 +109,11 @@ class TaskObserver(BaseObserver):
|
|||||||
self._observers.remove(observer)
|
self._observers.remove(observer)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Starts all proxy observer tasks."""
|
"""Start all proxy observer tasks."""
|
||||||
self._proxies = self._create_proxies(self._observers)
|
self._proxies = self._create_proxies(self._observers)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""Stops all proxy observer tasks."""
|
"""Stop all proxy observer tasks."""
|
||||||
if not self._proxies:
|
if not self._proxies:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -90,13 +121,20 @@ class TaskObserver(BaseObserver):
|
|||||||
await self._task_manager.cancel_task(proxy.task)
|
await self._task_manager.cancel_task(proxy.task)
|
||||||
|
|
||||||
async def on_push_frame(self, data: FramePushed):
|
async def on_push_frame(self, data: FramePushed):
|
||||||
|
"""Queue frame data for all managed observers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The frame push event data to distribute to observers.
|
||||||
|
"""
|
||||||
for proxy in self._proxies.values():
|
for proxy in self._proxies.values():
|
||||||
await proxy.queue.put(data)
|
await proxy.queue.put(data)
|
||||||
|
|
||||||
def _started(self) -> bool:
|
def _started(self) -> bool:
|
||||||
|
"""Check if the task observer has been started."""
|
||||||
return self._proxies is not None
|
return self._proxies is not None
|
||||||
|
|
||||||
def _create_proxy(self, observer: BaseObserver) -> Proxy:
|
def _create_proxy(self, observer: BaseObserver) -> Proxy:
|
||||||
|
"""Create a proxy for a single observer."""
|
||||||
queue = WatchdogQueue(self._task_manager)
|
queue = WatchdogQueue(self._task_manager)
|
||||||
task = self._task_manager.create_task(
|
task = self._task_manager.create_task(
|
||||||
self._proxy_task_handler(queue, observer),
|
self._proxy_task_handler(queue, observer),
|
||||||
@@ -106,6 +144,7 @@ class TaskObserver(BaseObserver):
|
|||||||
return proxy
|
return proxy
|
||||||
|
|
||||||
def _create_proxies(self, observers: List[BaseObserver]) -> Dict[BaseObserver, Proxy]:
|
def _create_proxies(self, observers: List[BaseObserver]) -> Dict[BaseObserver, Proxy]:
|
||||||
|
"""Create proxies for all observers."""
|
||||||
proxies = {}
|
proxies = {}
|
||||||
for observer in observers:
|
for observer in observers:
|
||||||
proxy = self._create_proxy(observer)
|
proxy = self._create_proxy(observer)
|
||||||
@@ -113,6 +152,7 @@ class TaskObserver(BaseObserver):
|
|||||||
return proxies
|
return proxies
|
||||||
|
|
||||||
async def _proxy_task_handler(self, queue: asyncio.Queue, observer: BaseObserver):
|
async def _proxy_task_handler(self, queue: asyncio.Queue, observer: BaseObserver):
|
||||||
|
"""Handle frame processing for a single observer."""
|
||||||
warning_reported = False
|
warning_reported = False
|
||||||
while True:
|
while True:
|
||||||
data = await queue.get()
|
data = await queue.get()
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Sequential pipeline merging for Pipecat.
|
||||||
|
|
||||||
|
This module provides a pipeline implementation that sequentially merges
|
||||||
|
the output from multiple pipelines, processing them one after another
|
||||||
|
in a specified order.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from pipecat.frames.frames import EndFrame, EndPipeFrame
|
from pipecat.frames.frames import EndFrame, EndPipeFrame
|
||||||
@@ -5,14 +18,31 @@ from pipecat.pipeline.pipeline import Pipeline
|
|||||||
|
|
||||||
|
|
||||||
class SequentialMergePipeline(Pipeline):
|
class SequentialMergePipeline(Pipeline):
|
||||||
"""This class merges the sink queues from a list of pipelines. Frames from
|
"""Pipeline that sequentially merges output from multiple pipelines.
|
||||||
each pipeline's sink are merged in the order of pipelines in the list."""
|
|
||||||
|
This pipeline merges the sink queues from a list of pipelines by processing
|
||||||
|
frames from each pipeline's sink sequentially in the order specified. Each
|
||||||
|
pipeline runs to completion before the next one begins processing.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, pipelines: List[Pipeline]):
|
def __init__(self, pipelines: List[Pipeline]):
|
||||||
|
"""Initialize the sequential merge pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pipelines: List of pipelines to merge sequentially. Pipelines will
|
||||||
|
be processed in the order they appear in this list.
|
||||||
|
"""
|
||||||
super().__init__([])
|
super().__init__([])
|
||||||
self.pipelines = pipelines
|
self.pipelines = pipelines
|
||||||
|
|
||||||
async def run_pipeline(self):
|
async def run_pipeline(self):
|
||||||
|
"""Run all pipelines sequentially and merge their output.
|
||||||
|
|
||||||
|
Processes each pipeline in order, consuming all frames from each
|
||||||
|
pipeline's sink until an EndFrame or EndPipeFrame is encountered,
|
||||||
|
then moves to the next pipeline. After all pipelines complete,
|
||||||
|
sends a final EndFrame to signal completion.
|
||||||
|
"""
|
||||||
for idx, pipeline in enumerate(self.pipelines):
|
for idx, pipeline in enumerate(self.pipelines):
|
||||||
while True:
|
while True:
|
||||||
frame = await pipeline.sink.get()
|
frame = await pipeline.sink.get()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class DTMFAggregator(FrameProcessor):
|
|||||||
|
|
||||||
The aggregator accumulates digits from InputDTMFFrame instances and flushes
|
The aggregator accumulates digits from InputDTMFFrame instances and flushes
|
||||||
when:
|
when:
|
||||||
|
|
||||||
- Timeout occurs (configurable idle period)
|
- Timeout occurs (configurable idle period)
|
||||||
- Termination digit is received (default: '#')
|
- Termination digit is received (default: '#')
|
||||||
- EndFrame or CancelFrame is received
|
- EndFrame or CancelFrame is received
|
||||||
@@ -63,6 +64,7 @@ class DTMFAggregator(FrameProcessor):
|
|||||||
|
|
||||||
self._digit_event = asyncio.Event()
|
self._digit_event = asyncio.Event()
|
||||||
self._aggregation_task: Optional[asyncio.Task] = None
|
self._aggregation_task: Optional[asyncio.Task] = None
|
||||||
|
self._interruption_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
|
async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
|
||||||
"""Process incoming frames and handle DTMF aggregation.
|
"""Process incoming frames and handle DTMF aggregation.
|
||||||
@@ -80,6 +82,7 @@ class DTMFAggregator(FrameProcessor):
|
|||||||
if self._aggregation:
|
if self._aggregation:
|
||||||
await self._flush_aggregation()
|
await self._flush_aggregation()
|
||||||
await self._stop_aggregation_task()
|
await self._stop_aggregation_task()
|
||||||
|
await self._stop_interruption_task()
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
elif isinstance(frame, InputDTMFFrame):
|
elif isinstance(frame, InputDTMFFrame):
|
||||||
# Push the DTMF frame downstream first
|
# Push the DTMF frame downstream first
|
||||||
@@ -99,7 +102,7 @@ class DTMFAggregator(FrameProcessor):
|
|||||||
|
|
||||||
# For first digit, schedule interruption in separate task
|
# For first digit, schedule interruption in separate task
|
||||||
if is_first_digit:
|
if is_first_digit:
|
||||||
asyncio.create_task(self._send_interruption_task())
|
self._interruption_task = self.create_task(self._send_interruption_task())
|
||||||
|
|
||||||
# Check for immediate flush conditions
|
# Check for immediate flush conditions
|
||||||
if frame.button == self._termination_digit:
|
if frame.button == self._termination_digit:
|
||||||
@@ -110,12 +113,13 @@ class DTMFAggregator(FrameProcessor):
|
|||||||
|
|
||||||
async def _send_interruption_task(self):
|
async def _send_interruption_task(self):
|
||||||
"""Send interruption frame safely in a separate task."""
|
"""Send interruption frame safely in a separate task."""
|
||||||
try:
|
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)
|
||||||
# Send the interruption frame
|
|
||||||
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)
|
async def _stop_interruption_task(self) -> None:
|
||||||
except Exception as e:
|
"""Stops the interruption task."""
|
||||||
# Log error but don't propagate
|
if self._interruption_task:
|
||||||
print(f"Error sending interruption: {e}")
|
await self.cancel_task(self._interruption_task)
|
||||||
|
self._interruption_task = None
|
||||||
|
|
||||||
def _create_aggregation_task(self) -> None:
|
def _create_aggregation_task(self) -> None:
|
||||||
"""Creates the aggregation task if it hasn't been created yet."""
|
"""Creates the aggregation task if it hasn't been created yet."""
|
||||||
|
|||||||
@@ -26,29 +26,6 @@ class GatedAggregator(FrameProcessor):
|
|||||||
until and not including the gate-closed frame. The aggregator maintains an
|
until and not including the gate-closed frame. The aggregator maintains an
|
||||||
internal gate state that controls whether frames are passed through immediately
|
internal gate state that controls whether frames are passed through immediately
|
||||||
or accumulated for later release.
|
or accumulated for later release.
|
||||||
|
|
||||||
Doctest: FIXME to work with asyncio
|
|
||||||
>>> from pipecat.frames.frames import ImageRawFrame
|
|
||||||
|
|
||||||
>>> async def print_frames(aggregator, frame):
|
|
||||||
... async for frame in aggregator.process_frame(frame):
|
|
||||||
... if isinstance(frame, TextFrame):
|
|
||||||
... print(frame.text)
|
|
||||||
... else:
|
|
||||||
... print(frame.__class__.__name__)
|
|
||||||
|
|
||||||
>>> aggregator = GatedAggregator(
|
|
||||||
... gate_close_fn=lambda x: isinstance(x, LLMResponseStartFrame),
|
|
||||||
... gate_open_fn=lambda x: isinstance(x, ImageRawFrame),
|
|
||||||
... start_open=False)
|
|
||||||
>>> asyncio.run(print_frames(aggregator, TextFrame("Hello")))
|
|
||||||
>>> asyncio.run(print_frames(aggregator, TextFrame("Hello again.")))
|
|
||||||
>>> asyncio.run(print_frames(aggregator, ImageRawFrame(image=bytes([]), size=(0, 0))))
|
|
||||||
ImageRawFrame
|
|
||||||
Hello
|
|
||||||
Hello again.
|
|
||||||
>>> asyncio.run(print_frames(aggregator, TextFrame("Goodbye.")))
|
|
||||||
Goodbye.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class LLMFullResponseAggregator(FrameProcessor):
|
|||||||
the complete response via an event handler.
|
the complete response via an event handler.
|
||||||
|
|
||||||
The aggregator provides an "on_completion" event that fires when a full
|
The aggregator provides an "on_completion" event that fires when a full
|
||||||
completion is available:
|
completion is available::
|
||||||
|
|
||||||
@aggregator.event_handler("on_completion")
|
@aggregator.event_handler("on_completion")
|
||||||
async def on_completion(
|
async def on_completion(
|
||||||
@@ -363,6 +363,7 @@ class LLMUserContextAggregator(LLMContextResponseAggregator):
|
|||||||
|
|
||||||
This aggregator handles the complex logic of aggregating user speech transcriptions
|
This aggregator handles the complex logic of aggregating user speech transcriptions
|
||||||
from STT services. It manages multiple scenarios including:
|
from STT services. It manages multiple scenarios including:
|
||||||
|
|
||||||
- Transcriptions received between VAD events
|
- Transcriptions received between VAD events
|
||||||
- Transcriptions received outside VAD events
|
- Transcriptions received outside VAD events
|
||||||
- Interim vs final transcriptions
|
- Interim vs final transcriptions
|
||||||
@@ -469,9 +470,9 @@ class LLMUserContextAggregator(LLMContextResponseAggregator):
|
|||||||
elif isinstance(frame, InterimTranscriptionFrame):
|
elif isinstance(frame, InterimTranscriptionFrame):
|
||||||
await self._handle_interim_transcription(frame)
|
await self._handle_interim_transcription(frame)
|
||||||
elif isinstance(frame, LLMMessagesAppendFrame):
|
elif isinstance(frame, LLMMessagesAppendFrame):
|
||||||
self.add_messages(frame.messages)
|
await self._handle_llm_messages_append(frame)
|
||||||
elif isinstance(frame, LLMMessagesUpdateFrame):
|
elif isinstance(frame, LLMMessagesUpdateFrame):
|
||||||
self.set_messages(frame.messages)
|
await self._handle_llm_messages_update(frame)
|
||||||
elif isinstance(frame, LLMSetToolsFrame):
|
elif isinstance(frame, LLMSetToolsFrame):
|
||||||
self.set_tools(frame.tools)
|
self.set_tools(frame.tools)
|
||||||
elif isinstance(frame, LLMSetToolChoiceFrame):
|
elif isinstance(frame, LLMSetToolChoiceFrame):
|
||||||
@@ -543,6 +544,16 @@ class LLMUserContextAggregator(LLMContextResponseAggregator):
|
|||||||
async def _cancel(self, frame: CancelFrame):
|
async def _cancel(self, frame: CancelFrame):
|
||||||
await self._cancel_aggregation_task()
|
await self._cancel_aggregation_task()
|
||||||
|
|
||||||
|
async def _handle_llm_messages_append(self, frame: LLMMessagesAppendFrame):
|
||||||
|
self.add_messages(frame.messages)
|
||||||
|
if frame.run_llm:
|
||||||
|
await self.push_context_frame()
|
||||||
|
|
||||||
|
async def _handle_llm_messages_update(self, frame: LLMMessagesUpdateFrame):
|
||||||
|
self.set_messages(frame.messages)
|
||||||
|
if frame.run_llm:
|
||||||
|
await self.push_context_frame()
|
||||||
|
|
||||||
async def _handle_input_audio(self, frame: InputAudioRawFrame):
|
async def _handle_input_audio(self, frame: InputAudioRawFrame):
|
||||||
for s in self.interruption_strategies:
|
for s in self.interruption_strategies:
|
||||||
await s.append_audio(frame.audio, frame.sample_rate)
|
await s.append_audio(frame.audio, frame.sample_rate)
|
||||||
@@ -654,6 +665,7 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
|
|||||||
"""Assistant LLM aggregator that processes bot responses and function calls.
|
"""Assistant LLM aggregator that processes bot responses and function calls.
|
||||||
|
|
||||||
This aggregator handles the complex logic of processing assistant responses including:
|
This aggregator handles the complex logic of processing assistant responses including:
|
||||||
|
|
||||||
- Text frame aggregation between response start/end markers
|
- Text frame aggregation between response start/end markers
|
||||||
- Function call lifecycle management
|
- Function call lifecycle management
|
||||||
- Context updates with timestamps
|
- Context updates with timestamps
|
||||||
@@ -765,9 +777,9 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
|
|||||||
elif isinstance(frame, TextFrame):
|
elif isinstance(frame, TextFrame):
|
||||||
await self._handle_text(frame)
|
await self._handle_text(frame)
|
||||||
elif isinstance(frame, LLMMessagesAppendFrame):
|
elif isinstance(frame, LLMMessagesAppendFrame):
|
||||||
self.add_messages(frame.messages)
|
await self._handle_llm_messages_append(frame)
|
||||||
elif isinstance(frame, LLMMessagesUpdateFrame):
|
elif isinstance(frame, LLMMessagesUpdateFrame):
|
||||||
self.set_messages(frame.messages)
|
await self._handle_llm_messages_update(frame)
|
||||||
elif isinstance(frame, LLMSetToolsFrame):
|
elif isinstance(frame, LLMSetToolsFrame):
|
||||||
self.set_tools(frame.tools)
|
self.set_tools(frame.tools)
|
||||||
elif isinstance(frame, LLMSetToolChoiceFrame):
|
elif isinstance(frame, LLMSetToolChoiceFrame):
|
||||||
@@ -806,6 +818,16 @@ class LLMAssistantContextAggregator(LLMContextResponseAggregator):
|
|||||||
timestamp_frame = OpenAILLMContextAssistantTimestampFrame(timestamp=time_now_iso8601())
|
timestamp_frame = OpenAILLMContextAssistantTimestampFrame(timestamp=time_now_iso8601())
|
||||||
await self.push_frame(timestamp_frame)
|
await self.push_frame(timestamp_frame)
|
||||||
|
|
||||||
|
async def _handle_llm_messages_append(self, frame: LLMMessagesAppendFrame):
|
||||||
|
self.add_messages(frame.messages)
|
||||||
|
if frame.run_llm:
|
||||||
|
await self.push_context_frame(FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
|
async def _handle_llm_messages_update(self, frame: LLMMessagesUpdateFrame):
|
||||||
|
self.set_messages(frame.messages)
|
||||||
|
if frame.run_llm:
|
||||||
|
await self.push_context_frame(FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
async def _handle_interruptions(self, frame: StartInterruptionFrame):
|
async def _handle_interruptions(self, frame: StartInterruptionFrame):
|
||||||
await self.push_aggregation()
|
await self.push_aggregation()
|
||||||
self._started = 0
|
self._started = 0
|
||||||
|
|||||||
@@ -210,9 +210,10 @@ class OpenAILLMContext:
|
|||||||
def from_standard_message(self, message):
|
def from_standard_message(self, message):
|
||||||
"""Convert from OpenAI message format to OpenAI message format (passthrough).
|
"""Convert from OpenAI message format to OpenAI message format (passthrough).
|
||||||
|
|
||||||
OpenAI's format allows both simple string content and structured content:
|
OpenAI's format allows both simple string content and structured content::
|
||||||
- Simple: {"role": "user", "content": "Hello"}
|
|
||||||
- Structured: {"role": "user", "content": [{"type": "text", "text": "Hello"}]}
|
Simple: {"role": "user", "content": "Hello"}
|
||||||
|
Structured: {"role": "user", "content": [{"type": "text", "text": "Hello"}]}
|
||||||
|
|
||||||
Since OpenAI is our standard format, this is a passthrough function.
|
Since OpenAI is our standard format, this is a passthrough function.
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user