You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FFmpeg compositor rewritten: monolithic xfade → memory-bounded segment + concat (ffmpeg_composer.py). The single xfadefilter_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:
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;
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 pipeline — generate_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.