diff --git a/README.md b/README.md index aa40419..9efff83 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ uv venv .venv source .venv/bin/activate uv pip install -r requirements.txt 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`. @@ -34,12 +34,21 @@ Or pass keys directly in `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 Pipecat-native endpoint: ```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`. @@ -54,7 +63,7 @@ Important defaults: Product endpoint: ```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. diff --git a/config.json b/config.json index 4a6ff7b..c14ab4e 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,10 @@ { "server": { "host": "0.0.0.0", - "port": 8001, - "cors_origins": ["http://localhost:3000", "http://localhost:8080"] + "port": 8000, + "cors_origins": ["http://localhost:3000", "http://localhost:8080"], + "serve_webpage": true, + "webpage_mount": "/demo" }, "audio": { "sample_rate_hz": 16000, diff --git a/config/openai.example.json b/config/openai.example.json index bdc5548..23207f7 100644 --- a/config/openai.example.json +++ b/config/openai.example.json @@ -1,7 +1,7 @@ { "server": { "host": "0.0.0.0", - "port": 8001, + "port": 8000, "cors_origins": ["*"] }, "audio": { diff --git a/config/siliconflow.json b/config/siliconflow.json index 4fe18a1..d7ff7cd 100644 --- a/config/siliconflow.json +++ b/config/siliconflow.json @@ -1,7 +1,7 @@ { "server": { "host": "0.0.0.0", - "port": 8001, + "port": 8000, "cors_origins": ["*"] }, "audio": { diff --git a/config/xfyun.json b/config/xfyun.json index 6494bba..2c37c8c 100644 --- a/config/xfyun.json +++ b/config/xfyun.json @@ -1,7 +1,7 @@ { "server": { "host": "0.0.0.0", - "port": 8001, + "port": 8000, "cors_origins": ["*"] }, "audio": { diff --git a/engine/config.py b/engine/config.py index 913d103..26b7f23 100644 --- a/engine/config.py +++ b/engine/config.py @@ -10,6 +10,8 @@ class ServerConfig: host: str = "0.0.0.0" port: int = 8000 cors_origins: list[str] = field(default_factory=list) + serve_webpage: bool = True + webpage_mount: str = "/demo" @dataclass(frozen=True) diff --git a/engine/main.py b/engine/main.py index b37064b..64180f0 100644 --- a/engine/main.py +++ b/engine/main.py @@ -2,19 +2,30 @@ from __future__ import annotations import argparse from functools import lru_cache +from pathlib import Path from fastapi import FastAPI, WebSocket from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles from .config import EngineConfig, load_config from .pipeline import run_product_voice_pipeline, run_voice_pipeline +WEBPAGE_DIR = Path(__file__).resolve().parent.parent / "examples" / "webpage" + @lru_cache(maxsize=8) def get_config(path: str = "config.json") -> EngineConfig: 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: config = get_config(config_path) 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=["*"], ) + webpage_mount = ( + _normalize_mount_path(config.server.webpage_mount) + if config.server.serve_webpage + else None + ) + @app.get("/health") async def health() -> dict[str, object]: return { @@ -40,6 +57,7 @@ def create_app(config_path: str = "config.json") -> FastAPI: "product_text_input": True, "product_text_interrupt": True, }, + "demo": webpage_mount, "llm_provider": config.services.llm.provider, "stt_provider": config.services.stt.provider, "tts_provider": config.services.tts.provider, @@ -55,6 +73,13 @@ def create_app(config_path: str = "config.json") -> FastAPI: await websocket.accept() 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 diff --git a/examples/webpage/README.md b/examples/webpage/README.md index 239a09a..8832eb8 100644 --- a/examples/webpage/README.md +++ b/examples/webpage/README.md @@ -39,29 +39,48 @@ examples/webpage/ ## Run -1. Start the engine (default port `8001`): +1. Start the engine (default port `8000`): ```bash cd AI-VideoAssistant-engine-v5-pipecat-minimal source .venv/bin/activate 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 - engine's CORS allow-list (see `config.json`). The default config - allows `http://localhost:8080`: +2. Open the demo page served by the same process: - ```bash - cd AI-VideoAssistant-engine-v5-pipecat-minimal/examples/webpage - python -m http.server 8080 + ```text + http://127.0.0.1:8000/demo/ ``` -3. Open in Chrome, Edge, or Safari. - - Click **Connect** (uses `ws://127.0.0.1:8001/ws-product` by default). - - Pick a microphone if needed, click **Enable mic**, and start - speaking. The browser will prompt for microphone access on first - use. Device names may appear only after permission is granted. + The default websocket URL is derived from the page host + (`ws://127.0.0.1:8000/ws-product`). Click **Connect**, pick a + microphone if needed, click **Enable mic**, and start speaking. + + 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 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` > qualifies; if you serve from another host, use HTTPS and a `wss://` diff --git a/examples/webpage/app.js b/examples/webpage/app.js index 1fba1e1..a41e216 100644 --- a/examples/webpage/app.js +++ b/examples/webpage/app.js @@ -18,6 +18,11 @@ const PROTOCOL = "va.ws.v1"; const MAX_WS_LOG_LINES = 120; const AUDIO_DELTA_LOG_INTERVAL_MS = 1000; +function defaultWsUrl() { + const scheme = location.protocol === "https:" ? "wss:" : "ws:"; + return `${scheme}//${location.host}/ws-product`; +} + const els = { url: document.getElementById("ws-url"), connectBtn: document.getElementById("connect-btn"), @@ -885,6 +890,8 @@ window.addEventListener("beforeunload", () => { } }); +els.url.value = defaultWsUrl(); + setStatus("idle", "Disconnected"); setConnectButton(); setMicButton(); diff --git a/examples/webpage/index.html b/examples/webpage/index.html index 925f326..deef69e 100644 --- a/examples/webpage/index.html +++ b/examples/webpage/index.html @@ -20,7 +20,7 @@