Blog Audio: Gemini TTS Narration for Blog Posts
Generate audio narration of blog posts using Google Gemini TTS.
How to use it
- Hit Copy SKILL.md — or use the Claude Code line below to get every file.
- Claude: ⋯ → Download .md, then Customize → Skills → Add → Upload skill.
ChatGPT: make a Project and paste it into Instructions.
Neither? Paste it at the top of a new chat — it works for that chat. - Describe your job in plain words. The AI follows the skill from there.
npx degit AgriciDaniel/claude-blog/skills/blog-audio#main ~/.claude/skills/blog-audio-2For one project only, change the path to .claude/skills/blog-audio-2. This skill also uses run.py, generate_audio.py, blog_audio_prepared.txt, blog_audio_dialogue.txt, Next.js — copying SKILL.md alone won't be enough. See the folder on GitHub.
Not working?
- Check which app you pasted it into — the steps above name the right one.
- Some skills need the paid tier of Claude or ChatGPT.
Paste into Claude, ChatGPT or Cursor.
Show the full text252 lines
Blog Audio: Gemini TTS Narration for Blog Posts
Generate professional audio narration of blog content using Google's Gemini TTS. Three modes: summary (200-300 word spoken overview), full article read-aloud, or two-speaker podcast dialogue. 30 voices, 80+ languages, HTML5 embed output.
Quick Reference
| Command | What it does |
|---|---|
/blog audio generate <file> |
Generate audio narration of a blog post |
/blog audio voices |
Show available voices with characteristics |
/blog audio setup |
Check/configure API key for Gemini TTS |
Prerequisites
- Python 3.11+ (venv managed automatically by
run.py) GOOGLE_AI_API_KEYenvironment variable (same key used by blog-image)- FFmpeg (for WAV-to-MP3 conversion; falls back to WAV if missing)
Always Use run.py Wrapper
# CORRECT:
python3 scripts/run.py generate_audio.py --text "..." --voice Charon --json
# WRONG:
python3 scripts/generate_audio.py --text "..." # Fails without venv
API Key Check (Gate Pattern)
Before generating audio, check for the API key:
test -n "${GOOGLE_AI_API_KEY:-}" && echo "GOOGLE_AI_API_KEY is set" || echo "GOOGLE_AI_API_KEY is not set"
- If set: proceed with generation
- If not set: guide the user:
"Audio generation requires a Google AI API key. Get one free at https://aistudio.google.com/apikey
Then set it:
export GOOGLE_AI_API_KEY=your-keyThis can be the same key used by/blog image, but it must be exported in the shell." - When called internally (from blog-write): return silently if key is missing. Never block the writing workflow.
Setup
For /blog audio setup:
- Check if
GOOGLE_AI_API_KEYis set in environment - If blog-image uses project
.mcp.json, confirm the referenced env var is exported - If not, guide user to https://aistudio.google.com/apikey
- Verify with a dry run:
python3 scripts/run.py generate_audio.py --text "Test" --dry-run --json
Voice Selection
For /blog audio voices:
Load references/voices.md and present the voice catalog to the user.
Ask the user which voice they prefer, or recommend based on content type:
- Article narration: Charon (Informative) or Sadaltager (Knowledgeable)
- Tutorial/how-to: Achird (Friendly) or Sulafat (Warm)
- News/analysis: Rasalgethi (Informative) or Schedar (Even)
- Lifestyle/wellness: Aoede (Breezy) or Vindemiatrix (Gentle)
- Dialogue host: Puck (Upbeat) or Laomedeia (Upbeat)
- Dialogue expert: Kore (Firm) or Charon (Informative)
Generation Workflow
For /blog audio generate <file>:
Step 1: Read the Blog Post
Read the file and extract:
- Title (from H1 or frontmatter)
- Full content (markdown body)
- Approximate word count
Step 2: Choose Mode
Ask the user (or auto-select if they specified --mode):
| Mode | When to use | Output |
|---|---|---|
| Summary | Quick audio overview (1-2 min) | 200-300 word spoken summary |
| Full | Complete read-aloud (5-15 min) | Full article as natural speech |
| Dialogue | Podcast-style (3-8 min) | Two-person conversation about the article |
Step 3: Prepare Text
Claude prepares the text; the script does TTS only.
Summary mode: Write a 200-300 word spoken summary of the article. Rules:
- Write as natural speech, not written text
- Open with the article's key finding or answer
- Cover 3-5 main takeaways
- Close with actionable advice
- No markdown, no "In this article...", no meta-commentary
- Use conversational transitions ("Here's what matters...", "The key finding is...")
Full mode: Strip the markdown content to clean spoken text:
- Headings become natural transitions ("Next, let's look at...")
- Links become plain text (remove URLs, keep anchor text)
- Images and charts: omit or briefly describe ("As the data shows...")
- Code blocks: describe verbally ("The code uses a for-loop to...")
- Lists: convert to natural sentences
- Remove frontmatter, schema markup, HTML tags
- Add brief intro: "This is [title], published on [date]."
Dialogue mode: Write a 2-person conversation script about the article:
- Speaker1 = Host (curious, asks good questions)
- Speaker2 = Expert (knowledgeable, gives clear answers)
- Format each line as:
Speaker1: What's the key takeaway here? - Cover the article's main points conversationally
- 15-25 exchanges (produces ~3-8 minutes)
- Natural, not stilted ("That's a great point" over "Indeed, as the research indicates")
Step 4: Select Voice
If the user chose a voice, use it. Otherwise, recommend based on mode:
- Summary/Full: default to Charon (Informative)
- Dialogue: default to Puck (Host) + Kore (Expert)
Step 5: Generate Audio
Write the prepared text to a file under the working directory, then call:
# Single voice (summary or full mode)
python3 scripts/run.py generate_audio.py \
--text-file blog_audio_prepared.txt \
--voice Charon \
--model flash \
--output audio/post-slug.mp3 \
--json
# Two voices (dialogue mode)
python3 scripts/run.py generate_audio.py \
--text-file blog_audio_dialogue.txt \
--voice Puck \
--voice2 Kore \
--model pro \
--output audio/post-slug-dialogue.mp3 \
--json
Model selection:
flash(default): maps togemini-3.1-flash-tts-preview, good for summaries and standard narration.flash31: explicit alias forgemini-3.1-flash-tts-preview.legacy-flash25: retained only for older compatibility.proorlegacy-pro25: maps togemini-2.5-pro-preview-tts, use only when needed.
Step 6: Deliver
Present the result to the user:
- File path: where the audio was saved
- Duration: human-readable (e.g., "3:42")
- Embed code: ready-to-paste HTML5 audio tag
- Cost: estimated API cost
- Placement suggestion: where to insert the embed in the blog post
Embedding Guide
Standard HTML (Hugo, Jekyll, static sites)
<audio controls preload="metadata">
<source src="audio/post-slug.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
MDX (Next.js, Gatsby)
<audio controls preload="metadata">
<source src="/audio/post-slug.mp3" type="audio/mpeg" />
</audio>
WordPress
[audio src="audio/post-slug.mp3"]
Placement
Insert the audio player after the introduction (below the first H2) or at the very top of the article with a label: "Listen to this article" or "Audio version".
Internal API (for blog-write)
When invoked internally from blog-write:
Input:
text: Prepared text (already cleaned by Claude)voice: Voice name (default: Charon)voice2: Second voice for dialogue (optional)model: flash or prooutput_path: Where to save the file
Output:
### Audio Narration
- **Path:** /path/to/audio/post-slug.mp3
- **Duration:** 3:42
- **Voice:** Charon
- **Embed:** `<audio controls preload="metadata"><source src="audio/post-slug.mp3" type="audio/mpeg"></audio>`
Graceful fallback: If GOOGLE_AI_API_KEY is not set, return immediately
with no error. The writing workflow continues without audio. Never block
blog-write because audio generation is unavailable.
Error Handling
| Error | Resolution |
|---|---|
| GOOGLE_AI_API_KEY not set | Get key at https://aistudio.google.com/apikey |
| FFmpeg not found | Install: sudo apt install ffmpeg. Falls back to WAV output. |
| Rate limited | Wait and retry. Check limits at https://aistudio.google.com/rate-limit |
| Text too long (>8,192 input tokens) | Split into sections around 7,800 tokens; the script chunks and stitches prepared text |
| Unknown voice name | Run /blog audio voices to see valid options |
| API error | Check key validity and model availability |
| API key missing (internal call) | Return silently: writing workflow continues |
Reference Documentation
Load on-demand: do NOT load all at startup:
references/voices.md: Full 30-voice catalog, recommendations by content type, dialogue pairings
| 1 | |
| 2 | name blog-audio |
| 3 | description > |
| 4 | Generate audio narration of blog posts using Google Gemini TTS. |
| 5 | Supports summary narration, full article read-aloud, and two-speaker |
| 6 | podcast/dialogue mode with 30 voice options. Outputs MP3 with HTML5 |
| 7 | audio embed code. Works standalone via /blog audio or internally from |
| 8 | blog-write. Falls back gracefully when API key is not configured. |
| 9 | Use when user says "blog audio", "narrate blog", "audio version", |
| 10 | "text to speech", "tts", "podcast mode", "read aloud", "audio narration", |
| 11 | "voice", "narration", "generate audio". |
| 12 | user-invokable true |
| 13 | argument-hint "[generate|voices|setup] [file-or-text] [--mode summary|full|dialogue] [--voice name]" |
| 14 | license MIT |
| 15 | metadata |
| 16 | author AgriciDaniel |
| 17 | version "2.2.0" |
| 18 | |
| 19 | |
| 20 | # Blog Audio: Gemini TTS Narration for Blog Posts |
| 21 | |
| 22 | Generate professional audio narration of blog content using Google's Gemini TTS. |
| 23 | Three modes: summary (200-300 word spoken overview), full article read-aloud, |
| 24 | or two-speaker podcast dialogue. 30 voices, 80+ languages, HTML5 embed output. |
| 25 | |
| 26 | ## Quick Reference |
| 27 | |
| 28 | | Command | What it does | |
| 29 | |---------|-------------| |
| 30 | | `/blog audio generate <file>` | Generate audio narration of a blog post | |
| 31 | | `/blog audio voices` | Show available voices with characteristics | |
| 32 | | `/blog audio setup` | Check/configure API key for Gemini TTS | |
| 33 | |
| 34 | ## Prerequisites |
| 35 | |
| 36 | Python 3.11+ (venv managed automatically by `run.py`) |
| 37 | `GOOGLE_AI_API_KEY` environment variable (same key used by blog-image) |
| 38 | FFmpeg (for WAV-to-MP3 conversion; falls back to WAV if missing) |
| 39 | |
| 40 | ## Always Use run.py Wrapper |
| 41 | |
| 42 | |
| 43 | # CORRECT: |
| 44 | python3 scripts/run.py generate_audio.py --text "..." --voice Charon --json |
| 45 | |
| 46 | # WRONG: |
| 47 | python3 scripts/generate_audio.py --text "..." # Fails without venv |
| 48 | |
| 49 | |
| 50 | ## API Key Check (Gate Pattern) |
| 51 | |
| 52 | Before generating audio, check for the API key: |
| 53 | |
| 54 | |
| 55 | test -n "${GOOGLE_AI_API_KEY:-}" && echo "GOOGLE_AI_API_KEY is set" || echo "GOOGLE_AI_API_KEY is not set" |
| 56 | |
| 57 | |
| 58 | If set: proceed with generation |
| 59 | If not set: guide the user: |
| 60 | "Audio generation requires a Google AI API key. Get one free at https://aistudio.google.com/apikey |
| 61 | Then set it: `export GOOGLE_AI_API_KEY=your-key` |
| 62 | This can be the same key used by `/blog image`, but it must be exported in the shell." |
| 63 | **When called internally** (from blog-write): return silently if key is missing. |
| 64 | Never block the writing workflow. |
| 65 | |
| 66 | ## Setup |
| 67 | |
| 68 | For `/blog audio setup`: |
| 69 | |
| 70 | Check if `GOOGLE_AI_API_KEY` is set in environment |
| 71 | If blog-image uses project `.mcp.json`, confirm the referenced env var is exported |
| 72 | If not, guide user to https://aistudio.google.com/apikey |
| 73 | Verify with a dry run: `python3 scripts/run.py generate_audio.py --text "Test" --dry-run --json` |
| 74 | |
| 75 | ## Voice Selection |
| 76 | |
| 77 | For `/blog audio voices`: |
| 78 | |
| 79 | Load `references/voices.md` and present the voice catalog to the user. |
| 80 | |
| 81 | Ask the user which voice they prefer, or recommend based on content type: |
| 82 | **Article narration**: Charon (Informative) or Sadaltager (Knowledgeable) |
| 83 | **Tutorial/how-to**: Achird (Friendly) or Sulafat (Warm) |
| 84 | **News/analysis**: Rasalgethi (Informative) or Schedar (Even) |
| 85 | **Lifestyle/wellness**: Aoede (Breezy) or Vindemiatrix (Gentle) |
| 86 | **Dialogue host**: Puck (Upbeat) or Laomedeia (Upbeat) |
| 87 | **Dialogue expert**: Kore (Firm) or Charon (Informative) |
| 88 | |
| 89 | ## Generation Workflow |
| 90 | |
| 91 | For `/blog audio generate <file>`: |
| 92 | |
| 93 | ### Step 1: Read the Blog Post |
| 94 | |
| 95 | Read the file and extract: |
| 96 | Title (from H1 or frontmatter) |
| 97 | Full content (markdown body) |
| 98 | Approximate word count |
| 99 | |
| 100 | ### Step 2: Choose Mode |
| 101 | |
| 102 | Ask the user (or auto-select if they specified `--mode`): |
| 103 | |
| 104 | | Mode | When to use | Output | |
| 105 | |------|-------------|--------| |
| 106 | | **Summary** | Quick audio overview (1-2 min) | 200-300 word spoken summary | |
| 107 | | **Full** | Complete read-aloud (5-15 min) | Full article as natural speech | |
| 108 | | **Dialogue** | Podcast-style (3-8 min) | Two-person conversation about the article | |
| 109 | |
| 110 | ### Step 3: Prepare Text |
| 111 | |
| 112 | Claude prepares the text; the script does TTS only. |
| 113 | |
| 114 | **Summary mode:** |
| 115 | Write a 200-300 word spoken summary of the article. Rules: |
| 116 | Write as natural speech, not written text |
| 117 | Open with the article's key finding or answer |
| 118 | Cover 3-5 main takeaways |
| 119 | Close with actionable advice |
| 120 | No markdown, no "In this article...", no meta-commentary |
| 121 | Use conversational transitions ("Here's what matters...", "The key finding is...") |
| 122 | |
| 123 | **Full mode:** |
| 124 | Strip the markdown content to clean spoken text: |
| 125 | Headings become natural transitions ("Next, let's look at...") |
| 126 | Links become plain text (remove URLs, keep anchor text) |
| 127 | Images and charts: omit or briefly describe ("As the data shows...") |
| 128 | Code blocks: describe verbally ("The code uses a for-loop to...") |
| 129 | Lists: convert to natural sentences |
| 130 | Remove frontmatter, schema markup, HTML tags |
| 131 | Add brief intro: "This is [title], published on [date]." |
| 132 | |
| 133 | **Dialogue mode:** |
| 134 | Write a 2-person conversation script about the article: |
| 135 | Speaker1 = Host (curious, asks good questions) |
| 136 | Speaker2 = Expert (knowledgeable, gives clear answers) |
| 137 | Format each line as: `Speaker1: What's the key takeaway here?` |
| 138 | Cover the article's main points conversationally |
| 139 | 15-25 exchanges (produces ~3-8 minutes) |
| 140 | Natural, not stilted ("That's a great point" over "Indeed, as the research indicates") |
| 141 | |
| 142 | ### Step 4: Select Voice |
| 143 | |
| 144 | If the user chose a voice, use it. Otherwise, recommend based on mode: |
| 145 | Summary/Full: default to Charon (Informative) |
| 146 | Dialogue: default to Puck (Host) + Kore (Expert) |
| 147 | |
| 148 | ### Step 5: Generate Audio |
| 149 | |
| 150 | Write the prepared text to a file under the working directory, then call: |
| 151 | |
| 152 | |
| 153 | # Single voice (summary or full mode) |
| 154 | python3 scripts/run.py generate_audio.py \ |
| 155 | --text-file blog_audio_prepared.txt \ |
| 156 | --voice Charon \ |
| 157 | --model flash \ |
| 158 | --output audio/post-slug.mp3 \ |
| 159 | --json |
| 160 | |
| 161 | # Two voices (dialogue mode) |
| 162 | python3 scripts/run.py generate_audio.py \ |
| 163 | --text-file blog_audio_dialogue.txt \ |
| 164 | --voice Puck \ |
| 165 | --voice2 Kore \ |
| 166 | --model pro \ |
| 167 | --output audio/post-slug-dialogue.mp3 \ |
| 168 | --json |
| 169 | |
| 170 | |
| 171 | **Model selection:** |
| 172 | `flash` (default): maps to `gemini-3.1-flash-tts-preview`, good for summaries and standard narration. |
| 173 | `flash31`: explicit alias for `gemini-3.1-flash-tts-preview`. |
| 174 | `legacy-flash25`: retained only for older compatibility. |
| 175 | `pro` or `legacy-pro25`: maps to `gemini-2.5-pro-preview-tts`, use only when needed. |
| 176 | |
| 177 | ### Step 6: Deliver |
| 178 | |
| 179 | Present the result to the user: |
| 180 | **File path**: where the audio was saved |
| 181 | **Duration**: human-readable (e.g., "3:42") |
| 182 | **Embed code**: ready-to-paste HTML5 audio tag |
| 183 | **Cost**: estimated API cost |
| 184 | **Placement suggestion**: where to insert the embed in the blog post |
| 185 | |
| 186 | ## Embedding Guide |
| 187 | |
| 188 | ### Standard HTML (Hugo, Jekyll, static sites) |
| 189 | |
| 190 | <audio controls preload="metadata"> |
| 191 | <source src="audio/post-slug.mp3" type="audio/mpeg"> |
| 192 | Your browser does not support the audio element. |
| 193 | </audio> |
| 194 | |
| 195 | |
| 196 | ### MDX (Next.js, Gatsby) |
| 197 | |
| 198 | <audio controls preload="metadata"> |
| 199 | <source src="/audio/post-slug.mp3" type="audio/mpeg" /> |
| 200 | </audio> |
| 201 | |
| 202 | |
| 203 | ### WordPress |
| 204 | |
| 205 | [audio src="audio/post-slug.mp3"] |
| 206 | |
| 207 | |
| 208 | ### Placement |
| 209 | Insert the audio player after the introduction (below the first H2) or at the |
| 210 | very top of the article with a label: "Listen to this article" or "Audio version". |
| 211 | |
| 212 | ## Internal API (for blog-write) |
| 213 | |
| 214 | When invoked internally from blog-write: |
| 215 | |
| 216 | **Input:** |
| 217 | `text`: Prepared text (already cleaned by Claude) |
| 218 | `voice`: Voice name (default: Charon) |
| 219 | `voice2`: Second voice for dialogue (optional) |
| 220 | `model`: flash or pro |
| 221 | `output_path`: Where to save the file |
| 222 | |
| 223 | **Output:** |
| 224 | |
| 225 | ### Audio Narration |
| 226 | - **Path:** /path/to/audio/post-slug.mp3 |
| 227 | - **Duration:** 3:42 |
| 228 | - **Voice:** Charon |
| 229 | - **Embed:** `<audio controls preload="metadata"><source src="audio/post-slug.mp3" type="audio/mpeg"></audio>` |
| 230 | |
| 231 | |
| 232 | **Graceful fallback:** If `GOOGLE_AI_API_KEY` is not set, return immediately |
| 233 | with no error. The writing workflow continues without audio. Never block |
| 234 | blog-write because audio generation is unavailable. |
| 235 | |
| 236 | ## Error Handling |
| 237 | |
| 238 | | Error | Resolution | |
| 239 | |-------|-----------| |
| 240 | | GOOGLE_AI_API_KEY not set | Get key at https://aistudio.google.com/apikey | |
| 241 | | FFmpeg not found | Install: `sudo apt install ffmpeg`. Falls back to WAV output. | |
| 242 | | Rate limited | Wait and retry. Check limits at https://aistudio.google.com/rate-limit | |
| 243 | | Text too long (>8,192 input tokens) | Split into sections around 7,800 tokens; the script chunks and stitches prepared text | |
| 244 | | Unknown voice name | Run `/blog audio voices` to see valid options | |
| 245 | | API error | Check key validity and model availability | |
| 246 | | API key missing (internal call) | Return silently: writing workflow continues | |
| 247 | |
| 248 | ## Reference Documentation |
| 249 | |
| 250 | Load on-demand: do NOT load all at startup: |
| 251 | `references/voices.md`: Full 30-voice catalog, recommendations by content type, dialogue pairings |
| 252 |