Files
esp32-server/app/main.py
kunthawat b00312cbe5 fix: load config.yaml on boot; issue real device token; live smoke test
- 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
2026-10-03 12:12:29 +07:00

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()