DEV Community

VoiceDeveloper
VoiceDeveloper

Posted on

How to Add AI Narration to Your Blog Posts

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:

  1. Picking a TTS service (spoiler: I love ElevenLabs)
  2. Setting up authentication
  3. Generating audio for a markdown post
  4. 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"
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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-build hook in config.toml that runs the Python script.
  • Next.js – create a scripts/generate-audio.js and call it from package.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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Extract plain text from your markdown.
  2. Send it to ElevenLabs’ API.
  3. Save the MP3 file.
  4. Embed an audio player in your post.
  5. 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)