Skip to content

Banker's Wrapped v1.6.0

Choose a tag to compare

@github-actions github-actions released this 29 Jun 20:49

Changed

  • FFmpeg compositor rewritten: monolithic xfade → memory-bounded segment + concat (ffmpeg_composer.py). The single xfade filter_complex buffered every looped input's frames until its staggered transition offset — several GB at 1792×1024 — and the OOM-killer SIGKILL'd it (returncode -9, 0 frames) on memory-limited containers, even after capping threads. The new compositor:
    1. renders each scene to its own short MP4 segment — only one image is in RAM at a time, so peak memory is a single small encode (~300 MB) instead of GBs;
    2. concatenates the segments with the concat demuxer using -c:v copy (stream copy — no decode, near-zero memory) and muxes the narration.
      Peak memory dropped from several GB to ~300 MB; it now runs on any container size (validated end-to-end against real B2 assets via scripts/recompose.py).
  • Transitions: crossfade → dip-to-black. True crossfades require holding two scenes' frames simultaneously — the exact buffering that OOMs — so each segment now fades in from / out to black, giving a memory-free cinematic dip between scenes. Output stays browser-safe (yuv420p, +faststart, CFR 25 fps, AAC).
  • Per-scene duration = narration_length / N (float), so the slideshow covers the narration exactly (no -shortest trimming artefacts).
  • scripts/recompose.py comparison labels clarified (segments (new) vs old-xfade) to match the new compositor.

Fixed

  • Share links no longer expire (GET /recap/{id}) — the endpoint returned the presigned video/thumbnail URLs minted at generation time, which expire after b2_presigned_url_expiry (1 h), so any share page or notebook Scenario C opened later showed a dead video. Now it regenerates presigned URLs from the stored B2 keys on every request, so links stay valid indefinitely.
  • Session DB path is configurable (SESSION_DB_PATH env) so the SQLite store can live on a Railway persistent volume and survive redeploys (Railway's default filesystem is ephemeral — sessions were wiped on every deploy).
  • Demo notebook (DEMO_RUNBOOK.ipynb) refreshed for v1.6.0: parallel-image timings (~2–3 min, was ~4–5), correct artifact count (14 files / 10 types), and Scenario C durability notes.
  • Demo notebook run_pipeline now drives the async backend correctly — it expected a synchronous full response, but POST /generate returns 202 + {session_id} and runs in the background, so print_summary hit KeyError: 'insights'. It now POSTs, then polls GET /recap/{id} until ready (with a 429 rate-limit message and a timeout). Added matplotlib + ipykernel to the dev group so the notebook's charts and kernel actually work from a clean clone.
  • Event loop no longer blocked by synchronous I/O during the pipelinegenerate_scene_image ran the genblaze pipeline .run() and a sync httpx.Client fetch directly inside an async function, freezing the loop for the entire ~3–4 min image phase. That starved the SSE progress stream: the connection dropped and the frontend hung on "Writing narrative script" even though the backend completed. Fixes:
    • genblaze_client.generate_scene_image — blocking genblaze run + HTTP fetch moved into asyncio.to_thread. Side benefit: the 5 image generations now actually run in parallel via the existing asyncio.gather (image phase ~3.3 min → ~40–50 s; total pipeline ~5.4 min → ~2.5–3 min).
    • MediaAgent — all 13 synchronous boto3 B2 calls (upload_bytes/upload_json/presigned_url) offloaded via small _b2_* to_thread helpers.
    • GET /recap/{id}/download — the blocking B2-download + ZIP build offloaded via asyncio.to_thread, so a ZIP download no longer stalls every other request.
    • Left synchronous intentionally: ffprobe (sub-ms local read), SQLite session store (sub-ms local), key-builders / pure logic / tests.