Inspiration

Every podcast episode and recorded talk hides a second production: the release kit. Transcript, chapters, show notes, titles, cover art, quote cards, captioned audiogram clips. Independent creators burn ~3 hours per episode across five disconnected tools — then store the results in a folder called final_final_v2 on a laptop that will eventually die.

We built the missing machine: drop in the audio, review what comes back, and seal it all to a permanent, verifiable archive. You made the episode. B-Side makes everything else.

What it does

One audio drop returns the entire release kit, live over SSE:

  • Transcript with word-level timings (AssemblyAI via Genblaze)
  • Direction — 3 titles, summary, show notes, timestamped chapters, and 3 pull quotes anchored verbatim to the spoken words, plus an art brief
  • Episode art — FLUX under a deterministic quality gate that rejects blank/flat frames and retries across the provider plan before a human ever sees them
  • Quote cards — model art + deterministic typography (image models can't spell; our typography engine can)
  • Audiograms — the creator's real voice under karaoke captions that light up word-by-word (ASS \k timing straight from the transcript), composed by ffmpeg as a first-class Genblaze provider step
  • Human review gate — approve, or reject with feedback; rejects regenerate with parent_run_id lineage you can inspect
  • Sealed release — every asset's bytes are fetched back from B2 and re-hashed; any mismatch fails the seal; the kit ships as a ZIP via presigned URL

Every asset has an evidence drawer with a "fetch bytes & re-hash now" button that does a live Backblaze B2 round trip in front of you (~1s, ✓ BYTES MATCH).

How we built it

FastAPI + a durable SQLite job queue (exponential backoff, per-provider circuit breakers, idempotent stages, orphan recovery) drives a seven-stage pipeline where Genblaze is the spine, not a wrapper: Pipeline.ingest for the creator's audio and transcript, AssemblyAIProvider for STT, chat() with a real cross-vendor fallback chain (Gemini → NVIDIA NIM Llama 3.3 70B), FLUX → Gemini image fallback, two first-party custom SyncProviders (quote cards, audiograms) built on the documented extension contract, a subclassed StepCache so reruns never pay twice, and ObjectStorageSink with hierarchical keys partitioned per show.

Backblaze B2 is the system of record — four planes in one private bucket (app documents rewritten after every stage, media, SDK-native run manifests, expiring scratch). The local database is disposable: /judge has a restore button that rebuilds the entire application state from the bucket alone. React + Vite on the front, a record-label liner-notes design system, hand-rolled CSS. Deployed on Railway; 26 tests (unit / live-B2 integration / e2e) + lint on GitHub Actions.

Challenges we ran into

  • FLUX black frames. Long prompts (>~250 chars) and empty payloads silently produce near-black images. We bisected it live, added prompt distillation — and turned the bug into a feature: a deterministic image evaluator that rejects blank/flat frames and retries across the fallback plan.
  • Media types lie. FLUX returns JPEG bytes labeled PNG, which downstream APIs reject. We do a 16-byte ranged read from B2 and sniff magic bytes — the object's actual type, not its claimed one.
  • Stale caches poison sinks. A cached step can point at a file:// asset that no longer exists. Our ValidatingStepCache verifies asset existence before honoring a hit.
  • Proving durability. We killed the live service mid-run on production — the queue recovered the orphaned job, resumed at the exact stage, and sealed the episode. Then we wiped the database and rebuilt everything from B2. Both are reproducible from /judge.

Accomplishments that we're proud of

The audiogram moment: a judge hears a real human voice while the exact words light up in sync — generated, composed, hash-verified, and archived by one pipeline. And the evidence drawer: every claim in this writeup has a button next to it.

What we learned

Deterministic quality gates catch failure, not taste — the human gate is the real bar. Model slugs rot fast, so every model is swappable via env. And "trust me" is not a storage story: fetched-byte verification changed how we think about generative pipelines.

What's next

Multi-episode batch runs, publish hooks (RSS, YouTube, socials) straight from the sealed kit, and voice-cloned multilingual audiograms — every derivative still sealed to B2 with lineage back to the original master.

Try it in 60 seconds (no sign-in)

  1. Open https://bside-production.up.railway.app/judge
  2. Click "Run the pipeline now" — watch every stage go green, live (~2 min)
  3. Open any asset's ⛨ verify drawer → "Fetch bytes & re-hash now" → ✓ BYTES MATCH
  4. Click "Restore state from B2" — the whole app rebuilds from the bucket alone
  5. Download the sealed kit ZIP ## What it does

Built With

Share this project:

Updates