Compare commits
1 Commits
mb/update-
...
aleix/queu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41695806e8 |
48
.github/workflows/android.yaml
vendored
Normal file
48
.github/workflows/android.yaml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: android
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "examples/simple-chatbot/client/android/**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
paths:
|
||||||
|
- "examples/simple-chatbot/client/android/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
sdk_git_ref:
|
||||||
|
type: string
|
||||||
|
description: "Which git ref of the app to build"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: build-android-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sdk:
|
||||||
|
name: "Simple chatbot demo"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.sdk_git_ref || github.ref }}
|
||||||
|
|
||||||
|
- name: "Install Java"
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Build demo app
|
||||||
|
working-directory: examples/simple-chatbot/client/android
|
||||||
|
run: ./gradlew :simple-chatbot-client:assembleDebug
|
||||||
|
|
||||||
|
- name: Upload demo APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Simple Chatbot Android Client
|
||||||
|
path: examples/simple-chatbot/client/android/simple-chatbot-client/build/outputs/apk/debug/simple-chatbot-client-debug.apk
|
||||||
34
.github/workflows/build.yaml
vendored
34
.github/workflows/build.yaml
vendored
@@ -21,20 +21,24 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v3
|
|
||||||
with:
|
|
||||||
version: "latest"
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install 3.10
|
id: setup_python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
- name: Install development dependencies
|
with:
|
||||||
run: uv sync --group dev
|
python-version: '3.10'
|
||||||
|
- name: Setup virtual environment
|
||||||
|
run: |
|
||||||
|
python -m venv .venv
|
||||||
|
- name: Install basic Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r dev-requirements.txt
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: uv build
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
- name: Install project in editable mode
|
python -m build
|
||||||
run: uv pip install --editable .
|
- name: Install project and other Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --editable .
|
||||||
|
|||||||
37
.github/workflows/coverage.yaml
vendored
37
.github/workflows/coverage.yaml
vendored
@@ -18,28 +18,35 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v3
|
|
||||||
with:
|
|
||||||
version: "latest"
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install 3.10
|
id: setup_python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
- name: Cache virtual environment
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
# We are hashing dev-requirements.txt and test-requirements.txt which
|
||||||
|
# contain all dependencies needed to run the tests.
|
||||||
|
key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('dev-requirements.txt') }}-${{ hashFiles('test-requirements.txt') }}
|
||||||
|
path: .venv
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
|
id: install_system_packages
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y portaudio19-dev
|
sudo apt-get install -y portaudio19-dev
|
||||||
|
- name: Setup virtual environment
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
uv sync --group dev --extra anthropic --extra aws --extra google --extra langchain
|
python -m venv .venv
|
||||||
|
- name: Install basic Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r dev-requirements.txt -r test-requirements.txt
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: |
|
run: |
|
||||||
uv run coverage run
|
source .venv/bin/activate
|
||||||
uv run coverage xml
|
coverage run
|
||||||
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
35
.github/workflows/format.yaml
vendored
35
.github/workflows/format.yaml
vendored
@@ -17,27 +17,30 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ruff-format:
|
ruff-format:
|
||||||
name: "Code quality checks"
|
name: "Formatting checker"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v3
|
|
||||||
with:
|
|
||||||
version: "latest"
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install 3.10
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
- name: Install development dependencies
|
python-version: "3.10"
|
||||||
run: uv sync --group dev
|
- name: Setup virtual environment
|
||||||
|
run: |
|
||||||
|
python -m venv .venv
|
||||||
|
- name: Install development Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r dev-requirements.txt
|
||||||
- name: Ruff formatter
|
- name: Ruff formatter
|
||||||
id: ruff-format
|
id: ruff-format
|
||||||
run: uv run ruff format --diff
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
- name: Ruff linter (all rules)
|
ruff format --diff
|
||||||
|
- name: Ruff import linter
|
||||||
id: ruff-check
|
id: ruff-check
|
||||||
run: uv run ruff check
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
ruff check --select I
|
||||||
|
|||||||
26
.github/workflows/publish.yaml
vendored
26
.github/workflows/publish.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
gitref:
|
gitref:
|
||||||
type: string
|
type: string
|
||||||
description: "what git tag to build (e.g. v0.0.74)"
|
description: "what git ref to build"
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -17,17 +17,23 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.gitref }}
|
ref: ${{ github.event.inputs.gitref }}
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v3
|
|
||||||
with:
|
|
||||||
version: "latest"
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install 3.10
|
id: setup_python
|
||||||
- name: Install development dependencies
|
uses: actions/setup-python@v4
|
||||||
run: uv sync --group dev
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Setup virtual environment
|
||||||
|
run: |
|
||||||
|
python -m venv .venv
|
||||||
|
- name: Install basic Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r dev-requirements.txt
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: uv build
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m build
|
||||||
- name: Upload wheels
|
- name: Upload wheels
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
25
.github/workflows/publish_test.yaml
vendored
25
.github/workflows/publish_test.yaml
vendored
@@ -12,16 +12,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
fetch-depth: 100
|
fetch-depth: 100
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v3
|
|
||||||
with:
|
|
||||||
version: "latest"
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install 3.10
|
id: setup_python
|
||||||
- name: Install development dependencies
|
uses: actions/setup-python@v4
|
||||||
run: uv sync --group dev
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Setup virtual environment
|
||||||
|
run: |
|
||||||
|
python -m venv .venv
|
||||||
|
- name: Install basic Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r dev-requirements.txt
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: uv build
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m build
|
||||||
- name: Upload wheels
|
- name: Upload wheels
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -31,7 +38,7 @@ jobs:
|
|||||||
publish-to-test-pypi:
|
publish-to-test-pypi:
|
||||||
name: "Publish to Test PyPI"
|
name: "Publish to Test PyPI"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build]
|
needs: [ build ]
|
||||||
environment:
|
environment:
|
||||||
name: testpypi
|
name: testpypi
|
||||||
url: https://pypi.org/p/pipecat-ai
|
url: https://pypi.org/p/pipecat-ai
|
||||||
|
|||||||
123
.github/workflows/python-compatibility.yaml
vendored
123
.github/workflows/python-compatibility.yaml
vendored
@@ -1,123 +0,0 @@
|
|||||||
name: Python Compatibility Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
paths: ['pyproject.toml']
|
|
||||||
pull_request:
|
|
||||||
branches: [main, develop]
|
|
||||||
paths: ['pyproject.toml']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-dev-environment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ['3.10.18', '3.11.13', '3.12.11', '3.13.5']
|
|
||||||
|
|
||||||
name: Dev Environment - Python ${{ matrix.python-version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install system dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
portaudio19-dev \
|
|
||||||
libcairo2-dev \
|
|
||||||
libgirepository1.0-dev \
|
|
||||||
pkg-config
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v4
|
|
||||||
with:
|
|
||||||
version: 'latest'
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
run: |
|
|
||||||
uv python install ${{ matrix.python-version }}
|
|
||||||
uv python pin ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Test uv sync with all extras (Python < 3.13)
|
|
||||||
if: "!startsWith(matrix.python-version, '3.13.')"
|
|
||||||
run: |
|
|
||||||
uv sync --group dev --all-extras --no-extra krisp
|
|
||||||
|
|
||||||
- name: Test uv sync without PyTorch extras (Python 3.13+)
|
|
||||||
if: startsWith(matrix.python-version, '3.13.')
|
|
||||||
run: |
|
|
||||||
uv sync --group dev --all-extras \
|
|
||||||
--no-extra krisp \
|
|
||||||
--no-extra ultravox \
|
|
||||||
--no-extra local-smart-turn \
|
|
||||||
--no-extra moondream \
|
|
||||||
--no-extra mlx-whisper
|
|
||||||
|
|
||||||
- name: Verify dev installation
|
|
||||||
run: |
|
|
||||||
uv run python --version
|
|
||||||
uv run python -c "import pipecat; print('✅ Dev environment - Pipecat imports successfully')"
|
|
||||||
|
|
||||||
test-user-experience:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ['3.10.18', '3.11.13', '3.12.11', '3.13.5']
|
|
||||||
|
|
||||||
name: User Experience - Python ${{ matrix.python-version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install system dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
portaudio19-dev \
|
|
||||||
libcairo2-dev \
|
|
||||||
libgirepository1.0-dev \
|
|
||||||
pkg-config
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v4
|
|
||||||
with:
|
|
||||||
version: 'latest'
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
run: |
|
|
||||||
uv python install ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Build local package
|
|
||||||
run: |
|
|
||||||
uv build
|
|
||||||
|
|
||||||
- name: Create test project
|
|
||||||
run: |
|
|
||||||
mkdir test-project
|
|
||||||
cd test-project
|
|
||||||
uv init --python ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Test comprehensive extras with uv add (Python 3.10-3.12)
|
|
||||||
if: "!startsWith(matrix.python-version, '3.13.')"
|
|
||||||
run: |
|
|
||||||
cd test-project
|
|
||||||
# Use uv add with built wheel to leverage dependency management
|
|
||||||
uv add "../dist/pipecat_ai-"*".whl[anthropic,assemblyai,asyncai,aws,aws-nova-sonic,azure,cartesia,cerebras,deepseek,daily,deepgram,elevenlabs,fal,fireworks,fish,gladia,google,grok,groq,gstreamer,heygen,inworld,koala,langchain,livekit,lmnt,local,mcp,mem0,mlx-whisper,moondream,nim,neuphonic,noisereduce,openai,openpipe,openrouter,perplexity,playht,qwen,rime,riva,runner,sambanova,sentry,local-smart-turn,remote-smart-turn,silero,simli,soniox,soundfile,speechmatics,tavus,together,tracing,ultravox,webrtc,websocket,whisper]"
|
|
||||||
|
|
||||||
- name: Test Python 3.13 compatible extras with uv add
|
|
||||||
if: startsWith(matrix.python-version, '3.13.')
|
|
||||||
run: |
|
|
||||||
cd test-project
|
|
||||||
# Use uv add with built wheel and Python 3.13 compatible extras
|
|
||||||
uv add "../dist/pipecat_ai-"*".whl[anthropic,assemblyai,asyncai,aws,aws-nova-sonic,azure,cartesia,cerebras,deepseek,daily,deepgram,elevenlabs,fal,fireworks,fish,gladia,google,grok,groq,gstreamer,heygen,inworld,koala,langchain,livekit,lmnt,local,mcp,mem0,nim,neuphonic,noisereduce,openai,openpipe,openrouter,perplexity,playht,qwen,rime,riva,runner,sambanova,sentry,remote-smart-turn,silero,simli,soniox,soundfile,speechmatics,tavus,together,tracing,webrtc,websocket,whisper]"
|
|
||||||
|
|
||||||
- name: Verify user installation
|
|
||||||
run: |
|
|
||||||
cd test-project
|
|
||||||
uv run python --version
|
|
||||||
uv run python -c "import pipecat; print('✅ User experience - Pipecat imports successfully')"
|
|
||||||
# Test that basic functionality works
|
|
||||||
uv run python -c "from pipecat.pipeline.pipeline import Pipeline; print('✅ Pipeline import works')"
|
|
||||||
56
.github/workflows/sync-quickstart.yaml
vendored
56
.github/workflows/sync-quickstart.yaml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: Sync Quickstart to pipecat-quickstart repo
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'examples/quickstart/**'
|
|
||||||
workflow_dispatch: # Manual trigger
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-quickstart:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout main repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Checkout quickstart repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: pipecat-ai/pipecat-quickstart
|
|
||||||
token: ${{ secrets.QUICKSTART_SYNC_TOKEN }}
|
|
||||||
path: quickstart-repo
|
|
||||||
|
|
||||||
- name: Sync files (excluding READMEs)
|
|
||||||
run: |
|
|
||||||
# Copy code files only, skip READMEs
|
|
||||||
cp examples/quickstart/bot.py quickstart-repo/
|
|
||||||
cp examples/quickstart/requirements.txt quickstart-repo/
|
|
||||||
cp examples/quickstart/env.example quickstart-repo/
|
|
||||||
|
|
||||||
# Copy any other files that aren't README.md
|
|
||||||
find examples/quickstart -type f \
|
|
||||||
-not -name "README.md" \
|
|
||||||
-not -name "*.md" \
|
|
||||||
-exec cp {} quickstart-repo/ \;
|
|
||||||
|
|
||||||
- name: Commit and push changes
|
|
||||||
run: |
|
|
||||||
cd quickstart-repo
|
|
||||||
git config user.name "GitHub Action"
|
|
||||||
git config user.email "action@github.com"
|
|
||||||
git add .
|
|
||||||
|
|
||||||
# Only commit if there are changes
|
|
||||||
if ! git diff --staged --quiet; then
|
|
||||||
git commit -m "Sync from pipecat main repo
|
|
||||||
|
|
||||||
Updated files from examples/quickstart/
|
|
||||||
Commit: ${{ github.sha }}
|
|
||||||
"
|
|
||||||
git push
|
|
||||||
else
|
|
||||||
echo "No changes to sync"
|
|
||||||
fi
|
|
||||||
34
.github/workflows/tests.yaml
vendored
34
.github/workflows/tests.yaml
vendored
@@ -22,23 +22,31 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v3
|
|
||||||
with:
|
|
||||||
version: "latest"
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install 3.10
|
id: setup_python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
- name: Cache virtual environment
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
# We are hashing dev-requirements.txt and test-requirements.txt which
|
||||||
|
# contain all dependencies needed to run the tests.
|
||||||
|
key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('dev-requirements.txt') }}-${{ hashFiles('test-requirements.txt') }}
|
||||||
|
path: .venv
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
|
id: install_system_packages
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y portaudio19-dev
|
sudo apt-get install -y portaudio19-dev
|
||||||
|
- name: Setup virtual environment
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
uv sync --group dev --extra anthropic --extra aws --extra google --extra langchain
|
python -m venv .venv
|
||||||
|
- name: Install basic Python dependencies
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r dev-requirements.txt -r test-requirements.txt
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
uv run pytest
|
source .venv/bin/activate
|
||||||
|
pytest
|
||||||
|
|||||||
42
.github/workflows/update-lockfile.yaml
vendored
42
.github/workflows/update-lockfile.yaml
vendored
@@ -1,42 +0,0 @@
|
|||||||
name: Update lockfile
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- 'pyproject.toml'
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch: # Allows manual triggering from GitHub UI
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-lockfile:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
# This gives the workflow permission to push back to the repo
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v1
|
|
||||||
|
|
||||||
- name: Update lockfile
|
|
||||||
run: uv lock
|
|
||||||
|
|
||||||
- name: Check for changes
|
|
||||||
id: verify-changed-files
|
|
||||||
run: |
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
echo "changed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "changed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit lockfile
|
|
||||||
if: steps.verify-changed-files.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
git config --local user.email "action@github.com"
|
|
||||||
git config --local user.name "GitHub Action"
|
|
||||||
git add uv.lock
|
|
||||||
git commit -m "chore: update uv.lock after dependency changes"
|
|
||||||
git push
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -31,6 +31,8 @@ MANIFEST
|
|||||||
fly.toml
|
fly.toml
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
examples/telnyx-chatbot/templates/streams.xml
|
||||||
|
examples/twilio-chatbot/templates/streams.xml
|
||||||
examples/**/node_modules/
|
examples/**/node_modules/
|
||||||
examples/**/.expo/
|
examples/**/.expo/
|
||||||
examples/**/dist/
|
examples/**/dist/
|
||||||
@@ -48,7 +50,4 @@ examples/**/web-build/
|
|||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
docs/api/_build/
|
docs/api/_build/
|
||||||
docs/api/api
|
docs/api/api
|
||||||
|
|
||||||
# uv
|
|
||||||
.python-version
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.1
|
rev: v0.9.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
language_version: python3
|
language_version: python3
|
||||||
args: [--fix]
|
args: [ --select, I, ]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
@@ -9,14 +9,22 @@ build:
|
|||||||
- python3-dev
|
- python3-dev
|
||||||
- libasound2-dev
|
- libasound2-dev
|
||||||
jobs:
|
jobs:
|
||||||
post_install:
|
pre_build:
|
||||||
- pip install uv
|
- python -m pip install --upgrade pip
|
||||||
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs --all-extras --no-extra krisp --no-extra gstreamer --no-extra ultravox --no-extra local_smart_turn --no-extra moondream --no-extra riva --no-extra mlx-whisper
|
- pip install wheel setuptools
|
||||||
|
post_build:
|
||||||
|
- echo "Build completed"
|
||||||
|
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: docs/api/conf.py
|
configuration: docs/api/conf.py
|
||||||
fail_on_warning: false
|
fail_on_warning: false
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/api/requirements.txt
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
|
|
||||||
search:
|
search:
|
||||||
ranking:
|
ranking:
|
||||||
api/*: 5
|
api/*: 5
|
||||||
|
|||||||
1062
CHANGELOG.md
1062
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
150
CONTRIBUTING.md
150
CONTRIBUTING.md
@@ -41,150 +41,36 @@ We use Ruff for code linting and formatting. Please ensure your code passes all
|
|||||||
|
|
||||||
We follow Google-style docstrings with these specific conventions:
|
We follow Google-style docstrings with these specific conventions:
|
||||||
|
|
||||||
**Regular Classes:**
|
- Class docstrings should fully document all parameters used in `__init__`
|
||||||
|
- We don't require separate docstrings for `__init__` methods when parameters are documented in the class docstring
|
||||||
|
- Property methods should have docstrings explaining their purpose and return value
|
||||||
|
|
||||||
- Class docstring describes the class purpose and key functionality
|
Example of correctly documented class:
|
||||||
- `__init__` method has its own docstring with complete `Args:` section documenting all parameters
|
|
||||||
- All public methods must have docstrings with `Args:` and `Returns:` sections as appropriate
|
|
||||||
|
|
||||||
**Dataclasses:**
|
|
||||||
|
|
||||||
- Class docstring describes the purpose and documents all fields in a `Parameters:` section
|
|
||||||
- No `__init__` docstring (auto-generated)
|
|
||||||
|
|
||||||
**Properties:**
|
|
||||||
|
|
||||||
- Must have docstrings with `Returns:` section
|
|
||||||
|
|
||||||
**Abstract Methods:**
|
|
||||||
|
|
||||||
- Must have docstrings explaining what subclasses should implement
|
|
||||||
|
|
||||||
**`__init__.py` Files:**
|
|
||||||
|
|
||||||
- **Skip docstrings** for pure import/re-export modules
|
|
||||||
- **Add brief docstrings** for top-level packages or those with initialization logic
|
|
||||||
|
|
||||||
**Enums:**
|
|
||||||
|
|
||||||
- Class docstring describes the enumeration purpose
|
|
||||||
- Use `Parameters:` section to document each enum value and its meaning
|
|
||||||
- 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:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Regular class
|
class MyClass:
|
||||||
class MyService(BaseService):
|
"""Class description.
|
||||||
"""Description of what the service does.
|
|
||||||
|
|
||||||
Provides detailed explanation of the service's functionality,
|
Additional details about the class.
|
||||||
key features, and usage patterns.
|
|
||||||
|
|
||||||
Supported features:
|
Args:
|
||||||
|
param1: Description of first parameter.
|
||||||
- Feature one with detailed explanation
|
param2: Description of second parameter.
|
||||||
- Feature two with additional context
|
|
||||||
- Feature three for advanced use cases
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, param1: str, old_param: str = None, **kwargs):
|
def __init__(self, param1, param2):
|
||||||
"""Initialize the service.
|
# No docstring required here as parameters are documented above
|
||||||
|
self.param1 = param1
|
||||||
Args:
|
self.param2 = param2
|
||||||
param1: Description of param1.
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sample_rate(self) -> int:
|
def some_property(self) -> str:
|
||||||
"""Get the current sample rate.
|
"""Get the formatted property value.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The sample rate in Hz.
|
A string representation of the property.
|
||||||
"""
|
"""
|
||||||
return self._sample_rate
|
return f"Property: {self.param1}"
|
||||||
|
|
||||||
async def process_data(self, data: str) -> bool:
|
|
||||||
"""Process the provided data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The data to process.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if processing succeeded.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Dataclass with code examples
|
|
||||||
@dataclass
|
|
||||||
class MessageFrame:
|
|
||||||
"""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:
|
|
||||||
messages: List of messages in OpenAI format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
messages: List[dict]
|
|
||||||
|
|
||||||
# Enum class
|
|
||||||
class Status(Enum):
|
|
||||||
"""Status codes for processing operations.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
PENDING: Operation is queued but not started.
|
|
||||||
RUNNING: Operation is currently in progress.
|
|
||||||
COMPLETED: Operation finished successfully.
|
|
||||||
FAILED: Operation encountered an error.
|
|
||||||
"""
|
|
||||||
|
|
||||||
PENDING = "pending"
|
|
||||||
RUNNING = "running"
|
|
||||||
COMPLETED = "completed"
|
|
||||||
FAILED = "failed"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|||||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# setup
|
||||||
|
FROM python:3.11.5
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /app
|
||||||
|
COPY *.py /app
|
||||||
|
COPY pyproject.toml /app
|
||||||
|
|
||||||
|
COPY src/ /app/src/
|
||||||
|
COPY examples/ /app/examples/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN ls --recursive /app/
|
||||||
|
RUN pip3 install --upgrade -r requirements.txt
|
||||||
|
RUN python -m build .
|
||||||
|
RUN pip3 install .
|
||||||
|
RUN pip3 install gunicorn
|
||||||
|
# If running on Ubuntu, Azure TTS requires some extra config
|
||||||
|
# https://learn.microsoft.com/en-us/azure/ai-services/speech-service/quickstarts/setup-platform?pivots=programming-language-python&tabs=linux%2Cubuntu%2Cdotnetcli%2Cdotnet%2Cjre%2Cmaven%2Cnodejs%2Cmac%2Cpypi
|
||||||
|
|
||||||
|
RUN wget -O - https://www.openssl.org/source/openssl-1.1.1w.tar.gz | tar zxf -
|
||||||
|
WORKDIR openssl-1.1.1w
|
||||||
|
RUN ./config --prefix=/usr/local
|
||||||
|
RUN make -j $(nproc)
|
||||||
|
RUN make install_sw install_ssldirs
|
||||||
|
RUN ldconfig -v
|
||||||
|
ENV SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
#ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
|
RUN apt clean
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install build-essential libssl-dev ca-certificates libasound2 wget
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
# run
|
||||||
|
CMD ["gunicorn", "--workers=2", "--log-level", "debug", "--chdir", "examples/server", "--capture-output", "daily-bot-manager:app", "--bind=0.0.0.0:8000"]
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
prune docs
|
|
||||||
prune examples
|
|
||||||
prune scripts
|
|
||||||
prune tests
|
|
||||||
148
README.md
148
README.md
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
**Pipecat** is an open-source Python framework for building real-time voice and multimodal conversational agents. Orchestrate audio and video, AI services, different transports, and conversation pipelines effortlessly—so you can focus on what makes your agent unique.
|
**Pipecat** is an open-source Python framework for building real-time voice and multimodal conversational agents. Orchestrate audio and video, AI services, different transports, and conversation pipelines effortlessly—so you can focus on what makes your agent unique.
|
||||||
|
|
||||||
> Want to dive right in? Try the [quickstart](https://docs.pipecat.ai/getting-started/quickstart).
|
|
||||||
|
|
||||||
## 🚀 What You Can Build
|
## 🚀 What You Can Build
|
||||||
|
|
||||||
- **Voice Assistants** – natural, streaming conversations with AI
|
- **Voice Assistants** – natural, streaming conversations with AI
|
||||||
@@ -31,11 +29,11 @@
|
|||||||
## 🎬 See it in action
|
## 🎬 See it in action
|
||||||
|
|
||||||
<p float="left">
|
<p float="left">
|
||||||
<a href="https://github.com/pipecat-ai/pipecat-examples/tree/main/simple-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat-examples/main/simple-chatbot/image.png" width="400" /></a>
|
<a href="https://github.com/pipecat-ai/pipecat/tree/main/examples/simple-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat/main/examples/simple-chatbot/image.png" width="400" /></a>
|
||||||
<a href="https://github.com/pipecat-ai/pipecat-examples/tree/main/storytelling-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat-examples/main/storytelling-chatbot/image.png" width="400" /></a>
|
<a href="https://github.com/pipecat-ai/pipecat/tree/main/examples/storytelling-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat/main/examples/storytelling-chatbot/image.png" width="400" /></a>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://github.com/pipecat-ai/pipecat-examples/tree/main/translation-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat-examples/main/translation-chatbot/image.png" width="400" /></a>
|
<a href="https://github.com/pipecat-ai/pipecat/tree/main/examples/translation-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat/main/examples/translation-chatbot/image.png" width="400" /></a>
|
||||||
<a href="https://github.com/pipecat-ai/pipecat-examples/tree/main/moondream-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat-examples/main/moondream-chatbot/image.png" width="400" /></a>
|
<a href="https://github.com/pipecat-ai/pipecat/tree/main/examples/moondream-chatbot"><img src="https://raw.githubusercontent.com/pipecat-ai/pipecat/main/examples/moondream-chatbot/image.png" width="400" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 📱 Client SDKs
|
## 📱 Client SDKs
|
||||||
@@ -51,123 +49,97 @@ 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), [NVIDIA Riva](https://docs.pipecat.ai/server/services/stt/riva), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [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) |
|
| 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), [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), [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), [Together AI](https://docs.pipecat.ai/server/services/llm/together) |
|
||||||
| Text-to-Speech | [Async](https://docs.pipecat.ai/server/services/tts/asyncai), [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [Groq](https://docs.pipecat.ai/server/services/tts/groq), [Inworld](https://docs.pipecat.ai/server/services/tts/inworld), [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), [NVIDIA Riva](https://docs.pipecat.ai/server/services/tts/riva), [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), [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), [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 | [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) |
|
| Video | [Tavus](https://docs.pipecat.ai/server/services/video/tavus), [Simli](https://docs.pipecat.ai/server/services/video/simli) |
|
||||||
| Video | [HeyGen](https://docs.pipecat.ai/server/services/video/heygen), [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 | [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)
|
||||||
|
|
||||||
## ⚡ Getting started
|
## ⚡ Getting started
|
||||||
|
|
||||||
You can get started with Pipecat running on your local machine, then move your agent processes to the cloud when you're ready.
|
You can get started with Pipecat running on your local machine, then move your agent processes to the cloud when you’re ready.
|
||||||
|
|
||||||
1. Install uv
|
```shell
|
||||||
|
# Install the module
|
||||||
|
pip install pipecat-ai
|
||||||
|
|
||||||
```bash
|
# Set up your environment
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
cp dot-env.template .env
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Need help?** Refer to the [uv install documentation](https://docs.astral.sh/uv/getting-started/installation/).
|
To keep things lightweight, only the core framework is included by default. If you need support for third-party AI services, you can add the necessary dependencies with:
|
||||||
|
|
||||||
2. Install the module
|
```shell
|
||||||
|
pip install "pipecat-ai[option,...]"
|
||||||
```bash
|
```
|
||||||
# For new projects
|
|
||||||
uv init my-pipecat-app
|
|
||||||
cd my-pipecat-app
|
|
||||||
uv add pipecat-ai
|
|
||||||
|
|
||||||
# Or for existing projects
|
|
||||||
uv add pipecat-ai
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Set up your environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
4. To keep things lightweight, only the core framework is included by default. If you need support for third-party AI services, you can add the necessary dependencies with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv add "pipecat-ai[option,...]"
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Using pip?** You can still use `pip install pipecat-ai` and `pip install "pipecat-ai[option,...]"` to get set up.
|
|
||||||
|
|
||||||
## 🧪 Code examples
|
## 🧪 Code examples
|
||||||
|
|
||||||
- [Foundational](https://github.com/pipecat-ai/pipecat/tree/main/examples/foundational) — small snippets that build on each other, introducing one or two concepts at a time
|
- [Foundational](https://github.com/pipecat-ai/pipecat/tree/main/examples/foundational) — small snippets that build on each other, introducing one or two concepts at a time
|
||||||
- [Example apps](https://github.com/pipecat-ai/pipecat-examples) — complete applications that you can use as starting points for development
|
- [Example apps](https://github.com/pipecat-ai/pipecat/tree/main/examples/) — complete applications that you can use as starting points for development
|
||||||
|
|
||||||
## 🛠️ Contributing to the framework
|
## 🛠️ Hacking on the framework itself
|
||||||
|
|
||||||
### Prerequisites
|
1. Set up a virtual environment before following these instructions. From the root of the repo:
|
||||||
|
|
||||||
**Python Version:** 3.10+
|
```shell
|
||||||
|
python3 -m venv venv
|
||||||
### Setup Steps
|
source venv/bin/activate
|
||||||
|
|
||||||
1. Clone the repository and navigate to it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/pipecat-ai/pipecat.git
|
|
||||||
cd pipecat
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install development and testing dependencies:
|
2. Install the development dependencies:
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
uv sync --group dev --all-extras --no-extra gstreamer --no-extra krisp --no-extra local
|
pip install -r dev-requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Install the git pre-commit hooks:
|
3. Install the git pre-commit hooks (these help ensure your code follows project rules):
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
uv run pre-commit install
|
pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Python 3.13+ Compatibility
|
4. Install the `pipecat-ai` package locally in editable mode:
|
||||||
|
|
||||||
Some features require PyTorch, which doesn't yet support Python 3.13+. Install using:
|
```shell
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
> The `-e` or `--editable` option allows you to modify the code without reinstalling.
|
||||||
uv sync --group dev --all-extras \
|
|
||||||
--no-extra gstreamer \
|
|
||||||
--no-extra krisp \
|
|
||||||
--no-extra local \
|
|
||||||
--no-extra local-smart-turn \
|
|
||||||
--no-extra mlx-whisper \
|
|
||||||
--no-extra moondream \
|
|
||||||
--no-extra ultravox
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Tip:** For full compatibility, use Python 3.12: `uv python pin 3.12`
|
5. Include optional dependencies as needed. For example:
|
||||||
|
|
||||||
> **Note**: Some extras (local, gstreamer) require system dependencies. See documentation if you encounter build errors.
|
```shell
|
||||||
|
pip install -e ".[daily,deepgram,cartesia,openai,silero]"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. (Optional) If you want to use this package from another directory:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install "path_to_this_repo[option,...]"
|
||||||
|
```
|
||||||
|
|
||||||
### Running tests
|
### Running tests
|
||||||
|
|
||||||
To run all tests, from the root directory:
|
Install the test dependencies:
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
uv run pytest
|
pip install -r test-requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Run a specific test suite:
|
From the root directory, run:
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
uv run pytest tests/test_name.py
|
pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setting up your editor
|
### Setting up your editor
|
||||||
|
|||||||
13
dev-requirements.txt
Normal file
13
dev-requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
build~=1.2.2
|
||||||
|
coverage~=7.6.12
|
||||||
|
grpcio-tools~=1.67.1
|
||||||
|
pip-tools~=7.4.1
|
||||||
|
pre-commit~=4.0.1
|
||||||
|
pyright~=1.1.397
|
||||||
|
pytest~=8.3.4
|
||||||
|
pytest-asyncio~=0.25.3
|
||||||
|
pytest-aiohttp==1.1.0
|
||||||
|
ruff~=0.11.1
|
||||||
|
setuptools~=70.0.0
|
||||||
|
setuptools_scm~=8.1.0
|
||||||
|
python-dotenv~=1.0.1
|
||||||
@@ -1,27 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Build docs using uv
|
|
||||||
echo "Installing dependencies with uv..."
|
|
||||||
uv sync --group docs --all-extras --no-extra krisp --no-extra gstreamer --no-extra ultravox --no-extra local_smart_turn --no-extra moondream --no-extra riva --no-extra mlx-whisper
|
|
||||||
|
|
||||||
# Check if sphinx-build is available
|
|
||||||
if ! uv run sphinx-build --version &> /dev/null; then
|
|
||||||
echo "Error: sphinx-build is not available" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean previous build
|
# Clean previous build
|
||||||
rm -rf _build
|
rm -rf _build
|
||||||
|
|
||||||
echo "Building documentation..."
|
|
||||||
# Build docs matching ReadTheDocs configuration
|
# Build docs matching ReadTheDocs configuration
|
||||||
uv run sphinx-build -b html -d _build/doctrees . _build/html -W --keep-going
|
sphinx-build -b html -d _build/doctrees . _build/html -W --keep-going
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
# Open docs (MacOS)
|
||||||
echo "Documentation built successfully!"
|
open _build/html/index.html
|
||||||
# Open docs (MacOS)
|
|
||||||
open _build/html/index.html
|
|
||||||
else
|
|
||||||
echo "Documentation build failed!" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
274
docs/api/conf.py
274
docs/api/conf.py
@@ -1,7 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -15,8 +13,7 @@ sys.path.insert(0, str(project_root / "src"))
|
|||||||
|
|
||||||
# Project information
|
# Project information
|
||||||
project = "pipecat-ai"
|
project = "pipecat-ai"
|
||||||
current_year = datetime.now().year
|
copyright = "2024, Daily"
|
||||||
copyright = f"2024-{current_year}, Daily" if current_year > 2024 else "2024, Daily"
|
|
||||||
author = "Daily"
|
author = "Daily"
|
||||||
|
|
||||||
# General configuration
|
# General configuration
|
||||||
@@ -27,60 +24,107 @@ extensions = [
|
|||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
]
|
]
|
||||||
|
|
||||||
suppress_warnings = [
|
|
||||||
"autodoc.mocked_object",
|
|
||||||
"toc.not_included",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Napoleon settings
|
# Napoleon settings
|
||||||
napoleon_google_docstring = True
|
napoleon_google_docstring = True
|
||||||
|
napoleon_numpy_docstring = False
|
||||||
napoleon_include_init_with_doc = True
|
napoleon_include_init_with_doc = True
|
||||||
|
|
||||||
# AutoDoc settings
|
# AutoDoc settings
|
||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
"members": True,
|
"members": True,
|
||||||
"member-order": "bysource",
|
"member-order": "bysource",
|
||||||
"undoc-members": False,
|
"special-members": "__init__",
|
||||||
"exclude-members": "__weakref__,model_config",
|
"undoc-members": True,
|
||||||
|
"exclude-members": "__weakref__",
|
||||||
|
"no-index": True,
|
||||||
"show-inheritance": True,
|
"show-inheritance": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mock imports for optional dependencies
|
# Mock imports for optional dependencies
|
||||||
autodoc_mock_imports = [
|
autodoc_mock_imports = [
|
||||||
# Krisp - has build issues on some platforms
|
"riva",
|
||||||
"pipecat_ai_krisp",
|
"livekit",
|
||||||
|
"pyht", # Base PlayHT package
|
||||||
|
"pyht.async_client", # PlayHT specific imports
|
||||||
|
"pyht.client",
|
||||||
|
"pyht.protos",
|
||||||
|
"pyht.protos.api_pb2",
|
||||||
|
"pipecat_ai_playht", # PlayHT wrapper
|
||||||
|
"aiortc",
|
||||||
|
"aiortc.mediastreams",
|
||||||
|
"cv2",
|
||||||
|
"av",
|
||||||
|
"pyneuphonic",
|
||||||
|
"mem0",
|
||||||
|
"mlx_whisper",
|
||||||
|
"anthropic",
|
||||||
|
"assemblyai",
|
||||||
|
"boto3",
|
||||||
|
"azure",
|
||||||
|
"cartesia",
|
||||||
|
"deepgram",
|
||||||
|
"elevenlabs",
|
||||||
|
"fal",
|
||||||
|
"gladia",
|
||||||
|
"google",
|
||||||
"krisp",
|
"krisp",
|
||||||
# System-specific GUI libraries
|
"langchain",
|
||||||
|
"lmnt",
|
||||||
|
"noisereduce",
|
||||||
|
"openai",
|
||||||
|
"openpipe",
|
||||||
|
"simli",
|
||||||
|
"soundfile",
|
||||||
|
"pipecat_ai_krisp",
|
||||||
|
"pyaudio",
|
||||||
"_tkinter",
|
"_tkinter",
|
||||||
"tkinter",
|
"tkinter",
|
||||||
# Platform-specific audio libraries (if needed)
|
"daily",
|
||||||
"gi",
|
"daily_python",
|
||||||
"gi.require_version",
|
"pydantic.BaseModel",
|
||||||
"gi.repository",
|
"pydantic.Field",
|
||||||
# OpenCV - sometimes has import issues during docs build
|
"pydantic._internal._model_construction",
|
||||||
"cv2",
|
"pydantic._internal._fields",
|
||||||
# Heavy ML packages excluded from ReadTheDocs
|
# Moondream dependencies
|
||||||
# ultravox dependencies
|
"torch",
|
||||||
|
"transformers",
|
||||||
|
"intel_extension_for_pytorch",
|
||||||
|
# Ultravox dependencies
|
||||||
|
"huggingface_hub",
|
||||||
"vllm",
|
"vllm",
|
||||||
"vllm.engine.arg_utils",
|
"vllm.engine.arg_utils",
|
||||||
# local-smart-turn dependencies
|
|
||||||
"coremltools",
|
|
||||||
"coremltools.models",
|
|
||||||
"coremltools.models.MLModel",
|
|
||||||
"torch",
|
|
||||||
"torch.nn",
|
|
||||||
"torch.nn.functional",
|
|
||||||
"torchaudio",
|
|
||||||
# moondream dependencies
|
|
||||||
"transformers",
|
|
||||||
"transformers.AutoTokenizer",
|
"transformers.AutoTokenizer",
|
||||||
"transformers.AutoFeatureExtractor",
|
# Langchain dependencies
|
||||||
"AutoFeatureExtractor",
|
"langchain_core",
|
||||||
"timm",
|
"langchain_core.messages",
|
||||||
"einops",
|
"langchain_core.runnables",
|
||||||
"intel_extension_for_pytorch",
|
"langchain_core.messages.AIMessageChunk",
|
||||||
"huggingface_hub",
|
"langchain_core.runnables.Runnable",
|
||||||
# riva dependencies
|
# LiveKit dependencies
|
||||||
|
"livekit",
|
||||||
|
"livekit.rtc",
|
||||||
|
"livekit_api",
|
||||||
|
"livekit_protocol",
|
||||||
|
"tenacity",
|
||||||
|
"tenacity.retry",
|
||||||
|
"tenacity.stop_after_attempt",
|
||||||
|
"tenacity.wait_exponential",
|
||||||
|
"rtc",
|
||||||
|
"rtc.Room",
|
||||||
|
"rtc.RoomOptions",
|
||||||
|
"rtc.AudioSource",
|
||||||
|
"rtc.LocalAudioTrack",
|
||||||
|
"rtc.TrackPublishOptions",
|
||||||
|
"rtc.TrackSource",
|
||||||
|
"rtc.AudioStream",
|
||||||
|
"rtc.AudioFrameEvent",
|
||||||
|
"rtc.AudioFrame",
|
||||||
|
"rtc.Track",
|
||||||
|
"rtc.TrackKind",
|
||||||
|
"rtc.RemoteParticipant",
|
||||||
|
"rtc.RemoteTrackPublication",
|
||||||
|
"rtc.DataPacket",
|
||||||
|
# Riva dependencies
|
||||||
"riva",
|
"riva",
|
||||||
"riva.client",
|
"riva.client",
|
||||||
"riva.client.Auth",
|
"riva.client.Auth",
|
||||||
@@ -90,45 +134,96 @@ autodoc_mock_imports = [
|
|||||||
"riva.client.AudioEncoding",
|
"riva.client.AudioEncoding",
|
||||||
"riva.client.proto.riva_tts_pb2",
|
"riva.client.proto.riva_tts_pb2",
|
||||||
"riva.client.SpeechSynthesisService",
|
"riva.client.SpeechSynthesisService",
|
||||||
# MLX dependencies (Apple Silicon specific)
|
# Local CoreML Smart Turn dependencies
|
||||||
"mlx",
|
"coremltools",
|
||||||
"mlx_whisper", # Note: might need underscore format too
|
"coremltools.models",
|
||||||
|
"coremltools.models.MLModel",
|
||||||
|
"torch",
|
||||||
|
"torch.nn",
|
||||||
|
"torch.nn.functional",
|
||||||
|
"transformers",
|
||||||
|
"transformers.AutoFeatureExtractor",
|
||||||
|
# Also add specific classes that are imported
|
||||||
|
"AutoFeatureExtractor",
|
||||||
]
|
]
|
||||||
|
|
||||||
# HTML output settings
|
# HTML output settings
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "sphinx_rtd_theme"
|
||||||
html_static_path = ["_static"] if os.path.exists("_static") else []
|
html_static_path = ["_static"]
|
||||||
autodoc_typehints = "signature" # Show type hints in the signature only, not in the docstring
|
autodoc_typehints = "description"
|
||||||
html_show_sphinx = False
|
html_show_sphinx = False
|
||||||
|
|
||||||
|
|
||||||
def import_core_modules():
|
def verify_modules():
|
||||||
"""Import core pipecat modules for autodoc to discover."""
|
"""Verify that required modules are available."""
|
||||||
core_modules = [
|
required_modules = {
|
||||||
"pipecat",
|
"services": [
|
||||||
"pipecat.frames",
|
"assemblyai",
|
||||||
"pipecat.pipeline",
|
"aws",
|
||||||
"pipecat.processors",
|
"cartesia",
|
||||||
"pipecat.services",
|
"deepgram",
|
||||||
"pipecat.transports",
|
"google",
|
||||||
"pipecat.audio",
|
"lmnt",
|
||||||
"pipecat.adapters",
|
"riva",
|
||||||
"pipecat.clocks",
|
"simli",
|
||||||
"pipecat.metrics",
|
],
|
||||||
"pipecat.observers",
|
"serializers": ["livekit"],
|
||||||
"pipecat.runner",
|
"vad": ["silero", "vad_analyzer"],
|
||||||
"pipecat.serializers",
|
"transports": {
|
||||||
"pipecat.sync",
|
"services": ["daily", "livekit"],
|
||||||
"pipecat.transcriptions",
|
"local": ["audio", "tk"],
|
||||||
"pipecat.utils",
|
"network": ["fastapi_websocket", "websocket_server"],
|
||||||
]
|
},
|
||||||
|
}
|
||||||
|
|
||||||
for module_name in core_modules:
|
# Skip importing modules that are in autodoc_mock_imports
|
||||||
try:
|
skipped_modules = set(autodoc_mock_imports)
|
||||||
__import__(module_name)
|
|
||||||
logger.info(f"Successfully imported {module_name}")
|
missing = []
|
||||||
except ImportError as e:
|
for category, modules in required_modules.items():
|
||||||
logger.warning(f"Failed to import {module_name}: {e}")
|
if isinstance(modules, dict):
|
||||||
|
# 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:
|
||||||
@@ -140,7 +235,36 @@ def clean_title(title: str) -> str:
|
|||||||
parts = title.split(".")
|
parts = title.split(".")
|
||||||
title = parts[-1]
|
title = parts[-1]
|
||||||
|
|
||||||
return title
|
# Special cases for service names and common acronyms
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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):
|
||||||
@@ -165,8 +289,9 @@ 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/examples"),
|
str(project_root / "src/pipecat/processors/gstreamer"),
|
||||||
str(project_root / "src/pipecat/tests"),
|
str(project_root / "src/pipecat/services/to_be_updated"),
|
||||||
|
str(project_root / "src/pipecat/vad"), # deprecated
|
||||||
"**/test_*.py",
|
"**/test_*.py",
|
||||||
"**/tests/*.py",
|
"**/tests/*.py",
|
||||||
]
|
]
|
||||||
@@ -207,4 +332,5 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
import_core_modules()
|
# Run module verification
|
||||||
|
verify_modules()
|
||||||
|
|||||||
@@ -1,20 +1,60 @@
|
|||||||
Pipecat API Reference
|
Pipecat API Reference Docs
|
||||||
=====================
|
==========================
|
||||||
|
|
||||||
Welcome to the Pipecat API reference.
|
Welcome to Pipecat's API reference documentation!
|
||||||
|
|
||||||
Use the navigation on the left to browse modules, or search using the search box.
|
Pipecat is an open source framework for building voice and multimodal assistants.
|
||||||
|
It provides a flexible pipeline architecture for connecting various AI services,
|
||||||
**New to Pipecat?** Check out the `main documentation <https://docs.pipecat.ai>`_ for tutorials, guides, and client SDK information.
|
audio processing, and transport layers.
|
||||||
|
|
||||||
Quick Links
|
Quick Links
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
* `GitHub Repository <https://github.com/pipecat-ai/pipecat>`_
|
* `GitHub Repository <https://github.com/pipecat-ai/pipecat>`_
|
||||||
* `Join our Community <https://discord.gg/pipecat>`_
|
* `Website <https://pipecat.ai>`_
|
||||||
|
|
||||||
|
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: 2
|
:maxdepth: 3
|
||||||
:caption: API Reference
|
:caption: API Reference
|
||||||
:hidden:
|
:hidden:
|
||||||
|
|
||||||
@@ -26,10 +66,16 @@ Quick Links
|
|||||||
Observers <api/pipecat.observers>
|
Observers <api/pipecat.observers>
|
||||||
Pipeline <api/pipecat.pipeline>
|
Pipeline <api/pipecat.pipeline>
|
||||||
Processors <api/pipecat.processors>
|
Processors <api/pipecat.processors>
|
||||||
Runner <api/pipecat.runner>
|
|
||||||
Serializers <api/pipecat.serializers>
|
Serializers <api/pipecat.serializers>
|
||||||
Services <api/pipecat.services>
|
Services <api/pipecat.services>
|
||||||
Sync <api/pipecat.sync>
|
Sync <api/pipecat.sync>
|
||||||
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`
|
||||||
53
docs/api/requirements.txt
Normal file
53
docs/api/requirements.txt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Sphinx dependencies
|
||||||
|
sphinx>=8.1.3
|
||||||
|
sphinx-rtd-theme
|
||||||
|
sphinx-markdown-builder
|
||||||
|
sphinx-autodoc-typehints
|
||||||
|
toml
|
||||||
|
|
||||||
|
# Install all extras individually to ensure they're properly resolved
|
||||||
|
pipecat-ai[anthropic]
|
||||||
|
pipecat-ai[assemblyai]
|
||||||
|
pipecat-ai[aws]
|
||||||
|
pipecat-ai[azure]
|
||||||
|
pipecat-ai[cartesia]
|
||||||
|
pipecat-ai[cerebras]
|
||||||
|
pipecat-ai[deepseek]
|
||||||
|
pipecat-ai[daily]
|
||||||
|
pipecat-ai[deepgram]
|
||||||
|
pipecat-ai[elevenlabs]
|
||||||
|
pipecat-ai[fal]
|
||||||
|
pipecat-ai[fireworks]
|
||||||
|
pipecat-ai[fish]
|
||||||
|
pipecat-ai[gladia]
|
||||||
|
pipecat-ai[google]
|
||||||
|
pipecat-ai[grok]
|
||||||
|
pipecat-ai[groq]
|
||||||
|
# pipecat-ai[krisp] # Mocked
|
||||||
|
pipecat-ai[koala]
|
||||||
|
# pipecat-ai[langchain] # Mocked
|
||||||
|
# pipecat-ai[livekit] # Mocked
|
||||||
|
pipecat-ai[lmnt]
|
||||||
|
pipecat-ai[local]
|
||||||
|
# pipecat-ai[local-smart-turn] # Mocked
|
||||||
|
# pipecat-ai[mem0] # Mocked
|
||||||
|
# pipecat-ai[mlx-whisper] # Mocked
|
||||||
|
# pipecat-ai[moondream] # Mocked
|
||||||
|
pipecat-ai[nim]
|
||||||
|
# pipecat-ai[neuphonic] # Mocked
|
||||||
|
pipecat-ai[noisereduce]
|
||||||
|
pipecat-ai[openai]
|
||||||
|
# pipecat-ai[openpipe]
|
||||||
|
# pipecat-ai[playht] # Mocked due to grpcio conflict with riva
|
||||||
|
pipecat-ai[qwen]
|
||||||
|
pipecat-ai[remote-smart-turn]
|
||||||
|
# pipecat-ai[riva] # Mocked
|
||||||
|
pipecat-ai[silero]
|
||||||
|
pipecat-ai[simli]
|
||||||
|
pipecat-ai[soundfile]
|
||||||
|
pipecat-ai[tavus]
|
||||||
|
pipecat-ai[together]
|
||||||
|
# pipecat-ai[ultravox] # Mocked
|
||||||
|
# pipecat-ai[webrtc] # Mocked
|
||||||
|
pipecat-ai[websocket]
|
||||||
|
pipecat-ai[whisper]
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
# Anthropic
|
# Anthropic
|
||||||
ANTHROPIC_API_KEY=...
|
ANTHROPIC_API_KEY=...
|
||||||
|
|
||||||
# Async
|
|
||||||
ASYNCAI_API_KEY=...
|
|
||||||
ASYNCAI_VOICE_ID=...
|
|
||||||
|
|
||||||
# AWS
|
# AWS
|
||||||
AWS_SECRET_ACCESS_KEY=...
|
AWS_SECRET_ACCESS_KEY=...
|
||||||
AWS_ACCESS_KEY_ID=...
|
AWS_ACCESS_KEY_ID=...
|
||||||
@@ -29,9 +25,6 @@ CARTESIA_API_KEY=...
|
|||||||
DAILY_API_KEY=...
|
DAILY_API_KEY=...
|
||||||
DAILY_SAMPLE_ROOM_URL=https://...
|
DAILY_SAMPLE_ROOM_URL=https://...
|
||||||
|
|
||||||
# Deepgram
|
|
||||||
DEEPGRAM_API_KEY=...
|
|
||||||
|
|
||||||
# ElevenLabs
|
# ElevenLabs
|
||||||
ELEVENLABS_API_KEY=...
|
ELEVENLABS_API_KEY=...
|
||||||
ELEVENLABS_VOICE_ID=...
|
ELEVENLABS_VOICE_ID=...
|
||||||
@@ -47,13 +40,6 @@ FIREWORKS_API_KEY=...
|
|||||||
|
|
||||||
# Gladia
|
# Gladia
|
||||||
GLADIA_API_KEY=...
|
GLADIA_API_KEY=...
|
||||||
GLADIA_REGION=...
|
|
||||||
|
|
||||||
# Google
|
|
||||||
GOOGLE_API_KEY=...
|
|
||||||
GOOGLE_CLOUD_PROJECT_ID=...
|
|
||||||
GOOGLE_TEST_CREDENTIALS=...
|
|
||||||
GOOGLE_VERTEX_TEST_CREDENTIALS=...
|
|
||||||
|
|
||||||
# LMNT
|
# LMNT
|
||||||
LMNT_API_KEY=...
|
LMNT_API_KEY=...
|
||||||
@@ -90,9 +76,6 @@ GROQ_API_KEY=...
|
|||||||
# Grok
|
# Grok
|
||||||
GROK_API_KEY=...
|
GROK_API_KEY=...
|
||||||
|
|
||||||
# Inworld
|
|
||||||
INWORLD_API_KEY=...
|
|
||||||
|
|
||||||
# Together.ai
|
# Together.ai
|
||||||
TOGETHER_API_KEY=...
|
TOGETHER_API_KEY=...
|
||||||
|
|
||||||
@@ -112,31 +95,9 @@ OPENROUTER_API_KEY=...
|
|||||||
PIPER_BASE_URL=...
|
PIPER_BASE_URL=...
|
||||||
|
|
||||||
# Smart turn
|
# Smart turn
|
||||||
LOCAL_SMART_TURN_MODEL_PATH=...
|
LOCAL_SMART_TURN_MODEL_PATH=
|
||||||
FAL_SMART_TURN_API_KEY=...
|
FAL_SMART_TURN_API_KEY=...
|
||||||
|
|
||||||
# Twilio
|
# Twilio
|
||||||
TWILIO_ACCOUNT_SID=...
|
TWILIO_ACCOUNT_SID=
|
||||||
TWILIO_AUTH_TOKEN=...
|
TWILIO_AUTH_TOKEN=
|
||||||
|
|
||||||
# MiniMax
|
|
||||||
MINIMAX_API_KEY=...
|
|
||||||
MINIMAX_GROUP_ID=...
|
|
||||||
|
|
||||||
# Sarvam AI
|
|
||||||
SARVAM_API_KEY=...
|
|
||||||
|
|
||||||
# Soniox
|
|
||||||
SONIOX_API_KEY=
|
|
||||||
|
|
||||||
# Speechmatics
|
|
||||||
SPEECHMATICS_API_KEY=...
|
|
||||||
|
|
||||||
# SambaNova
|
|
||||||
SAMBANOVA_API_KEY=...
|
|
||||||
|
|
||||||
# Sentry
|
|
||||||
SENTRY_DSN=...
|
|
||||||
|
|
||||||
# Heygen
|
|
||||||
HEYGEN_API_KEY=...
|
|
||||||
@@ -1,31 +1,88 @@
|
|||||||
# Pipecat Examples
|
|
||||||
|
|
||||||
This directory contains examples to help you learn how to build with Pipecat.
|
|
||||||
|
|
||||||
## Getting Started
|
# Pipecat — Examples
|
||||||
|
|
||||||
New to Pipecat? Start here:
|
## Foundational snippets
|
||||||
|
Small snippets that build on each other, introducing one or two concepts at a time.
|
||||||
|
|
||||||
- **[Quickstart](quickstart/)** - Get your first voice AI bot running in 5 minutes _(coming soon)_
|
➡️ [Take a look](https://github.com/pipecat-ai/pipecat/tree/main/examples/foundational)
|
||||||
- **[Client/Server Web](client-server-web/)** - Learn to build web applications with Pipecat's client SDKs _(coming soon)_
|
|
||||||
- **[Phone Bot with Twilio](phone-bot-twilio/)** - Connect your bot to a phone number _(coming soon)_
|
|
||||||
|
|
||||||
## Foundational Examples
|
## Chatbot examples
|
||||||
|
Collection of self-contained real-time voice and video AI demo applications built with Pipecat.
|
||||||
|
|
||||||
Single-file examples that introduce core Pipecat concepts one at a time. These examples:
|
### Quickstart
|
||||||
|
|
||||||
- Build on each other progressively
|
Each project has its own set of dependencies and configuration variables. They intentionally avoids shared code across projects — you can grab whichever demo folder you want to work with as a starting point.
|
||||||
- Focus on specific features or integrations
|
|
||||||
- Are used for testing with every Pipecat release
|
|
||||||
|
|
||||||
See the **[Foundational Examples README](foundational/)** for the complete list.
|
We recommend you start with a virtual environment:
|
||||||
|
|
||||||
## More Advanced Examples
|
```shell
|
||||||
|
cd pipecat-ai/examples/simple-chatbot
|
||||||
|
|
||||||
Ready to explore complex use cases? Visit **[pipecat-examples](https://github.com/pipecat-ai/pipecat-examples)** for:
|
python -m venv venv
|
||||||
|
|
||||||
- Production-ready applications
|
source venv/bin/activate
|
||||||
- Multi-platform client implementations
|
|
||||||
- Telephony integrations
|
pip install -r requirements.txt
|
||||||
- Multimodal and creative applications
|
```
|
||||||
- Deployment and monitoring examples
|
|
||||||
|
Next, follow the steps in the README for each demo.
|
||||||
|
|
||||||
|
ℹ️ Make sure you `pip install -r requirements.txt` for each demo project, so you can be sure to have the necessary service dependencies that extend the functionality of Pipecat. You can read more about the framework architecture [here](https://github.com/pipecat-ai/pipecat/tree/main/docs).
|
||||||
|
|
||||||
|
## Projects:
|
||||||
|
|
||||||
|
| Project | Description | Services |
|
||||||
|
|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
|
||||||
|
| [Simple Chatbot](simple-chatbot) | Basic voice-driven conversational bot. A good starting point for learning the flow of the framework. | Deepgram, ElevenLabs, OpenAI, Daily, Daily Prebuilt UI |
|
||||||
|
| [Storytelling Chatbot](storytelling-chatbot) | Stitches together multiple third-party services to create a collaborative storytime experience. | Deepgram, ElevenLabs, OpenAI, Fal, Daily, Custom UI |
|
||||||
|
| [Translation Chatbot](translation-chatbot) | Listens for user speech, then translates that speech to Spanish and speaks the translation back. Demonstrates multi-participant use-cases. | Deepgram, Azure, OpenAI, Daily, Daily Prebuilt UI |
|
||||||
|
| [Moondream Chatbot](moondream-chatbot) | Demonstrates how to add vision capabilities to GPT4. **Note: works best with a GPU** | Deepgram, ElevenLabs, OpenAI, Moondream, Daily, Daily Prebuilt UI |
|
||||||
|
| [Patient intake](patient-intake) | A chatbot that can call functions in response to user input. | Deepgram, ElevenLabs, OpenAI, Daily, Daily Prebuilt UI |
|
||||||
|
| [Phone Chatbot](phone-chatbot) | A chatbot that connects to PSTN/SIP phone calls, powered by Daily or Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio |
|
||||||
|
| [Twilio Chatbot](twilio-chatbot) | A chatbot that connects to an incoming phone call from Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio |
|
||||||
|
| [studypal](studypal) | A chatbot to have a conversation about any article on the web | |
|
||||||
|
| [WebSocket Chatbot Server](websocket-server) | A real-time websocket server that handles audio streaming and bot interactions with speech-to-text and text-to-speech capabilities. | Cartesia, Deepgram, OpenAI, Websockets |
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> These example projects use Daily as a WebRTC transport and can be joined using their hosted Prebuilt UI.
|
||||||
|
> It provides a quick way to join a real-time session with your bot and test your ideas without building any frontend code. If you'd like to see an example of a custom UI, try Storybot.
|
||||||
|
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
For each of these demos we've included a `Dockerfile`. Out of the box, this should provide everything needed to get the respective demo running on a VM:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build username/app:tag .
|
||||||
|
|
||||||
|
docker run -p 7860:7860 --env-file ./.env username/app:tag
|
||||||
|
|
||||||
|
docker push ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL
|
||||||
|
|
||||||
|
If you're working with a custom UI (such as with the Storytelling Chatbot), it's important to ensure your deployment platform supports HTTPS, as accessing user devices such as mics and webcams requires SSL.
|
||||||
|
|
||||||
|
If you try to run a custom UI without SSL, you may see an error in the console telling you that `navigator` is undefined, or no devices are available.
|
||||||
|
|
||||||
|
### Are these examples production ready?
|
||||||
|
|
||||||
|
Yes, kind of.
|
||||||
|
|
||||||
|
These demos attempt to keep things simple and are unopinionated regarding environment or scalability.
|
||||||
|
|
||||||
|
We're using FastAPI to spawn a subprocess for the bots / agents — useful for small tests, but not so great for production grade apps with many concurrent users. You can see how this works in each project's `start` endpoint in `server.py`.
|
||||||
|
|
||||||
|
Creating virtualized worker pools and on-demand instances is out of scope for these examples, but we hope to add some examples to this repo soon!
|
||||||
|
|
||||||
|
For projects that have CUDA as a requirement, such as Moondream Chatbot, be sure to deploy to a GPU-powered platform (such as [fly.io](https://fly.io) or [Runpod](https://runpod.io).)
|
||||||
|
|
||||||
|
## Getting help
|
||||||
|
|
||||||
|
➡️ [Join our Discord](https://discord.gg/pipecat)
|
||||||
|
|
||||||
|
➡️ [Reach us on Twitter](https://x.com/pipecat_ai)
|
||||||
|
|||||||
45
examples/bot-ready-signalling/README.md
Normal file
45
examples/bot-ready-signalling/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Bot ready signaling
|
||||||
|
|
||||||
|
A simple Pipecat example demonstrating how to handle signaling between the client and the bot,
|
||||||
|
ensuring that the bot starts sending audio only when the client is available,
|
||||||
|
thereby avoiding the risk of cutting off the beginning of the audio.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### First, start the bot server:
|
||||||
|
|
||||||
|
1. Navigate to the server directory:
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
```
|
||||||
|
2. Create and activate a virtual environment:
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
3. Install requirements:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
4. Copy env.example to .env and configure:
|
||||||
|
- Add your API keys
|
||||||
|
5. Start the server:
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next, connect using the client app:
|
||||||
|
|
||||||
|
For client-side setup, refer to the [JavaScript Guide](client/javascript/README.md).
|
||||||
|
|
||||||
|
## Important Note
|
||||||
|
|
||||||
|
Ensure the bot server is running before using any client implementations.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- Node.js 16+ (for JavaScript)
|
||||||
|
- Daily API key
|
||||||
|
- Cartesia API key
|
||||||
|
- Modern web browser with WebRTC support
|
||||||
27
examples/bot-ready-signalling/client/javascript/README.md
Normal file
27
examples/bot-ready-signalling/client/javascript/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# JavaScript Implementation
|
||||||
|
|
||||||
|
Basic implementation using the [Pipecat JavaScript SDK](https://docs.pipecat.ai/client/js/introduction).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Run the bot server. See the [server README](../../README).
|
||||||
|
|
||||||
|
2. Navigate to the `client/javascript` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client/javascript
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the client app:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Visit http://localhost:5173 in your browser.
|
||||||
34
examples/bot-ready-signalling/client/javascript/index.html
Normal file
34
examples/bot-ready-signalling/client/javascript/index.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AI Chatbot</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status">
|
||||||
|
Status: <span id="connection-status">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="connect-btn">Connect</button>
|
||||||
|
<button id="disconnect-btn" disabled>Disconnect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio id="bot-audio" autoplay></audio>
|
||||||
|
|
||||||
|
<div class="debug-panel">
|
||||||
|
<h3>Debug Info</h3>
|
||||||
|
<div id="debug-log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/app.js"></script>
|
||||||
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1076
examples/bot-ready-signalling/client/javascript/package-lock.json
generated
Normal file
1076
examples/bot-ready-signalling/client/javascript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
examples/bot-ready-signalling/client/javascript/package.json
Normal file
20
examples/bot-ready-signalling/client/javascript/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@daily-co/daily-js": "0.74.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
216
examples/bot-ready-signalling/client/javascript/src/app.js
Normal file
216
examples/bot-ready-signalling/client/javascript/src/app.js
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024–2025, Daily
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Daily from "@daily-co/daily-js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatbotClient handles the connection and media management for a real-time
|
||||||
|
* voice interaction with an AI bot.
|
||||||
|
*/
|
||||||
|
class ChatbotClient {
|
||||||
|
constructor() {
|
||||||
|
// Initialize client state
|
||||||
|
this.dailyCallObject = null;
|
||||||
|
this.setupDOMElements();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up references to DOM elements and create necessary media elements
|
||||||
|
*/
|
||||||
|
setupDOMElements() {
|
||||||
|
// Get references to UI control elements
|
||||||
|
this.connectBtn = document.getElementById('connect-btn');
|
||||||
|
this.disconnectBtn = document.getElementById('disconnect-btn');
|
||||||
|
this.statusSpan = document.getElementById('connection-status');
|
||||||
|
this.debugLog = document.getElementById('debug-log');
|
||||||
|
|
||||||
|
// Create an audio element for bot's voice output
|
||||||
|
this.botAudio = document.createElement('audio');
|
||||||
|
this.botAudio.autoplay = true;
|
||||||
|
this.botAudio.playsInline = true;
|
||||||
|
document.body.appendChild(this.botAudio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up event listeners for connect/disconnect buttons
|
||||||
|
*/
|
||||||
|
setupEventListeners() {
|
||||||
|
this.connectBtn.addEventListener('click', () => this.connect());
|
||||||
|
this.disconnectBtn.addEventListener('click', () => this.disconnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a timestamped message to the debug log
|
||||||
|
*/
|
||||||
|
log(message) {
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.textContent = `${new Date().toISOString()} - ${message}`;
|
||||||
|
|
||||||
|
// Add styling based on message type
|
||||||
|
if (message.startsWith('User: ')) {
|
||||||
|
entry.style.color = '#2196F3'; // blue for user
|
||||||
|
} else if (message.startsWith('Bot: ')) {
|
||||||
|
entry.style.color = '#4CAF50'; // green for bot
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debugLog.appendChild(entry);
|
||||||
|
this.debugLog.scrollTop = this.debugLog.scrollHeight;
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the connection status display
|
||||||
|
*/
|
||||||
|
updateStatus(status) {
|
||||||
|
this.statusSpan.textContent = status;
|
||||||
|
this.log(`Status: ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEventToConsole (evt) {
|
||||||
|
this.log(`Received event: ${evt.action}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up listeners for track events (start/stop)
|
||||||
|
* This handles new tracks being added during the session
|
||||||
|
*/
|
||||||
|
setupTrackListeners() {
|
||||||
|
if (!this.dailyCallObject) return;
|
||||||
|
|
||||||
|
this.dailyCallObject.on("joined-meeting", () => {
|
||||||
|
this.updateStatus('Connected');
|
||||||
|
this.connectBtn.disabled = true;
|
||||||
|
this.disconnectBtn.disabled = false;
|
||||||
|
this.log('Client connected');
|
||||||
|
});
|
||||||
|
this.dailyCallObject.on("track-started", (evt) => {
|
||||||
|
if (evt.track.kind === "audio" && evt.participant.local === false) {
|
||||||
|
this.log("Audio track started.")
|
||||||
|
this.setupAudioTrack(evt.track);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.dailyCallObject.on("track-stopped", this.handleEventToConsole.bind(this));
|
||||||
|
this.dailyCallObject.on("participant-joined", this.handleEventToConsole.bind(this));
|
||||||
|
this.dailyCallObject.on("participant-updated", this.handleEventToConsole.bind(this));
|
||||||
|
this.dailyCallObject.on("participant-left", () => {
|
||||||
|
// When the bot leaves, we are also disconnecting from the call
|
||||||
|
this.disconnect()
|
||||||
|
});
|
||||||
|
this.dailyCallObject.on("left-meeting", () => {
|
||||||
|
this.updateStatus('Disconnected');
|
||||||
|
this.connectBtn.disabled = false;
|
||||||
|
this.disconnectBtn.disabled = true;
|
||||||
|
this.log('Client disconnected');
|
||||||
|
});
|
||||||
|
this.dailyCallObject.on("error", this.handleEventToConsole.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up an audio track for playback
|
||||||
|
* Handles both initial setup and track updates
|
||||||
|
*/
|
||||||
|
setupAudioTrack(track) {
|
||||||
|
this.log(`Setting up audio track, track state: ${track.readyState}, muted: ${track.muted}`);
|
||||||
|
|
||||||
|
// Check if we're already playing this track
|
||||||
|
if (this.botAudio.srcObject) {
|
||||||
|
const oldTrack = this.botAudio.srcObject.getAudioTracks()[0];
|
||||||
|
if (oldTrack?.id === track.id) return;
|
||||||
|
}
|
||||||
|
// Create a new MediaStream with the track and set it as the audio source
|
||||||
|
this.botAudio.srcObject = new MediaStream([track]);
|
||||||
|
this.botAudio.onplaying = async (event) => {
|
||||||
|
this.log("onplaying")
|
||||||
|
this.log("Will send the audio message to play the audio at the next tick")
|
||||||
|
this.dailyCallObject.sendAppMessage("playable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchRoomInfo() {
|
||||||
|
let connectUrl = '/connect'
|
||||||
|
let res = await fetch(connectUrl, {
|
||||||
|
method: "POST",
|
||||||
|
mode: "cors",
|
||||||
|
headers: new Headers({
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and connect to the bot
|
||||||
|
* This sets up the RTVI client, initializes devices, and establishes the connection
|
||||||
|
*/
|
||||||
|
async connect() {
|
||||||
|
try {
|
||||||
|
// Initialize the client
|
||||||
|
this.dailyCallObject = Daily.createCallObject({
|
||||||
|
subscribeToTracksAutomatically: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up listeners for media track events
|
||||||
|
this.setupTrackListeners();
|
||||||
|
|
||||||
|
this.log('Creating the bot...');
|
||||||
|
let roomInfo = await this.fetchRoomInfo()
|
||||||
|
|
||||||
|
// Connect to the bot
|
||||||
|
this.log('Connecting to bot...');
|
||||||
|
// Only for making debugger easier
|
||||||
|
window.callObject = this.dailyCallObject;
|
||||||
|
await this.dailyCallObject.join({
|
||||||
|
url: roomInfo.room_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('Connection complete');
|
||||||
|
} catch (error) {
|
||||||
|
// Handle any errors during connection
|
||||||
|
this.log(`Error connecting: ${error.message}`);
|
||||||
|
this.log(`Error stack: ${error.stack}`);
|
||||||
|
this.updateStatus('Error');
|
||||||
|
|
||||||
|
// Clean up if there's an error
|
||||||
|
if (this.dailyCallObject) {
|
||||||
|
try {
|
||||||
|
await this.dailyCallObject.leave();
|
||||||
|
} catch (disconnectError) {
|
||||||
|
this.log(`Error during disconnect: ${disconnectError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the bot and clean up media resources
|
||||||
|
*/
|
||||||
|
async disconnect() {
|
||||||
|
if (this.dailyCallObject) {
|
||||||
|
try {
|
||||||
|
// Disconnect the RTVI client
|
||||||
|
await this.dailyCallObject.leave();
|
||||||
|
await this.dailyCallObject.destroy();
|
||||||
|
this.dailyCallObject = null;
|
||||||
|
|
||||||
|
// Clean up audio
|
||||||
|
if (this.botAudio.srcObject) {
|
||||||
|
this.botAudio.srcObject.getTracks().forEach((track) => track.stop());
|
||||||
|
this.botAudio.srcObject = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`Error disconnecting: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the client when the page loads
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new ChatbotClient();
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-left: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connect-btn {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#disconnect-btn {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bot-video-container {
|
||||||
|
width: 640px;
|
||||||
|
height: 360px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px auto;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bot-video-container video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug-log {
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// Proxy /api requests to the backend server
|
||||||
|
'/connect': {
|
||||||
|
target: 'http://0.0.0.0:7860', // Replace with your backend URL
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
1
examples/bot-ready-signalling/client/react-native/.nvmrc
Normal file
1
examples/bot-ready-signalling/client/react-native/.nvmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
22.14
|
||||||
60
examples/bot-ready-signalling/client/react-native/README.md
Normal file
60
examples/bot-ready-signalling/client/react-native/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# React Native Implementation
|
||||||
|
|
||||||
|
Basic implementation using the [Pipecat React Native SDK](https://docs.pipecat.ai/client/react-native/introduction).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Expo requirements
|
||||||
|
|
||||||
|
This project cannot be used with an [Expo Go](https://docs.expo.dev/workflow/expo-go/) app because [it requires custom native code](https://docs.expo.io/workflow/customizing/).
|
||||||
|
|
||||||
|
When a project requires custom native code or a config plugin, we need to transition from using [Expo Go](https://docs.expo.dev/workflow/expo-go/)
|
||||||
|
to a [development build](https://docs.expo.dev/development/introduction/).
|
||||||
|
|
||||||
|
More details about the custom native code used by this demo can be found in [rn-daily-js-expo-config-plugin](https://github.com/daily-co/rn-daily-js-expo-config-plugin).
|
||||||
|
|
||||||
|
### Building remotely
|
||||||
|
|
||||||
|
If you do not have experience with Xcode and Android Studio builds or do not have them installed locally on your computer, you will need to follow [this guide from Expo to use EAS Build](https://docs.expo.dev/development/create-development-builds/#create-and-install-eas-build).
|
||||||
|
|
||||||
|
### Building locally
|
||||||
|
|
||||||
|
You will need to have installed locally on your computer:
|
||||||
|
- [Xcode](https://developer.apple.com/xcode/) to build for iOS;
|
||||||
|
- [Android Studio](https://developer.android.com/studio) to build for Android;
|
||||||
|
|
||||||
|
#### Install the demo dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the version of node specified in .nvmrc
|
||||||
|
nvm i
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm i
|
||||||
|
|
||||||
|
# Before a native app can be compiled, the native source code must be generated.
|
||||||
|
npx expo prebuild
|
||||||
|
|
||||||
|
# Configure the environment variable to connect to the local server
|
||||||
|
cp env.example .env
|
||||||
|
# edit .env and add your local ip address, for example: http://192.168.1.16:7860
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running on Android
|
||||||
|
|
||||||
|
After plugging in an Android device [configured for debugging](https://developer.android.com/studio/debug/dev-options), run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run android
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running on iOS
|
||||||
|
|
||||||
|
Run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run ios
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Connect to the server
|
||||||
|
Use the http://localhost:5173 in your app.
|
||||||
75
examples/bot-ready-signalling/client/react-native/app.json
Normal file
75
examples/bot-ready-signalling/client/react-native/app.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "bot-ready-rn",
|
||||||
|
"slug": "bot-ready-rn",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"fallbackToCacheTimeout": 0
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true,
|
||||||
|
"bitcode": false,
|
||||||
|
"bundleIdentifier": "co.daily.expo.BotReady",
|
||||||
|
"infoPlist": {
|
||||||
|
"UIBackgroundModes": [
|
||||||
|
"voip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appleTeamId": "EEBGKV9N3N"
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#FFFFFF"
|
||||||
|
},
|
||||||
|
"package": "co.daily.expo.BotReady",
|
||||||
|
"permissions": [
|
||||||
|
"android.permission.ACCESS_NETWORK_STATE",
|
||||||
|
"android.permission.BLUETOOTH",
|
||||||
|
"android.permission.CAMERA",
|
||||||
|
"android.permission.INTERNET",
|
||||||
|
"android.permission.MODIFY_AUDIO_SETTINGS",
|
||||||
|
"android.permission.RECORD_AUDIO",
|
||||||
|
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||||
|
"android.permission.WAKE_LOCK",
|
||||||
|
"android.permission.FOREGROUND_SERVICE",
|
||||||
|
"android.permission.FOREGROUND_SERVICE_CAMERA",
|
||||||
|
"android.permission.FOREGROUND_SERVICE_MICROPHONE",
|
||||||
|
"android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION",
|
||||||
|
"android.permission.POST_NOTIFICATIONS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@config-plugins/react-native-webrtc",
|
||||||
|
"@daily-co/config-plugin-rn-daily-js",
|
||||||
|
[
|
||||||
|
"expo-build-properties",
|
||||||
|
{
|
||||||
|
"android": {
|
||||||
|
"minSdkVersion": 24,
|
||||||
|
"compileSdkVersion": 35,
|
||||||
|
"targetSdkVersion": 34,
|
||||||
|
"buildToolsVersion": "35.0.0"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"deploymentTarget": "15.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = function(api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ['babel-preset-expo'],
|
||||||
|
plugins: [["module:react-native-dotenv"]],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
API_BASE_URL=http://YOUR_LOCAL_IP:7860
|
||||||
7
examples/bot-ready-signalling/client/react-native/index.js
vendored
Normal file
7
examples/bot-ready-signalling/client/react-native/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { registerRootComponent } from "expo";
|
||||||
|
|
||||||
|
import App from "./src/App";
|
||||||
|
|
||||||
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
// It also ensures that the environment is set up appropriately
|
||||||
|
registerRootComponent(App);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||||
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
|
||||||
|
module.exports = getDefaultConfig(__dirname);
|
||||||
10627
examples/bot-ready-signalling/client/react-native/package-lock.json
generated
Normal file
10627
examples/bot-ready-signalling/client/react-native/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "bot-ready-rn",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start --dev-client",
|
||||||
|
"android": "expo run:android --device",
|
||||||
|
"ios": "expo run:ios --device",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@config-plugins/react-native-webrtc": "^10.0.0",
|
||||||
|
"@daily-co/config-plugin-rn-daily-js": "0.0.7",
|
||||||
|
"@daily-co/react-native-daily-js": "^0.70.0",
|
||||||
|
"@daily-co/react-native-webrtc": "^118.0.3-daily.2",
|
||||||
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
|
"expo": "^52.0.0",
|
||||||
|
"expo-build-properties": "~0.13.1",
|
||||||
|
"expo-dev-client": "~5.0.5",
|
||||||
|
"expo-splash-screen": "~0.29.16",
|
||||||
|
"expo-status-bar": "~2.0.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-native": "0.76.3",
|
||||||
|
"react-native-background-timer": "^2.4.1",
|
||||||
|
"react-native-dotenv": "^3.4.11",
|
||||||
|
"react-native-get-random-values": "^1.11.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.9"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
121
examples/bot-ready-signalling/client/react-native/src/App.js
vendored
Normal file
121
examples/bot-ready-signalling/client/react-native/src/App.js
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {SafeAreaView, View, Text, Button, StyleSheet, ScrollView} from 'react-native';
|
||||||
|
import Daily from "@daily-co/react-native-daily-js";
|
||||||
|
import { API_BASE_URL } from "@env";
|
||||||
|
|
||||||
|
const CallScreen = () => {
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState('Disconnected');
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [callObject, setCallObject] = useState(null);
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (callObject) {
|
||||||
|
setupTrackListeners(callObject);
|
||||||
|
}
|
||||||
|
}, [callObject]);
|
||||||
|
|
||||||
|
const log = (message) => {
|
||||||
|
setLogs((prevLogs) => [...prevLogs, `${new Date().toISOString()} - ${message}`]);
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupTrackListeners = (callObject) => {
|
||||||
|
callObject.on("joined-meeting", () => {
|
||||||
|
setConnectionStatus('Connected');
|
||||||
|
setIsConnected(true);
|
||||||
|
log('Client connected');
|
||||||
|
});
|
||||||
|
callObject.on("left-meeting", () => {
|
||||||
|
setConnectionStatus('Disconnected');
|
||||||
|
setIsConnected(false);
|
||||||
|
log('Client disconnected');
|
||||||
|
});
|
||||||
|
callObject.on("participant-left", () => {
|
||||||
|
// When the bot leaves, we are also disconnecting from the call
|
||||||
|
disconnect().catch((err) => {
|
||||||
|
log(`Failed to disconnect ${err}`);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// Trigger so the bot can start sending audio
|
||||||
|
callObject.on("track-started", (evt) => {
|
||||||
|
if (evt.track.kind === "audio" && evt.participant.local === false) {
|
||||||
|
handleEventToConsole(evt)
|
||||||
|
log("Sending the message that will trigger the bot to play the audio.")
|
||||||
|
callObject.sendAppMessage("playable")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
callObject.on("error", (evt) => log(`Error: ${evt.error}`));
|
||||||
|
// Other events just for awareness
|
||||||
|
callObject.on("track-stopped", handleEventToConsole);
|
||||||
|
callObject.on("participant-joined", handleEventToConsole);
|
||||||
|
callObject.on("participant-updated", handleEventToConsole);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventToConsole = (evt) => {
|
||||||
|
log(`Received event: ${evt.action}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = async () => {
|
||||||
|
try {
|
||||||
|
const callObject = Daily.createCallObject({ subscribeToTracksAutomatically: true });
|
||||||
|
setCallObject(callObject);
|
||||||
|
const connectionUrl = `${API_BASE_URL}/connect`
|
||||||
|
const res = await fetch(connectionUrl, { method: "POST", headers: { "Content-Type": "application/json" } });
|
||||||
|
const roomInfo = await res.json();
|
||||||
|
await callObject.join({ url: roomInfo.room_url });
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error connecting: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = async () => {
|
||||||
|
if (callObject) {
|
||||||
|
try {
|
||||||
|
await callObject.leave();
|
||||||
|
await callObject.destroy();
|
||||||
|
setCallObject(null);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error disconnecting: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.statusBar}>
|
||||||
|
<Text>Status: <Text style={styles.status}>{connectionStatus}</Text></Text>
|
||||||
|
<View style={styles.controls}>
|
||||||
|
<Button
|
||||||
|
title={isConnected ? "Disconnect" : "Connect"}
|
||||||
|
onPress={isConnected ? disconnect : connect}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.debugPanel}>
|
||||||
|
<Text style={styles.debugTitle}>Debug Info</Text>
|
||||||
|
<ScrollView style={styles.debugLog}>
|
||||||
|
{logs.map((logEntry, index) => (
|
||||||
|
<Text key={index} style={styles.logText}>{logEntry}</Text>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
safeArea: { flex: 1, backgroundColor: '#f0f0f0', padding: 20 },
|
||||||
|
container: { flex: 1, margin: 20 },
|
||||||
|
statusBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 10, backgroundColor: '#fff', borderRadius: 8, marginBottom: 20 },
|
||||||
|
status: { fontWeight: 'bold' },
|
||||||
|
controls: { flexDirection: 'row', gap: 10 },
|
||||||
|
debugPanel: { height: '80%', backgroundColor: '#fff', borderRadius: 8, padding: 20},
|
||||||
|
debugTitle: { fontSize: 16, fontWeight: 'bold' },
|
||||||
|
debugLog: { height: '100%', overflow: 'scroll', backgroundColor: '#f8f8f8', padding: 10, borderRadius: 4, fontFamily: 'monospace', fontSize: 12, lineHeight: 1.4 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CallScreen;
|
||||||
50
examples/bot-ready-signalling/server/README.md
Normal file
50
examples/bot-ready-signalling/server/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Bot ready signaling Server
|
||||||
|
|
||||||
|
A FastAPI server that manages bot instances and provide endpoint for Pipecat client connections.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- `POST /connect` - Pipecat client connection endpoint
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Copy `env.example` to `.env` and configure:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Required API Keys
|
||||||
|
DAILY_API_KEY= # Your Daily API key
|
||||||
|
CARTESIA_API_KEY= # Your Cartesia API key
|
||||||
|
|
||||||
|
# Optional Configuration
|
||||||
|
DAILY_API_URL= # Optional: Daily API URL (defaults to https://api.daily.co/v1)
|
||||||
|
DAILY_SAMPLE_ROOM_URL= # Optional: Fixed room URL for development
|
||||||
|
HOST= # Optional: Host address (defaults to 0.0.0.0)
|
||||||
|
FAST_API_PORT= # Optional: Port number (defaults to 7860)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
Set up and activate your virtual environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use the local version of `pipecat` in this repo rather than the last published version, also run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --editable "../../../[daily,cartesia,openai]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
3
examples/bot-ready-signalling/server/env.example
Normal file
3
examples/bot-ready-signalling/server/env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev)
|
||||||
|
DAILY_API_KEY=
|
||||||
|
CARTESIA_API_KEY=
|
||||||
4
examples/bot-ready-signalling/server/requirements.txt
Normal file
4
examples/bot-ready-signalling/server/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
python-dotenv
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn
|
||||||
|
pipecat-ai[daily,cartesia,openai]
|
||||||
64
examples/bot-ready-signalling/server/runner.py
Normal file
64
examples/bot-ready-signalling/server/runner.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
||||||
|
|
||||||
|
|
||||||
|
async def configure(aiohttp_session: aiohttp.ClientSession):
|
||||||
|
(url, token, _) = await configure_with_args(aiohttp_session)
|
||||||
|
return (url, token)
|
||||||
|
|
||||||
|
|
||||||
|
async def configure_with_args(
|
||||||
|
aiohttp_session: aiohttp.ClientSession, parser: Optional[argparse.ArgumentParser] = None
|
||||||
|
):
|
||||||
|
if not parser:
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument(
|
||||||
|
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--apikey",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Daily API Key (needed to create an owner token for the room)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL")
|
||||||
|
key = args.apikey or os.getenv("DAILY_API_KEY")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers."
|
||||||
|
)
|
||||||
|
|
||||||
|
daily_rest_helper = DailyRESTHelper(
|
||||||
|
daily_api_key=key,
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a meeting token for the given room with an expiration 1 hour in
|
||||||
|
# the future.
|
||||||
|
expiry_time: float = 60 * 60
|
||||||
|
|
||||||
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
|
return (url, token, args)
|
||||||
147
examples/bot-ready-signalling/server/server.py
Normal file
147
examples/bot-ready-signalling/server/server.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
# Dictionary to track bot processes: {pid: (process, room_url)}
|
||||||
|
bot_procs = {}
|
||||||
|
|
||||||
|
# Store Daily API helpers
|
||||||
|
daily_helpers = {}
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
"""Cleanup function to terminate all bot processes.
|
||||||
|
|
||||||
|
Called during server shutdown.
|
||||||
|
"""
|
||||||
|
for entry in bot_procs.values():
|
||||||
|
proc = entry[0]
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""FastAPI lifespan manager that handles startup and shutdown tasks.
|
||||||
|
|
||||||
|
- Creates aiohttp session
|
||||||
|
- Initializes Daily API helper
|
||||||
|
- Cleans up resources on shutdown
|
||||||
|
"""
|
||||||
|
aiohttp_session = aiohttp.ClientSession()
|
||||||
|
daily_helpers["rest"] = DailyRESTHelper(
|
||||||
|
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
await aiohttp_session.close()
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize FastAPI app with lifespan manager
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
# Configure CORS to allow requests from any origin
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_room_and_token() -> tuple[str, str]:
|
||||||
|
"""Helper function to create a Daily room and generate an access token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, str]: A tuple containing (room_url, token)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If room creation or token generation fails
|
||||||
|
"""
|
||||||
|
room = await daily_helpers["rest"].create_room(DailyRoomParams())
|
||||||
|
if not room.url:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to create room")
|
||||||
|
|
||||||
|
token = await daily_helpers["rest"].get_token(room.url)
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room.url}")
|
||||||
|
|
||||||
|
return room.url, token
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/connect")
|
||||||
|
async def bot_connect(request: Request) -> Dict[Any, Any]:
|
||||||
|
"""Connect endpoint that creates a room and returns connection credentials.
|
||||||
|
|
||||||
|
This endpoint is called by client to establish a connection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[Any, Any]: Authentication bundle containing room_url and token
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If room creation, token generation, or bot startup fails
|
||||||
|
"""
|
||||||
|
print("Creating room for RTVI connection")
|
||||||
|
room_url, token = await create_room_and_token()
|
||||||
|
print(f"Room URL: {room_url}")
|
||||||
|
|
||||||
|
# Start the bot process
|
||||||
|
try:
|
||||||
|
bot_file = "signalling_bot"
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[f"python3 -m {bot_file} -u {room_url} -t {token}"],
|
||||||
|
shell=True,
|
||||||
|
bufsize=1,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
)
|
||||||
|
bot_procs[proc.pid] = (proc, room_url)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}")
|
||||||
|
|
||||||
|
# Return the authentication bundle in format expected by DailyTransport
|
||||||
|
return {"room_url": room_url, "token": token}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
# Parse command line arguments for server configuration
|
||||||
|
default_host = os.getenv("HOST", "0.0.0.0")
|
||||||
|
default_port = int(os.getenv("FAST_API_PORT", "7860"))
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Daily Travel Companion FastAPI server")
|
||||||
|
parser.add_argument("--host", type=str, default=default_host, help="Host address")
|
||||||
|
parser.add_argument("--port", type=int, default=default_port, help="Port number")
|
||||||
|
parser.add_argument("--reload", action="store_true", help="Reload code on change")
|
||||||
|
|
||||||
|
config = parser.parse_args()
|
||||||
|
|
||||||
|
# Start the FastAPI server
|
||||||
|
uvicorn.run(
|
||||||
|
"server:app",
|
||||||
|
host=config.host,
|
||||||
|
port=config.port,
|
||||||
|
reload=config.reload,
|
||||||
|
)
|
||||||
95
examples/bot-ready-signalling/server/signalling_bot.py
Normal file
95
examples/bot-ready-signalling/server/signalling_bot.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from runner import configure
|
||||||
|
|
||||||
|
from pipecat.frames.frames import AudioRawFrame, EndFrame, OutputAudioRawFrame, TTSSpeakFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineTask
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SilenceFrame(OutputAudioRawFrame):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
sample_rate: int,
|
||||||
|
duration: float,
|
||||||
|
):
|
||||||
|
# Initialize the parent class with the silent frame's data
|
||||||
|
super().__init__(
|
||||||
|
audio=self.create_silent_audio_frame(sample_rate, 1, duration).audio,
|
||||||
|
sample_rate=sample_rate,
|
||||||
|
num_channels=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_silent_audio_frame(
|
||||||
|
sample_rate: int, num_channels: int, duration: float
|
||||||
|
) -> AudioRawFrame:
|
||||||
|
"""Create an AudioRawFrame containing silence."""
|
||||||
|
frame_size = num_channels * 2 # 2 bytes per sample for 16-bit audio
|
||||||
|
total_frames = int(sample_rate * duration)
|
||||||
|
total_bytes = total_frames * frame_size
|
||||||
|
silent_audio = bytes(total_bytes) # Create a byte array filled with zeros
|
||||||
|
return AudioRawFrame(audio=silent_audio, sample_rate=sample_rate, num_channels=num_channels)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
(room_url, _) = await configure(session)
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url, None, "Say One Thing", DailyParams(audio_out_enabled=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
task = PipelineTask(Pipeline([tts, transport.output()]))
|
||||||
|
|
||||||
|
# Register an event handler so we can play the audio when we receive a specific message
|
||||||
|
@transport.event_handler("on_app_message")
|
||||||
|
async def on_app_message(transport, message, sender):
|
||||||
|
logger.debug(f"Received app message: {message} - {sender}")
|
||||||
|
if "playable" not in message:
|
||||||
|
return
|
||||||
|
await task.queue_frames(
|
||||||
|
[
|
||||||
|
SilenceFrame(
|
||||||
|
sample_rate=task.params.audio_out_sample_rate,
|
||||||
|
duration=0.5,
|
||||||
|
),
|
||||||
|
TTSSpeakFrame(f"Hello there, how are you doing today ?"),
|
||||||
|
EndFrame(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
161
examples/chatbot-audio-recording/.gitignore
vendored
Normal file
161
examples/chatbot-audio-recording/.gitignore
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
runpod.toml
|
||||||
15
examples/chatbot-audio-recording/Dockerfile
Normal file
15
examples/chatbot-audio-recording/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.10-bullseye
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
RUN mkdir /app/assets
|
||||||
|
RUN mkdir /app/utils
|
||||||
|
COPY *.py /app/
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 7860
|
||||||
|
|
||||||
|
CMD ["python3", "server.py"]
|
||||||
37
examples/chatbot-audio-recording/README.md
Normal file
37
examples/chatbot-audio-recording/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Simple Chatbot
|
||||||
|
|
||||||
|
<img src="image.png" width="420px">
|
||||||
|
|
||||||
|
This app connects you to a chatbot powered by GPT-4, complete with animations generated by Stable Video Diffusion.
|
||||||
|
|
||||||
|
See a video of it in action: https://x.com/kwindla/status/1778628911817183509
|
||||||
|
|
||||||
|
And a quick video walkthrough of the code: https://www.loom.com/share/13df1967161f4d24ade054e7f8753416
|
||||||
|
|
||||||
|
ℹ️ The first time, things might take extra time to get started since VAD (Voice Activity Detection) model needs to be downloaded.
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
cp env.example .env # and add your credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, visit `http://localhost:7860/` in your browser to start a chatbot session.
|
||||||
|
|
||||||
|
## Build and test the Docker image
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t chatbot .
|
||||||
|
docker run --env-file .env -p 7860:7860 chatbot
|
||||||
|
```
|
||||||
162
examples/chatbot-audio-recording/bot.py
Normal file
162
examples/chatbot-audio-recording/bot.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import wave
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from runner import configure
|
||||||
|
|
||||||
|
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.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor
|
||||||
|
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
# Create the recordings directory if it doesn't exist
|
||||||
|
os.makedirs("recordings", exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def save_audio(audio: bytes, sample_rate: int, num_channels: int, name: str):
|
||||||
|
if len(audio) > 0:
|
||||||
|
filename = os.path.join(
|
||||||
|
"recordings",
|
||||||
|
f"{name}_conversation_recording{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.wav",
|
||||||
|
)
|
||||||
|
with io.BytesIO() as buffer:
|
||||||
|
with wave.open(buffer, "wb") as wf:
|
||||||
|
wf.setsampwidth(2)
|
||||||
|
wf.setnchannels(num_channels)
|
||||||
|
wf.setframerate(sample_rate)
|
||||||
|
wf.writeframes(audio)
|
||||||
|
async with aiofiles.open(filename, "wb") as file:
|
||||||
|
await file.write(buffer.getvalue())
|
||||||
|
print(f"Merged audio saved to {filename}")
|
||||||
|
else:
|
||||||
|
print("No audio data to save")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
(room_url, token) = await configure(session)
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Chatbot",
|
||||||
|
DailyParams(
|
||||||
|
audio_out_enabled=True,
|
||||||
|
audio_in_enabled=True,
|
||||||
|
video_out_enabled=False,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
transcription_enabled=True,
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# transcription_settings=DailyTranscriptionSettings(
|
||||||
|
# language="es",
|
||||||
|
# tier="nova",
|
||||||
|
# model="2-general"
|
||||||
|
# )
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tts = ElevenLabsTTSService(
|
||||||
|
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||||
|
#
|
||||||
|
# English
|
||||||
|
#
|
||||||
|
voice_id="cgSgspJ2msm6clMCkdW9",
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# model="eleven_multilingual_v2",
|
||||||
|
# voice_id="gD1IexrzCvsXPHUuT0s3",
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
#
|
||||||
|
# English
|
||||||
|
#
|
||||||
|
"content": "You are Chatbot, a friendly, helpful robot. 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, but keep your responses brief. Start by introducing yourself. Keep all your response to 12 words or fewer.",
|
||||||
|
#
|
||||||
|
# Spanish
|
||||||
|
#
|
||||||
|
# "content": "Eres Chatbot, un amigable y útil robot. Tu objetivo es demostrar tus capacidades de una manera breve. Tus respuestas se convertiran a audio así que nunca no debes incluir caracteres especiales. Contesta a lo que el usuario pregunte de una manera creativa, útil y breve. Empieza por presentarte a ti mismo.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
# NOTE: Watch out! This will save all the conversation in memory. You
|
||||||
|
# can pass `buffer_size` to get periodic callbacks.
|
||||||
|
audiobuffer = AudioBufferProcessor(enable_turn_audio=True)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # microphone
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
audiobuffer, # used to buffer the audio in the pipeline
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
||||||
|
|
||||||
|
@audiobuffer.event_handler("on_audio_data")
|
||||||
|
async def on_audio_data(buffer, audio, sample_rate, num_channels):
|
||||||
|
await save_audio(audio, sample_rate, num_channels, "full")
|
||||||
|
|
||||||
|
@audiobuffer.event_handler("on_user_turn_audio_data")
|
||||||
|
async def on_user_turn_audio_data(buffer, audio, sample_rate, num_channels):
|
||||||
|
await save_audio(audio, sample_rate, num_channels, "user")
|
||||||
|
|
||||||
|
@audiobuffer.event_handler("on_bot_turn_audio_data")
|
||||||
|
async def on_bot_turn_audio_data(buffer, audio, sample_rate, num_channels):
|
||||||
|
await save_audio(audio, sample_rate, num_channels, "bot")
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await audiobuffer.start_recording()
|
||||||
|
await transport.capture_participant_transcription(participant["id"])
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_participant_left")
|
||||||
|
async def on_participant_left(transport, participant, reason):
|
||||||
|
print(f"Participant left: {participant}")
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
4
examples/chatbot-audio-recording/env.example
Normal file
4
examples/chatbot-audio-recording/env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev)
|
||||||
|
DAILY_API_KEY=7df...
|
||||||
|
OPENAI_API_KEY=sk-PL...
|
||||||
|
ELEVENLABS_API_KEY=aeb...
|
||||||
5
examples/chatbot-audio-recording/requirements.txt
Normal file
5
examples/chatbot-audio-recording/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
aiofiles
|
||||||
|
python-dotenv
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn
|
||||||
|
pipecat-ai[daily,openai,silero,elevenlabs]
|
||||||
55
examples/chatbot-audio-recording/runner.py
Normal file
55
examples/chatbot-audio-recording/runner.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
||||||
|
|
||||||
|
|
||||||
|
async def configure(aiohttp_session: aiohttp.ClientSession):
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument(
|
||||||
|
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--apikey",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Daily API Key (needed to create an owner token for the room)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL")
|
||||||
|
key = args.apikey or os.getenv("DAILY_API_KEY")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers."
|
||||||
|
)
|
||||||
|
|
||||||
|
daily_rest_helper = DailyRESTHelper(
|
||||||
|
daily_api_key=key,
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a meeting token for the given room with an expiration 1 hour in
|
||||||
|
# the future.
|
||||||
|
expiry_time: float = 60 * 60
|
||||||
|
|
||||||
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
|
return (url, token)
|
||||||
139
examples/chatbot-audio-recording/server.py
Normal file
139
examples/chatbot-audio-recording/server.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams
|
||||||
|
|
||||||
|
MAX_BOTS_PER_ROOM = 1
|
||||||
|
|
||||||
|
# Bot sub-process dict for status reporting and concurrency control
|
||||||
|
bot_procs = {}
|
||||||
|
|
||||||
|
daily_helpers = {}
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
# Clean up function, just to be extra safe
|
||||||
|
for entry in bot_procs.values():
|
||||||
|
proc = entry[0]
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
aiohttp_session = aiohttp.ClientSession()
|
||||||
|
daily_helpers["rest"] = DailyRESTHelper(
|
||||||
|
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
await aiohttp_session.close()
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def start_agent(request: Request):
|
||||||
|
print(f"!!! Creating room")
|
||||||
|
room = await daily_helpers["rest"].create_room(DailyRoomParams())
|
||||||
|
print(f"!!! Room URL: {room.url}")
|
||||||
|
# Ensure the room property is present
|
||||||
|
if not room.url:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Missing 'room' property in request data. Cannot start agent without a target room!",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if there is already an existing process running in this room
|
||||||
|
num_bots_in_room = sum(
|
||||||
|
1 for proc in bot_procs.values() if proc[1] == room.url and proc[0].poll() is None
|
||||||
|
)
|
||||||
|
if num_bots_in_room >= MAX_BOTS_PER_ROOM:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Max bot limited reach for room: {room.url}")
|
||||||
|
|
||||||
|
# Get the token for the room
|
||||||
|
token = await daily_helpers["rest"].get_token(room.url)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room.url}")
|
||||||
|
|
||||||
|
# Spawn a new agent, and join the user session
|
||||||
|
# Note: this is mostly for demonstration purposes (refer to 'deployment' in README)
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[f"python3 -m bot -u {room.url} -t {token}"],
|
||||||
|
shell=True,
|
||||||
|
bufsize=1,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
)
|
||||||
|
bot_procs[proc.pid] = (proc, room.url)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}")
|
||||||
|
|
||||||
|
return RedirectResponse(room.url)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/status/{pid}")
|
||||||
|
def get_status(pid: int):
|
||||||
|
# Look up the subprocess
|
||||||
|
proc = bot_procs.get(pid)
|
||||||
|
|
||||||
|
# If the subprocess doesn't exist, return an error
|
||||||
|
if not proc:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Bot with process id: {pid} not found")
|
||||||
|
|
||||||
|
# Check the status of the subprocess
|
||||||
|
if proc[0].poll() is None:
|
||||||
|
status = "running"
|
||||||
|
else:
|
||||||
|
status = "finished"
|
||||||
|
|
||||||
|
return JSONResponse({"bot_id": pid, "status": status})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
default_host = os.getenv("HOST", "0.0.0.0")
|
||||||
|
default_port = int(os.getenv("FAST_API_PORT", "7860"))
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Daily Storyteller FastAPI server")
|
||||||
|
parser.add_argument("--host", type=str, default=default_host, help="Host address")
|
||||||
|
parser.add_argument("--port", type=int, default=default_port, help="Port number")
|
||||||
|
parser.add_argument("--reload", action="store_true", help="Reload code on change")
|
||||||
|
|
||||||
|
config = parser.parse_args()
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"server:app",
|
||||||
|
host=config.host,
|
||||||
|
port=config.port,
|
||||||
|
reload=config.reload,
|
||||||
|
)
|
||||||
39
examples/daily-custom-tracks/README.md
Normal file
39
examples/daily-custom-tracks/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Daily Custom Tracks
|
||||||
|
|
||||||
|
This example shows how to send and receive Daily custom tracks. We will run a simple `daily-python` application to send an audio file with a custom track (named "pipecat") to a room. Then, the Pipecat bot will mirror that custom track into another custom track (named "pipecat-mirror") in the same room.
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the bot
|
||||||
|
|
||||||
|
Start the bot by giving it a Daily room URL.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python bot.py -u ROOM_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will wait for the first participant to join. Then, it will mirror a custom track named "pipecat" into a new custom track named "pipecat-mirror".
|
||||||
|
|
||||||
|
## Run the sender
|
||||||
|
|
||||||
|
Now, run the custom track sender. This is a simple `daily-python` application that opens and audio file and sends it as a custom track to the same Daily room.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python custom_track_sender.py -u ROOM_URL -i office-ambience-mono-16000.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open client
|
||||||
|
|
||||||
|
Finally, open the client so you can hear both custom tracks.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the client is opened, copy the URL of the Daily room and join it. You should be able to select which custom track you want to hear.
|
||||||
87
examples/daily-custom-tracks/bot.py
Normal file
87
examples/daily-custom-tracks/bot.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from loguru import logger
|
||||||
|
from runner import configure
|
||||||
|
|
||||||
|
from pipecat.frames.frames import Frame, InputAudioRawFrame, OutputAudioRawFrame
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTrackMirrorProcessor(FrameProcessor):
|
||||||
|
def __init__(self, transport_destination: str, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._transport_destination = transport_destination
|
||||||
|
|
||||||
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
if isinstance(frame, InputAudioRawFrame) and frame.transport_source:
|
||||||
|
output_frame = OutputAudioRawFrame(
|
||||||
|
audio=frame.audio,
|
||||||
|
sample_rate=frame.sample_rate,
|
||||||
|
num_channels=frame.num_channels,
|
||||||
|
)
|
||||||
|
output_frame.transport_destination = self._transport_destination
|
||||||
|
await self.push_frame(output_frame)
|
||||||
|
else:
|
||||||
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
(room_url, _) = await configure(session)
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
None,
|
||||||
|
"Custom tracks mirror",
|
||||||
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
microphone_out_enabled=False, # Disable since we just use custom tracks
|
||||||
|
audio_out_destinations=["pipecat-mirror"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # Transport user input
|
||||||
|
CustomTrackMirrorProcessor("pipecat-mirror"),
|
||||||
|
transport.output(), # Transport bot output
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=16000,
|
||||||
|
audio_out_sample_rate=16000,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_audio(participant["id"], audio_source="pipecat")
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
74
examples/daily-custom-tracks/custom_track_sender.py
Normal file
74
examples/daily-custom-tracks/custom_track_sender.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
from daily import CallClient, CustomAudioSource, Daily
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument("-u", "--url", type=str, required=True, help="URL of the Daily room to join")
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--input", type=str, required=True, help="Input audio file (needs 16000 sample rate)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
audio = AudioSegment.from_mp3(args.input)
|
||||||
|
|
||||||
|
raw_bytes = audio.raw_data
|
||||||
|
sample_rate = audio.frame_rate
|
||||||
|
channels = audio.channels
|
||||||
|
|
||||||
|
print(f"Length: {len(raw_bytes)} bytes")
|
||||||
|
print(f"Sample rate: {sample_rate}, Channels: {channels}")
|
||||||
|
|
||||||
|
# Initialize the Daily context & create call client
|
||||||
|
Daily.init()
|
||||||
|
|
||||||
|
client = CallClient()
|
||||||
|
|
||||||
|
# Join the room and indicate we have a custom track named "pipecat".
|
||||||
|
client.join(
|
||||||
|
args.url,
|
||||||
|
client_settings={
|
||||||
|
"publishing": {
|
||||||
|
"camera": False,
|
||||||
|
"microphone": False,
|
||||||
|
"customAudio": {"pipecat": True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Just sleep for a couple of seconds. To do this well we should really use
|
||||||
|
# completions.
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Create the custom audio source. This is where we will write our audio.
|
||||||
|
audio_source = CustomAudioSource(sample_rate, channels)
|
||||||
|
|
||||||
|
# Create an audio track and assign it our audio source.
|
||||||
|
client.add_custom_audio_track("pipecat", audio_source)
|
||||||
|
|
||||||
|
# Just sleep for a second. To do this well we should really use completions.
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Just write one second of audio until we have read all the file.
|
||||||
|
chunk_size = sample_rate * channels * 2
|
||||||
|
while len(raw_bytes) > 0:
|
||||||
|
chunk = raw_bytes[:chunk_size]
|
||||||
|
raw_bytes = raw_bytes[chunk_size:]
|
||||||
|
audio_source.write_frames(chunk)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.leave()
|
||||||
|
|
||||||
|
# Just sleep for a second. To do this well we should really use completions.
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
client.release()
|
||||||
173
examples/daily-custom-tracks/index.html
Normal file
173
examples/daily-custom-tracks/index.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>daily custom tracks</title>
|
||||||
|
</head>
|
||||||
|
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.js"></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.css"
|
||||||
|
/>
|
||||||
|
<script>
|
||||||
|
function enableButton(buttonId, enable) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
button.disabled = !enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableJoinButton(enable) {
|
||||||
|
enableButton("join-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableLeaveButton(enable) {
|
||||||
|
enableButton("leave-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlayers(query) {
|
||||||
|
const items = document.querySelectorAll(query);
|
||||||
|
if (items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyParticipantPlayers(participantId) {
|
||||||
|
destroyPlayers(`audio[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`button[data-participant-id="${participantId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startPlayer(player, track) {
|
||||||
|
player.muted = false;
|
||||||
|
player.autoplay = true;
|
||||||
|
if (track != null) {
|
||||||
|
player.srcObject = new MediaStream([track]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAudioPlayer(track, participantId) {
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
const player = document.createElement("audio");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
// Create a new button for controlling audio
|
||||||
|
const audioControlButton = document.createElement("button");
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
audioControlButton.innerText = track._mediaTag == "cam-audio" ? "english" : track._mediaTag;
|
||||||
|
audioControlButton.dataset.participantId = participantId;
|
||||||
|
audioControlButton.onclick = () => {
|
||||||
|
if (player.paused) {
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
audioControlButton.className = "ui primary red button"
|
||||||
|
} else {
|
||||||
|
player.pause();
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioContainer.appendChild(player);
|
||||||
|
audioContainer.appendChild(audioControlButton);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
player.pause()
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTracks(participantId) {
|
||||||
|
console.log(`subscribing to track`);
|
||||||
|
|
||||||
|
if (participantId === "local") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callObject.updateParticipant(participantId, {
|
||||||
|
setSubscribedTracks: {
|
||||||
|
audio: true,
|
||||||
|
video: false,
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDaily() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
window.callObject = window.DailyIframe.createCallObject({});
|
||||||
|
|
||||||
|
callObject.on("participant-joined", (e) => {
|
||||||
|
if (!e.participant.local) {
|
||||||
|
console.log("participant-joined", e.participant);
|
||||||
|
subscribeToTracks(e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("participant-left", (e) => {
|
||||||
|
console.log("participant-left", e.participant.session_id);
|
||||||
|
destroyParticipantPlayers(e.participant.session_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("track-started", async (e) => {
|
||||||
|
console.log("track-started", e.track);
|
||||||
|
if (e.track.kind === "audio") {
|
||||||
|
await buildAudioPlayer(e.track, e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinRoom() {
|
||||||
|
enableJoinButton(false);
|
||||||
|
enableLeaveButton(true);
|
||||||
|
|
||||||
|
const meetingUrl = document.getElementById("meeting-url").value;
|
||||||
|
|
||||||
|
callObject.join({
|
||||||
|
url: meetingUrl,
|
||||||
|
startVideoOff: true,
|
||||||
|
startAudioOff: true,
|
||||||
|
subscribeToTracksAutomatically: false,
|
||||||
|
receiveSettings: {
|
||||||
|
base: { video: { layer: 0 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveRoom() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
callObject.leave();
|
||||||
|
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
audioContainer.replaceChildren();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body onload="startDaily()">
|
||||||
|
<div class="ui centered page grid" style="margin-top: 30px">
|
||||||
|
<div class="ten wide column">
|
||||||
|
<div class="ui form" style="margin-top: 30px">
|
||||||
|
<div class="field">
|
||||||
|
<label>Meeting URL</label>
|
||||||
|
<input id="meeting-url" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui centered aligned header" style="margin-top: 30px">
|
||||||
|
<button id="join-button" class="ui primary button" onclick="joinRoom()">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
<button id="leave-button" class="ui button" onclick="leaveRoom()">
|
||||||
|
Leave
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="audio-container"></div><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
examples/daily-custom-tracks/office-ambience-mono-16000.mp3
Normal file
BIN
examples/daily-custom-tracks/office-ambience-mono-16000.mp3
Normal file
Binary file not shown.
2
examples/daily-custom-tracks/requirements.txt
Normal file
2
examples/daily-custom-tracks/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pydub
|
||||||
|
pipecat-ai[daily]
|
||||||
55
examples/daily-custom-tracks/runner.py
Normal file
55
examples/daily-custom-tracks/runner.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
||||||
|
|
||||||
|
|
||||||
|
async def configure(aiohttp_session: aiohttp.ClientSession):
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument(
|
||||||
|
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--apikey",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Daily API Key (needed to create an owner token for the room)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL")
|
||||||
|
key = args.apikey or os.getenv("DAILY_API_KEY")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers."
|
||||||
|
)
|
||||||
|
|
||||||
|
daily_rest_helper = DailyRESTHelper(
|
||||||
|
daily_api_key=key,
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a meeting token for the given room with an expiration 1 hour in
|
||||||
|
# the future.
|
||||||
|
expiry_time: float = 60 * 60
|
||||||
|
|
||||||
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
|
return (url, token)
|
||||||
15
examples/daily-multi-translation/Dockerfile
Normal file
15
examples/daily-multi-translation/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.10-bullseye
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
RUN mkdir /app/assets
|
||||||
|
RUN mkdir /app/utils
|
||||||
|
COPY *.py /app/
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 7860
|
||||||
|
|
||||||
|
CMD ["python3", "server.py"]
|
||||||
39
examples/daily-multi-translation/README.md
Normal file
39
examples/daily-multi-translation/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Daily Multi Translation
|
||||||
|
|
||||||
|
This example shows how to use Daily to stream multiple simultaneous translations using a single transport. Daily provides custom tracks and in this example we will simultaneously translate incoming audio in English to Spanish, French and German, each of them being sent to a custom track.
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
cp env.example .env # and add your credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, visit `http://localhost:7860/` in your browser. This will open a Daily Prebuilt room where you will speak in English (make sure you are not muted).
|
||||||
|
|
||||||
|
## Open client
|
||||||
|
|
||||||
|
Next, you need to open the client that will listen to the translations.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the client is opened, copy the URL of the Daily room created above and join it. You should be able to select which translation you want to hear.
|
||||||
|
|
||||||
|
## Build and test the Docker image
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t daily-multi-translation .
|
||||||
|
docker run --env-file .env -p 7860:7860 daily-multi-translation
|
||||||
|
```
|
||||||
165
examples/daily-multi-translation/bot.py
Normal file
165
examples/daily-multi-translation/bot.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
from runner import configure
|
||||||
|
|
||||||
|
from pipecat.audio.mixers.soundfile_mixer import SoundfileMixer
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.observers.loggers.transcription_log_observer import TranscriptionLogObserver
|
||||||
|
from pipecat.pipeline.parallel_pipeline import ParallelPipeline
|
||||||
|
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.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
BACKGROUND_SOUND_FILE = "office-ambience-mono-16000.mp3"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
(room_url, token) = await configure(session)
|
||||||
|
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Multi translation bot",
|
||||||
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
audio_out_mixer={
|
||||||
|
"spanish": SoundfileMixer(
|
||||||
|
sound_files={"office": BACKGROUND_SOUND_FILE}, default_sound="office"
|
||||||
|
),
|
||||||
|
"french": SoundfileMixer(
|
||||||
|
sound_files={"office": BACKGROUND_SOUND_FILE}, default_sound="office"
|
||||||
|
),
|
||||||
|
"german": SoundfileMixer(
|
||||||
|
sound_files={"office": BACKGROUND_SOUND_FILE}, default_sound="office"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
audio_out_destinations=["spanish", "french", "german"],
|
||||||
|
microphone_out_enabled=False, # Disable since we just use custom tracks
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||||
|
|
||||||
|
tts_spanish = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="cefcb124-080b-4655-b31f-932f3ee743de",
|
||||||
|
transport_destination="spanish",
|
||||||
|
)
|
||||||
|
tts_french = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="8832a0b5-47b2-4751-bb22-6a8e2149303d",
|
||||||
|
transport_destination="french",
|
||||||
|
)
|
||||||
|
tts_german = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="38aabb6a-f52b-4fb0-a3d1-988518f4dc06",
|
||||||
|
transport_destination="german",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages_spanish = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You will be provided with a sentence in English, and your task is to only translate it into Spanish.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
messages_french = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You will be provided with a sentence in English, and your task is to only translate it into French.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
messages_german = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You will be provided with a sentence in English, and your task is to only translate it into German.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
llm_spanish = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
llm_french = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
llm_german = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
context_spanish = OpenAILLMContext(messages_spanish)
|
||||||
|
context_aggregator_spanish = llm_spanish.create_context_aggregator(context_spanish)
|
||||||
|
|
||||||
|
context_french = OpenAILLMContext(messages_french)
|
||||||
|
context_aggregator_french = llm_french.create_context_aggregator(context_french)
|
||||||
|
|
||||||
|
context_german = OpenAILLMContext(messages_german)
|
||||||
|
context_aggregator_german = llm_german.create_context_aggregator(context_german)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # Transport user input
|
||||||
|
stt,
|
||||||
|
ParallelPipeline(
|
||||||
|
# Spanish pipeline.
|
||||||
|
[
|
||||||
|
context_aggregator_spanish.user(),
|
||||||
|
llm_spanish,
|
||||||
|
tts_spanish,
|
||||||
|
context_aggregator_spanish.assistant(),
|
||||||
|
],
|
||||||
|
# French pipeline.
|
||||||
|
[
|
||||||
|
context_aggregator_french.user(),
|
||||||
|
llm_french,
|
||||||
|
tts_french,
|
||||||
|
context_aggregator_french.assistant(),
|
||||||
|
],
|
||||||
|
# German pipeline.
|
||||||
|
[
|
||||||
|
context_aggregator_german.user(),
|
||||||
|
llm_german,
|
||||||
|
tts_german,
|
||||||
|
context_aggregator_german.assistant(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
transport.output(), # Transport bot output
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=16000,
|
||||||
|
audio_out_sample_rate=16000,
|
||||||
|
allow_interruptions=True,
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
report_only_initial_ttfb=True,
|
||||||
|
),
|
||||||
|
observers=[TranscriptionLogObserver()],
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
5
examples/daily-multi-translation/env.example
Normal file
5
examples/daily-multi-translation/env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev)
|
||||||
|
DAILY_API_KEY=7df...
|
||||||
|
OPENAI_API_KEY=sk-PL...
|
||||||
|
DEEPGRAM_API_KEY=efb...
|
||||||
|
CARTESIA_API_KEY=aeb...
|
||||||
202
examples/daily-multi-translation/index.html
Normal file
202
examples/daily-multi-translation/index.html
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>daily multi translation</title>
|
||||||
|
</head>
|
||||||
|
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.1.1.min.js"
|
||||||
|
integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.js"></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.6/semantic.min.css"
|
||||||
|
/>
|
||||||
|
<script>
|
||||||
|
function enableButton(buttonId, enable) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
button.disabled = !enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableJoinButton(enable) {
|
||||||
|
enableButton("join-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableLeaveButton(enable) {
|
||||||
|
enableButton("leave-button", enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlayers(query) {
|
||||||
|
const items = document.querySelectorAll(query);
|
||||||
|
if (items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyParticipantPlayers(participantId) {
|
||||||
|
destroyPlayers(`video[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`audio[data-participant-id="${participantId}"]`);
|
||||||
|
destroyPlayers(`button[data-participant-id="${participantId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startPlayer(player, track) {
|
||||||
|
player.muted = false;
|
||||||
|
player.autoplay = true;
|
||||||
|
if (track != null) {
|
||||||
|
player.srcObject = new MediaStream([track]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildVideoPlayer(track, participantId) {
|
||||||
|
const videoContainer = document.getElementById("video-container");
|
||||||
|
const player = document.createElement("video");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
videoContainer.appendChild(player);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
await player.play();
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAudioPlayer(track, participantId) {
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
const player = document.createElement("audio");
|
||||||
|
player.dataset.participantId = participantId;
|
||||||
|
|
||||||
|
// Create a new button for controlling audio
|
||||||
|
const audioControlButton = document.createElement("button");
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
audioControlButton.innerText = track._mediaTag == "cam-audio" ? "english" : track._mediaTag;
|
||||||
|
audioControlButton.dataset.participantId = participantId;
|
||||||
|
audioControlButton.onclick = () => {
|
||||||
|
if (player.paused) {
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
audioControlButton.className = "ui primary red button"
|
||||||
|
} else {
|
||||||
|
player.pause();
|
||||||
|
audioControlButton.className = "ui primary green button"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioContainer.appendChild(player);
|
||||||
|
audioContainer.appendChild(audioControlButton);
|
||||||
|
|
||||||
|
await startPlayer(player, track);
|
||||||
|
player.pause()
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTracks(participantId) {
|
||||||
|
console.log(`subscribing to track`);
|
||||||
|
|
||||||
|
if (participantId === "local") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callObject.updateParticipant(participantId, {
|
||||||
|
setSubscribedTracks: {
|
||||||
|
audio: true,
|
||||||
|
video: true,
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDaily() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
window.callObject = window.DailyIframe.createCallObject({});
|
||||||
|
|
||||||
|
callObject.on("participant-joined", (e) => {
|
||||||
|
if (!e.participant.local) {
|
||||||
|
console.log("participant-joined", e.participant);
|
||||||
|
subscribeToTracks(e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("participant-left", (e) => {
|
||||||
|
console.log("participant-left", e.participant.session_id);
|
||||||
|
destroyParticipantPlayers(e.participant.session_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
callObject.on("track-started", async (e) => {
|
||||||
|
console.log("track-started", e.track);
|
||||||
|
if (e.track.kind === "video") {
|
||||||
|
await buildVideoPlayer(e.track, e.participant.session_id);
|
||||||
|
} else if (e.track.kind === "audio") {
|
||||||
|
await buildAudioPlayer(e.track, e.participant.session_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinRoom() {
|
||||||
|
enableJoinButton(false);
|
||||||
|
enableLeaveButton(true);
|
||||||
|
|
||||||
|
const meetingUrl = document.getElementById("meeting-url").value;
|
||||||
|
|
||||||
|
callObject.join({
|
||||||
|
url: meetingUrl,
|
||||||
|
startVideoOff: true,
|
||||||
|
startAudioOff: true,
|
||||||
|
subscribeToTracksAutomatically: false,
|
||||||
|
receiveSettings: {
|
||||||
|
base: { video: { layer: 0 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveRoom() {
|
||||||
|
enableJoinButton(true);
|
||||||
|
enableLeaveButton(false);
|
||||||
|
|
||||||
|
callObject.leave();
|
||||||
|
|
||||||
|
const videoContainer = document.getElementById("video-container");
|
||||||
|
videoContainer.replaceChildren();
|
||||||
|
|
||||||
|
const audioContainer = document.getElementById("audio-container");
|
||||||
|
audioContainer.replaceChildren();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body onload="startDaily()">
|
||||||
|
<div class="ui centered page grid" style="margin-top: 30px">
|
||||||
|
<div class="ten wide column">
|
||||||
|
<div class="ui form" style="margin-top: 30px">
|
||||||
|
<div class="field">
|
||||||
|
<label>Meeting URL</label>
|
||||||
|
<input id="meeting-url" value="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui centered aligned header" style="margin-top: 30px">
|
||||||
|
<button id="join-button" class="ui primary button" onclick="joinRoom()">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
<button id="leave-button" class="ui button" onclick="leaveRoom()">
|
||||||
|
Leave
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="audio-container"></div><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tile" class="ui container" style="margin-top: 30px">
|
||||||
|
<div id="tile" class="ui center aligned grid">
|
||||||
|
<div id="video-container" class="ui segment"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
examples/daily-multi-translation/office-ambience-mono-16000.mp3
Normal file
BIN
examples/daily-multi-translation/office-ambience-mono-16000.mp3
Normal file
Binary file not shown.
5
examples/daily-multi-translation/requirements.txt
Normal file
5
examples/daily-multi-translation/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
aiofiles
|
||||||
|
python-dotenv
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn
|
||||||
|
pipecat-ai[daily,deepgram,openai,silero,cartesia]
|
||||||
55
examples/daily-multi-translation/runner.py
Normal file
55
examples/daily-multi-translation/runner.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper
|
||||||
|
|
||||||
|
|
||||||
|
async def configure(aiohttp_session: aiohttp.ClientSession):
|
||||||
|
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||||
|
parser.add_argument(
|
||||||
|
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--apikey",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Daily API Key (needed to create an owner token for the room)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL")
|
||||||
|
key = args.apikey or os.getenv("DAILY_API_KEY")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
raise Exception(
|
||||||
|
"No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers."
|
||||||
|
)
|
||||||
|
|
||||||
|
daily_rest_helper = DailyRESTHelper(
|
||||||
|
daily_api_key=key,
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a meeting token for the given room with an expiration 1 hour in
|
||||||
|
# the future.
|
||||||
|
expiry_time: float = 60 * 60
|
||||||
|
|
||||||
|
token = await daily_rest_helper.get_token(url, expiry_time)
|
||||||
|
|
||||||
|
return (url, token)
|
||||||
139
examples/daily-multi-translation/server.py
Normal file
139
examples/daily-multi-translation/server.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomParams
|
||||||
|
|
||||||
|
MAX_BOTS_PER_ROOM = 1
|
||||||
|
|
||||||
|
# Bot sub-process dict for status reporting and concurrency control
|
||||||
|
bot_procs = {}
|
||||||
|
|
||||||
|
daily_helpers = {}
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
# Clean up function, just to be extra safe
|
||||||
|
for entry in bot_procs.values():
|
||||||
|
proc = entry[0]
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
aiohttp_session = aiohttp.ClientSession()
|
||||||
|
daily_helpers["rest"] = DailyRESTHelper(
|
||||||
|
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
await aiohttp_session.close()
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def start_agent(request: Request):
|
||||||
|
print(f"!!! Creating room")
|
||||||
|
room = await daily_helpers["rest"].create_room(DailyRoomParams())
|
||||||
|
print(f"!!! Room URL: {room.url}")
|
||||||
|
# Ensure the room property is present
|
||||||
|
if not room.url:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Missing 'room' property in request data. Cannot start agent without a target room!",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if there is already an existing process running in this room
|
||||||
|
num_bots_in_room = sum(
|
||||||
|
1 for proc in bot_procs.values() if proc[1] == room.url and proc[0].poll() is None
|
||||||
|
)
|
||||||
|
if num_bots_in_room >= MAX_BOTS_PER_ROOM:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Max bot limited reach for room: {room.url}")
|
||||||
|
|
||||||
|
# Get the token for the room
|
||||||
|
token = await daily_helpers["rest"].get_token(room.url)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room.url}")
|
||||||
|
|
||||||
|
# Spawn a new agent, and join the user session
|
||||||
|
# Note: this is mostly for demonstration purposes (refer to 'deployment' in README)
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[f"python3 -m bot -u {room.url} -t {token}"],
|
||||||
|
shell=True,
|
||||||
|
bufsize=1,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
)
|
||||||
|
bot_procs[proc.pid] = (proc, room.url)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}")
|
||||||
|
|
||||||
|
return RedirectResponse(room.url)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/status/{pid}")
|
||||||
|
def get_status(pid: int):
|
||||||
|
# Look up the subprocess
|
||||||
|
proc = bot_procs.get(pid)
|
||||||
|
|
||||||
|
# If the subprocess doesn't exist, return an error
|
||||||
|
if not proc:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Bot with process id: {pid} not found")
|
||||||
|
|
||||||
|
# Check the status of the subprocess
|
||||||
|
if proc[0].poll() is None:
|
||||||
|
status = "running"
|
||||||
|
else:
|
||||||
|
status = "finished"
|
||||||
|
|
||||||
|
return JSONResponse({"bot_id": pid, "status": status})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
default_host = os.getenv("HOST", "0.0.0.0")
|
||||||
|
default_port = int(os.getenv("FAST_API_PORT", "7860"))
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Daily Storyteller FastAPI server")
|
||||||
|
parser.add_argument("--host", type=str, default=default_host, help="Host address")
|
||||||
|
parser.add_argument("--port", type=int, default=default_port, help="Port number")
|
||||||
|
parser.add_argument("--reload", action="store_true", help="Reload code on change")
|
||||||
|
|
||||||
|
config = parser.parse_args()
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"server:app",
|
||||||
|
host=config.host,
|
||||||
|
port=config.port,
|
||||||
|
reload=config.reload,
|
||||||
|
)
|
||||||
13
examples/deployment/flyio-example/Dockerfile
Normal file
13
examples/deployment/flyio-example/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.11-bullseye
|
||||||
|
|
||||||
|
# Open port 7860 for http service
|
||||||
|
ENV FAST_API_PORT=7860
|
||||||
|
EXPOSE 7860
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY *.py .
|
||||||
|
COPY ./requirements.txt requirements.txt
|
||||||
|
RUN pip3 install --no-cache-dir --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
# Start the FastAPI server
|
||||||
|
CMD python3 bot_runner.py --port ${FAST_API_PORT}
|
||||||
39
examples/deployment/flyio-example/README.md
Normal file
39
examples/deployment/flyio-example/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Fly.io deployment example
|
||||||
|
|
||||||
|
This project modifies the `bot_runner.py` server to launch a new machine for each user session. This is a recommended approach for production vs. running shell processess as your deployment will quickly run out of system resources under load.
|
||||||
|
|
||||||
|
For this example, we are using Daily as a WebRTC transport and provisioning a new room and token for each session. You can use another transport, such as WebSockets, by modifying the `bot.py` and `bot_runner.py` files accordingly.
|
||||||
|
|
||||||
|
## Setting up your fly.io deployment
|
||||||
|
|
||||||
|
### Create your fly.toml file
|
||||||
|
|
||||||
|
You can copy the `example-fly.toml` as a reference. Be sure to change the app name to something unique.
|
||||||
|
|
||||||
|
### Create your .env file
|
||||||
|
|
||||||
|
Copy the base `env.example` to `.env` and enter the necessary API keys.
|
||||||
|
|
||||||
|
`FLY_APP_NAME` should match that in the `fly.toml` file.
|
||||||
|
|
||||||
|
### Launch a new fly.io project
|
||||||
|
|
||||||
|
`fly launch` or `fly launch --org your-org-name`
|
||||||
|
|
||||||
|
### Set the necessary app secrets from your .env
|
||||||
|
|
||||||
|
Note: you can do this manually via the fly.io dashboard under the "secrets" sub-section of your deployment (e.g. "https://fly.io/apps/fly-app-name/secrets") or run the following terminal command:
|
||||||
|
|
||||||
|
`cat .env | tr '\n' ' ' | xargs flyctl secrets set`
|
||||||
|
|
||||||
|
### Deploy your machine
|
||||||
|
|
||||||
|
`fly deploy`
|
||||||
|
|
||||||
|
## Connecting to your bot
|
||||||
|
|
||||||
|
Send a post request to your running fly.io instance:
|
||||||
|
|
||||||
|
`curl --location --request POST 'https://YOUR_FLY_APP_NAME/'`
|
||||||
|
|
||||||
|
This request will wait until the machine enters into a `starting` state, before returning the a room URL and token to join.
|
||||||
107
examples/deployment/flyio-example/bot.py
Normal file
107
examples/deployment/flyio-example/bot.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.frames.frames import EndFrame
|
||||||
|
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.elevenlabs.tts import ElevenLabsTTSService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
daily_api_key = os.getenv("DAILY_API_KEY", "")
|
||||||
|
daily_api_url = os.getenv("DAILY_API_URL", "https://api.daily.co/v1")
|
||||||
|
|
||||||
|
|
||||||
|
async def main(room_url: str, token: str):
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"Chatbot",
|
||||||
|
DailyParams(
|
||||||
|
api_url=daily_api_url,
|
||||||
|
api_key=daily_api_key,
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
video_out_enabled=False,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
transcription_enabled=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tts = ElevenLabsTTSService(
|
||||||
|
api_key=os.getenv("ELEVENLABS_API_KEY", ""),
|
||||||
|
voice_id=os.getenv("ELEVENLABS_VOICE_ID", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are Chatbot, a friendly, helpful robot. Your output will be converted to audio so don't include special characters other than '!' or '?' in your answers. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by saying hello.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_transcription(participant["id"])
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_participant_left")
|
||||||
|
async def on_participant_left(transport, participant, reason):
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
@transport.event_handler("on_call_state_updated")
|
||||||
|
async def on_call_state_updated(transport, state):
|
||||||
|
if state == "left":
|
||||||
|
# Here we don't want to cancel, we just want to finish sending
|
||||||
|
# whatever is queued, so we use an EndFrame().
|
||||||
|
await task.queue_frame(EndFrame())
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Pipecat Bot")
|
||||||
|
parser.add_argument("-u", type=str, help="Room URL")
|
||||||
|
parser.add_argument("-t", type=str, help="Token")
|
||||||
|
config = parser.parse_args()
|
||||||
|
|
||||||
|
asyncio.run(main(config.u, config.t))
|
||||||
209
examples/deployment/flyio-example/bot_runner.py
Normal file
209
examples/deployment/flyio-example/bot_runner.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import (
|
||||||
|
DailyRESTHelper,
|
||||||
|
DailyRoomObject,
|
||||||
|
DailyRoomParams,
|
||||||
|
DailyRoomProperties,
|
||||||
|
)
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------ Configuration ------------ #
|
||||||
|
|
||||||
|
MAX_SESSION_TIME = 5 * 60 # 5 minutes
|
||||||
|
REQUIRED_ENV_VARS = [
|
||||||
|
"DAILY_API_KEY",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"ELEVENLABS_API_KEY",
|
||||||
|
"ELEVENLABS_VOICE_ID",
|
||||||
|
"FLY_API_KEY",
|
||||||
|
"FLY_APP_NAME",
|
||||||
|
]
|
||||||
|
|
||||||
|
FLY_API_HOST = os.getenv("FLY_API_HOST", "https://api.machines.dev/v1")
|
||||||
|
FLY_APP_NAME = os.getenv("FLY_APP_NAME", "pipecat-fly-example")
|
||||||
|
FLY_API_KEY = os.getenv("FLY_API_KEY", "")
|
||||||
|
FLY_HEADERS = {"Authorization": f"Bearer {FLY_API_KEY}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
daily_helpers = {}
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------- API ----------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
aiohttp_session = aiohttp.ClientSession()
|
||||||
|
daily_helpers["rest"] = DailyRESTHelper(
|
||||||
|
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=aiohttp_session,
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
await aiohttp_session.close()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------- Main ----------------- #
|
||||||
|
|
||||||
|
|
||||||
|
async def spawn_fly_machine(room_url: str, token: str):
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# Use the same image as the bot runner
|
||||||
|
async with session.get(
|
||||||
|
f"{FLY_API_HOST}/apps/{FLY_APP_NAME}/machines", headers=FLY_HEADERS
|
||||||
|
) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
text = await r.text()
|
||||||
|
raise Exception(f"Unable to get machine info from Fly: {text}")
|
||||||
|
|
||||||
|
data = await r.json()
|
||||||
|
image = data[0]["config"]["image"]
|
||||||
|
|
||||||
|
# Machine configuration
|
||||||
|
cmd = f"python3 bot.py -u {room_url} -t {token}"
|
||||||
|
cmd = cmd.split()
|
||||||
|
worker_props = {
|
||||||
|
"config": {
|
||||||
|
"image": image,
|
||||||
|
"auto_destroy": True,
|
||||||
|
"init": {"cmd": cmd},
|
||||||
|
"restart": {"policy": "no"},
|
||||||
|
"guest": {"cpu_kind": "shared", "cpus": 1, "memory_mb": 1024},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spawn a new machine instance
|
||||||
|
async with session.post(
|
||||||
|
f"{FLY_API_HOST}/apps/{FLY_APP_NAME}/machines", headers=FLY_HEADERS, json=worker_props
|
||||||
|
) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
text = await r.text()
|
||||||
|
raise Exception(f"Problem starting a bot worker: {text}")
|
||||||
|
|
||||||
|
data = await r.json()
|
||||||
|
# Wait for the machine to enter the started state
|
||||||
|
vm_id = data["id"]
|
||||||
|
|
||||||
|
async with session.get(
|
||||||
|
f"{FLY_API_HOST}/apps/{FLY_APP_NAME}/machines/{vm_id}/wait?state=started",
|
||||||
|
headers=FLY_HEADERS,
|
||||||
|
) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
text = await r.text()
|
||||||
|
raise Exception(f"Bot was unable to enter started state: {text}")
|
||||||
|
|
||||||
|
print(f"Machine joined room: {room_url}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def start_bot(request: Request) -> JSONResponse:
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
# Is this a webhook creation request?
|
||||||
|
if "test" in data:
|
||||||
|
return JSONResponse({"test": True})
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use specified room URL, or create a new one if not specified
|
||||||
|
room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", "")
|
||||||
|
|
||||||
|
if not room_url:
|
||||||
|
params = DailyRoomParams(properties=DailyRoomProperties())
|
||||||
|
try:
|
||||||
|
room: DailyRoomObject = await daily_helpers["rest"].create_room(params=params)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Unable to provision room {e}")
|
||||||
|
else:
|
||||||
|
# Check passed room URL exists, we should assume that it already has a sip set up
|
||||||
|
try:
|
||||||
|
room: DailyRoomObject = await daily_helpers["rest"].get_room_from_url(room_url)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Room not found: {room_url}")
|
||||||
|
|
||||||
|
# Give the agent a token to join the session
|
||||||
|
token = await daily_helpers["rest"].get_token(room.url, MAX_SESSION_TIME)
|
||||||
|
|
||||||
|
if not room or not token:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room_url}")
|
||||||
|
|
||||||
|
# Launch a new fly.io machine, or run as a shell process (not recommended)
|
||||||
|
run_as_process = os.getenv("RUN_AS_PROCESS", False)
|
||||||
|
|
||||||
|
if run_as_process:
|
||||||
|
try:
|
||||||
|
subprocess.Popen(
|
||||||
|
[f"python3 -m bot -u {room.url} -t {token}"],
|
||||||
|
shell=True,
|
||||||
|
bufsize=1,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await spawn_fly_machine(room.url, token)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to spawn VM: {e}")
|
||||||
|
|
||||||
|
# Grab a token for the user to join with
|
||||||
|
user_token = await daily_helpers["rest"].get_token(room.url, MAX_SESSION_TIME)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"room_url": room.url,
|
||||||
|
"token": user_token,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Check environment variables
|
||||||
|
for env_var in REQUIRED_ENV_VARS:
|
||||||
|
if env_var not in os.environ:
|
||||||
|
raise Exception(f"Missing environment variable: {env_var}.")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Pipecat Bot Runner")
|
||||||
|
parser.add_argument(
|
||||||
|
"--host", type=str, default=os.getenv("HOST", "0.0.0.0"), help="Host address"
|
||||||
|
)
|
||||||
|
parser.add_argument("--port", type=int, default=os.getenv("PORT", 7860), help="Port number")
|
||||||
|
parser.add_argument(
|
||||||
|
"--reload", action="store_true", default=False, help="Reload code on change"
|
||||||
|
)
|
||||||
|
|
||||||
|
config = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run("bot_runner:app", host=config.host, port=config.port, reload=config.reload)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Pipecat runner shutting down...")
|
||||||
8
examples/deployment/flyio-example/env.example
Normal file
8
examples/deployment/flyio-example/env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
DAILY_API_KEY=
|
||||||
|
DAILY_SAMPLE_ROOM_URL= # Enter a Daily room URL to use a set room URL each time (useful for local testing)
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ELEVENLABS_API_KEY=
|
||||||
|
ELEVENLABS_VOICE_ID=
|
||||||
|
FLY_API_KEY=
|
||||||
|
FLY_APP_NAME=
|
||||||
|
RUN_AS_PROCESS= # Spawn fly.io machine for each session or run as local process
|
||||||
25
examples/deployment/flyio-example/example-fly.toml
Normal file
25
examples/deployment/flyio-example/example-fly.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# fly.toml app configuration file generated for pipecat-fly-example on 2024-07-01T15:04:53+01:00
|
||||||
|
#
|
||||||
|
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||||
|
#
|
||||||
|
|
||||||
|
app = 'pipecat-fly-example'
|
||||||
|
primary_region = 'sjc'
|
||||||
|
|
||||||
|
[build]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
FLY_APP_NAME = 'pipecat-fly-example'
|
||||||
|
|
||||||
|
[http_service]
|
||||||
|
internal_port = 7860
|
||||||
|
force_https = true
|
||||||
|
auto_stop_machines = true
|
||||||
|
auto_start_machines = true
|
||||||
|
min_machines_running = 0
|
||||||
|
processes = ['app']
|
||||||
|
|
||||||
|
[[vm]]
|
||||||
|
memory = 512
|
||||||
|
cpu_kind = 'shared'
|
||||||
|
cpus = 1
|
||||||
5
examples/deployment/flyio-example/requirements.txt
Normal file
5
examples/deployment/flyio-example/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pipecat-ai[daily,openai,silero]
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
python-dotenv
|
||||||
|
loguru
|
||||||
91
examples/deployment/modal-example/.gitignore
vendored
Normal file
91
examples/deployment/modal-example/.gitignore
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
.installed.cfg
|
||||||
|
.eggs/
|
||||||
|
downloads/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual Environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# Testing and Coverage
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.pytest_cache/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Logs and Databases
|
||||||
|
*.log
|
||||||
|
*.db
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Build and Documentation
|
||||||
|
docs/_build/
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
*.sage.py
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
.pyre/
|
||||||
|
.pytype/
|
||||||
|
cython_debug/
|
||||||
|
.ipynb_checkpoints
|
||||||
37
examples/deployment/modal-example/README.md
Normal file
37
examples/deployment/modal-example/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Deploying Pipecat to Modal.com
|
||||||
|
|
||||||
|
Barebones deployment example for [modal.com](https://www.modal.com)
|
||||||
|
|
||||||
|
1. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/active # or OS equivalent
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Setup .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can configure your Modal app to use [secrets](https://modal.com/docs/guide/secrets)
|
||||||
|
|
||||||
|
3. Test the app locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
modal serve app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Deploy to production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
modal deploy app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration options
|
||||||
|
|
||||||
|
This app sets some sensible defaults for reducing cold starts, such as `minkeep_warm=1`, which will keep at least 1 warm instance ready for your bot function.
|
||||||
|
|
||||||
|
It has been configured to only allow a concurrency of 1 (`max_inputs=1`) as each user will require their own running function.
|
||||||
80
examples/deployment/modal-example/app.py
Normal file
80
examples/deployment/modal-example/app.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import modal
|
||||||
|
from bot import _voice_bot_process
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
MAX_SESSION_TIME = 15 * 60 # 15 minutes
|
||||||
|
|
||||||
|
app = modal.App("pipecat-modal")
|
||||||
|
|
||||||
|
|
||||||
|
image = modal.Image.debian_slim(python_version="3.12").pip_install_from_requirements(
|
||||||
|
"requirements.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.function(
|
||||||
|
image=image,
|
||||||
|
cpu=1.0,
|
||||||
|
secrets=[modal.Secret.from_dotenv()],
|
||||||
|
keep_warm=1,
|
||||||
|
enable_memory_snapshot=True,
|
||||||
|
max_inputs=1, # Do not reuse instances across requests
|
||||||
|
retries=0,
|
||||||
|
)
|
||||||
|
def launch_bot_process(room_url: str, token: str):
|
||||||
|
_voice_bot_process(room_url, token)
|
||||||
|
|
||||||
|
|
||||||
|
@app.function(
|
||||||
|
image=image,
|
||||||
|
secrets=[modal.Secret.from_dotenv()],
|
||||||
|
)
|
||||||
|
@modal.web_endpoint(method="POST")
|
||||||
|
async def start():
|
||||||
|
from pipecat.transports.services.helpers.daily_rest import (
|
||||||
|
DailyRESTHelper,
|
||||||
|
DailyRoomParams,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Request received")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
daily_rest_helper = DailyRESTHelper(
|
||||||
|
daily_api_key=os.getenv("DAILY_API_KEY", ""),
|
||||||
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
||||||
|
aiohttp_session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create new Daily room
|
||||||
|
room = await daily_rest_helper.create_room(DailyRoomParams())
|
||||||
|
if not room.url:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Unable to create room",
|
||||||
|
)
|
||||||
|
logger.info(f"Created room: {room.url}")
|
||||||
|
|
||||||
|
# Create bot token for room
|
||||||
|
token = await daily_rest_helper.get_token(room.url, MAX_SESSION_TIME)
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get token for room: {room.url}")
|
||||||
|
|
||||||
|
logger.info(f"Bot token created: {token}")
|
||||||
|
|
||||||
|
# Spawn a new bot process
|
||||||
|
launch_bot_process.spawn(room_url=room.url, token=token)
|
||||||
|
|
||||||
|
# Return room URL to the user to join
|
||||||
|
# Note: in production, you would want to return a token to the user
|
||||||
|
return JSONResponse(content={"room_url": room.url, token: token})
|
||||||
95
examples/deployment/modal-example/bot.py
Normal file
95
examples/deployment/modal-example/bot.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024–2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
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.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||||
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
async def main(room_url: str, token: str):
|
||||||
|
transport = DailyTransport(
|
||||||
|
room_url,
|
||||||
|
token,
|
||||||
|
"bot",
|
||||||
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
transcription_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY", ""), voice_id="71a7ad14-091c-4e8e-a314-022ece01c121"
|
||||||
|
)
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
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)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(),
|
||||||
|
context_aggregator.user(),
|
||||||
|
llm,
|
||||||
|
tts,
|
||||||
|
transport.output(),
|
||||||
|
context_aggregator.assistant(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
allow_interruptions=True,
|
||||||
|
enable_metrics=True,
|
||||||
|
enable_usage_metrics=True,
|
||||||
|
report_only_initial_ttfb=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_first_participant_joined")
|
||||||
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
await transport.capture_participant_transcription(participant["id"])
|
||||||
|
messages.append({"role": "system", "content": "Please introduce yourself to the user."})
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_participant_left")
|
||||||
|
async def on_participant_left(transport, participant, reason):
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
runner = PipelineRunner()
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
||||||
|
|
||||||
|
def _voice_bot_process(room_url: str, token: str):
|
||||||
|
asyncio.run(main(room_url, token))
|
||||||
3
examples/deployment/modal-example/env.example
Normal file
3
examples/deployment/modal-example/env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DAILY_API_KEY=
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
CARTESIA_API_KEY=
|
||||||
4
examples/deployment/modal-example/requirements.txt
Normal file
4
examples/deployment/modal-example/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
python-dotenv==1.0.1
|
||||||
|
modal==0.71.3
|
||||||
|
pipecat-ai[daily,silero,cartesia,openai]
|
||||||
|
fastapi==0.115.6
|
||||||
178
examples/deployment/pipecat-cloud-daily-pstn-server/README.md
Normal file
178
examples/deployment/pipecat-cloud-daily-pstn-server/README.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Handling PSTN/SIP Dial-in on Pipecat Cloud
|
||||||
|
|
||||||
|
This repository contains two server implementations for handling
|
||||||
|
the pinless dial-in workflow in Pipecat Cloud. This is the companion to the
|
||||||
|
Pipecat Cloud [pstn_sip starter image](https://github.com/daily-co/pipecat-cloud-images/tree/main/pipecat-starters/pstn_sip).
|
||||||
|
In addition you can use `/api/dial` to trigger dial-out, and
|
||||||
|
eventually, call-transfers.
|
||||||
|
|
||||||
|
1. [FastAPI Server](fastapi-webhook-server/README.md) -
|
||||||
|
A FastAPI implementation that handles PSTN (Public Switched Telephone
|
||||||
|
Network) and SIP (Session Initiation Protocol) calls using the Daily API.
|
||||||
|
|
||||||
|
2. [Next.js Serverless](nextjs-webhook-server/README.md) -
|
||||||
|
A Next.js API implementation designed for deployment on Vercel's
|
||||||
|
serverless platform.
|
||||||
|
|
||||||
|
Both implementations provide:
|
||||||
|
|
||||||
|
- HMAC signature validation for pinless webhook
|
||||||
|
- Structured logging
|
||||||
|
- Support for dial-in and dial-out settings
|
||||||
|
- Voicemail detection and call transfer functionality (coming soon)
|
||||||
|
- Test request handling
|
||||||
|
|
||||||
|
## Choosing an Implementation
|
||||||
|
|
||||||
|
- Use the **FastAPI Server** if you:
|
||||||
|
|
||||||
|
- Need a standalone server
|
||||||
|
- Prefer Python and FastAPI
|
||||||
|
- Want to deploy to traditional hosting platforms
|
||||||
|
|
||||||
|
- Use the **Next.js Serverless** implementation if you:
|
||||||
|
- Want serverless deployment
|
||||||
|
- Prefer JavaScript/TypeScript
|
||||||
|
- Already use Next.js and Vercel for other projects
|
||||||
|
- Need quick scaling and zero maintenance
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Both implementations require similar environment variables:
|
||||||
|
|
||||||
|
- `PIPECAT_CLOUD_API_KEY`: Pipecat Cloud API Key, begins with pk\_\*
|
||||||
|
- `AGENT_NAME`: Your Daily agent name
|
||||||
|
- `PINLESS_HMAC_SECRET`: Your HMAC secret for request verification
|
||||||
|
- `LOG_LEVEL`: (Optional) Logging level (defaults to 'info')
|
||||||
|
|
||||||
|
See the individual README files in each implementation directory for
|
||||||
|
specific setup instructions.
|
||||||
|
|
||||||
|
### Phone number setup
|
||||||
|
|
||||||
|
You can buy a phone number through the Pipecat Cloud Dashboard:
|
||||||
|
|
||||||
|
1. Go to `Settings` > `Telephony`
|
||||||
|
2. Follow the UI to purchase a phone number
|
||||||
|
3. Configure the webhook URL to receive incoming calls (e.g. `https://my-webhook-url.com/api/dial`)
|
||||||
|
|
||||||
|
Or purchase the number using Daily's
|
||||||
|
[PhoneNumbers API](https://docs.daily.co/reference/rest-api/phone-numbers).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url https://api.daily.co/v1/domain-dialin-config \
|
||||||
|
--header 'Authorization: Bearer $TOKEN' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"type": "pinless_dialin",
|
||||||
|
"name_prefix": "Customer1",
|
||||||
|
"phone_number": "+1PURCHASED_NUM",
|
||||||
|
"room_creation_api": "https://example.com/api/dial",
|
||||||
|
"hold_music_url": "https://example.com/static/ringtone.mp3",
|
||||||
|
"timeout_config": {
|
||||||
|
"message": "No agent is available right now"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The API will return a static SIP URI (`sip_uri`) that can be called
|
||||||
|
from other SIP services.
|
||||||
|
|
||||||
|
### `room_creation_api`
|
||||||
|
|
||||||
|
To make and receive calls currently you have to host a server that
|
||||||
|
handles incoming calls. In the coming weeks, incoming calls will be
|
||||||
|
directly handled within Daily and we will expose an endpoint similar
|
||||||
|
to `{service}/start` that will manage this for you.
|
||||||
|
|
||||||
|
In the meantime, the server described below serves as the webhook
|
||||||
|
handler for the `room_creation_api`. Configure your pinless phone
|
||||||
|
number or SIP interconnect to the `ngrok` tunnel or
|
||||||
|
the actual server URL, append `/api/dial` to the webhook URL.
|
||||||
|
|
||||||
|
## Example curl commands
|
||||||
|
|
||||||
|
Note: Replace `http://localhost:3000` with your actual server URL and
|
||||||
|
phone numbers with valid values for your use case.
|
||||||
|
|
||||||
|
### Dialin Request
|
||||||
|
|
||||||
|
The server will receive a request when a call is received from Daily.
|
||||||
|
|
||||||
|
### Dialout Request
|
||||||
|
|
||||||
|
Dial a number, will use any purchased number
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Dial a number with callerId, which is the UUID of a purchased number.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
"callerId": "purchased_phone_uuid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Dial a number
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
"callerId": "purchased_phone_uuid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Request with Voicemail Detection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"To": "+1234567890",
|
||||||
|
"From": "+1987654321",
|
||||||
|
"callId": "call-uuid-123",
|
||||||
|
"callDomain": "domain-uuid-456",
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
"callerId": "purchased_phone_uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"voicemail_detection": {
|
||||||
|
"testInPrebuilt": true
|
||||||
|
},
|
||||||
|
"call_transfer": {
|
||||||
|
"mode": "dialout",
|
||||||
|
"speakSummary": true,
|
||||||
|
"storeSummary": true,
|
||||||
|
"operatorNumber": "+1234567890",
|
||||||
|
"testInPrebuilt": true
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# FastAPI server for handling Daily PSTN/SIP Webhook
|
||||||
|
|
||||||
|
A FastAPI server that handles PSTN (Public Switched Telephone Network) and SIP (Session Initiation Protocol) calls using the Daily API.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
|
||||||
|
2. Navigate to the `fastapi-webhook-server` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd fastapi-webhook-server
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Copy `env.example` to `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update `.env` with your credentials:
|
||||||
|
|
||||||
|
- `AGENT_NAME`: Your Daily agent name
|
||||||
|
- `PIPECAT_CLOUD_API_KEY`: Your Daily API key
|
||||||
|
- `PINLESS_HMAC_SECRET`: Your HMAC secret for request verification
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
Start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will run on `http://localhost:7860` and you can expose it via ngrok for testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
`ngrok http 7860`
|
||||||
|
```
|
||||||
|
|
||||||
|
> Tip: Use a subdomain for a consistent URL (e.g. `ngrok http -subdomain=mydomain http://localhost:7860`)
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /
|
||||||
|
|
||||||
|
Health check endpoint that returns a "Hello, World!" message.
|
||||||
|
|
||||||
|
### POST /api/dial
|
||||||
|
|
||||||
|
Initiates a PSTN/SIP call with the following request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid",
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+14158483432",
|
||||||
|
"callerId": "+14152251493"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"voicemail_detection": {
|
||||||
|
"testInPrebuilt": true
|
||||||
|
},
|
||||||
|
"call_transfer": {
|
||||||
|
"mode": "dialout",
|
||||||
|
"speakSummary": true,
|
||||||
|
"storeSummary": true,
|
||||||
|
"operatorNumber": "+14152250006",
|
||||||
|
"testInPrebuilt": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Returns a JSON object containing:
|
||||||
|
|
||||||
|
- `status`: Success/failure status
|
||||||
|
- `data`: Response from Daily API
|
||||||
|
- `room_properties`: Properties of the created Daily room
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- 401: Invalid signature
|
||||||
|
- 400: Invalid authorization header (e.g. missing Daily API key in bot.py)
|
||||||
|
- 405: Method not allowed (e.g. incorrect route on the webhook URL)
|
||||||
|
- 500: Server errors (missing API key, network issues)
|
||||||
|
- Other status codes are passed through from the Daily API
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
AGENT_NAME="your-agent-name"
|
||||||
|
PIPECAT_CLOUD_API_KEY="your-daily-api-key"
|
||||||
|
PINLESS_HMAC_SECRET="hmac-secret-pinless-dialin"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
|
pydantic
|
||||||
|
loguru
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
# server.py
|
||||||
|
|
||||||
|
|
||||||
|
import base64 # for calculating hmac signature
|
||||||
|
import hmac
|
||||||
|
import os # for accessing environment variables
|
||||||
|
import time # for setting expiration time
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class RoomRequest(BaseModel):
|
||||||
|
test: Optional[str] = Field(None, alias="Test", description="Test field")
|
||||||
|
To: Optional[str] = Field(None, alias="to", description="Destination phone number")
|
||||||
|
From: Optional[str] = Field(None, alias="from", description="Source phone number")
|
||||||
|
callId: Optional[str] = Field(None, alias="call_id", description="Unique call identifier")
|
||||||
|
callDomain: Optional[str] = Field(
|
||||||
|
None, alias="call_domain", description="Call domain identifier"
|
||||||
|
)
|
||||||
|
dialout_settings: Optional[List[Dict[str, Any]]] = Field(
|
||||||
|
None, description="An array of phone numbers or SIP URIs to dialout to"
|
||||||
|
)
|
||||||
|
voicemail_detection: Optional[Dict[str, Any]] = Field(
|
||||||
|
None, description="A flag to perform voicemail or answeing-machine detection"
|
||||||
|
)
|
||||||
|
call_transfer: Optional[Dict[str, Any]] = Field(None, description="to initiate a call transfer")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
populate_by_name = True
|
||||||
|
alias_generator = None
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
body can contain any fields, but for handling PSTN/SIP,
|
||||||
|
we recommend sending the following custom values:
|
||||||
|
dialin, dialout, voicemail detection, and call transfer
|
||||||
|
|
||||||
|
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid"
|
||||||
|
These need to be remapped to dialin_settings
|
||||||
|
|
||||||
|
"dialout_settings": [
|
||||||
|
{"phoneNumber": "+14158483432", "callerId": "+14152251493"},
|
||||||
|
{"sipUri": "sip:username@sip.hostname"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
voicemail_detection:{
|
||||||
|
testInPrebuilt: true
|
||||||
|
},
|
||||||
|
|
||||||
|
"call_transfer": {
|
||||||
|
"mode": "dialout",
|
||||||
|
"speakSummary": true,
|
||||||
|
"storeSummary": true,
|
||||||
|
"operatorNumber": "+14152250006",
|
||||||
|
"testInPrebuilt": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def read_root():
|
||||||
|
return {"message": "Hello, World!"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/dial")
|
||||||
|
async def dial(request: RoomRequest, raw_request: Request):
|
||||||
|
logger.info("Incoming request to /dial:")
|
||||||
|
logger.info(f"Headers: {dict(raw_request.headers)}")
|
||||||
|
raw_body = await raw_request.body()
|
||||||
|
raw_body_str = raw_body.decode()
|
||||||
|
logger.info(f"Raw body: {raw_body_str}")
|
||||||
|
logger.info(f"Parsed body: {request.dict()}")
|
||||||
|
|
||||||
|
# calculate signature and compare/verify
|
||||||
|
hmac_secret = os.getenv("PINLESS_HMAC_SECRET")
|
||||||
|
timestamp = raw_request.headers.get("x-pinless-timestamp")
|
||||||
|
signature = raw_request.headers.get("x-pinless-signature")
|
||||||
|
|
||||||
|
if not hmac_secret:
|
||||||
|
logger.debug("Skipping HMAC validation - PINLESS_HMAC_SECRET not set")
|
||||||
|
elif timestamp and signature:
|
||||||
|
message = timestamp + "." + raw_body_str
|
||||||
|
|
||||||
|
base64_decoded_secret = base64.b64decode(hmac_secret)
|
||||||
|
computed_signature = base64.b64encode(
|
||||||
|
hmac.new(base64_decoded_secret, message.encode(), "sha256").digest()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
if computed_signature != signature:
|
||||||
|
logger.error(f"Invalid signature. Expected {signature}, got {computed_signature}")
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid signature")
|
||||||
|
else:
|
||||||
|
logger.debug("Skipping HMAC validation - no signature headers present")
|
||||||
|
|
||||||
|
if request.test == "test":
|
||||||
|
logger.debug("Test request received")
|
||||||
|
return {"status": "success", "message": "Test request received"}
|
||||||
|
|
||||||
|
dialin_settings = None
|
||||||
|
# these fields are camelCase in the request
|
||||||
|
required_fields = ["To", "From", "callId", "callDomain"]
|
||||||
|
if all(
|
||||||
|
field in request.dict() and request.dict()[field] is not None for field in required_fields
|
||||||
|
):
|
||||||
|
# transform from camelCase to snake_case because daily-python expects snake_case
|
||||||
|
dialin_settings = {
|
||||||
|
"From": request.From,
|
||||||
|
"To": request.To,
|
||||||
|
"call_id": request.callId,
|
||||||
|
"call_domain": request.callDomain,
|
||||||
|
# transform from camelCase to snake_case
|
||||||
|
}
|
||||||
|
logger.debug(f"Populated dialin_settings from request: {dialin_settings}")
|
||||||
|
|
||||||
|
daily_room_properties = {
|
||||||
|
"enable_dialout": request.dialout_settings is not None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialin_settings is not None:
|
||||||
|
sip_config = {
|
||||||
|
"display_name": request.From,
|
||||||
|
"sip_mode": "dial-in",
|
||||||
|
"num_endpoints": 2 if request.call_transfer is not None else 1,
|
||||||
|
"codecs": {"audio": ["OPUS"]},
|
||||||
|
}
|
||||||
|
daily_room_properties["sip"] = sip_config
|
||||||
|
|
||||||
|
# Setting default expiry to 5 minutes from now
|
||||||
|
daily_room_properties["exp"] = int(time.time()) + (5 * 60)
|
||||||
|
|
||||||
|
logger.debug(f"Daily room properties: {daily_room_properties}")
|
||||||
|
payload = {
|
||||||
|
"createDailyRoom": True,
|
||||||
|
"dailyRoomProperties": daily_room_properties,
|
||||||
|
"body": {
|
||||||
|
"dialin_settings": dialin_settings,
|
||||||
|
"dialout_settings": request.dialout_settings,
|
||||||
|
"voicemail_detection": request.voicemail_detection,
|
||||||
|
"call_transfer": request.call_transfer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pcc_api_key = os.getenv("PIPECAT_CLOUD_API_KEY")
|
||||||
|
agent_name = os.getenv("AGENT_NAME", "my-first-agent")
|
||||||
|
|
||||||
|
if not pcc_api_key:
|
||||||
|
raise HTTPException(status_code=500, detail="DAILY_API_KEY environment variable is not set")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {pcc_api_key}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
url = f"https://api.pipecat.daily.co/v1/public/{agent_name}/start"
|
||||||
|
|
||||||
|
logger.debug(f"Making API call to Daily: {url} {headers} {payload}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
response_data = response.json()
|
||||||
|
logger.debug(f"Response: {response_data}")
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": response_data,
|
||||||
|
"room_properties": daily_room_properties,
|
||||||
|
}
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
# Pass through the status code and error details from the Daily API
|
||||||
|
status_code = e.response.status_code
|
||||||
|
error_detail = e.response.json() if e.response.content else str(e)
|
||||||
|
logger.error(f"HTTP error: {error_detail}")
|
||||||
|
raise HTTPException(status_code=status_code, detail=error_detail)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Request error: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=7860)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Server stopped manually")
|
||||||
53
examples/deployment/pipecat-cloud-daily-pstn-server/nextjs-webhook-server/.gitignore
vendored
Normal file
53
examples/deployment/pipecat-cloud-daily-pstn-server/nextjs-webhook-server/.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Next.js server for handling Daily PSTN/SIP Webhook
|
||||||
|
|
||||||
|
Next.js API routes for handling Daily PSTN/SIP Pipecat requests.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- API endpoint for handling Daily PSTN/SIP Pipecat requests
|
||||||
|
- HMAC signature validation
|
||||||
|
- Structured logging with Pino
|
||||||
|
- Support for dial-in and dial-out settings
|
||||||
|
- Voicemail detection and call transfer functionality
|
||||||
|
- Test request handling
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
|
||||||
|
2. Navigate to the `nextjs-webhook-server` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd nextjs-webhook-server
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create `.env.local` file with your credentials:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.local.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update your `.env` with your secrets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PIPECAT_CLOUD_API_KEY=pk_*
|
||||||
|
AGENT_NAME=my-first-agent
|
||||||
|
PINLESS_HMAC_SECRET=your_hmac_secret
|
||||||
|
LOG_LEVEL=info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the server
|
||||||
|
|
||||||
|
Run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will run on `http://localhost:7860` and you can expose it via ngrok for testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
`ngrok http 7860`
|
||||||
|
```
|
||||||
|
|
||||||
|
> Tip: Use a subdomain for a consistent URL (e.g. `ngrok http -subdomain=mydomain http://localhost:7860`)
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api
|
||||||
|
|
||||||
|
Returns a simple "Hello, World!" message with a cute cat emoji to verify the server is running.
|
||||||
|
|
||||||
|
### POST /api/dial
|
||||||
|
|
||||||
|
Handles dial-in and dial-out requests for Pipecat Cloud.
|
||||||
|
|
||||||
|
#### Test Requests
|
||||||
|
|
||||||
|
The endpoint handles test requests when a webhook is configured. Send a request with `"Test": "test"` to verify your setup:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Test": "test"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production Request Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// for dial-in from webhook
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid",
|
||||||
|
// for making a dial out to a phone or SIP
|
||||||
|
"dialout_settings": [
|
||||||
|
{ "phoneNumber": "+14158483432", "callerId": "purchased_phone_uuid" },
|
||||||
|
{ "sipUri": "sip:username@sip.hostname.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The application is configured for Vercel deployment:
|
||||||
|
|
||||||
|
1. Push your code to a Git repository
|
||||||
|
2. Import your project in Vercel dashboard
|
||||||
|
3. Configure environment variables:
|
||||||
|
- `PIPECAT_CLOUD_API_KEY`
|
||||||
|
- `AGENT_NAME`
|
||||||
|
- `PINLESS_HMAC_SECRET`
|
||||||
|
- `LOG_LEVEL` (optional, defaults to 'info')
|
||||||
|
4. Deploy!
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- HMAC signature validation for request authentication
|
||||||
|
- Environment variables for sensitive credentials
|
||||||
|
- Method validation (POST only for /dial)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
AGENT_NAME=my-first-agent
|
||||||
|
PIPECAT_CLOUD_API_KEY=your_daily_api_key
|
||||||
|
PINLESS_HMAC_SECRET=your_hmac_secret
|
||||||
|
LOG_LEVEL="info"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user