demo mount to fastapi

This commit is contained in:
Xin Wang
2026-05-22 10:23:17 +08:00
parent 0d82acc0d2
commit 2c7f0f440b
10 changed files with 86 additions and 22 deletions

View File

@@ -25,7 +25,7 @@ uv venv .venv
source .venv/bin/activate source .venv/bin/activate
uv pip install -r requirements.txt uv pip install -r requirements.txt
export OPENAI_API_KEY=... export OPENAI_API_KEY=...
uv run uvicorn engine.main:app --reload --host 0.0.0.0 --port 8001 uv run uvicorn engine.main:app --reload --host 0.0.0.0 --port 8000
``` ```
Or pass keys directly in `config.json`. Or pass keys directly in `config.json`.
@@ -34,12 +34,21 @@ Or pass keys directly in `config.json`.
uv run python -m engine.main --config ./config.json uv run python -m engine.main --config ./config.json
``` ```
Browser demo (served from the same process when `server.serve_webpage` is
true in `config.json`):
```text
http://localhost:8000/demo/
```
See `examples/webpage/README.md` for details.
## Protocols ## Protocols
Pipecat-native endpoint: Pipecat-native endpoint:
```text ```text
ws://localhost:8001/ws ws://localhost:8000/ws
``` ```
The websocket payloads are Pipecat protobuf frames. A client should use Pipecat's websocket client/serializer stack or generate frames compatible with `pipecat.frames.protobufs.frames_pb2`. The websocket payloads are Pipecat protobuf frames. A client should use Pipecat's websocket client/serializer stack or generate frames compatible with `pipecat.frames.protobufs.frames_pb2`.
@@ -54,7 +63,7 @@ Important defaults:
Product endpoint: Product endpoint:
```text ```text
ws://localhost:8001/ws-product ws://localhost:8000/ws-product
``` ```
This endpoint uses a stable JSON/base64 protocol named `va.ws.v1`. It is meant for browser, mobile, or other product applications that should not depend on Pipecat's internal protobuf frame schema. This endpoint uses a stable JSON/base64 protocol named `va.ws.v1`. It is meant for browser, mobile, or other product applications that should not depend on Pipecat's internal protobuf frame schema.

View File

@@ -1,8 +1,10 @@
{ {
"server": { "server": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 8001, "port": 8000,
"cors_origins": ["http://localhost:3000", "http://localhost:8080"] "cors_origins": ["http://localhost:3000", "http://localhost:8080"],
"serve_webpage": true,
"webpage_mount": "/demo"
}, },
"audio": { "audio": {
"sample_rate_hz": 16000, "sample_rate_hz": 16000,

View File

@@ -1,7 +1,7 @@
{ {
"server": { "server": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 8001, "port": 8000,
"cors_origins": ["*"] "cors_origins": ["*"]
}, },
"audio": { "audio": {

View File

@@ -1,7 +1,7 @@
{ {
"server": { "server": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 8001, "port": 8000,
"cors_origins": ["*"] "cors_origins": ["*"]
}, },
"audio": { "audio": {

View File

@@ -1,7 +1,7 @@
{ {
"server": { "server": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 8001, "port": 8000,
"cors_origins": ["*"] "cors_origins": ["*"]
}, },
"audio": { "audio": {

View File

@@ -10,6 +10,8 @@ class ServerConfig:
host: str = "0.0.0.0" host: str = "0.0.0.0"
port: int = 8000 port: int = 8000
cors_origins: list[str] = field(default_factory=list) cors_origins: list[str] = field(default_factory=list)
serve_webpage: bool = True
webpage_mount: str = "/demo"
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -2,19 +2,30 @@ from __future__ import annotations
import argparse import argparse
from functools import lru_cache from functools import lru_cache
from pathlib import Path
from fastapi import FastAPI, WebSocket from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from .config import EngineConfig, load_config from .config import EngineConfig, load_config
from .pipeline import run_product_voice_pipeline, run_voice_pipeline from .pipeline import run_product_voice_pipeline, run_voice_pipeline
WEBPAGE_DIR = Path(__file__).resolve().parent.parent / "examples" / "webpage"
@lru_cache(maxsize=8) @lru_cache(maxsize=8)
def get_config(path: str = "config.json") -> EngineConfig: def get_config(path: str = "config.json") -> EngineConfig:
return load_config(path) return load_config(path)
def _normalize_mount_path(path: str) -> str:
normalized = path.strip() or "/demo"
if not normalized.startswith("/"):
normalized = f"/{normalized}"
return normalized.rstrip("/") or "/"
def create_app(config_path: str = "config.json") -> FastAPI: def create_app(config_path: str = "config.json") -> FastAPI:
config = get_config(config_path) config = get_config(config_path)
app = FastAPI(title="AI VideoAssistant Engine v5 Pipecat Minimal", version="0.1.0") app = FastAPI(title="AI VideoAssistant Engine v5 Pipecat Minimal", version="0.1.0")
@@ -28,6 +39,12 @@ def create_app(config_path: str = "config.json") -> FastAPI:
allow_headers=["*"], allow_headers=["*"],
) )
webpage_mount = (
_normalize_mount_path(config.server.webpage_mount)
if config.server.serve_webpage
else None
)
@app.get("/health") @app.get("/health")
async def health() -> dict[str, object]: async def health() -> dict[str, object]:
return { return {
@@ -40,6 +57,7 @@ def create_app(config_path: str = "config.json") -> FastAPI:
"product_text_input": True, "product_text_input": True,
"product_text_interrupt": True, "product_text_interrupt": True,
}, },
"demo": webpage_mount,
"llm_provider": config.services.llm.provider, "llm_provider": config.services.llm.provider,
"stt_provider": config.services.stt.provider, "stt_provider": config.services.stt.provider,
"tts_provider": config.services.tts.provider, "tts_provider": config.services.tts.provider,
@@ -55,6 +73,13 @@ def create_app(config_path: str = "config.json") -> FastAPI:
await websocket.accept() await websocket.accept()
await run_product_voice_pipeline(websocket, config) await run_product_voice_pipeline(websocket, config)
if config.server.serve_webpage and WEBPAGE_DIR.is_dir() and webpage_mount:
app.mount(
webpage_mount,
StaticFiles(directory=str(WEBPAGE_DIR), html=True),
name="webpage",
)
return app return app

View File

@@ -39,29 +39,48 @@ examples/webpage/
## Run ## Run
1. Start the engine (default port `8001`): 1. Start the engine (default port `8000`):
```bash ```bash
cd AI-VideoAssistant-engine-v5-pipecat-minimal cd AI-VideoAssistant-engine-v5-pipecat-minimal
source .venv/bin/activate source .venv/bin/activate
export OPENAI_API_KEY=... export OPENAI_API_KEY=...
uvicorn engine.main:app --host 127.0.0.1 --port 8001 uvicorn engine.main:app --host 127.0.0.1 --port 8000
``` ```
2. In another terminal, serve the page from a port that's on the 2. Open the demo page served by the same process:
engine's CORS allow-list (see `config.json`). The default config
allows `http://localhost:8080`:
```bash ```text
cd AI-VideoAssistant-engine-v5-pipecat-minimal/examples/webpage http://127.0.0.1:8000/demo/
python -m http.server 8080
``` ```
3. Open <http://localhost:8080> in Chrome, Edge, or Safari. The default websocket URL is derived from the page host
- Click **Connect** (uses `ws://127.0.0.1:8001/ws-product` by default). (`ws://127.0.0.1:8000/ws-product`). Click **Connect**, pick a
- Pick a microphone if needed, click **Enable mic**, and start microphone if needed, click **Enable mic**, and start speaking.
speaking. The browser will prompt for microphone access on first
use. Device names may appear only after permission is granted. Mount path and on/off are controlled in `config.json`:
```json
"server": {
"serve_webpage": true,
"webpage_mount": "/demo"
}
```
Set `"serve_webpage": false` in production if you serve the UI elsewhere.
### Standalone static server (optional)
You can still serve the files from another port for UI-only iteration.
Add that origin to `server.cors_origins` in `config.json` if needed:
```bash
cd AI-VideoAssistant-engine-v5-pipecat-minimal/examples/webpage
python -m http.server 8080
```
Then open <http://localhost:8080> and point the URL field at
`ws://127.0.0.1:8000/ws-product`.
> The browser's mic API requires a secure context. `http://localhost` > The browser's mic API requires a secure context. `http://localhost`
> qualifies; if you serve from another host, use HTTPS and a `wss://` > qualifies; if you serve from another host, use HTTPS and a `wss://`

View File

@@ -18,6 +18,11 @@ const PROTOCOL = "va.ws.v1";
const MAX_WS_LOG_LINES = 120; const MAX_WS_LOG_LINES = 120;
const AUDIO_DELTA_LOG_INTERVAL_MS = 1000; const AUDIO_DELTA_LOG_INTERVAL_MS = 1000;
function defaultWsUrl() {
const scheme = location.protocol === "https:" ? "wss:" : "ws:";
return `${scheme}//${location.host}/ws-product`;
}
const els = { const els = {
url: document.getElementById("ws-url"), url: document.getElementById("ws-url"),
connectBtn: document.getElementById("connect-btn"), connectBtn: document.getElementById("connect-btn"),
@@ -885,6 +890,8 @@ window.addEventListener("beforeunload", () => {
} }
}); });
els.url.value = defaultWsUrl();
setStatus("idle", "Disconnected"); setStatus("idle", "Disconnected");
setConnectButton(); setConnectButton();
setMicButton(); setMicButton();

View File

@@ -20,7 +20,7 @@
<input <input
id="ws-url" id="ws-url"
type="text" type="text"
value="ws://127.0.0.1:8001/ws-product" placeholder="ws://host/ws-product"
spellcheck="false" spellcheck="false"
autocomplete="off" autocomplete="off"
/> />