Back to main

From Link to Podcast: A Small Pipeline for Listening to the Web

[ AUTHORIAL INTENT & AI DISCLOSURE ]

This post was drafted with Hermes to document an automation pipeline built and tested locally.

Forensic Hygiene Active
View Policy Standard →

I wanted a small personal pipeline that turns links into podcast episodes.

Not a production media platform. Not a fully automated publishing business. Just a practical loop where I can send a link to my agent, ask for a podcast episode, and later listen to the result from a normal podcast app.

The first useful version now works:

link
→ source extraction
→ source notes
→ spoken two-host script
→ text-to-speech audio
→ local archive
→ Obsidian note
→ MP3 conversion
→ static RSS feed
→ podcast app

The important part is not any single model call. The value is in the boring connective tissue: stable folders, recoverable scripts, podcast-compatible metadata, and a workflow that can be repeated with a sentence like:

podcast this <url>

Why Build This Instead of Just Summarizing?

A text summary is useful when I am sitting at a screen. A podcast episode is useful when I am walking, doing chores, or away from the laptop.

The source can be an X post, a paper, a YouTube video, or an article. The output should be a short explanatory conversation that I can subscribe to once and keep receiving in the same place.

The first working feed is public and static:

https://ilteris.github.io/hermes-audio-feed/feed.xml

The publishing target is deliberately uninteresting: a GitHub Pages repo with an RSS file, MP3 files, transcripts, and metadata. Podcast apps already know how to consume that.

The Shape of the System

The pipeline has two sides:

  1. Episode generation: extract the source, understand it, write a script, generate audio.
  2. Episode publishing: convert to MP3, update RSS, push to static hosting, verify the live URLs.

The local archive sits between them.

~/voice-memos/hermes-audio-overviews/
  <episode-slug>/
    source_notes.md
    transcript.json
    dialogue_spoken.md
    dialogue_spoken_audio.ogg
    metadata files...

The public feed repo is separate:

~/Code/hermes-audio-feed/
  feed.xml
  index.html
  assets/
    cover.png
  episodes/
    2026-07-07-system-design-interview-prep.mp3
  transcripts/
    2026-07-07-system-design-interview-prep.md
  metadata/
    2026-07-07-system-design-interview-prep.json

That separation matters. The archive can hold messy source data and intermediate files. The feed repo only contains files that should be published.

Step 1: Extract the Source Truthfully

The first rule is simple: do not invent source material.

For X posts, the pipeline tries the API-style reader first. If that fails, it uses a browser/CDP fallback that opens the post and extracts the visible text. That fallback turned out to matter because X often returns service or plan-related errors.

For YouTube videos, the pipeline uses the transcript helper and stores both JSON and timestamped text.

For papers, it downloads the PDF, extracts text, and writes source notes before scripting. In one test, a DAIR.AI post pointed to an arXiv paper. The pipeline resolved the paper, downloaded it, extracted the text, and only then wrote the episode.

The archive keeps the raw materials:

source_extract_raw.txt
transcript.json
transcript_timestamps.txt
paper.pdf
paper.txt
youtube_metadata.json

This makes the episode auditable. If the script sounds wrong, I can inspect exactly what it was based on.

Step 2: Write Source Notes Before Writing the Episode

The pipeline writes source_notes.md as a bridge between raw extraction and audio scripting.

This file captures:

  • the original URL
  • title and author/channel when available
  • key claims
  • important numbers
  • caveats
  • local files produced during extraction
  • useful framing for the episode

For a paper, source notes look like a compact reading memo. For a YouTube video, they look like chapter notes. For an X post, they capture the post text and any linked sources.

This step keeps the script grounded. It also gives me a durable note even if I never listen to the audio again.

Step 3: Write for Ears, Not for Eyes

This was the most important quality improvement.

The first version of the pipeline wrote an essay and split it between two voices. That technically worked, but it sounded flat. The voices were not the only problem. The writing was wrong for audio.

The better version writes a spoken two-host script from the start.

The default format is:

  • Host A: curious generalist, asks natural follow-up questions.
  • Host B: technical explainer, keeps the explanation concrete.

The script uses short turns:

A: Okay, this sounds very chip-industry-specific. What is the simple version?

B: The simple version is: the AI chip is not just waiting on math. It is also waiting on memory.

A: So this is a data movement problem?

B: Exactly.

The difference is obvious in the generated audio. Short turns, simple repair moves, and real questions make the same TTS voices sound more natural.

The rule going forward is:

spoken script quality first, TTS provider second

Better voices would help, but better writing helped immediately.

Step 4: Generate Two-Host Audio

The local TTS helper now defaults to Kokoro-FastAPI, the containerized CPU setup from Ariya Hidayat’s Kokoro TTS post. It exposes an OpenAI-compatible speech API at:

http://127.0.0.1:8880/v1

With Docker Desktop running, the server comes from:

docker run --name hermes-kokoro-fastapi -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu

The helper reads the same Markdown input:

A: First host line.

B: Second host line.

Then it generates one clip per turn through the local FastAPI service, alternating Kokoro voices.

The final podcast defaults are:

provider: kokoro-api
voice_a: af_sarah
voice_b: am_michael
speed: 1.02

The voice choice came from listening, not from the model card alone.

  • af_sarah works as the guiding host: clear, practical, and not overly dramatic.
  • am_michael works better as the Kokoro explainer host for this pipeline.
  • am_adam and am_fenrir are still usable alternate B voices.
  • bf_emma is available in the local Kokoro voice list, but Sarah + Michael became the preferred pair for this pipeline.

Earlier tests used af_bella, af_nicole, am_adam, am_fenrir, and slower pacing. The current default avoids Bella and Nicole, keeps Adam/Fenrir as alternates, and runs the pipeline slightly faster. New episodes default to Sarah + Michael at 1.02 speed.

Direct kokoro-onnx is still available only as a fallback. Edge TTS is the last fallback if Kokoro is unavailable:

  • Host A: en-US-AvaNeural
  • Host B: en-US-AndrewNeural
  • Edge rate: +0%

The helper keeps per-turn clips. That matters because long episodes can have more than 100 turns. If turn 87 fails, the script should resume from the existing clips instead of starting over.

The output for local listening is OGG:

dialogue_spoken_audio.ogg

Telegram can deliver that file directly as media.

Step 5: Save the Obsidian Note

Every episode gets a note in my Obsidian vault:

~/Code/obsidian-vault/30-39 Knowledge/Audio Overviews/

The note contains:

  • source link
  • summary
  • local archive path
  • podcast feed link
  • source notes
  • full transcript

This turns the podcast output into more than audio. It becomes part of the knowledge base.

If I later want to search for the system design interview episode, I can find the transcript and source notes locally, without relying on the podcast app.

Step 6: Convert to MP3 for Podcast Apps

The internal audio can be OGG, but the RSS feed should publish MP3.

Some podcast apps accept OGG. Many do not. MP3 is boring and compatible.

The publisher script converts the generated audio and writes:

episodes/<slug>.mp3
transcripts/<slug>.md
metadata/<slug>.json

It also regenerates feed.xml and index.html.

Step 7: Generate Podcast-Compatible RSS

The first RSS feed was too minimal. It worked in some readers, but YouTube Music initially showed the show without episodes.

The fix was to make the feed more complete:

  • normal RSS channel metadata
  • itunes:image
  • normal RSS <image>
  • itunes:owner
  • itunes:type
  • atom:link rel="self"
  • per-episode itunes:episodeType
  • MP3 enclosures with audio/mpeg
  • reachable cover art
  • human-readable summaries in both <description> and <itunes:summary>

One small lesson: do not let the podcast app surface a source URL as the episode description. Write a real summary into the RSS fields.

A typical item points to the MP3 like this:

<enclosure
  url="https://ilteris.github.io/hermes-audio-feed/episodes/2026-07-07-system-design-interview-prep.mp3"
  length="10826540"
  type="audio/mpeg" />

Step 8: Publish as Static Files

The feed is just a static site.

Publishing means:

cd ~/Code/hermes-audio-feed
git add feed.xml index.html episodes/ transcripts/ metadata/
git commit -m "Add podcast episode"
git push origin main

GitHub Pages handles the rest.

After pushing, the pipeline waits for Pages to build and then checks the live URLs:

curl -L -I https://ilteris.github.io/hermes-audio-feed/feed.xml
curl -L -I https://ilteris.github.io/hermes-audio-feed/episodes/<slug>.mp3
curl -L -I https://ilteris.github.io/hermes-audio-feed/assets/cover.png

The feed is also parsed as XML to confirm the expected number of <item> entries and enclosure URLs.

Step 9: Subscribe Once, Then Let Episodes Appear

The final user experience is simple:

  1. Add the RSS feed to a podcast app.
  2. Ask Hermes to make an episode from a link.
  3. Wait for the app to refresh.
  4. Listen normally.

YouTube Music sometimes caches the feed for a few minutes. That is a podcast app behavior, not a generation problem. The verification step checks the live RSS directly so I know whether the feed is correct before waiting on app refresh.

The Scripts Behind the Loop

There are three local scripts in the current version:

~/.hermes/scripts/audio_overview_prepare.py
~/.hermes/scripts/audio_overview_dialogue_tts.py
~/.hermes/scripts/audio_overview_publish_podcast.py

Their responsibilities are intentionally narrow:

  • prepare: extract source material and create an archive folder
  • dialogue TTS: turn A: / B: Markdown into stitched audio
  • publish podcast: convert audio to MP3, update RSS, push-ready files

There is also a Hermes skill called audio-overview-podcasts that records the workflow and defaults. That is what makes the behavior repeatable from a natural request.

What Is Still Rough

The pipeline works, but it is not finished.

A few rough edges remain:

  • Kokoro voices are a clear improvement, but the exact host pairing may still need tuning.
  • The best quality gain so far came from more spoken scripts, not better voices.
  • Podcast apps cache feeds on their own schedule.
  • Long sources need careful compression into a useful 8–12 minute episode.
  • The feed is public. That is acceptable for this use case, but it is not a private publishing model.

I also want the workflow to produce better episode titles, better short descriptions, and maybe a small web page per episode with sources and transcript.

Why This Feels Useful

The interesting part is how small the system is.

There is no custom podcast backend. No database. No queue. No account system. No media CMS.

It is just:

  • local files
  • scripts
  • Markdown
  • TTS
  • ffmpeg
  • RSS
  • GitHub Pages
  • a podcast app

That is enough to turn web reading into an audio queue.

For me, the broader pattern is the same one I keep coming back to: agents become useful when they can close the gap between intent and artifact.

In this case the intent is:

make this link listenable

The artifact is:

a podcast episode in my feed

Everything in the middle is just plumbing. But the plumbing is the product.

Back to main