- main() previously used Config() defaults, silently ignoring config.yaml (port + token); now loads config.yaml from repo root (BRIDGE_CONFIG override) - config.yaml: port 8766 (8765 taken on this host), real sha256 token for xiaozhi-main - smoke_live.py: live WS verification (health, auth rejection x2, handshake, full turn) - README: correct run command (python -m app.main) - HANDOFF/plan: Phase 1.5 live-verified evidence
63 lines
1.7 KiB
Python
63 lines
1.7 KiB
Python
"""Hermes-Xiaozhi Bridge — FastAPI app (REQ-001, REQ-012).
|
|
|
|
Binds LAN/localhost only (REQ-012); the Cloudflare tunnel is external. The
|
|
single device-facing endpoint delegates the whole session to
|
|
:func:`xiaozhi.websocket.DeviceLoop`.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import FastAPI, WebSocket
|
|
|
|
from .config import Config
|
|
from .hermes import VoiceProfile
|
|
from .security import DeviceAuth
|
|
from .xiaozhi.websocket import DeviceLoop
|
|
|
|
|
|
def make_app(cfg: Config | None = None) -> FastAPI:
|
|
cfg = cfg or Config()
|
|
app = FastAPI(title="hermes-xiaozhi-bridge")
|
|
app.state.cfg = cfg
|
|
|
|
# AC-004: assert the voice contract at startup, not mid-conversation.
|
|
profile = VoiceProfile(cfg.hermes.reasoning, cfg.hermes.tools)
|
|
auth = DeviceAuth(
|
|
{d.device_id: d.token_hash for d in cfg.security.devices},
|
|
enabled=cfg.security.enabled,
|
|
)
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict:
|
|
return {"status": "ok", "service": "hermes-xiaozhi-bridge"}
|
|
|
|
@app.websocket(cfg.ws_path)
|
|
async def xiaozhi_endpoint(ws: WebSocket) -> None:
|
|
loop = DeviceLoop(cfg, profile, auth)
|
|
await loop.run(ws)
|
|
|
|
return app
|
|
|
|
|
|
def main() -> None:
|
|
import uvicorn
|
|
|
|
cfg = _load_config()
|
|
uvicorn.run(make_app(cfg), host=cfg.host, port=cfg.port)
|
|
|
|
|
|
def _load_config() -> Config:
|
|
"""Load config.yaml from the repo root; fall back to all-mock defaults."""
|
|
import os
|
|
|
|
path = os.environ.get(
|
|
"BRIDGE_CONFIG",
|
|
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.yaml"),
|
|
)
|
|
if os.path.exists(path):
|
|
return Config.from_file(path)
|
|
return Config()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|