harry0703/MoneyPrinterTurbo · error · SoniloError

Sonilo returned an invalid streaming event

Error message

Sonilo returned an invalid streaming event

What it means

Raised by _parse_event when a line parses as valid JSON but is not a JSON object, or lacks a string 'type' field. Every Sonilo streaming event is required to carry a string discriminator (error, title, complete, audio_chunk); a top-level array, a bare string or number, or an event with a missing or non-string type is treated as a protocol violation.

Solutions

  1. Log the offending parsed value (temporarily log the event before the isinstance checks) to see its actual shape.
  2. Check the Sonilo changelog and docs for a streaming schema change (for example type renamed) and update the checks in _parse_event accordingly.
  3. Fix test fixtures to include a string type field on every streamed line.
  4. If a keep-alive or comment line is the cause, skip it explicitly by exact match instead of loosening the object check.
Defensive patterns

Strategy: type-guard

Type guard

def is_valid_sonilo_event(event: object) -> bool:
    return isinstance(event, dict) and isinstance(event.get("type"), str)

Try / catch

try:
    event = _parse_event(raw_line)
except SoniloError as exc:
    if "invalid streaming event" in str(exc):
        logger.warning(f"protocol drift, raw line: {raw_line[:200]!r}")
        raise  # strict policy: never skip unknown shapes silently
    raise

Prevention

When it happens

Trigger: The provider sends bare payloads such as the string ok or a JSON array on the stream; an event object where type is null or numeric; a partial JSON document that happens to parse to a non-dict; an API version change renaming type to event or kind.

Common situations: Provider protocol evolution; test harnesses streaming fixtures without the type field; middleware injecting keep-alive JSON scalars between events.

Related errors


AI-assisted analysis of harry0703/MoneyPrinterTurbo@1f9f19c202 (2026-08-14). Data as JSON: /api/errors/edad1fe92d09c255. Report an issue: GitHub.

Appendix: source

Thrown at app/services/sonilo.py:210

        raise SoniloError(f"failed to generate Sonilo video proxy: {detail}")
    proxy_size = os.path.getsize(proxy_path) if os.path.isfile(proxy_path) else 0
    if proxy_size <= 0 or proxy_size > MAX_PROXY_BYTES:
        _remove_file(proxy_path)
        raise SoniloError("Sonilo video proxy is empty or exceeds the 300 MB limit")
    logger.info(
        f"Sonilo video proxy prepared: source={video_path}, size={proxy_size} bytes"
    )
    return proxy_path


def _parse_event(raw_line: bytes) -> dict[str, Any]:
    """严格解析单条 NDJSON,禁止静默忽略截断或非对象响应。"""
    try:
        event = json.loads(raw_line.decode("utf-8"))
    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise SoniloError("Sonilo returned malformed streaming data") from exc
    if not isinstance(event, dict) or not isinstance(event.get("type"), str):
        raise SoniloError("Sonilo returned an invalid streaming event")
    return event


def _stream_audio(response: requests.Response, temp_audio_path: str) -> tuple[int, str]:
    """
    把第一条配乐流按事件顺序写入临时文件,并限制最大体积。

    API 可能同时返回多条候选流;当前产品只需要一条 BGM,所以固定选择
    stream_index=0。只有收到 complete 事件并通过 FFmpeg 完整解码后才会发布。
    """
    total_bytes = 0
    title = ""
    completed = False
    with open(temp_audio_path, "wb") as output:
        for raw_line in response.iter_lines():
            if not raw_line:
                continue
            event = _parse_event(raw_line)

View on GitHub (pinned to 1f9f19c202)