harry0703/MoneyPrinterTurbo · error · SoniloError

Sonilo returned malformed streaming data

Error message

Sonilo returned malformed streaming data

What it means

Raised by _parse_event when a single NDJSON line from the Sonilo streaming response cannot be decoded as UTF-8 or parsed as JSON. The parser is deliberately strict: truncated lines (network cut mid-chunk), BOMs, HTML error pages injected by a proxy, or garbage all fail here rather than being silently skipped.

Source

Thrown at app/services/sonilo.py:208

        _remove_file(proxy_path)
        detail = (result.stderr or "").strip().replace("\n", " ")[-500:]
        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:

View on GitHub (pinned to 1f9f19c202)

Solutions

  1. Capture the failing raw line (add temporary logging of raw_line before the _parse_event call) to see whether it is truncated JSON, HTML, or another framing.
  2. If it is truncated JSON from a flaky network, treat it as retryable: retry the generation request.
  3. If it is HTML from a proxy, bypass or fix the proxy for the streaming endpoint.
  4. If the provider changed framing (for example SSE data prefixes), strip the prefix or adapt _parse_event to the new protocol.

Example fix

# temporary diagnosis
event = _parse_event(raw_line)  # before
logger.debug(f"raw sonilo line: {raw_line[:200]!r}")
event = _parse_event(raw_line)  # after (remove once diagnosed)
Defensive patterns

Strategy: retry

Type guard

def looks_like_ndjson(line: str) -> bool:
    s = line.strip()
    return s.startswith("{") and s.endswith("}")

Try / catch

try:
    audio = request_bgm(video, out, prompt)
except SoniloError as exc:
    if "malformed streaming data" in str(exc):
        retry_with_backoff(request_bgm, attempts=2)  # truncation is transient
    raise

Prevention

When it happens

Trigger: The connection drops mid-line so iter_lines yields a truncated JSON fragment; an intercepting proxy returns an HTML 502 page on the streaming endpoint; the provider sends SSE-style 'data: {...}' framing instead of raw NDJSON; a response compressed or garbled by a misconfigured gateway.

Common situations: Unstable networks during long generations; corporate proxies rewriting streaming responses; provider deploys an API change that switches framing; chunked-encoding handling bugs in an intermediary.

Understand the failure class

Related errors


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