Xiaozhi WebSocket endpoint with handshake/auth, mock STT/TTS/Hermes backends, Thai chunker, barge-in queue, latency logger, GPU planner, voice-profile guard (reasoning=none, session_search only). CON-002: no GPU/audio libs loaded at import. MUST-NOT-001: all model names/tokens from config.
50 lines
1.3 KiB
Python
50 lines
1.3 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 = Config()
|
|
uvicorn.run(make_app(cfg), host=cfg.host, port=cfg.port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|