I Spent a Weekend Building a Podcast Generator for My Own Too-Long Blog Posts

Codex CLI and Claude Code, one weekend, and a 107-minute episode that cost $1.55.

I have a recurring problem with my own writing: my posts get long. The last one on multi-model engineering came out at 834 content elements, ten images and nine tables. That is more than most people will read in one sitting. Audio suits that length better than scrolling does.

So last weekend I built the thing I actually wanted: point a tool at one of my published posts, and get back a narrated podcast episode plus everything I need to cut a video from it. Not a demo — something I have already used on two of my own articles.

This post is about what it does, what it costs, and the three things I got wrong before I got them right.

The repository is public and MIT licensed: https://github.com/thomassuedbroecker/podcast-from-blog-post-generation-public.

The result in YouTube of the long blog post:

1. What it actually does

Six commands, each writing into a local run directory, each reading what the previous one wrote:

StepWhat it doesNeeds
extractOpens the rendered page with Playwright and Chromium, preserves headings, paragraphs, lists, quotes, code, links, images, figures and tables in source orderNothing
describe-visualsDescribes every image and every table through a local agent CLICodex CLI or Claude Code CLI
transitionsWrites a spoken lead-in for every headingThe same CLI
narrateAssembles the complete spoken script, deterministicallyNothing
synthesizeTurns the reviewed script into audioOpenAI text-to-speech, paid
video-planRebuilds every video cue against the real audioNothing

The staging is the point. Finish and review the script first, then generate audio. Every correction you make before synthesize is free. Every correction after it is a re-synthesis. You can stop, inspect, fix, and resume at any step, and nothing is regenerated unless you ask.

You get more than an audio file. Each run also writes:

  • A cue sheet that says what to show, and when
  • A plan for each chapter: which image, table, or audio file to place, and at what time
  • Two subtitle files (`.srt`) you can import into a video editor or into YouTube
  • The chapter list for the YouTube description, ready to paste
  • A notes file with every link from the article, and the credit for the source

2. The weekend, honestly

First commit Saturday 13:45. Audio synthesis working Saturday 18:59. Measured video timings and the licensing review on Sunday. Twenty-three commits, about 7,200 lines of Python and 2,900 lines of tests, 145 tests passing.

I used Codex CLI and Claude Code to build it, and the finished tool then calls an agent CLI itself for the two jobs that genuinely need a model. Which brings me to the first thing I got wrong.

3. Three things I learned

3.1. My run could not tell me which model wrote the text

The tool records where every sentence came from. When I went back to check which model had written the 108 chapter transitions in my reference episode, the artifact said this:

{
 "heading_id": "heading-0006",
 "source": "codex-cli",
 "model_identifier": "codex-cli",
 "warnings": []
}

codex-cli is the CLI, not the model. I had not pinned a model, so no model flag was passed, and the CLI used whatever it was configured to use. The run knows which tool produced the text. It does not know which model.

For someone who writes about multi-model engineering, that is an uncomfortable finding — and exactly the traceability gap I keep pointing at in other people’s platforms. The fix was cheap: pass --model and the value is recorded on every transition and every description.

python -m podcast_blog transitions \
 --run "./runs/my-episode" \
 --provider codex-cli \
 --model gpt-5-codex

If a run has to be reproducible, pin the model. Otherwise a CLI upgrade quietly changes your narrator between episodes.

3.2. Estimated timings are not good enough for a video

The first version derived every cue position from the narration word count at 150 words per minute. It looked fine. Then I generated the audio.

Value
Estimated runtime100.2 minutes
Actual runtime106.8 minutes
Drift by the end~6 minutes

Six minutes of drift puts every image on the wrong side of a cut. So video-plan now reads the durations ffprobe measured for each chunk out of the audio manifest and repositions everything against them. Chapter starts become exact. A cue inside a chapter is interpolated against the measured chunk containing it, so it can sit a second or two off — and the artifact says which basis it is on, estimated_from_word_count or measured_from_generated_audio, rather than pretending.

The two bases are never mixed. If the audio does not cover every chapter, the whole plan falls back to estimates and warns, because a partial measurement would shift every later cue silently.

Rebuilding the plan is free and calls nothing, so I can re-run it as often as I like.

3.3. A transcript should not be a guess

The subtitles are not speech recognition. They are an alignment of the exact text that was sent to text-to-speech, so the transcript cannot disagree with what is spoken. It also means the transcript exists before the audio does.

There are two tracks, deliberately:

  • transcript.srt — the full narration, split at sentence boundaries, wrapped to two lines.
  • on-screen-text.srt — only the article’s own verbatim labels, captions and pull-quotes, as a separate text layer.

here is a catch, and I ran into it in **Camtasia**. This is my experience with that one editor — I have not tested whether Final Cut, DaVinci Resolve or YouTube behave the same way. In Camtasia, an imported subtitle track does not get the nicer styling that its own auto-caption feature produces. I wanted that styling, so for the first episode I used the auto-captions instead of my own file. Those captions come from speech recognition, which means they guess at words and get some of them wrong. Checking every line by hand takes a long time, and even after that review some mistakes are still in the finished video.

So in Camtasia the trade-off is this: my generated file has the right words and plain styling, the auto-caption has nice styling and the wrong words. My own file cannot misspell anything, because it is the exact text that was sent to text-to-speech. Next time I will take the correct words.

4. What it costs

This is the part I wanted to know before I started, so here it is up front.

StepCost
extract, narrate, video-planFree. No credentials, no network beyond fetching the page
describe-visuals, transitionsWhatever your Codex or Claude plan already covers
synthesize$0.015 per minute of finished audio with tts-1

Two real episodes:

EpisodeCharactersRequestsAudioCost
4 chapters29,9909~30 min$0.45
17 chapters103,53134106.8 min$1.55

One hundred and seven minutes of narrated audio for one dollar fifty-five. The command prints the character count and the estimated cost before it sends anything, and --chunk-limit 1 gives you a smoke test for a fraction of a cent.

The call volume on the CLI side is the thing to watch, not the dollars: transitions fires once per heading, which was 108 calls for that article. describe-visuals fires once per image and once per table.

5. What leaves my machine

Given what I usually write about, I was not going to hand-wave this. Every provider declares its own behaviour, and python -m podcast_blog providers prints it:

StepLeaves the machine
extractOnly the request that fetches the page
describe-visualsArticle text, and images with Codex CLI
transitionsThe sentence before each heading, plus the heading
narrate, video-planNothing
synthesizeThe full narration text

The agent CLIs report hybrid execution, never local — the process runs on my machine, but the model call leaves it unless the CLI is explicitly pointed at a local model. Claiming otherwise would be the exact marketing move I complain about.

Webpage content is treated as untrusted data throughout: extracted text is fenced inside explicit data markers with an instruction that it is data and not instructions, the prompt is piped through stdin rather than argv, the child process environment is restricted to an allowlist, and every call has a timeout. My OPENAI_API_KEY is deliberately not in the Codex allowlist, so that process never sees it.

6. What the license does and does not give you

The code is MIT. That covers the software and nothing else.

It does not cover the article you point it at, the extracted images and screenshots, or the audio generated from them. Those belong to the author of the source article — which is fine when it is my own post, and is a conversation you need to have with the author when it is not. Everything a run produces lands in a runs/ directory that Git ignores.

The two rules I follow when publishing an episode: the generated voice is disclosed as synthetic, and the attribution file that carries the source URL, the author and every extracted link ships with the episode.

Worth knowing: every Python dependency is permissive, but FFmpeg’s licence depends on how your binary was built. The standard macOS Homebrew build is GPL-3.0, not LGPL. It is called as a separate process, so it does not affect the project’s licence — but it is the kind of detail that is easy to state wrongly.

7. What it does not do

  • It does not bypass paywalls, authentication or access controls.
  • Loudness normalisation and chapter metadata are not implemented; the merge is a plain FFmpeg concatenation.
  • The deterministic composer cannot expand an abbreviation on first use, because that needs knowledge it does not have.
  • Tables of contents are summarised with a heading-match heuristic that can misfire.
  • Many WordPress images carry neither a caption nor alt text, so the on-screen text layer can end up sparse — 9 cues across 107 minutes, in my case.

8. Try it on your own posts

python -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/python -m playwright install chromium

python -m podcast_blog extract --url "<your post>" --output ./runs/my-episode
python -m podcast_blog narrate --run ./runs/my-episode --provider composer

Those two commands cost nothing and need no credentials. Read narration/complete-script.md, and you will know within a minute whether the rest is worth your $1.55.

9. Two disclosures, and why they are separate

There are two different things to disclose here, and mixing them would blur both. The first is about how this post and the code were made. The second is about what the tool produces and where my numbers come from. One is about authorship; the other is about the output.

1. How this post and the application were made

Note: This post reflects my own ideas, implementation work, and experience. AI was used as a writing, discussion, research, and structured-thinking aid to help organize and clarify the arguments, but not to define the conclusions. In this case AI also wrote code: the application described here was built with Codex CLI and Claude Code across one weekend, under my direction, review, and testing. The architecture decisions, the staging of the pipeline, the trade-offs, and the conclusions above are mine.

2. The generated audio, and the figures in this post

Note: The episodes this tool produces use a synthetic voice generated by OpenAI text-to-speech. Disclosing that is not optional — OpenAI requires it, and the voices may not be used to suggest that a real person said something they did not say. The cost, character, and duration figures in this post are measured from my own runs rather than estimated. Model pricing changes over time, so check the current rates before planning a long episode.

#Podcast, #AICoding, #AICodingAssistants, #AgenticAI, #ClaudeCode, #OpenAICodex, #TextToSpeech, #OpenAI, #ContentRepurposing, #Python, #Playwright, #FFmpeg, #VideoEditing, #WordPress, #DeveloperTools, #SoftwareEngineering, #AIEngineering

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Blog at WordPress.com.

Up ↑