{"record":{"id":"3c06c3a9d4eef029","repo":"harry0703/MoneyPrinterTurbo","slug":"sonilo-stream-ended-before-completion","errorCode":null,"errorMessage":"Sonilo stream ended before completion","messagePattern":"Sonilo stream ended before completion","errorType":"exception","errorClass":"SoniloError","httpStatus":null,"severity":"error","filePath":"app/services/sonilo.py","lineNumber":265,"sourceCode":"                continue\n            encoded_chunk = event.get(\"data\") or event.get(\"audio\")\n            if not isinstance(encoded_chunk, str) or not encoded_chunk:\n                raise SoniloError(\"Sonilo returned an empty audio chunk\")\n            try:\n                chunk = base64.b64decode(encoded_chunk, validate=True)\n            except (binascii.Error, ValueError) as exc:\n                raise SoniloError(\"Sonilo returned an invalid audio chunk\") from exc\n            if not chunk:\n                raise SoniloError(\"Sonilo returned an empty audio chunk\")\n            total_bytes += len(chunk)\n            if total_bytes > MAX_GENERATED_AUDIO_BYTES:\n                raise SoniloError(\"Sonilo audio exceeds the 30 MB limit\")\n            output.write(chunk)\n        output.flush()\n        os.fsync(output.fileno())\n\n    if not completed:\n        raise SoniloError(\"Sonilo stream ended before completion\")\n    if total_bytes <= 0:\n        raise SoniloError(\"Sonilo returned no audio data\")\n    return total_bytes, title\n\n\ndef _request_bgm(video_path: str, output_path: str, prompt: str) -> str:\n    \"\"\"请求配乐并在完整协议及音频校验通过后原子保存。\"\"\"\n    output_dir = os.path.dirname(os.path.abspath(output_path))\n    os.makedirs(output_dir, exist_ok=True)\n    descriptor, temp_audio_path = tempfile.mkstemp(\n        prefix=\".sonilo-audio-\",\n        suffix=Path(output_path).suffix or \".m4a\",\n        dir=output_dir,\n    )\n    os.close(descriptor)\n    try:\n        logger.info(\n            f\"requesting Sonilo background music: video={video_path}, \"","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/sonilo.py#L247-L283","documentation":"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.","triggerScenarios":"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.","commonSituations":"Long generations exceeding an intermediary's idle or stream timeout (nginx proxy_read_timeout, ALB defaults); flaky networks; provider rolling restarts during generation.","solutions":["If a reverse proxy fronts the app or the Sonilo base URL, raise its streaming and idle timeouts above the worst generation time.","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).","Retry the request; an unterminated stream is usually transient.","If persistent, capture the tail of the stream (last event types) and check the provider status and changelog for terminal-event renames."],"exampleFix":"# nginx before\nlocation / { proxy_read_timeout 60s; }\n# after\nlocation / { proxy_read_timeout 1200s; proxy_buffering off; }","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    audio = request_bgm(video, out, prompt)\nexcept SoniloError as exc:\n    if \"stream ended before completion\" in str(exc):\n        # truncated stream: safe to retry the whole request;\n        # the library already discards the partial temp file\n        retry_with_backoff(request_bgm, attempts=2, base_delay=5)\n    raise","preventionTips":["Set all intermediary read and idle timeouts (nginx proxy_read_timeout, load-balancer idle timeout) above worst-case generation time.","Keep the sonilo_timeout config near the 1800-second ceiling for long generations; short timeouts guarantee this error."],"tags":["sonilo","streaming","truncation","timeout","network"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}