"""Xiaozhi WebSocket handshake frame models (PoC). CON-004: the exact field set of the Xiaozhi ``hello`` frame is a firmware detail. These models capture the fields the plan names (Device-Id, Client-Id, Authorization, audio_params) plus the standard handshake fields. Every constant that must match the firmware is marked ``# FIRMWARE``. """ from __future__ import annotations import uuid from typing import Any, Optional from pydantic import BaseModel, Field class AudioParams(BaseModel): # FIRMWARE: Opus params the device reports/accepts. format: str = "opus" sample_rate: int = 16000 channels: int = 1 frame_duration: int = 20 # ms class Hello(BaseModel): """Device -> Bridge greeting (first text frame).""" type: str = "hello" device_id: str = "" client_id: Optional[str] = None authorization: Optional[str] = None # FIRMWARE: token field name protocol_version: int = 1 # FIRMWARE audio_params: AudioParams = Field(default_factory=AudioParams) class ServerHello(BaseModel): """Bridge -> Device acknowledgement, firmware v2.5.0-compatible. FIRMWARE: ParseServerHello requires ``transport == "websocket"`` and reads ``session_id`` + ``audio_params`` (sample_rate, frame_duration). """ type: str = "hello" transport: str = "websocket" session_id: str = "" audio_params: AudioParams = Field(default_factory=AudioParams) server_time: int = 0 def build_hello(payload: dict) -> Hello: """Parse a raw hello dict; tolerate firmware field-name drift. Tolerant parsing is intentional: firmware builds differ, and the PoC must connect, not reject, on a missing optional field. """ data: dict[str, Any] = dict(payload) data.setdefault("type", "hello") # Common firmware aliases. for src, dst in (("deviceId", "device_id"), ("Device-Id", "device_id")): if src in data and "device_id" not in data: data["device_id"] = data[src] if "audio" in data and "audio_params" not in data: data["audio_params"] = data.pop("audio") return Hello.model_validate(data) def build_server_hello(device_hello: Hello) -> ServerHello: """Firmware-compatible server hello (transport=websocket + session_id).""" import time return ServerHello( session_id=str(uuid.uuid4()), audio_params=device_hello.audio_params, server_time=int(time.time() * 1000), )