import json from collections.abc import Iterable, Iterator from typing import Any VALID_TYPES = { "stage", "voice", "skill", "text_delta", "sentence_end", "vision_note", "audio", "debug", "done", "error", } def normalize_event(event: dict[str, Any]) -> dict[str, Any]: event_type = event.get("type") if event_type not in VALID_TYPES: return {"type": "debug", "message": f"unknown event type: {event_type}", "raw": event} if event_type == "stage": return { "type": "stage", "expression": event.get("expression", "idle"), "motion": event.get("motion", "breathe"), "intensity": _clamp(event.get("intensity", 0.5), 0.0, 1.0), } if event_type == "voice": return { "type": "voice", "style": event.get("style", "neutral"), "speed": _clamp(event.get("speed", 1.0), 0.6, 1.4), "energy": _clamp(event.get("energy", 0.5), 0.0, 1.0), } if event_type == "text_delta": return {"type": "text_delta", "text": str(event.get("text", ""))} return event def parse_sse_lines(lines: Iterable[str]) -> Iterator[dict[str, Any]]: for line in lines: if not line: continue if line.startswith("data:"): line = line[5:].strip() if line == "[DONE]": yield {"type": "done"} continue try: parsed = json.loads(line) except json.JSONDecodeError: yield {"type": "debug", "message": "invalid json event", "raw": line} continue yield normalize_event(parsed) def dumps_event(event: dict[str, Any]) -> str: return json.dumps(normalize_event(event), ensure_ascii=False) def _clamp(value: Any, low: float, high: float) -> float: try: number = float(value) except (TypeError, ValueError): number = (low + high) / 2 return max(low, min(high, number))