virtual-characters / src /stream_protocol.py
ShadowInk's picture
Upload complete Space runtime files
6bcddd0 verified
Raw
History Blame Contribute Delete
2 kB
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))