Why Add AI Narration to Your Blog?
Reading is great, but listening is even better when you’re on a commute, working out, or just want to give your eyes a break. Adding a voice version of your posts can boost accessibility, increase time‑on‑page, and make your content feel more personal. With modern text‑to‑speech (TTS) APIs you can generate high‑quality narration in a few lines of code—no need to record yourself or hire a voice actor.
In this post I’ll walk you through:
- Picking a TTS service (spoiler: I love ElevenLabs)
- Setting up authentication
- Generating audio for a markdown post
- Embedding the result back into your blog
You can follow along with Python, JavaScript, or a simple curl command—pick the language you’re most comfortable with.
1. Choose the Right TTS Engine
There are plenty of TTS providers, from Google Cloud to Amazon Polly. Most of them give you a robotic‑sounding voice, which is fine for quick prototypes but not ideal for a polished blog. ElevenLabs stands out because:
- It uses deep‑learning voice cloning to produce natural‑sounding speech.
- You can pick from several pre‑trained voices or upload a short sample to create a custom voice.
- The pricing model is generous for developers experimenting with a few hours of audio per month.
If you haven’t signed up yet, grab your free trial here: https://try.elevenlabs.io/kr07zfuqn1bp
2. Get an API Key
After you register, go to the API tab in the ElevenLabs dashboard and create a new key. Keep it secret—treat it like a password. For the examples below I’ll store it in an environment variable called ELEVENLABS_API_KEY.
export ELEVENLABS_API_KEY="your-secret-key-here"
3. Convert Markdown to Plain Text
Most blog engines store posts in Markdown. The TTS service works best with plain text, so strip out the markdown syntax first. You can use a tiny helper library like mistune (Python) or marked (Node) to do the conversion.
Python example
import mistune
def markdown_to_text(md_content: str) -> str:
# Convert markdown to HTML, then strip tags
html = mistune.html(md_content)
# Simple tag removal – for production use a proper HTML parser
return ''.join(html.split('<')[0::2])
JavaScript example
import { marked } from 'marked';
import { JSDOM } from 'jsdom';
function markdownToText(md) {
const html = marked(md);
const dom = new JSDOM(html);
return dom.window.document.body.textContent;
}
4. Call the ElevenLabs API
4.1 Using curl
If you just want to test things quickly, a curl request is enough:
curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/voice-id" \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello, welcome to my blog! This post shows how to add AI narration.",
"voice_settings": {
"stability": 0.75,
"similarity_boost": 0.85
}
}' \
--output narration.mp3
Replace voice-id with the ID of the voice you want to use (you can find it in the ElevenLabs dashboard).
4.2 Python snippet
import os
import requests
API_URL = "https://api.elevenlabs.io/v1/text-to-speech"
VOICE_ID = "your-voice-id" # e.g. "21m00Tcm4TlvDq8ikWAM"
def generate_audio(text: str, out_path: str = "output.mp3"):
headers = {
"xi-api-key": os.getenv("ELEVENLABS_API_KEY"),
"Content-Type": "application/json"
}
payload = {
"text": text,
"voice_settings": {
"stability": 0.75,
"similarity_boost": 0.85
}
}
response = requests.post(f"{API_URL}/{VOICE_ID}", json=payload, headers=headers)
response.raise_for_status()
with open(out_path, "wb") as f:
f.write(response.content)
print(f"🗣️ Audio saved to {out_path}")
# Example usage
if __name__ == "__main__":
md = open("post.md").read()
plain = markdown_to_text(md)
generate_audio(plain, "post-narration.mp3")
4.3 Node.js version
import fetch from 'node-fetch';
import fs from 'fs';
import { markdownToText } from './markdown-utils.js'; // assume you exported the function above
const API_URL = 'https://api.elevenlabs.io/v1/text-to-speech';
const VOICE_ID = 'your-voice-id';
async function generateAudio(mdPath, outPath = 'output.mp3') {
const md = fs.readFileSync(mdPath, 'utf-8');
const text = markdownToText(md);
const response = await fetch(`${API_URL}/${VOICE_ID}`, {
method: 'POST',
headers: {
'xi-api-key': process.env.ELEVENLABS_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
voice_settings: {
stability: 0.75,
similarity_boost: 0.85,
},
}),
});
if (!response.ok) {
const err = await response.text();
throw new Error(`ElevenLabs error: ${err}`);
}
const buffer = await response.buffer();
fs.writeFileSync(outPath, buffer);
console.log(`✅ Saved narration to ${outPath}`);
}
// Run
generateAudio('post.md');
All three examples do the same thing: take the cleaned‑up text, send it to ElevenLabs, and write the MP3 file locally.
5. Embed the Audio Player
Now that you have post-narration.mp3, you can embed it directly in your HTML template. Most static site generators (like Hugo, Jekyll, or Astro) let you add a custom snippet to the post layout.
<audio controls preload="metadata">
<source src="/audio/post-narration.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
If you’re using a JavaScript‑based blog (e.g., Next.js), you could lazy‑load the file only when the user clicks a “Listen” button.
import { useState } from 'react';
export default function AudioToggle({ src }) {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Hide' : 'Listen to this post'}
</button>
{show && (
<audio controls autoPlay>
<source src={src} type="audio/mpeg" />
</audio>
)}
</>
);
}
Place the component in your post layout and point src to the generated MP3 file.
6. Automate the Workflow
Manually running a script for every new post can become tedious. Hook the generation step into your build pipeline:
-
Hugo – add a
post-buildhook inconfig.tomlthat runs the Python script. -
Next.js – create a
scripts/generate-audio.jsand call it frompackage.json(npm run build && node scripts/generate-audio.js). - GitHub Actions – spin up a CI job that builds the site, runs the TTS script, and pushes the resulting audio files back to the repo (or to an S3 bucket).
Here’s a minimal GitHub Action that runs the Python script on every push to main:
name: Generate Audio Narrations
on:
push:
branches: [ main ]
jobs:
narrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install deps
run: pip install -r requirements.txt
- name: Generate audio
env:
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
run: |
for md in content/**/*.md; do
python generate_audio.py "$md"
done
- name: Upload audio assets
uses: actions/upload-artifact@v3
with:
name: narrations
path: output/*.mp3
Now every time you merge a new post, the audio file is created automatically and attached to the workflow run.
7. Tweak Voice Settings for Better Results
ElevenLabs lets you adjust two main parameters:
| Parameter | Description | Typical range |
|---|---|---|
stability |
How consistent the voice stays over long text | 0.5 – 0.9 |
similarity_boost |
How closely the output matches the chosen voice | 0.75 – 1.0 |
Play with these values until the narration feels natural for your content. For example, a tech tutorial might benefit from a slightly higher stability (0.85) to keep the tone steady, while a storytelling post could use a lower stability (0.6) for a more expressive feel.
8. Keep Accessibility in Mind
Adding audio is great, but you should still provide the original text for search engines and users who prefer reading. Also consider:
- Adding a transcript (
<track kind="captions">) for hearing‑impaired users. - Using descriptive
aria-labels on the audio controls. - Offering a download link for the MP3 so users can listen offline.
9. Wrap‑Up
You now have a complete pipeline:
- Extract plain text from your markdown.
- Send it to ElevenLabs’ API.
- Save the MP3 file.
- Embed an audio player in your post.
- Automate the whole thing with your build system.
The result is a more inclusive, engaging blog that feels modern without a huge amount of overhead.
Ready to give your readers a voice?
Grab your ElevenLabs API key and start generating narrations today: https://try.elevenlabs.io/kr07zfuqn1bp. Happy coding, and enjoy the sound of your own content!
Top comments (0)