harry0703/MoneyPrinterTurbo · error · SoniloError

Sonilo stream ended before completion

Error message

Sonilo stream ended before completion

What it means

Raised when the streaming response ends (iter_lines exhausted, connection closed) without a complete event ever arriving. Per the protocol, audio chunks are only publishable after the provider signals completion; an unterminated stream is treated as a truncated generation and the temp file is never promoted to the output path.

Source

Thrown at app/services/sonilo.py:265

                continue
            encoded_chunk = event.get("data") or event.get("audio")
            if not isinstance(encoded_chunk, str) or not encoded_chunk:
                raise SoniloError("Sonilo returned an empty audio chunk")
            try:
                chunk = base64.b64decode(encoded_chunk, validate=True)
            except (binascii.Error, ValueError) as exc:
                raise SoniloError("Sonilo returned an invalid audio chunk") from exc
            if not chunk:
                raise SoniloError("Sonilo returned an empty audio chunk")
            total_bytes += len(chunk)
            if total_bytes > MAX_GENERATED_AUDIO_BYTES:
                raise SoniloError("Sonilo audio exceeds the 30 MB limit")
            output.write(chunk)
        output.flush()
        os.fsync(output.fileno())

    if not completed:
        raise SoniloError("Sonilo stream ended before completion")
    if total_bytes <= 0:
        raise SoniloError("Sonilo returned no audio data")
    return total_bytes, title


def _request_bgm(video_path: str, output_path: str, prompt: str) -> str:
    """请求配乐并在完整协议及音频校验通过后原子保存。"""
    output_dir = os.path.dirname(os.path.abspath(output_path))
    os.makedirs(output_dir, exist_ok=True)
    descriptor, temp_audio_path = tempfile.mkstemp(
        prefix=".sonilo-audio-",
        suffix=Path(output_path).suffix or ".m4a",
        dir=output_dir,
    )
    os.close(descriptor)
    try:
        logger.info(
            f"requesting Sonilo background music: video={video_path}, "

View on GitHub (pinned to 1f9f19c202)

Solutions

  1. If a reverse proxy fronts the app or the Sonilo base URL, raise its streaming and idle timeouts above the worst generation time.
  2. Check the sonilo_timeout config: the read timeout caps how long the stream may stall; ensure it exceeds the real generation length (capped at 1800 seconds in _request_timeout).
  3. Retry the request; an unterminated stream is usually transient.
  4. If persistent, capture the tail of the stream (last event types) and check the provider status and changelog for terminal-event renames.

Example fix

# nginx before
location / { proxy_read_timeout 60s; }
# after
location / { proxy_read_timeout 1200s; proxy_buffering off; }
Defensive patterns

Strategy: retry

Try / catch

try:
    audio = request_bgm(video, out, prompt)
except SoniloError as exc:
    if "stream ended before completion" in str(exc):
        # truncated stream: safe to retry the whole request;
        # the library already discards the partial temp file
        retry_with_backoff(request_bgm, attempts=2, base_delay=5)
    raise

Prevention

When it happens

Trigger: The server closes the connection mid-generation (idle timeout, crash, load-balancer cut); the network drops between the last audio_chunk and the terminal complete event; the provider sends a different terminal event name (for example done) after an API change; generation is aborted server-side without an error event.

Common situations: Long generations exceeding an intermediary's idle or stream timeout (nginx proxy_read_timeout, ALB defaults); flaky networks; provider rolling restarts during generation.

Related errors


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