The Rabbit Hole
A personal weekly deep-dive app I built that turns one chosen topic into three narrated ~10-minute episodes, released Monday, Wednesday, and Friday with measured chapter timestamps, full show notes, a real podcast feed, and an automatic archive to my second brain. A Mac worker writes; a Next.js webapp reads.
Text is discoverable. Narrated long-form isn’t. I wanted a weekly deep-dive on a topic I pick, whatever the week demands, rendered as radio-quality audio I can consume in the car or on a walk. I also wanted the professionalism of a real podcast, measured chapters, show notes, a valid RSS feed, and I wanted the whole thing to run itself once a week with two taps. The Rabbit Hole is that system.
What I built
A four-layer architecture I now use as the reference pattern for every phone-first personal app I build.
Worker (Mac). A launchd poll plus a Sunday picker kicks the week off. A bash orchestrator runs two-pass Claude content generation for each of three days, passes every draft through a ruthless editor quality gate (avg ≥ 8.0, min ≥ 7), renders each section through Kokoro TTS, and stitches the episode with ffmpeg earcons and per-episode artwork.
Queue (Vercel Blob). Pending topic, current state, episodes, chapters, artwork, feed. The worker writes; the webapp reads. Never the other way round.
Webapp (Next.js 16). A /pick route to choose the week’s topic from the phone. A /library route to browse every past week. A /library/<week>/day/<n> page with an audio desk, chapter navigation, and day-to-day nav. Passphrase middleware, no OAuth, single user.
Notifications (ntfy). One topic, six pushes a week, no per-section progress spam. Sunday trigger, worker starting, week ready, then Mon/Wed/Fri daily release reminders.
Why measured chapters matter
Word-count-estimated chapter timestamps drift 10 to 60 seconds over a 10-minute clip. Unusable for a scrubbable player. I inject ###CHAPTER### markers in the input text, render each section through Kokoro separately, sum cumulative audio samples, and record startTime = cumulative_samples / sample_rate per marker. The player reads that array. Tapping a chapter lands on the exact word, not an estimate.
The quality gate
A two-pass ruthless editor scores every draft on six axes. Below bar, one tightening regen pass with the critique injected. Most drafts pass first or second try. It’s the same lesson I’ve learned on every AI pipeline I’ve built: a smaller model with a strict critic outperforms a larger model with no critic, every time.
What it delivers
A week of deep-dive audio I actually listen to. The first real run, Week 17, was three episodes on Naturalistic Decision Making, generated and shipped end-to-end with zero API cost (everything runs through the Claude CLI under my Max subscription). Seven minutes of compute to produce thirty minutes of listening I’d otherwise never have made time to make.