I Gave My Agents a Voice… And It’s Wildly Useful

TL;DR
A changelog nobody reads is a story nobody heard. A coding agent reads your real git history and writes a two-speaker script, the ElevenLabs Text to Dialogue API turns it into a host-and-guest conversation, and ffmpeg stitches the episode. The complete one-hour build.
Your release notes are the most honest page on your site and the least read one. Users do not read changelogs - they scroll past walls of text. Put the same release notes in a two-voice conversation, a host and a guest talking through what shipped and why, and the same people listen to the whole thing on a walk.
That conversation used to take a studio, two warm bodies, and an afternoon. It now takes one API endpoint built for exactly this: ElevenLabs Text to Dialogue turns a list of speaker-tagged text lines into natural two-voice audio with the Eleven v3 model, with audio tags like [laughing] and [cautiously] steering the delivery. You give it lines, it gives you a conversation.
This guide is the complete build: seven steps, under an hour, every step ending in something you can run. OpenCode writes the dialogue script from your real git history using the same headless opencode run pattern our cron automation guide covers, so the episode is about what actually shipped. It is the audio-only sibling of the auto-narrated changelog video pipeline: that build makes a video of the demo, this one makes a conversation about the release.
| Resource | Description |
|---|---|
| Text to Dialogue API reference | The POST /v1/text-to-dialogue endpoint, request body, limits |
| Text to Dialogue overview | What the Eleven v3 model does, audio tags, supported formats |
| Text to Dialogue quickstart | SDK install, CLI auth, and a first request |
| Eleven v3 prompting guide | Audio tags and delivery control |
| ElevenLabs API pricing | Per-1000-character rates and included monthly characters |
| Studio Create Podcast reference | The one-shot podcast endpoint for the upgrade path |
| GenFM cost help center | What GenFM text generation and conversion cost |
| OpenCode Docs | Install and the opencode run non-interactive mode |
Prerequisites: a machine with Node.js 18 or newer (for fetch and the render script), a git repository with a CHANGELOG.md or at least a few weeks of real commits, an ElevenLabs account, and an LLM provider key.
Install OpenCode with the official one-liner from the OpenCode docs:
curl -fsSL https://opencode.ai/install | bash
Authenticate a provider (opencode auth login), then confirm the capability the whole pipeline depends on - one task, one answer, no interactive session:
opencode run --model opencode/deepseek-v4-flash "print the current directory tree, two levels deep"
If that prints a tree and exits cleanly, the worker side is proven. On model choice: writing a dialogue script is a narrow, bounded task, which is exactly what a budget model is good at, and the DeepSeek V4 Flash 0731 guide covers the why. The script step costs fractions of a cent; Step 6 has the honest audio numbers.
What you have now: a proven headless agent command and a repo to point it at.
In the ElevenLabs dashboard, generate an API key from your profile settings, then export it. The key is sent as the xi-api-key header on every request, per the API reference:
export ELEVEN_API_KEY="your-key-here"
List the voices available to your account with the voices endpoint - no request body, the key in the header is enough:
curl -H "xi-api-key: $ELEVEN_API_KEY" "https://api.elevenlabs.io/v2/voices" \
| jq -r '.voices[] | "\(.voice_id) \(.name)"' | head -20
Every row is a voice ID plus a name. Pick two voices that sound clearly different - a steady host and a livelier guest works well - then export their IDs:
export HOST_VOICE="the-id-of-your-host-voice"
export GUEST_VOICE="the-id-of-your-guest-voice"
What you have now: a key and two voice IDs, which is everything the audio side needs.
The episode must match what actually shipped, and the cheapest source of truth is the commit history. This is the same trick the changelog video build uses for its narration script, applied to dialogue: the agent reads the real diffs instead of the aspirational feature list you wrote two weeks ago.
Run this from the repo root:
opencode run --model opencode/deepseek-v4-flash \
"Run 'git log --oneline -30' and read CHANGELOG.md if it exists. Write a two-speaker podcast script about the last release: a HOST and a GUEST discussing what shipped, why it matters, and what the listener should try first. Rules: every line starts with 'HOST:' or 'GUEST:' followed by the spoken text; alternate speakers every 1-3 lines; use squared-bracket audio tags like [cheerfully], [laughing], [cautiously] at the start of a few lines to vary delivery; keep product names and command names verbatim; plain conversational English, no jargon; aim for 3,000 to 4,000 characters total. Output only the script lines, nothing else." > script.txt
Two requirements matter in that prompt: the HOST:/GUEST: prefixes, because the render script maps them to voice IDs, and the character target - the API is most reliable at or below 2,000 total characters per request and wants longer scripts split into chunks (Text to Dialogue overview, as of 2026-08-31), so ~3,500 characters across two batches is a comfortable episode size.
Review the script before spending credits: under 4,000 characters, real speaker alternation, product names intact - anything else gets one rewrite.
What you have now: script.txt - a speaker-tagged dialogue script grounded in real commits.
The render script does three moves: parse the speaker lines into {text, voice_id} pairs, batch them under 1,900 characters each (headroom below the 2,000-character guide), and POST each batch to the Text to Dialogue endpoint. Save it as render-episode.mjs:
import { readFileSync, writeFileSync } from "node:fs";
const lines = readFileSync("script.txt", "utf8")
.split("\n")
.map((l) => l.trim())
.filter(Boolean)
.map((l) => {
const [speaker, ...rest] = l.split(":");
const voice_id = speaker === "HOST" ? process.env.HOST_VOICE : process.env.GUEST_VOICE;
return { voice_id, text: rest.join(":").trim() };
});
const batches = [];
let current = [];
let size = 0;
for (const line of lines) {
if (size + line.text.length > 1900 && current.length) {
batches.push(current);
current = [];
size = 0;
}
current.push(line);
size += line.text.length;
}
if (current.length) batches.push(current);
for (const [i, batch] of batches.entries()) {
const res = await fetch("https://api.elevenlabs.io/v1/text-to-dialogue", {
method: "POST",
headers: { "xi-api-key": process.env.ELEVEN_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ model_id: "eleven_v3", inputs: batch }),
});
if (!res.ok) {
console.error(`batch ${i + 1} failed: ${res.status} ${await res.text()}`);
process.exit(1);
}
writeFileSync(`chunk_${i + 1}.mp3`, Buffer.from(await res.arrayBuffer()));
console.log(`rendered chunk ${i + 1} (${batch.length} lines)`);
}
Run it:
node render-episode.mjs
The request body matches the API reference: inputs as text plus voice_id pairs, model_id defaulting to eleven_v3. The square-bracket audio tags in the script text steer emotional delivery - [giggling], [whispering] - a feature the docs note is still under active development, with results that vary, as of 2026-08-31. If a batch fails, the script prints the status and stops rather than stacking half-rendered files.
What you have now: one chunk_N.mp3 per batch, alternating between your two voices.
From the archive
Aug 31, 2026 • 8 min read
Aug 31, 2026 • 9 min read
Aug 31, 2026 • 10 min read
Aug 28, 2026 • 5 min read
Concatenate the chunks with ffmpeg:
printf "file 'chunk_1.mp3'\nfile 'chunk_2.mp3'\n" > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy release-notes-$(date +%Y-%m).mp3
(Adjust the printf to however many chunks Step 4 produced.) Listen end to end before anything else - a human ear is the right tool here:
open release-notes-$(date +%Y-%m).mp3
What you have now: release-notes-YYYY-MM.mp3, a finished two-voice episode about the release, publishable as-is.
First listen usually needs passes, and they are all cheap:
seed parameter - an integer between 0 and 4294967295 - that makes repeated requests sample consistently; set one and keep it while you tune. Determinism is best-effort, not guaranteed, per the same reference, as of 2026-08-31.Now the honest numbers, from the API pricing page, as of 2026-08-31: Text to Speech (Eleven v3 grade) is billed at $0.10 per 1,000 characters. The free tier includes 10,000 characters a month, which the pricing table values at roughly 10 minutes of speech, and the Starter plan at $6 a month includes 60,000 characters. A focused 3,500-character episode costs about $0.35 at API rates, or about a third of the free monthly grant.
What you have now: a repeatable render - same script, same seed, same voices - that produces a consistent episode in about a minute.
Publish the MP3 wherever your audience already listens - a podcast host with an RSS feed, or a simple /podcast page on your site - and keep the render script in the repo. Two refinements make this a habit instead of a chore:
POST /v1/studio/podcasts) that generates and converts an entire podcast project from a text or URL source in one call - the API form of the feature their help center calls GenFM, as of 2026-08-31. Honest caveats from the docs: Create a podcast requires a paid subscription (Studio plans help center), the broader Studio API is available upon request (Studio API info), and ElevenLabs covers the script-generation LLM cost while the audio conversion is billed at standard rates (GenFM cost). The build in this post needs none of that - it runs on the public endpoint on any tier today.What you have now: a shipped episode, a checked-in pipeline, and a path from release commit to podcast feed without you.
Text to Speech at Eleven v3 grade is $0.10 per 1,000 characters per the API pricing page, as of 2026-08-31. A typical 3,500-character episode is about $0.35; the free tier's 10,000 included characters cover roughly two short episodes a month.
Yes. The Text to Dialogue API accepts up to 10 unique voice IDs per request - see the inputs section of the API reference, as of 2026-08-31 - so adding a third host is a line and a voice ID away.
The Text to Dialogue overview, as of 2026-08-31, recommends keeping the total character count per request at or below 2,000 for reliable generation, splitting longer text and concatenating the audio client-side. The render script does exactly that, splitting at speaker-line boundaries.
Eleven v3 is a dialogue-capable model - the capabilities page lists podcast audio as a first-class use case - and the square-bracket audio tags give direction-level control over delivery. It is nondeterministic, so plan for a couple of takes or a fixed seed while you tune.
This build runs entirely on public API endpoints you own. GenFM-style tools generate the script for you inside a product; here the script comes from your actual git history and the audio comes from the Text to Dialogue endpoint at $0.10 per 1,000 characters. Build it once and it runs on a schedule.
| Source | URL |
|---|---|
| Text to Dialogue API reference | https://elevenlabs.io/docs/api-reference/text-to-dialogue/convert |
| Text to Dialogue overview | https://elevenlabs.io/docs/overview/capabilities/text-to-dialogue |
| Text to Dialogue quickstart | https://elevenlabs.io/docs/eleven-api/guides/cookbooks/text-to-dialogue |
| Eleven v3 prompting guide | https://elevenlabs.io/docs/overview/capabilities/text-to-speech/best-practices#prompting-eleven-v3 |
| ElevenLabs API pricing | https://elevenlabs.io/pricing/api |
| Studio Create Podcast reference | https://elevenlabs.io/docs/api-reference/studio/create-podcast |
| Studio API information | https://elevenlabs.io/docs/api-reference/studio-api-information |
| GenFM cost help center | https://elevenlabs.io/docs/help-center/product/distribution-publishing/gen-fm/how-much-does-gen-fm-cost |
| Studio plans help center | https://elevenlabs.io/docs/help-center/product/studio/studio/on-what-plans-can-i-use-studio |
| OpenCode Docs | https://opencode.ai/docs/ |
Some links to tools above are referral links - see our affiliate disclosure.
Last updated: August 31, 2026
Read next
The agent finishes, the summary scrolls past, and you will read it later. Build the fix: a coding agent that ends every run with a plain-language summary, piped into ElevenLabs text-to-speech and out as an MP3 you can listen to on the way to work. The complete one-hour build.
9 min readRelease notes nobody reads are a content problem with a mechanical fix: have a coding agent write the narration script from real git history, record the demo with Screen Studio, and let Descript narrate and edit it. A complete one-hour build.
8 min readAn agent CLI plus a cron schedule turns recurring dev chores into background work: dependency bumps, doc freshness checks, morning briefs. The pattern, the guardrails, and where to run it - your own hardware or a cloud host.
11 min readTechnical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
Google's AI notebook that lets you ground a Gemini chat in your own uploaded sources. Generates summaries, mind maps, an...
View ToolAI meeting notes that augment your own typing. Records the call, captures what you type, and fills in the gaps with cont...
View ToolAI-native code editor forked from VS Code. Composer mode rewrites multiple files at once. Tab autocomplete predicts your...
View ToolOpen-source AI pair programming in your terminal. Works with any LLM - Claude, GPT, Gemini, local models. Git-aware ed...
View ToolCreate show notes, clips, titles, and promotional assets from podcast source material.
View AppDescribe your company and agent teams handle operations.
View AppDo a task once with AI, get a reusable agent forever.
View AppInstall Ollama and LM Studio, pull your first model, and run AI locally for coding, chat, and automation - with zero cloud dependency.
Getting StartedInstall the dd CLI and scaffold your first AI-powered app in under a minute.
Getting StartedReal-time prompt loop with history, completions, and multiline input.
Claude Code
Creating an AI-Enhanced Podcast Web App: Comprehensive Tutorial Repo: https://github.com/developersdigest/llm-podcast-engine You can obtain these API keys from the following sources: ...

Repo Coming Soon! FOLLOW ME → Website: https://dub.sh/dev-digest → X/Twitter: https://dub.sh/dd-x → GitHub: https://git.new/devdigest TOOLS I USE → Wispr Flow (voice-to-text): https://dub...

Try out GitKraken here: https://gitkraken.cello.so/myw3K67IkCr to get 50% GitKraken Pro. In this video, we explore GitKraken, a robust Git GUI that not only visualizes your Git repository...

The agent finishes, the summary scrolls past, and you will read it later. Build the fix: a coding agent that ends every...

Release notes nobody reads are a content problem with a mechanical fix: have a coding agent write the narration script f...

An agent CLI plus a cron schedule turns recurring dev chores into background work: dependency bumps, doc freshness check...

DeepSeek shipped the official V4 Flash release on July 31, 2026. The re-post-trained 0731 build beats V4-Pro-Preview on...

A fair, sourced comparison of the TTS APIs developers reach for in 2026: OpenAI, ElevenLabs, xAI Grok, and Cartesia. Qua...

The Rime CLI streams natural-sounding text-to-speech straight from your terminal, so Claude Code, Codex, Devin, and Open...

New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.