demo mount to fastapi
This commit is contained in:
15
README.md
15
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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://`
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user