Releases: iarjunganesh/bankers-wrapped
Releases · iarjunganesh/bankers-wrapped
Release list
Banker's Wrapped v1.9.0
Maintenance + release-hardening pass ahead of submission. No runtime behavior change.
Changed
- Dependencies refreshed (
uv lock --upgrade): uvicorn 0.51, fastapi 0.139, openai 2.45,
boto3 1.43.46, mypy 2.2, plus patch-level transitives. No major bumps; the Genblaze
subpackages (genblaze-core0.3.4,genblaze-s30.3.4,genblaze-gmicloud0.3.2) were
already the newest published — the v0.4.0 release wave. - Coverage 97.8% → 99.6% (134 → 148 tests): analytics achievements + ACHIEVER path,
invalid-category fallback, narrative prompt/NIM/0-scene branches, and recap
413 / pipeline-failure / malformed-insights / empty-artifacts / progress-callback paths. - Coverage claim synced 98% → 99% across README, CLAUDE.md, and submission docs;
regeneratedvo_08-production+vo_full-referencedemo voiceover clips to match.
Fixed
config.openai_modeldefault was a stalegpt-4o— it is a fallbackmodels_used.llm
label only, now set to the real chat model and documented as such.- Clarified that
config.gmi_chat_modeldefaults to NIM's free dev tier on purpose
(prod overrides viaGMI_CHAT_MODEL); added an ADR-011 cross-reference in ADR-003. - Accuracy: corrected Semantic Kernel references (README badge/diagram/tech-stack,
ADR-002,base.pydocstring, scaffold changelog line) — it was never a dependency or
used; agents are a hand-rolled typed asyncBaseAgentpipeline, now documented as such.
Banker's Wrapped v1.8.1
The first live post-flip run exposed that WS-1's chat path could never reach NVIDIA NIM: the
Genblaze chat wrapper always posts to GMI's endpoint, so nvidia-nim/...-prefixed ids (and the
documented "NIM fallback") 404'd — only mocked tests exercised the route. Found before any
credits were burned (the 404 bills nothing).
Fixed
- Real NVIDIA NIM routing (
GenblazeClient.generate_script_text):nvidia-nim/-prefixed
model ids are stripped and redirected to NIM's OpenAI-compatible endpoint via the SDK chat
wrapper'sbase_urloverride (newnvidia_nim_api_key/nvidia_nim_base_urlclient params).
Provider provenance is derived from the route actually taken, not the model-id prefix. - Automatic NIM fallback now exists (
NarrativeAgent._try_genblaze): after the primary
model exhausts its attempts (provider failure or invalid JSON), one final SDK-routed attempt
runs against NVIDIA NIM — previously claimed in docs but not implemented. GMI_CHAT_MODELdefault was invalid: GMI's catalog serves nometa-llama/*models
(verified viaGET /v1/models). Production model is nowopenai/gpt-5.4-mini
($0.75/$4.50 per M tokens ≈ $0.005/run); both routes live-validated with 1-token probes.llm.cost_usdingeneration.jsonis now computed from GMI's published per-token pricing
when the SDK reports none (the SDK intentionally returnscost_usd=None); NIM runs record $0.
Changed
- 5 new tests (NIM-fallback success/exhaustion in NarrativeAgent; chat routing +
cost computation in GenblazeClient); coverage 97.8%. - Docs sweep: README (pipeline diagram, timing table, tech stack,
models_usedexample,
release badge →releases/latest), DEVPOST providers table, COSTS per-run table,
CLAUDE.md,.env.example— all reflect GMIopenai/gpt-5.4-miniprimary + NIM fallback.
Banker's Wrapped v1.8.0
All five v1.7.0-roadmap workstreams shipped (the v1.7.0 tag marks the roadmap itself; this release is the implementation). Coverage 98% (gate 80%), 135 tests.
Added
- WS-2 — B2 as source of truth (ADR-008): sessions now survive Railway redeploys.
session_metadata.jsonis a self-contained manifest — fullInsightsSummarysnapshot,status,processing_time_ms,models_used, and every B2 key (including its own); uploaded last so itsb2_keysmap is complete.- Flat
index/{session_id}.jsonobject written to B2 at pipeline start mapssession_id → user_id(share URLs stay clean). GET /recap/{id}andGET /recap/{id}/downloadfall back to the B2 manifest when the SQLite row is missing; presigned URLs are re-minted per request. SQLite is now a cache (ADR-004 amended).B2Client: newdownload_json,exists,session_index_keyhelpers.
- WS-3 — B2 lifecycle + artifact integrity (ADR-009):
generation.jsonnow carries anartifactslist — SHA-256 + size for each of the 12 content artifacts uploaded per session, computed from the exact bytes stored.- Bucket retention rule committed as
infra/b2-lifecycle.json(45-day expiry — outlives the Aug 5–11 judging window for all hackathon sessions) and applied idempotently byscripts/apply_b2_lifecycle.py.
- WS-1 — Genblaze LLM routing (ADR-007): narrative script generation now runs through Genblaze SDK chat with provider-prefixed backend models, making 3 of 4 AI steps Genblaze-orchestrated.
GenblazeClient.generate_script_text()— same non-blocking (asyncio.to_thread) + tenacity-retry pattern as image gen; returns text + provenance (provider, model, latency, retries, tokens,cost_usd).NarrativeAgentis SDK-only for script generation (no direct provider path in-agent).NARRATIVE_PROVIDERselects backend model mode via SDK (genblazemodel id vsnvidia-nim/<model>), with schema retry on invalid JSON.generation.jsoncarriesllmprovenance andmodels_used.llmreflects the backend model used.- Default narrative provider mode is now
genblaze, withGMI_CHAT_MODELdefaulting tonvidia-nim/meta/llama-3.1-70b-instructfor NVIDIA NIM via SDK.
- WS-4 — Plaid sandbox connector (ADR-010): optional "Connect a bank (sandbox)" ingestion path.
backend/ingest/plaid_connector.py: Plaid REST via httpx (async; noplaid-pythondep) — link token, public-token exchange, paginated/transactions/get, category mapping to our taxonomy, sign-convention flip (Plaid outflow-positive → ours income-positive).- Plaid transactions are serialised back to our CSV schema and fed to the unchanged pipeline — identical B2 artifact trail,
DocumentAgentrevalidates everything. POST /api/v1/plaid/link-token+POST /api/v1/plaid/exchange(rate-limited 5/hr/IP, same SSE progress). Feature-flagged: withoutPLAID_CLIENT_ID/PLAID_SECRETthe routes answer 404 and the app is unchanged;/healthnow reportsplaid_enabled.- Frontend: "🏦 Connect a bank (sandbox)" button (Plaid Link via CDN, loaded on demand), shown only when the backend reports Plaid enabled.
- Test hermeticity fix:
test_settingsnow explicitly blanks Plaid keys so a developer's real.envcan never trigger live calls in the suite.
- WS-5 — hardening + polish (prompt 16):
- ADR-011: the v1.6.0 compositor + non-blocking-loop redesign recorded as a decision record.
tests/load/k6_smoke.js: manual k6 smoke test — /health p95 < 500 ms under 20 VUs; rate limiter must 429 (never 5xx) under /generate abuse.- Narrative prompt: Scene 4 advice must name the actual top spending category and cite a concrete number from the data (no generic advice).
Fixed
- Removed aspirational PostgreSQL claims (no
DATABASE_URLcode path exists): B2 is the source of truth, SQLite is a cache, Postgres stays a documented scale path only. - B2 lifecycle rule shape: the S3-compatible API requires the
Daysrule paired with anExpiredObjectDeleteMarker_markerrule (same prefix). /healthnow reports the real app version (was hardcoded1.0.0) andplaid_enabled.
Changed
- Coverage 93% → 98% (135 tests): SSE progress stream, B2 helper, Plaid HTTP/pagination/CSV-quoting, ffprobe, provider-init, and B2-fallback edge cases now covered.
Banker's Wrapped v1.7.0
Full plan: docs/ROADMAP-v1.7.0.md. Goal: lift every judging criterion
toward >9.5. Decision records ADR-007–010; build specs prompts 12–16.
Planned
- WS-1 — Genblaze as sole AI layer (ADR-007) — route the narrative LLM through Genblaze (GMI chat/reasoning) so 3 of 4 AI steps go through it; optional Genblaze video scene. Needs GMI credit top-up.
- WS-2 — B2 as source of truth (ADR-008) — self-contained session manifest on B2 +
get_recapfallback, so share links survive Railway redeploys (fixes the ephemeral-SQLite gap). No credits. - WS-3 — B2 lifecycle + integrity (ADR-009) — retention rule + SHA-256 per artifact in
generation.json. No credits. - WS-4 — Plaid sandbox connector (ADR-010) — optional "connect a bank" ingestion path; CSV stays default. No credits.
- WS-5 — Submission polish + hardening++ (prompt 16) — 7th ADR, k6 load test, security review, CI badge, honest Postgres claim, demo assets, utility signal.
Banker's Wrapped v1.6.0
Changed
- FFmpeg compositor rewritten: monolithic xfade → memory-bounded segment + concat (
ffmpeg_composer.py). The singlexfadefilter_complexbuffered 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 viascripts/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
-shortesttrimming artefacts). scripts/recompose.pycomparison labels clarified (segments (new)vsold-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 afterb2_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_PATHenv) 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_pipelinenow drives the async backend correctly — it expected a synchronous full response, butPOST /generatereturns202 + {session_id}and runs in the background, soprint_summaryhitKeyError: 'insights'. It now POSTs, then pollsGET /recap/{id}until ready (with a 429 rate-limit message and a timeout). Addedmatplotlib+ipykernelto 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_imageran the genblaze pipeline.run()and a synchttpx.Clientfetch directly inside anasyncfunction, 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 intoasyncio.to_thread. Side benefit: the 5 image generations now actually run in parallel via the existingasyncio.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_threadhelpers.GET /recap/{id}/download— the blocking B2-download + ZIP build offloaded viaasyncio.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.
Banker's Wrapped v1.5.0
Fixed
- FFmpeg OOM-kill on Railway (0 frames, encoder opened then died in ~1 s) (
ffmpeg_composer.py) — libx264 auto-detects the host core count (32 on Railway), not the container's allocation, and spawned 32 thread contexts whose buffers blew past the container memory limit → SIGKILL at encoder init. Capped concurrency with-filter_complex_threads 2and-threads 4; negligible speed impact on a ~75 s video. Also log the FFmpegreturncodeon failure (137 = SIGKILL/OOM, 139 = SIGSEGV) for unambiguous diagnosis. - Progress timer for active step (
page.tsx) — each SSE event marks a step's start, but the UI treated arrival as done, so "Writing narrative script" flipped to ✅ instantly and showed no timer during the ~30 s NIM call. Now the latest-started step is active (live timer counting from its own start event) and a step is done only once a later step starts. Removed thegenerating_images/scriptingspecial-casing — it falls out naturally. - FFmpeg xfade "constant frame rate" failure on Railway (
ffmpeg_composer.py) — looped image inputs (-loop 1) have an undefined rate (1/0); stricter ffmpeg builds (Railway's apt package vs. local 8.1.1) reject this with "The inputs needs to be a constant frame rate; current rate of 1/0 is invalid" → 0-frame output, pipelinefailed. Fixed by setting-framerate 25on each input andfps=25in each scene's filter chain so xfade receives true CFR. Verified by rebuilding the failed session's B2 assets locally (avg_frame_rate=25/1). - Browser playback "file is corrupt" (
ffmpeg_composer.py) — output was H.264 High 4:4:4 /yuvj444pbecause seedream JPEGs are full-range 4:4:4 and the in-filterformat=yuv420pdid not survive xfade format negotiation. Browsers cannot decode 4:4:4 H.264 (VLC can — hence "plays locally, corrupt on web"). Forced-pix_fmt yuv420pas an explicit output option →High/ 4:2:0, decodable everywhere. Added-movflags +faststartso themoovatom is at the front for progressive streaming. Regression test asserts both flags. - Composer migrated to xfade
filter_complexas the permanent path (was concat demuxer). Scene duration now auto-stretches from the probed audio length so the video always covers the full narration;-shortesttrims to the exact audio end — fixes narration being cut off (~40 s) on longer scripts. ffprobepath resolution — derivingffprobefromffmpegvia a naivereplace("ffmpeg","ffprobe")mangled install directories such asffmpeg-8.1.1-full_build; now replaces only the filename. Same bug fixed inscripts/recompose.py.- Thumbnail key extension
.png→.jpg(b2_client.py) — scene 0 is a JPEG; the.pngkey produced a content-type mismatch and a broken share-page thumbnail. - Progress timer double-count (
page.tsx) — per-step duration was measured against the previous event, so "Generating scenes" and "Composing video" both counted the same span (the "6m30s → 5m2s" jump). Now measured forward:(next step start) − (this step start). Pipeline timer also starts on the first SSE event (safe against React batched updates that skippedlength === 1). - FFmpeg 0-frame bug (root cause of 500 on Railway) —
n_total = n + 1without a matching lavfi input caused xfade to reference non-existent[v5]; added branded ending card (dark overlay +drawtexttitle) as the 6th input. - Dockerfile — added
fonts-liberationsodrawtextfinds a system font on Debian slim. - SSE race condition (
progress.py) — frontend opens SSE before the POST creates the session; previously returned 404 immediately ("spinner hangs forever"). Now polls up to 10 s for session creation. - SSE final event
ts: 0— terminal event now usestime.time()so per-step duration is correct for the last stage. - Demo button —
fetch("/data/synthetic/…")now hasokcheck +.catch()to surface errors visibly instead of silently sending a garbage blob.
Added
scripts/recompose.py— offline tool that downloads a session's existing B2 assets and rebuilds the MP4 locally with zero API cost (no image/LLM/TTS calls). Used to isolate and prove the pixel-format fix. Supports--prefix USER_ID/SESSION_ID(paste straight from the B2 console),--ffmpeg PATH, gracefulNoSuchKeymessaging, and UTF-8 console output on Windows.- Video
posterthumbnail — the recap thumbnail is set as the<video poster>on both the main and share pages, so the preview frame shows instantly while the file buffers. - Per-step latency (
page.tsx+globals.css) — each completed step shows elapsed time (e.g.44.4s,1m 4s) from consecutive SSE timestamps. Steps container widened to 360 px;.bw-step-durationclass added. FFMPEG_BINconfig (config.py+media_agent.py) — override FFmpeg binary path via env var without rebuilding.- README Interactive Demo Notebook section — scenario table + quickstart added before "What Is This?" so it's the first thing judges see.
Changed
- Progress UI (
page.tsx) — collapsed the five per-scene rows into a single "Generating scenes + narration — X/5 scenes" sub-label; that step stays active (⏳) untilcomposing_videofires (thegenerating_imagesevent marks the start of media work). The "Uploading all artifacts" label corrected to "Saving recap video to Backblaze B2" (allpipeline/assets are already uploaded during scene generation). Estimated-time weights retuned to a realistic ~5 min total (observed 4–7 min).
Banker's Wrapped v1.4.0
Added
- Full asset manifest — B2 now stores 10 artifact types per session:
analytics.json(financial insights snapshot),prompts.json(image prompts + SHA-256 hashes per scene),generation.json(model, provider, latency_ms, retry_count per pipeline step),thumbnail.png(scene 0 reused as preview image) - Tenacity retry — all Genblaze media calls (
generate_scene_image,generate_narration_audio) wrapped with 3-attempt exponential backoff (2–30 s).retry_countandlatency_mstracked inImageResult/AudioResult - 5-scene cinematic narrative —
NarrativeAgentnow requests a 5-act script: Opening / Personality Reveal → Big Achievement → Spending Insight → Personalized Advice → Motivational Close - FFmpeg xfade transitions —
FFmpegComposerrewritten tofilter_complexpipeline: 0.5 s crossfade between every scene pair + global 0.5 s fade-in / fade-out - SSE stages
composing_videoanduploading_to_b2added; emitted fromMediaAgentviaprogress_callback(7 stages total) - Download ZIP endpoint —
GET /api/v1/recap/{session_id}/downloadstreams an in-memory ZIP containing all B2 artifacts for the session thumbnail_urlfield added toRecapResponseand stored in session metadataB2Client.download_bytes()— reads an object body from B2 (used by ZIP endpoint)- Frontend: thumbnail image on main and share pages, "Download full package" ZIP button, full B2 key paths per artifact, 7-step SSE progress bar, scene + artifact count in footer
- 93 tests, 93% coverage (gate: 80%)
Changed
NarrativeAgentuser prompt updated to request 5-scene JSONMediaAgent.__init__now accepts optionalprogress_callback: Callable[[str, str], None]MediaAgentInputgains optionalanalytics_output: AnalyticsAgentOutput | None = NoneMediaAgentOutputgainsthumbnail_url: strFFmpegComposer.compose()uses single-filter_complexcommand (replaces concat demuxer)