From 9493a5a42451004ec0e2988d3f008e8cebfc9ebb Mon Sep 17 00:00:00 2001 From: mattie ruth backman Date: Thu, 16 Jul 2026 15:27:04 -0400 Subject: [PATCH] Add file-upload example Demonstrates how a client can upload files (or reference them by URL) into the user context mid-conversation, and how to manage those files on the server side. Client (vanilla JS): - Upload a local file or reference a URL, with an optional prompt, via client.sendFile() - Works across SmallWebRTC, Daily, LiveKit, and WebSocket transports - Upload button is enabled only while the bot is ready Server: - bot.py accepts an --llm argument to run against different LLMs (OpenAI, Bedrock, Google, Google Vertex, Anthropic) - Uploaded files land in a local uploads folder by default - bot_s3.py and bot_gcs.py show custom file storage backends (Amazon S3, Google Cloud Storage) via create_file_storage() - README covers setup for each transport and storage option --- file-upload/.gitignore | 231 +++++ file-upload/README.md | 202 ++++ file-upload/client/env.example | 9 + file-upload/client/index.html | 74 ++ file-upload/client/package-lock.json | 1370 ++++++++++++++++++++++++++ file-upload/client/package.json | 21 + file-upload/client/src/app.js | 407 ++++++++ file-upload/client/src/config.js | 115 +++ file-upload/client/src/style.css | 354 +++++++ file-upload/server/Dockerfile | 19 + file-upload/server/bot.py | 250 +++++ file-upload/server/bot_gcs.py | 122 +++ file-upload/server/bot_s3.py | 137 +++ file-upload/server/env.example | 54 + file-upload/server/pcc-deploy.toml | 6 + file-upload/server/pyproject.toml | 23 + 16 files changed, 3394 insertions(+) create mode 100644 file-upload/.gitignore create mode 100644 file-upload/README.md create mode 100644 file-upload/client/env.example create mode 100644 file-upload/client/index.html create mode 100644 file-upload/client/package-lock.json create mode 100644 file-upload/client/package.json create mode 100644 file-upload/client/src/app.js create mode 100644 file-upload/client/src/config.js create mode 100644 file-upload/client/src/style.css create mode 100644 file-upload/server/Dockerfile create mode 100644 file-upload/server/bot.py create mode 100644 file-upload/server/bot_gcs.py create mode 100644 file-upload/server/bot_s3.py create mode 100644 file-upload/server/env.example create mode 100644 file-upload/server/pcc-deploy.toml create mode 100644 file-upload/server/pyproject.toml diff --git a/file-upload/.gitignore b/file-upload/.gitignore new file mode 100644 index 00000000..1f55ff27 --- /dev/null +++ b/file-upload/.gitignore @@ -0,0 +1,231 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$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 + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.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 +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.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/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +# Pipecat eval artifacts +*.eval.log +*.debug.log + +# Node.js (client/) +client/node_modules/ +client/.next/ +client/out/ +client/dist/ +client/.vite/ +client/.turbo/ +client/.vercel +client/.env*.local +client/*.tsbuildinfo diff --git a/file-upload/README.md b/file-upload/README.md new file mode 100644 index 00000000..b0cf7fcc --- /dev/null +++ b/file-upload/README.md @@ -0,0 +1,202 @@ +# file-upload + +This example demonstrates how clients can upload files and images to provide +visual context to the bot during a voice conversation. Users can either select +a local file from their computer or provide a URL pointing to an image or +document. In both cases an optional text prompt can accompany the upload to +guide the LLM's response. + +## How it works + +1. **Upload.** The client POSTs the file to the upload endpoint the runner + advertised as `fileUploadUrl` in its `/start` response. The endpoint hands + the file to the bot's storage backend and returns a URL (`pipecat:` + for local storage, `gs://`/`s3://` for the cloud-bucket bots). +2. **Send.** The client passes that URL back in an RTVI `send-file` message + (`client.sendFile`). The bot stores only the URL in the LLM context — + small files can also be sent inline, skipping the upload. +3. **Resolve.** At completion time the LLM service's `FileResolver` resolves + the URL per provider: a URL the provider can fetch itself is passed + straight through, and anything else is downloaded by the bot and inlined. + The LLM then reasons about the content and responds with audio as it would + for any other user turn. + +## The bots + +Three bots share one pipeline (defined in `bot.py`) and differ only in where +uploads are stored: + +| Bot | Storage | Minted URL | Read directly by | +| ------------ | -------------------- | --------------- | -------------------------------- | +| `bot.py` | Local disk | `pipecat:` | — (always inlined by the bot) | +| `bot_gcs.py` | Google Cloud Storage | `gs://...` | Gemini on Vertex AI (`-llm vertex`) | +| `bot_s3.py` | Amazon S3 | `s3://...` | AWS Bedrock (`-llm bedrock`) | + +Every bot works with every LLM provider (`-llm anthropic|openai|bedrock|gemini|vertex`). +The "read directly" column is the pass-through fast path: when the provider +can reach the bucket through its own IAM, the bot never touches the bytes. +With any other provider, the bot downloads the file from the bucket with its +own credentials and sends the provider the bytes instead. + +The cloud-bucket bots show the host-agnostic way to plug in custom storage: +implement `FileStorage` (`GCSFileStorage`/`S3FileStorage` here) and declare a +`create_file_storage()` function alongside `bot()`. Whichever host runs the +bot discovers the function there, backs its upload endpoint with the returned +storage, and injects it into the bot as `runner_args.file_storage`, which the +bot hands to its LLM service's `FileResolver` — so the same bot file runs +unchanged under the development runner or a cloud platform's base image. +`bot.py` declares no storage of its own and gets the runner's local-disk +default (`-u/--uploads-folder` or `PIPECAT_UPLOADS_FOLDER`). + +Concepts this example is meant to demonstrate: + +- Client → Server file upload via RTVI (`client.sendFile`) +- Handling both local file uploads and URL-based content references +- `FileResolver` on the LLM service for resolving file URLs per provider, + including `gs://`/`s3://` pass-through to providers with bucket access +- Custom `FileStorage` backends (local disk, GCS, S3) for the upload + endpoint, declared host-agnostically via `create_file_storage()` +- `onBotOutput` with per-word spoken highlighting in the client UI + +## Configuration + +- **Bot Type**: Web +- **Transport(s)**: SmallWebRTC, Daily, LiveKit (WebRTC), WebSocket +- **Pipeline**: Cascade + - **STT**: Deepgram + - **LLM**: Anthropic Claude (default), OpenAI, AWS Bedrock, or Google + Gemini via the developer API or Vertex AI — select with `-llm` + - **TTS**: Cartesia + +## Setup + +### Server + +1. **Navigate to server directory**: + + ```bash + cd server + ``` + +2. **Install dependencies**: + + ```bash + uv sync + ``` + +3. **Configure environment variables**: + + ```bash + cp env.example .env + # Edit .env and add your API keys + ``` + +4. **Run the bot**: + + ```bash + uv run bot.py + ``` + + Uploaded files are saved to the folder named by `PIPECAT_UPLOADS_FOLDER` + in `.env` (or pass `-u `). To store uploads in a cloud bucket + instead, run one of the storage-backend variants (after setting + `GCS_UPLOADS_BUCKET` / `S3_UPLOADS_BUCKET` and credentials in `.env`): + + ```bash + uv run bot_gcs.py -llm vertex # uploads in GCS; Vertex Gemini reads gs:// directly + uv run bot_s3.py -llm bedrock # uploads in Amazon S3 + ``` + + The runner serves every transport; the caller selects which one (a web/mobile + client picks its transport when it connects; a telephony provider connects to + `/ws`). + +### Client + +1. **Navigate to client directory**: + + ```bash + cd client + ``` + +2. **Install dependencies**: + + ```bash + npm install + ``` + +3. **Configure environment variables**: + + ```bash + cp env.example .env.local + # Edit .env.local if needed (defaults to localhost:7860) + ``` + + > **Note:** Environment variables in Vite are bundled into the client and exposed in the browser. For production applications that require secret protection, consider implementing a backend proxy server to handle API requests and manage sensitive credentials securely. + +4. **Run development server**: + + ```bash + npm run dev + ``` + +5. **Open browser**: + + http://localhost:5173 + +## Project Structure + +``` +file-upload/ +├── server/ # Python bot server +│ ├── bot.py # Main bot (local-disk upload storage) +│ ├── bot_gcs.py # Same bot, uploads in Google Cloud Storage +│ ├── bot_s3.py # Same bot, uploads in Amazon S3 +│ ├── pyproject.toml # Python dependencies +│ ├── env.example # Environment variables template +│ ├── .env # Your API keys (git-ignored) +│ ├── Dockerfile # Container image for Pipecat Cloud +│ └── pcc-deploy.toml # Pipecat Cloud deployment config +├── client/ # Vanilla application +│ ├── src/ # Client source code +│ ├── package.json # Node dependencies +│ └── ... +├── .gitignore # Git ignore patterns +└── README.md # This file +``` + +## Deploying to Pipecat Cloud + +Note that the upload endpoint is currently a development-runner feature, so +files larger than the transport's message size limit can't be uploaded when the +bot is deployed to Pipecat Cloud — production deployments need their own upload +endpoint (see `PipecatClient.uploadFile(file, uploadFileParams)`) until Pipecat +Cloud serves one backed by the same `create_file_storage()` declaration this +example uses. Sending small files, and referencing files by URL, work in both +environments. + +This project is configured for deployment to Pipecat Cloud. You can learn how to deploy to Pipecat Cloud in the [Pipecat Quickstart Guide](https://docs.pipecat.ai/getting-started/quickstart#step-2-deploy-to-production). + +Refer to the [Pipecat Cloud Documentation](https://docs.pipecat.ai/deployment/pipecat-cloud/introduction) to learn more about configuring, deploying, and managing your agents in Pipecat Cloud. + +## Building with an AI coding agent + +Extending this bot with Claude Code, Codex, or another AI coding assistant? Give it live, accurate Pipecat context instead of stale training data with the **Pipecat Context Hub** — a local index of Pipecat docs, examples, and API source your agent queries over MCP: + +```bash +# Build the local index (first run takes a couple of minutes) +uvx pipecat-ai-context-hub@latest refresh + +# Add it to your agent (use the line for the one you use) +claude mcp add pipecat-context-hub -- uvx pipecat-ai-context-hub serve # Claude Code +codex mcp add pipecat-context-hub -- uvx pipecat-ai-context-hub serve # Codex +``` + +MCP servers load at session start, so add it before opening your coding session. See the [Pipecat Context Hub docs](https://docs.pipecat.ai/api-reference/context-hub) for the full setup. + +## Learn More + +- [Pipecat Documentation](https://docs.pipecat.ai/) +- [Pipecat GitHub](https://github.com/pipecat-ai/pipecat) +- [Pipecat Examples](https://github.com/pipecat-ai/pipecat-examples) +- [Discord Community](https://discord.gg/pipecat) \ No newline at end of file diff --git a/file-upload/client/env.example b/file-upload/client/env.example new file mode 100644 index 00000000..cc086d05 --- /dev/null +++ b/file-upload/client/env.example @@ -0,0 +1,9 @@ +# For local development: +VITE_BOT_START_URL="http://localhost:7860/start" + +# For Pipecat Cloud (replace {agentName} with your agent name): +# VITE_BOT_START_URL="https://api.pipecat.daily.co/v1/public/{agentName}/start" +# VITE_BOT_START_PUBLIC_API_KEY="your-pipecat-cloud-public-api-key-here" + +# Note: In production, these values will be exposed in the browser bundle. +# For production apps, consider using a backend proxy to protect secrets. \ No newline at end of file diff --git a/file-upload/client/index.html b/file-upload/client/index.html new file mode 100644 index 00000000..8587d824 --- /dev/null +++ b/file-upload/client/index.html @@ -0,0 +1,74 @@ + + + + + + + Voice UI Kit - Vanilla JS + + + +
+ +
+
+ + +
+
+ + + +
+
+ + +
+

Conversation

+
+
+ + +
+

Events

+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/file-upload/client/package-lock.json b/file-upload/client/package-lock.json new file mode 100644 index 00000000..14e4bf50 --- /dev/null +++ b/file-upload/client/package-lock.json @@ -0,0 +1,1370 @@ +{ + "name": "file-upload", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "file-upload", + "version": "0.0.0", + "dependencies": { + "@pipecat-ai/client-js": "^1.14.0", + "@pipecat-ai/daily-transport": "^1.7.0", + "@pipecat-ai/livekit-transport": "^1.1.0", + "@pipecat-ai/small-webrtc-transport": "^1.11.0", + "@pipecat-ai/websocket-transport": "^1.8.0" + }, + "devDependencies": { + "vite": "^8.0.16" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.16.0.tgz", + "integrity": "sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.16.0.tgz", + "integrity": "sha512-3FdYJys34VhFnBQsjiV9wurBkX/DBACQAkAk+nCKdO1p8ROVNi7gmaRAcba+4qq8hQq8rYVukuutwvGCvd4kJA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@typescript/vfs": "^1.6.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@daily-co/daily-js": { + "version": "0.94.0", + "resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.94.0.tgz", + "integrity": "sha512-xOWvaHPiphmZqIqvK4qmQude7bS1WhxfComd3SWRJCH5htjP8WJ3RrTfykchNwXZR8ziLy1Mr5XdzQ+KAyDHtw==", + "license": "BSD-2-Clause", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@sentry/browser": "^8.33.1", + "bowser": "^2.8.1", + "dequal": "^2.0.3", + "events": "^3.1.0" + }, + "engines": { + "node": ">=22.14.0" + } + }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.52.1.tgz", + "integrity": "sha512-X8DxKlNbzk6Lh+v630+mjdFHSotdAFwIyzxSRSUql2DbTxCnlpa7JsvFGc6Na3mXiex2OFh0iiQtWwOBP0rIYA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@livekit/protocol/node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pipecat-ai/client-js": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@pipecat-ai/client-js/-/client-js-1.14.0.tgz", + "integrity": "sha512-5v7OvTOewzoNiu06ne9HHr7kD3oSJoGh5RMtvKCnYCd4JB6AcH8w0z7PDrhHR3rjNMrnA+kQQ6bs7b7E7VP21g==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/events": "^3.0.3", + "bowser": "^2.11.0", + "clone-deep": "^4.0.1", + "events": "^3.3.0", + "typed-emitter": "^2.1.0", + "uuid": "^11.1.1" + } + }, + "node_modules/@pipecat-ai/daily-transport": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@pipecat-ai/daily-transport/-/daily-transport-1.7.0.tgz", + "integrity": "sha512-5/HjFEmlEjgORe5FoJC9cKLq0aYEWn5GtVsXWWG83NxVAlTpZvQh1x6QbtTdmEolg1C9li/fVRwkmBqKjI+fnA==", + "license": "BSD-2-Clause", + "dependencies": { + "@daily-co/daily-js": "^0.94.0" + }, + "peerDependencies": { + "@pipecat-ai/client-js": "~1.14.0" + } + }, + "node_modules/@pipecat-ai/livekit-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pipecat-ai/livekit-transport/-/livekit-transport-1.1.0.tgz", + "integrity": "sha512-qpguBsIEUALYtbZEzH39iWDKj+jOhWVE/E0Ru4NaB4VSMx0osBCBx4+Vep5cHewS4MTWISpbgu+qb4NXstimPw==", + "license": "BSD-2-Clause", + "dependencies": { + "livekit-client": "^2.21.0" + }, + "peerDependencies": { + "@pipecat-ai/client-js": "~1.14.0" + } + }, + "node_modules/@pipecat-ai/small-webrtc-transport": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@pipecat-ai/small-webrtc-transport/-/small-webrtc-transport-1.11.0.tgz", + "integrity": "sha512-HY37HpKQ3Rye97DWqosRN6+SArvp4DMQcQYT+N5RR5sQuWBEYq3sWKo8jXnkm20YQBndqZxy2tVhnNx1JjHI/g==", + "license": "BSD-2-Clause", + "dependencies": { + "@daily-co/daily-js": "^0.94.0", + "dequal": "^2.0.3", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@pipecat-ai/client-js": "~1.14.0" + } + }, + "node_modules/@pipecat-ai/websocket-transport": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@pipecat-ai/websocket-transport/-/websocket-transport-1.8.0.tgz", + "integrity": "sha512-VmtqJvxPefP69erEDB246+fiel4uvzjZ9CcHSkYGe585lTl1egkm+f++xwB7k/QWZSjrbl9SOrOy6vYsnplWtA==", + "license": "BSD-2-Clause", + "dependencies": { + "@daily-co/daily-js": "^0.94.0", + "@protobuf-ts/plugin": "^2.11.0", + "@protobuf-ts/runtime": "^2.11.0", + "x-law": "^0.3.1" + }, + "peerDependencies": { + "@pipecat-ai/client-js": "~1.14.0" + } + }, + "node_modules/@protobuf-ts/plugin": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.11.1.tgz", + "integrity": "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.4.0", + "@bufbuild/protoplugin": "^2.4.0", + "@protobuf-ts/protoc": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "typescript": "^3.9" + }, + "bin": { + "protoc-gen-dump": "bin/protoc-gen-dump", + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.2.tgz", + "integrity": "sha512-GnKod+gL/Y+1FUM/RGV8q6le1CoyiGbT40MitEK7eVwWe+bfTRq1gN7ioupyHFMUg1RlQkDQ4/sENmio/uow5A==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.2.tgz", + "integrity": "sha512-XQy//NWbL0mLLM5w8wNDWMNpXz39VUyW2397dUrH8++kR63WhUVAvTOtL0o0GMVadSAzl1b08oHP9zSUNFQwcg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.2.tgz", + "integrity": "sha512-+W43Z697EVe/OgpGW07B773sa8xO1UbpnW0Cr+E+3FMDb6ZbXlaBUoagPTUkkQPdwBe35SDh6r8y2M3EOPGbxg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.2.tgz", + "integrity": "sha512-P/jGiuR7dRLG9IzD/463fLgiibyYceauav/9prRG0ZxJm1AtuO02OKball2Fs3bbzdzwHCTlcsUuL2ivDF4b5A==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.2.tgz", + "integrity": "sha512-xHuPIEKhx9zw5quWvv4YgZprnwoVMCfxIhmOIf6KJ9iizyUHeUDcKpLS59xERroqwX4RpvK+l/27AZu4zfZlzQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.2", + "@sentry-internal/feedback": "8.55.2", + "@sentry-internal/replay": "8.55.2", + "@sentry-internal/replay-canvas": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.2.tgz", + "integrity": "sha512-YlEBwybUcOQ/KjMHDmof1vwweVnBtBxYlQp7DE3fOdtW4pqqdHWTnTntQs4VgYfxzjJYgtkd9LHlGtg8qy+JVQ==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "license": "MIT" + }, + "node_modules/@typescript/vfs": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.5.tgz", + "integrity": "sha512-gobLHQLmvQGJInFk31NvgMd1uYzRXsd8qgnOssyLD055R2QPnw1ubRbBQ4+T+5qC7IQVuYXdvNDE2G7tMDVHgw==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jose": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/livekit-client": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.23.0.tgz", + "integrity": "sha512-j9Kg86nJjBjNFUD+ukp2jwMCGa0k+YD5gU//RzAM+ZY3pbG631QCte7fQjwqWMmtBzhDJL9nKYuOLMv/9IbAxg==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.52.1", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "machina": "^7.0.0", + "sdp-transform": "^2.15.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "9.0.6" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/machina": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/machina/-/machina-7.0.1.tgz", + "integrity": "sha512-0Xkgs8NeBvO2hW6bjJT8ewLWrMfh1haMPstaghpASWjXkLjRhtDiaJG1Kb7mslaOHxZYxpH7PIjpmirRVYbMMA==", + "license": "MIT", + "engines": { + "node": ">=22.22" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sdp": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz", + "integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/webrtc-adapter": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.6.tgz", + "integrity": "sha512-CHbl2ZQbxx164IgWRgzJno4hWtM4tFbRam1QfI3Yxhs3w/DvqluVxVWeXs3oL5/fbGkSNLKo0Ty5MgUWceNhog==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, + "node_modules/x-law": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/x-law/-/x-law-0.3.1.tgz", + "integrity": "sha512-Nvo6OKj6UL2LuzAc08uJkwIDkK2PsTEdpLiY82NkwMptuRpAA1V7arUl7ZY12BcgRYNq8uh1pdAu7G6VeQn7Hg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/file-upload/client/package.json b/file-upload/client/package.json new file mode 100644 index 00000000..b80af02f --- /dev/null +++ b/file-upload/client/package.json @@ -0,0 +1,21 @@ +{ + "name": "file-upload", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@pipecat-ai/client-js": "^1.14.0", + "@pipecat-ai/daily-transport": "^1.7.0", + "@pipecat-ai/livekit-transport": "^1.1.0", + "@pipecat-ai/small-webrtc-transport": "^1.11.0", + "@pipecat-ai/websocket-transport": "^1.8.0" + }, + "devDependencies": { + "vite": "^8.0.16" + } +} diff --git a/file-upload/client/src/app.js b/file-upload/client/src/app.js new file mode 100644 index 00000000..fb72dc62 --- /dev/null +++ b/file-upload/client/src/app.js @@ -0,0 +1,407 @@ +import { PipecatClient, RTVIEvent } from '@pipecat-ai/client-js'; +import { + AVAILABLE_TRANSPORTS, + DEFAULT_TRANSPORT, + TRANSPORT_CONFIG, + createTransport, +} from './config'; + +class VoiceChatClient { + constructor() { + this.client = null; + this.transportType = DEFAULT_TRANSPORT; + this.isConnected = false; + + this.setupDOM(); + this.setupEventListeners(); + this.addEvent('initialized', 'Client initialized'); + } + + setupDOM() { + this.transportSelect = document.getElementById('transport-select'); + this.connectBtn = document.getElementById('connect-btn'); + this.micBtn = document.getElementById('mic-btn'); + this.micStatus = document.getElementById('mic-status'); + this.conversationLog = document.getElementById('conversation-log'); + this.eventsLog = document.getElementById('events-log'); + this.uploadBtn = document.getElementById('upload-btn'); + this.uploadModal = document.getElementById('upload-modal'); + this.closeBtn = document.querySelector('.close-btn'); + this.uploadConfirmBtn = document.getElementById('upload-confirm-btn'); + this.fileInput = document.getElementById('file-input'); + this.urlInput = document.getElementById('url-input'); + this.filePrompt = document.getElementById('file-prompt'); + this.uploadTabs = document.querySelectorAll('.tab-link'); + this.tabContents = document.querySelectorAll('.tab-content'); + this.lastConversationBubble = null; + this.botSpans = {}; + + // Populate transport selector with available transports + this.transportSelect.innerHTML = ''; + AVAILABLE_TRANSPORTS.forEach((transport) => { + const option = document.createElement('option'); + option.value = transport; + option.textContent = + transport.charAt(0).toUpperCase() + transport.slice(1); + if (transport === 'smallwebrtc') { + option.textContent = 'SmallWebRTC'; + } else if (transport === 'daily') { + option.textContent = 'Daily'; + } else if (transport === 'livekit') { + option.textContent = 'LiveKit'; + } else if (transport === 'websocket') { + option.textContent = 'WebSocket'; + } + this.transportSelect.appendChild(option); + }); + + // Hide transport selector if only one transport + if (AVAILABLE_TRANSPORTS.length === 1) { + this.transportSelect.parentElement.style.display = 'none'; + } + + // Add placeholder message + this.addConversationMessage( + 'Connect to start talking with your bot', + 'placeholder', + ); + } + + setupEventListeners() { + this.transportSelect.addEventListener('change', (e) => { + this.transportType = e.target.value; + this.addEvent('transport-changed', this.transportType); + }); + + this.connectBtn.addEventListener('click', () => { + if (this.isConnected) { + this.disconnect(); + } else { + this.connect(); + } + }); + + this.micBtn.addEventListener('click', () => { + if (this.client) { + const newState = !this.client.isMicEnabled; + this.client.enableMic(newState); + this.updateMicButton(newState); + } + }); + + this.uploadBtn.addEventListener('click', () => { + this.uploadModal.style.display = 'block'; + }); + + this.closeBtn.addEventListener('click', () => { + this.uploadModal.style.display = 'none'; + }); + + window.addEventListener('click', (event) => { + if (event.target === this.uploadModal) { + this.uploadModal.style.display = 'none'; + } + }); + + this.uploadConfirmBtn.addEventListener('click', () => { + this.handleContentUpload(); + }); + + this.uploadTabs.forEach((tab) => { + tab.addEventListener('click', () => { + const target = tab.getAttribute('data-tab'); + this.uploadTabs.forEach((t) => t.classList.remove('active')); + tab.classList.add('active'); + this.tabContents.forEach((c) => c.classList.remove('active')); + document.getElementById(target).classList.add('active'); + }); + }); + } + + handleContentUpload() { + const activeTab = document.querySelector('.tab-link.active').dataset.tab; + const prompt = this.filePrompt.value; + if (activeTab === 'file-tab') { + this.handleFileUpload(prompt); + } else { + this.handleUrlUpload(prompt); + } + } + + handleUrlUpload(prompt) { + const url = this.urlInput.value; + if (!url) { + alert('Please enter a URL.'); + return; + } + + let pathname; + try { + pathname = new URL(url).pathname; + } catch { + alert('Please enter a valid URL.'); + return; + } + + const rtvi_file = { + format: pathname.split('.').pop(), + source: { type: 'url', url }, + }; + + if (this.client && this.isConnected) { + this.client + .sendFile(rtvi_file, prompt) + .then(() => { + this.addConversationMessage( + `Referencing URL: ${url}` + + (prompt ? ` with prompt: ${prompt}` : ''), + 'user', + ); + }) + .catch((error) => console.error('URL upload error:', error)); + } + + this.urlInput.value = ''; + this.filePrompt.value = ''; + this.uploadModal.style.display = 'none'; + } + + handleFileUpload(prompt) { + const file = this.fileInput.files[0]; + if (!file) { + alert('Please select a file to upload.'); + return; + } + + if (this.client && this.isConnected) { + this.client + .sendFile(file, prompt) + .then(() => { + this.addConversationMessage( + `Uploaded file: ${file.name}` + + (prompt ? ` with prompt: ${prompt}` : ''), + 'user', + ); + }) + .catch((error) => console.error('File upload error:', error)); + } + + this.fileInput.value = ''; + this.filePrompt.value = ''; + this.uploadModal.style.display = 'none'; + } + + async connect() { + try { + this.addEvent('connecting', `Using ${this.transportType} transport`); + + // Create transport using config + const transport = await createTransport(this.transportType); + + // Create client + this.client = new PipecatClient({ + transport, + enableMic: true, + enableCam: false, + callbacks: { + onConnected: () => { + this.onConnected(); + }, + onDisconnected: () => { + this.onDisconnected(); + }, + onTransportStateChanged: (state) => { + this.addEvent('transport-state', state); + }, + onBotReady: () => { + this.uploadBtn.disabled = false; + this.addEvent('bot-ready', 'Bot is ready to talk'); + }, + onUserTranscript: (data) => { + if (data.final) { + this.addConversationMessage(data.text, 'user'); + } + }, + onBotOutput: (data) => { + if (data.will_be_spoken && data.spoken_status !== 'new') { + this.highlightSpokenText(data); + return; + } + this.addConversationMessage(data.text, 'bot', data.segment_id); + }, + onError: (error) => { + this.addEvent('error', error.message); + }, + }, + }); + window.client = this.client; // Expose client for debugging + + // Setup audio + this.setupAudio(); + + // Start bot and connect using config + const connectParams = TRANSPORT_CONFIG[this.transportType]; + if (this.transportType === 'websocket') { + // WebSocket connects in two steps: start the bot to obtain the + // WebSocket URL (and optional token), then connect to it. + const { wsUrl, token } = await this.client.startBot(connectParams); + await this.client.connect({ + wsUrl: token ? `${wsUrl}?token=${encodeURIComponent(token)}` : wsUrl, + }); + } else { + await this.client.startBotAndConnect(connectParams); + } + } catch (error) { + this.addEvent('error', error.message); + console.error('Connection error:', error); + } + } + + async disconnect() { + if (this.client) { + await this.client.disconnect(); + } + } + + setupAudio() { + this.client.on(RTVIEvent.TrackStarted, (track, participant) => { + if (!participant?.local && track.kind === 'audio') { + this.addEvent('track-started', 'Bot audio track'); + const audio = document.createElement('audio'); + audio.autoplay = true; + audio.srcObject = new MediaStream([track]); + document.body.appendChild(audio); + } + }); + } + + onConnected() { + this.isConnected = true; + this.connectBtn.textContent = 'Disconnect'; + this.connectBtn.classList.add('disconnect'); + this.micBtn.disabled = false; + this.transportSelect.disabled = true; + this.updateMicButton(this.client.isMicEnabled); + this.addEvent('connected', 'Successfully connected to bot'); + + // Clear placeholder + if (this.conversationLog.querySelector('.placeholder')) { + this.conversationLog.innerHTML = ''; + } + } + + onDisconnected() { + this.isConnected = false; + this.connectBtn.textContent = 'Connect'; + this.connectBtn.classList.remove('disconnect'); + this.micBtn.disabled = true; + this.uploadBtn.disabled = true; + this.transportSelect.disabled = false; + this.updateMicButton(false); + this.addEvent('disconnected', 'Disconnected from bot'); + } + + updateMicButton(enabled) { + this.micStatus.textContent = enabled ? 'Mic is On' : 'Mic is Off'; + this.micBtn.style.backgroundColor = enabled ? '#10b981' : '#1f2937'; + } + + // Escape text before building innerHTML so model output can't inject markup + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + formatBotText(text) { + return this.escapeHtml(text).replace(/\n/g, '
'); + } + + highlightSpokenText(data) { + const curSpan = this.botSpans[data.segment_id]; + if (!curSpan) return; + const accumulatedText = this.formatBotText( + data.spoken_progress.accumulated_text, + ); + const remainingText = this.formatBotText(data.spoken_progress.remaining_text); + curSpan.innerHTML = `${accumulatedText}${remainingText}`; + this.conversationLog.scrollTop = this.conversationLog.scrollHeight; + } + + addConversationMessage(text, role, segmentId) { + if (role === 'placeholder') { + const messageDiv = document.createElement('div'); + messageDiv.className = 'conversation-message placeholder'; + messageDiv.textContent = text; + this.conversationLog.appendChild(messageDiv); + this.conversationLog.scrollTop = this.conversationLog.scrollHeight; + return; + } + + // Append bot segments to the current bubble; start a new bubble on role change + if (this.lastConversationBubble?.role === role && role === 'bot') { + const textSpan = document.createElement('span'); + textSpan.innerHTML = this.formatBotText(text); + if (segmentId) this.botSpans[segmentId] = textSpan; + this.lastConversationBubble.appendChild(document.createTextNode(' ')); + this.lastConversationBubble.appendChild(textSpan); + this.conversationLog.scrollTop = this.conversationLog.scrollHeight; + return; + } + + const messageDiv = document.createElement('div'); + messageDiv.className = `conversation-message ${role}`; + messageDiv.role = role; + this.lastConversationBubble = messageDiv; + + const roleLabel = document.createElement('div'); + roleLabel.className = 'role'; + roleLabel.textContent = role === 'user' ? 'You' : 'Bot'; + messageDiv.appendChild(roleLabel); + + if (role === 'bot') { + const textSpan = document.createElement('span'); + textSpan.innerHTML = this.formatBotText(text); + if (segmentId) this.botSpans[segmentId] = textSpan; + messageDiv.appendChild(textSpan); + } else { + const textDiv = document.createElement('div'); + textDiv.textContent = text; + messageDiv.appendChild(textDiv); + } + + this.conversationLog.appendChild(messageDiv); + this.conversationLog.scrollTop = this.conversationLog.scrollHeight; + } + + addEvent(eventName, data) { + const eventDiv = document.createElement('div'); + eventDiv.className = 'event-entry'; + + const timestamp = new Date().toLocaleTimeString(); + const timestampSpan = document.createElement('span'); + timestampSpan.className = 'timestamp'; + timestampSpan.textContent = timestamp; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'event-name'; + nameSpan.textContent = eventName; + + const dataSpan = document.createElement('span'); + dataSpan.className = 'event-data'; + dataSpan.textContent = + typeof data === 'string' ? data : JSON.stringify(data); + + eventDiv.appendChild(timestampSpan); + eventDiv.appendChild(nameSpan); + eventDiv.appendChild(dataSpan); + + this.eventsLog.appendChild(eventDiv); + this.eventsLog.scrollTop = this.eventsLog.scrollHeight; + } +} + +// Initialize when DOM is loaded +window.addEventListener('DOMContentLoaded', () => { + new VoiceChatClient(); +}); diff --git a/file-upload/client/src/config.js b/file-upload/client/src/config.js new file mode 100644 index 00000000..da5442a4 --- /dev/null +++ b/file-upload/client/src/config.js @@ -0,0 +1,115 @@ +/** + * Project configuration + * This file is auto-generated by pipecat-cli + */ + +export const AVAILABLE_TRANSPORTS = ['smallwebrtc', 'daily', 'livekit', 'websocket']; + +export const DEFAULT_TRANSPORT = 'smallwebrtc'; + +const botStartUrl = + import.meta.env.VITE_BOT_START_URL || 'http://localhost:7860/start'; +const botStartPublicApiKey = import.meta.env.VITE_BOT_START_PUBLIC_API_KEY; + +if (!import.meta.env.VITE_BOT_START_URL) { + console.warn( + 'VITE_BOT_START_URL not configured, using default: http://localhost:7860/start', + ); +} + +const dailyConfig = { + endpoint: botStartUrl, + requestData: { + createDailyRoom: true, + dailyRoomProperties: { start_video_off: true }, + transport: 'daily', + }, +}; + +if (botStartPublicApiKey) { + dailyConfig.headers = new Headers({ + Authorization: `Bearer ${botStartPublicApiKey}`, + }); +} + +const smallWebRTCConfig = { + endpoint: botStartUrl, + requestData: { + createDailyRoom: false, + enableDefaultIceServers: true, + transport: 'webrtc', + }, +}; + +if (botStartPublicApiKey) { + smallWebRTCConfig.headers = new Headers({ + Authorization: `Bearer ${botStartPublicApiKey}`, + }); +} + +const livekitConfig = { + endpoint: botStartUrl, + requestData: { + transport: 'livekit', + }, +}; + +if (botStartPublicApiKey) { + livekitConfig.headers = new Headers({ + Authorization: `Bearer ${botStartPublicApiKey}`, + }); +} + +const websocketConfig = { + endpoint: botStartUrl, + requestData: { + transport: 'websocket', + }, +}; + +if (botStartPublicApiKey) { + websocketConfig.headers = new Headers({ + Authorization: `Bearer ${botStartPublicApiKey}`, + }); +} + +export const TRANSPORT_CONFIG = { + daily: dailyConfig, + livekit: livekitConfig, + smallwebrtc: smallWebRTCConfig, + websocket: websocketConfig, +}; + +/** + * Create a transport instance based on the transport type + * Uses dynamic imports to only load the required transport library + */ +export async function createTransport(transportType) { + switch (transportType) { + case 'smallwebrtc': { + const { SmallWebRTCTransport } = + await import('@pipecat-ai/small-webrtc-transport'); + return new SmallWebRTCTransport(); + } + + case 'daily': { + const { DailyTransport } = await import('@pipecat-ai/daily-transport'); + return new DailyTransport(); + } + + case 'livekit': { + const { LiveKitTransport } = + await import('@pipecat-ai/livekit-transport'); + return new LiveKitTransport(); + } + + case 'websocket': { + const { WebSocketTransport } = + await import('@pipecat-ai/websocket-transport'); + return new WebSocketTransport(); + } + + default: + throw new Error(`Unsupported transport type: ${transportType}`); + } +} diff --git a/file-upload/client/src/style.css b/file-upload/client/src/style.css new file mode 100644 index 00000000..f106a0cf --- /dev/null +++ b/file-upload/client/src/style.css @@ -0,0 +1,354 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + background-color: #000; + color: #e5e7eb; + height: 100vh; + overflow: hidden; +} + +.container { + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #333; +} + +.transport-selector { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.transport-selector label { + font-weight: 500; +} + +#transport-select { + padding: 0.5rem 1rem; + background-color: #1f2937; + color: #e5e7eb; + border: 1px solid #374151; + border-radius: 0.375rem; + cursor: pointer; + font-size: 0.875rem; +} + +#transport-select:focus { + outline: none; + border-color: #10b981; +} + +.controls { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.control-btn, +.connect-btn { + padding: 0.5rem 1rem; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.control-btn { + background-color: #1f2937; + color: #e5e7eb; + border: 1px solid #374151; +} + +.control-btn:hover:not(:disabled) { + background-color: #374151; +} + +.control-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.connect-btn { + background-color: #10b981; + color: #000; +} + +.connect-btn:hover { + background-color: #059669; +} + +.connect-btn.disconnect { + background-color: #ef4444; + color: #fff; +} + +.connect-btn.disconnect:hover { + background-color: #dc2626; +} + +/* Conversation Panel */ +.conversation-panel { + flex: 1; + padding: 1rem; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.conversation-panel h3 { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; +} + +#conversation-log { + flex: 1; + overflow-y: auto; + background-color: #111; + border: 1px solid #333; + border-radius: 0.5rem; + padding: 1rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.conversation-message { + margin-bottom: 0.75rem; + padding: 0.5rem; + border-radius: 0.25rem; +} + +.conversation-message.user { + background-color: #1e40af; + color: #e0e7ff; +} + +.conversation-message.bot { + background-color: #065f46; + color: #d1fae5; +} + +.conversation-message .role { + font-weight: 600; + margin-bottom: 0.25rem; +} + +.conversation-message.placeholder { + color: #6b7280; + font-style: italic; + background-color: transparent; +} + +/* Events Panel */ +.events-panel { + height: 256px; + padding: 0 1rem 1rem 1rem; + border-top: 1px solid #333; +} + +.events-panel h3 { + margin: 1rem 0; + font-size: 1rem; + font-weight: 600; +} + +#events-log { + height: calc(100% - 3rem); + overflow-y: auto; + background-color: #111; + border: 1px solid #333; + border-radius: 0.5rem; + padding: 0.75rem; + font-family: 'Courier New', monospace; + font-size: 0.75rem; + line-height: 1.4; +} + +.event-entry { + margin-bottom: 0.5rem; + display: flex; + gap: 0.5rem; +} + +.event-entry .timestamp { + color: #6b7280; + flex-shrink: 0; +} + +.event-entry .event-name { + color: #10b981; + font-weight: 600; + flex-shrink: 0; +} + +.event-entry .event-data { + color: #9ca3af; +} + +/* Scrollbar styling */ +#conversation-log::-webkit-scrollbar, +#events-log::-webkit-scrollbar { + width: 8px; +} + +#conversation-log::-webkit-scrollbar-track, +#events-log::-webkit-scrollbar-track { + background: #1f2937; + border-radius: 4px; +} + +#conversation-log::-webkit-scrollbar-thumb, +#events-log::-webkit-scrollbar-thumb { + background: #374151; + border-radius: 4px; +} + +#conversation-log::-webkit-scrollbar-thumb:hover, +#events-log::-webkit-scrollbar-thumb:hover { + background: #4b5563; +} + +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: #1f2937; + margin: 15% auto; + padding: 20px; + border: 1px solid #374151; + border-radius: 0.5rem; + width: 80%; + max-width: 500px; + position: relative; +} + +.close-btn { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close-btn:hover, +.close-btn:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +.modal-content h2 { + margin-top: 0; +} + +.modal-content p { + margin-bottom: 1rem; +} + +#file-input { + margin-bottom: 1rem; + width: 100%; + padding: 0.5rem; + background-color: #111; + border: 1px solid #333; + border-radius: 0.375rem; + color: #e5e7eb; +} + +#file-prompt { + width: 100%; + padding: 0.75rem; + border: 1px solid #333; + border-radius: 0.375rem; + background-color: #111; + color: #e5e7eb; + font-size: 0.875rem; + resize: vertical; + margin-bottom: 1rem; +} + +#upload-confirm-btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.375rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + background-color: #10b981; + color: #000; + width: 100%; +} + +#upload-confirm-btn:hover { + background-color: #059669; +} + +/* Upload Modal Tabs */ +.upload-tabs { + display: flex; + margin-bottom: 1rem; + border-bottom: 1px solid #374151; +} + +.tab-link { + background: none; + border: none; + color: #9ca3af; + padding: 0.75rem 1rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} + +.tab-link.active { + color: #e5e7eb; + border-bottom-color: #10b981; +} + +.tab-content { + display: none; + margin-bottom: 1rem; +} + +.tab-content.active { + display: block; +} + +#url-input { + width: 100%; + padding: 0.5rem; + background-color: #111; + border: 1px solid #333; + border-radius: 0.375rem; + color: #e5e7eb; +} diff --git a/file-upload/server/Dockerfile b/file-upload/server/Dockerfile new file mode 100644 index 00000000..d73b7cee --- /dev/null +++ b/file-upload/server/Dockerfile @@ -0,0 +1,19 @@ +FROM dailyco/pipecat-base:latest + +# Enable bytecode compilation +ENV UV_COMPILE_BYTECODE=1 + +# Copy from the cache instead of linking since it's a mounted volume +ENV UV_LINK_MODE=copy + +# Uncomment if you wish to print a summary of the features available in the base image +# ENV PCC_LOG_FEATURES_SUMMARY=true + +# Install the project's dependencies using the lockfile and settings +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-dev + +# Copy the application code +COPY ./bot.py bot.py \ No newline at end of file diff --git a/file-upload/server/bot.py b/file-upload/server/bot.py new file mode 100644 index 00000000..149f1b4f --- /dev/null +++ b/file-upload/server/bot.py @@ -0,0 +1,250 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""file-upload - Pipecat Voice Agent + +This bot uses a cascade pipeline: Speech-to-Text → LLM → Text-to-Speech + +Clients can send files and images into the conversation with RTVI's +``send-file`` message, either inline or by first uploading to the endpoint +the runner advertises as ``fileUploadUrl`` in its ``/start`` response +(enabled by ``PIPECAT_UPLOADS_FOLDER`` or ``-u/--uploads-folder``). The +upload endpoint stores the file and returns a URL the client passes back in +``send-file``; the LLM service resolves that URL at completion time through +its ``FileResolver``, wired to the same storage backend via +``runner_args.file_storage``. See ``bot_gcs.py`` / ``bot_s3.py`` for backing +uploads with a cloud bucket instead of local disk. + +Required AI services: +- Deepgram (Speech-to-Text) +- Anthropic, OpenAI, AWS Bedrock, or Google Gemini via the developer API or + Vertex AI (LLM; select with -llm, defaults to anthropic) +- Cartesia (Text-to-Speech) + +Run the bot using:: + + uv run bot.py + uv run bot.py -llm openai +""" + +import argparse +import os + +from dotenv import load_dotenv +from loguru import logger +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.worker import PipelineParams, PipelineWorker +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.processors.frameworks.rtvi import RTVIProcessor +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.serializers.protobuf import ProtobufFrameSerializer +from pipecat.services.anthropic.llm import AnthropicLLMService +from pipecat.services.aws.llm import AWSBedrockLLMService +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.google.llm import GoogleLLMService +from pipecat.services.google.vertex.llm import GoogleVertexLLMService +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.livekit.transport import LiveKitParams, LiveKitTransport +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams +from pipecat.utils.file_resolver import FileResolver +from pipecat.workers.runner import WorkerRunner + +load_dotenv(override=True) + +SYSTEM_INSTRUCTION = ( + "You are a helpful assistant in a voice conversation. Users can upload images or files to " + "share visual context with you, and you should refer to that content in your responses. " + "Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting " + "that can't be spoken. Keep responses concise." +) + + +def create_llm_service(llm_name: str, file_resolver: FileResolver): + """Create the LLM service for `llm_name` (anthropic, openai, bedrock, gemini, or vertex). + + Every service gets the `file_resolver` so it can resolve file URLs the + provider can't fetch itself (uploaded files, private URLs) into bytes at + completion time. Vertex Gemini reads gs:// URLs directly instead, so with + bot_gcs.py it exercises the pass-through path: the file never moves + through the bot. + """ + if llm_name == "vertex": + # Authenticates via Application Default Credentials + # (`gcloud auth application-default login`), which must be able to + # call Vertex AI and — for gs:// pass-through — read the bucket. + return GoogleVertexLLMService( + project_id=os.environ["GOOGLE_CLOUD_PROJECT_ID"], + settings=GoogleVertexLLMService.Settings(system_instruction=SYSTEM_INSTRUCTION), + file_resolver=file_resolver, + ) + if llm_name == "openai": + return OpenAILLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAILLMService.Settings(system_instruction=SYSTEM_INSTRUCTION), + file_resolver=file_resolver, + ) + if llm_name == "bedrock": + return AWSBedrockLLMService( + aws_access_key=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + aws_region=os.getenv("AWS_REGION"), + settings=AWSBedrockLLMService.Settings( + model="us.amazon.nova-2-lite-v1:0", + system_instruction=SYSTEM_INSTRUCTION, + ), + file_resolver=file_resolver, + ) + if llm_name == "gemini": + return GoogleLLMService( + api_key=os.getenv("GOOGLE_API_KEY"), + settings=GoogleLLMService.Settings(system_instruction=SYSTEM_INSTRUCTION), + file_resolver=file_resolver, + ) + return AnthropicLLMService( + api_key=os.getenv("ANTHROPIC_API_KEY"), + settings=AnthropicLLMService.Settings(system_instruction=SYSTEM_INSTRUCTION), + file_resolver=file_resolver, + ) + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments) -> None: + """Run the voice bot for this session. + + Args: + transport: The transport for this session, built by ``create_transport`` + (or by hand for the dial-out/SIP production flows). + runner_args: Runner session arguments. Carries the request ``body`` + (e.g. dial-out settings, SIP call details) and ``session_id``; the + standard web/telephony pipelines don't need it. + """ + logger.info("Starting bot") + + # Speech-to-Text service + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + # Text-to-Speech service + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice=os.getenv("CARTESIA_VOICE_ID", "71a7ad14-091c-4e8e-a314-022ece01c121"), + ), + ) + + # LLM service - defaults to anthropic; select another with `-llm openai|bedrock|gemini|vertex` + # The resolver shares the host's storage backend, injected as + # runner_args.file_storage, so URLs returned by the upload endpoint + # resolve to the uploaded bytes. + llm_name = getattr(runner_args.cli_args, "llm", None) or "anthropic" + file_resolver = FileResolver(file_storage=runner_args.file_storage) + llm = create_llm_service(llm_name, file_resolver) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + rtvi = RTVIProcessor() + + # Pipeline - assembled from reusable components + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + worker = PipelineWorker( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + rtvi_processor=rtvi, + ) + + @worker.rtvi.event_handler("on_client_ready") + async def on_client_ready(rtvi): + # Kick off the conversation + context.add_message( + { + "role": "developer", + "content": "Greet the user and let them know they can upload images or files using the Upload File button, and you'll be able to see and discuss what they share.", + } + ) + await worker.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info("Client connected") + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info("Client disconnected") + await worker.cancel() + + runner = WorkerRunner(handle_sigint=False) + + await runner.add_workers(worker) + await runner.run() + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point.""" + + transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "livekit": lambda: LiveKitParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "websocket": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + add_wav_header=False, + serializer=ProtobufFrameSerializer(), + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + } + + transport = await create_transport(runner_args, transport_params) + + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + parser = argparse.ArgumentParser() + parser.add_argument( + "-llm", + "--llm", + choices=["anthropic", "openai", "bedrock", "gemini", "vertex"], + default="anthropic", + help="LLM provider to use (default: anthropic)", + ) + main(parser) diff --git a/file-upload/server/bot_gcs.py b/file-upload/server/bot_gcs.py new file mode 100644 index 00000000..318075b5 --- /dev/null +++ b/file-upload/server/bot_gcs.py @@ -0,0 +1,122 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""file-upload with client uploads stored in Google Cloud Storage. + +The same bot as ``bot.py``, but uploads are backed by a GCS bucket instead of +the local uploads folder: the ``create_file_storage()`` function below is +discovered by whichever host runs the bot — the development runner here, the +same way a cloud platform's base image would — and backs its upload endpoint, +so an upload is written to the bucket and the endpoint returns a ``gs://`` +URL, which the client passes back in its ``send-file`` message. + +At completion time the URL is resolved per provider: Gemini on Vertex AI can +read ``gs://`` URLs itself through its own IAM, so they pass straight through; +every other provider — including Gemini via the developer API, which can't +read ``gs://`` — has the ``FileResolver`` download the file from the bucket +with this process's Google credentials and inline the bytes. + +Requires: + +- ``google-cloud-storage`` +- Application Default Credentials with read/write access to the bucket + (``GOOGLE_APPLICATION_CREDENTIALS``, or ``gcloud auth application-default login``) +- ``GCS_UPLOADS_BUCKET`` set to the bucket name + +Run the bot using:: + + uv run bot_gcs.py + uv run bot_gcs.py -llm vertex # gs:// pass-through; needs GOOGLE_CLOUD_PROJECT_ID +""" + +import argparse +import asyncio +import mimetypes +import os +import uuid +from pathlib import Path + +from dotenv import load_dotenv +from google.api_core.exceptions import NotFound +from google.cloud import storage +from pipecat.utils.file_storage import FileStorage + +# The pipeline is identical to bot.py's; only the storage backend differs. +from bot import bot # noqa: F401 (re-exported for the runner to discover) + +load_dotenv(override=True) + + +class GCSFileStorage(FileStorage): + """Client uploads in a Google Cloud Storage bucket, addressed as ``gs://`` URLs.""" + + def __init__(self, bucket: str, prefix: str = "uploads/"): + """Initialize GCS-backed upload storage. + + Args: + bucket: Name of the bucket to store uploads in. + prefix: Object-name prefix for uploads within the bucket. + """ + self._bucket = storage.Client().bucket(bucket) + self._prefix = prefix + + async def save(self, filename: str, contents: bytes) -> str: + """Upload `contents` to a new randomly-named object and return its ``gs://`` URL.""" + blob_name = f"{self._prefix}{uuid.uuid4().hex}{Path(filename).suffix}" + content_type, _ = mimetypes.guess_type(filename, strict=False) + blob = self._bucket.blob(blob_name) + await asyncio.to_thread( + blob.upload_from_string, + contents, + content_type=content_type or "application/octet-stream", + ) + return f"gs://{self._bucket.name}/{blob_name}" + + async def load(self, file_url: str) -> bytes: + """Download and return the object stored under `file_url`.""" + blob = self._blob_for(file_url) + try: + return await asyncio.to_thread(blob.download_as_bytes) + except NotFound as e: + raise FileNotFoundError(file_url) from e + + async def delete(self, file_url: str) -> None: + """Delete the object stored under `file_url`, if it exists.""" + try: + await asyncio.to_thread(self._blob_for(file_url).delete) + except (FileNotFoundError, NotFound): + pass + + def _blob_for(self, file_url: str) -> storage.Blob: + """Validate `file_url` and return the blob it maps to.""" + prefix = f"gs://{self._bucket.name}/" + if not file_url.startswith(prefix): + raise FileNotFoundError(file_url) + return self._bucket.blob(file_url.removeprefix(prefix)) + + +def create_file_storage() -> FileStorage: + """Storage backend for client uploads, discovered by the host alongside ``bot()``. + + The host calls this at startup, serves its upload endpoint from the + returned backend, and injects it into the bot as + ``runner_args.file_storage``. + """ + return GCSFileStorage(bucket=os.environ["GCS_UPLOADS_BUCKET"]) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + parser = argparse.ArgumentParser() + parser.add_argument( + "-llm", + "--llm", + choices=["anthropic", "openai", "bedrock", "gemini", "vertex"], + default="anthropic", + help="LLM provider to use (default: anthropic)", + ) + main(parser) diff --git a/file-upload/server/bot_s3.py b/file-upload/server/bot_s3.py new file mode 100644 index 00000000..44d962b7 --- /dev/null +++ b/file-upload/server/bot_s3.py @@ -0,0 +1,137 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""file-upload with client uploads stored in Amazon S3. + +The same bot as ``bot.py``, but uploads are backed by an S3 bucket instead of +the local uploads folder: the ``create_file_storage()`` function below is +discovered by whichever host runs the bot — the development runner here, the +same way a cloud platform's base image would — and backs its upload endpoint, +so an upload is written to the bucket and the endpoint returns an ``s3://`` +URL, which the client passes back in its ``send-file`` message. + +At completion time the URL is resolved per provider: AWS Bedrock can read +``s3://`` URLs itself through its own IAM (the bucket must be readable by the +account making the Bedrock call), so they pass straight through; every other +provider has the ``FileResolver`` download the file from the bucket with this +process's AWS credentials and inline the bytes. + +Requires: + +- AWS credentials with read/write access to the bucket (the standard + ``AWS_ACCESS_KEY_ID``/``AWS_SECRET_ACCESS_KEY``/``AWS_REGION`` env vars or + any other botocore credential source) +- ``S3_UPLOADS_BUCKET`` set to the bucket name + +Run the bot using:: + + uv run bot_s3.py + uv run bot_s3.py -llm bedrock +""" + +import argparse +import mimetypes +import os +import uuid +from pathlib import Path +from typing import Any + +import aiobotocore.session +from botocore.exceptions import ClientError +from dotenv import load_dotenv +from pipecat.utils.file_storage import FileStorage + +# The pipeline is identical to bot.py's; only the storage backend differs. +from bot import bot # noqa: F401 (re-exported for the runner to discover) + +load_dotenv(override=True) + + +class S3FileStorage(FileStorage): + """Client uploads in an S3 bucket, addressed as ``s3://`` URLs.""" + + def __init__(self, bucket: str, region: str | None = None, prefix: str = "uploads/"): + """Initialize S3-backed upload storage. + + Args: + bucket: Name of the bucket to store uploads in. + region: AWS region of the bucket; defaults to the credential + chain's region. + prefix: Key prefix for uploads within the bucket. + """ + self._session = aiobotocore.session.get_session() + self._bucket = bucket + self._region = region + self._prefix = prefix + + def _client(self) -> Any: + return self._session.create_client("s3", region_name=self._region) + + async def save(self, filename: str, contents: bytes) -> str: + """Upload `contents` to a new randomly-named key and return its ``s3://`` URL.""" + key = f"{self._prefix}{uuid.uuid4().hex}{Path(filename).suffix}" + content_type, _ = mimetypes.guess_type(filename, strict=False) + async with self._client() as s3: + await s3.put_object( + Bucket=self._bucket, + Key=key, + Body=contents, + ContentType=content_type or "application/octet-stream", + ) + return f"s3://{self._bucket}/{key}" + + async def load(self, file_url: str) -> bytes: + """Download and return the object stored under `file_url`.""" + key = self._key_for(file_url) + try: + async with self._client() as s3: + response = await s3.get_object(Bucket=self._bucket, Key=key) + async with response["Body"] as stream: + return await stream.read() + except ClientError as e: + if e.response.get("Error", {}).get("Code") in ("NoSuchKey", "404"): + raise FileNotFoundError(file_url) from e + raise + + async def delete(self, file_url: str) -> None: + """Delete the object stored under `file_url`, if it exists.""" + try: + key = self._key_for(file_url) + except FileNotFoundError: + return + async with self._client() as s3: + await s3.delete_object(Bucket=self._bucket, Key=key) + + def _key_for(self, file_url: str) -> str: + """Validate `file_url` and return the object key it maps to.""" + prefix = f"s3://{self._bucket}/" + if not file_url.startswith(prefix): + raise FileNotFoundError(file_url) + return file_url.removeprefix(prefix) + + +def create_file_storage() -> FileStorage: + """Storage backend for client uploads, discovered by the host alongside ``bot()``. + + The host calls this at startup, serves its upload endpoint from the + returned backend, and injects it into the bot as + ``runner_args.file_storage``. + """ + return S3FileStorage(bucket=os.environ["S3_UPLOADS_BUCKET"], region=os.getenv("AWS_REGION")) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + parser = argparse.ArgumentParser() + parser.add_argument( + "-llm", + "--llm", + choices=["anthropic", "openai", "bedrock", "gemini", "vertex"], + default="anthropic", + help="LLM provider to use (default: anthropic)", + ) + main(parser) diff --git a/file-upload/server/env.example b/file-upload/server/env.example new file mode 100644 index 00000000..74bb1610 --- /dev/null +++ b/file-upload/server/env.example @@ -0,0 +1,54 @@ +# file-upload - Environment Variables +# Copy this file to .env and fill in your API keys + +PIPECAT_UPLOADS_FOLDER=/tmp/pipecat-uploads + +# Cloud-bucket upload storage (only for the storage-backend variants) +# bot_gcs.py: bucket name; auth via Application Default Credentials +# (GOOGLE_APPLICATION_CREDENTIALS or `gcloud auth application-default login`) +# GCS_UPLOADS_BUCKET= +# bot_s3.py: bucket name; auth via the AWS_* variables below +# S3_UPLOADS_BUCKET= + +# Daily (Transport) +DAILY_API_KEY= +# Optional: leave empty to create a new room +# DAILY_ROOM_URL= + +# LiveKit (Transport; only needed when connecting with the LiveKit transport) +# LIVEKIT_URL= +# LIVEKIT_API_KEY= +# LIVEKIT_API_SECRET= + + + +# Cartesia (STT/TTS) +CARTESIA_API_KEY= +# Optional: Defaults to "71a7ad14-091c-4e8e-a314-022ece01c121" +# CARTESIA_VOICE_ID= + +# Deepgram (STT/TTS) +DEEPGRAM_API_KEY= + + +# LLM (select provider with `uv run bot.py -llm `; default: anthropic) + +# Anthropic Claude +ANTHROPIC_API_KEY= +ANTHROPIC_MODEL= + +# OpenAI +OPENAI_API_KEY= + +# AWS Bedrock +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION= + +# Google Gemini (developer API) +GOOGLE_API_KEY= + +# Google Gemini via Vertex AI (`-llm vertex`; auth via Application Default +# Credentials — `gcloud auth application-default login`) +# GOOGLE_CLOUD_PROJECT_ID= + diff --git a/file-upload/server/pcc-deploy.toml b/file-upload/server/pcc-deploy.toml new file mode 100644 index 00000000..77aed33f --- /dev/null +++ b/file-upload/server/pcc-deploy.toml @@ -0,0 +1,6 @@ +agent_name = "file-upload" +secret_set = "file-upload-secrets" +agent_profile = "agent-1x" + +[scaling] + min_agents = 1 \ No newline at end of file diff --git a/file-upload/server/pyproject.toml b/file-upload/server/pyproject.toml new file mode 100644 index 00000000..4582d124 --- /dev/null +++ b/file-upload/server/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "file-upload" +version = "0.1.0" +description = "Voice AI bot built with Pipecat" +requires-python = ">=3.11" +dependencies = [ + "pipecat-ai[anthropic,aws,cartesia,daily,deepgram,google,livekit,openai,runner,silero,websocket,webrtc]>=1.13.0", + "pipecatcloud", + # For bot_gcs.py (GCS-backed uploads); S3-backed uploads (bot_s3.py) use + # aiobotocore, which pipecat-ai[aws] already provides. + "google-cloud-storage>=2.18.0,<4", +] + +[dependency-groups] +dev = [ + "pyright>=1.1.404,<2", + "ruff>=0.12.11,<1", +] + +[tool.ruff] +line-length = 100 +[tool.ruff.lint] +select = ["I", "UP"]