How I built a podcast with my cloned voice, from script to blog
The full pipeline behind the fghisi podcast: notes, a script written with Claude, review in a pull request, a cloned voice on ElevenLabs, post-production in ffmpeg, GitHub releases, Eleventy and an RSS feed. With the tools, the commands, the costs and the mistakes that cost me credits.
Why a podcast
I write on the blog about software engineering and leadership. But many of the people I wanted to reach don't read long articles. They listen, in the car, at the gym, while doing the dishes.
Recording every week didn't fit my routine. So I wanted to test an idea: what if the podcast were born the same way as the code of this site? My notes, a script reviewed in a pull request and automation doing the rest.
The result is the fghisi podcast (in Portuguese). Each episode starts from a book, one of my articles or a lesson learned, and is narrated in my voice, cloned with artificial intelligence. The ideas and opinions are mine. AI helps me turn them into audio.
Before writing a line of code, I designed the whole pipeline in a document. This article follows that design, step by step, and tells what changed when it met the real world.
The first design
This was the first design of the pipeline, as it stood in the document before any code. The core idea was already there: I act at two points, the notes and the review, and the rest runs on its own. Two blocks, the voice clone and the voice guide, would be set up once and reused in every episode.
Almost all of it was built. What changed along the way is further down, in "The plan and reality".
The flow of an episode
There are six steps. Two are mine and the others run on their own:
- Voice cloning, done once and reused in every episode.
- Notes, a
notas.mdfile I write on anep/<date>-<topic>branch. It's the only fully manual step, and it's what guarantees the episode carries my opinion. - Script, written with Claude from the notes and a voice guide.
- Review, in the pull request. I read the script out loud and adjust whatever doesn't sound like me.
- Audio: the voice is generated on ElevenLabs and assembled in ffmpeg, with consistent loudness and a soundtrack.
- Publishing: the MP3 goes to a GitHub release and the site gets the episode page and the RSS feed item.
The human time per episode is writing the notes, about an hour after reading, and reviewing the script, around twenty minutes.
The stack
The rule I gave myself was to use what the site already had, whenever possible. This is the stack that stuck, with the alternatives I considered:
| Step | Tool | Alternative considered | Why |
|---|---|---|---|
| Cloned voice | ElevenLabs | Resemble AI, open models (F5-TTS, Chatterbox) | The most natural Portuguese voice I found, with a simple API |
| Script | Claude, by Anthropic | Writing it by hand | Follows the voice guide and each episode type's structure consistently |
| Notes | Markdown in the repository | Readwise, to pull Kindle highlights | Versioned, free, editable anywhere |
| Post-production | ffmpeg | Auphonic | Free and runs on GitHub Actions |
| Audio files | Releases of a public GitHub repository | Cloudflare R2, a Hostinger folder | No new account, no password, no cost |
| Site and feed | Eleventy, this blog's generator | Astro, a podcast hosting service | The episode lives on my domain and ships in the same build as the blog |
| Orchestration | GitHub Actions | n8n, Make | It's already the path for the site's code, no extra server |
The scripts are in Python, standard library only. API calls are plain urllib, no SDK. That made the pipeline easy to test offline and to run on my computer.
The repository ended up like this:
podcast/
config/
guia-de-voz.md how I speak: opening, closing, rhythm, rules
prompt.md script instructions for each episode type
pronuncia.json names and terms spelled the way they're said
trilha.mp3 the soundtrack of every episode
episodios/
_modelo/ note templates: book, article and lesson
2026-10-de-senior-a-principal/
notas.md I write this
roteiro.md generated and reviewed in the PR
notas-do-episodio.md
linkedin.md
scripts/ gerar_roteiro, sintetizar, pos_producao, publicar, remixar
Step 1: cloning the voice
ElevenLabs offers two kinds of clone:
| Option | Audio needed | Quality | Minimum plan |
|---|---|---|---|
| Instant Voice Clone | 1 to 3 minutes | Good to start; loses naturalness in long audio | Starter |
| Professional Voice Clone | 30 minutes, ideally 3 hours | Almost indistinguishable from the original voice | Creator |
The plan was to start with the instant clone to validate the whole pipeline, while I gathered hours of recording for the professional one. Once it was ready, swapping the voice_id would be enough. That's what I'm doing.
For the recording, this is the checklist I followed:
- Room: a quiet room, with curtains, a rug or clothes nearby to dampen echo, and no fan running.
- Gear: the microphone always at the same distance, about a hand's width from the mouth.
- Format: WAV, 44.1 or 48 kHz, mono, no music and no aggressive noise reduction.
- Content: speak in the podcast's conversational tone, don't read a text. The clone copies how the sample sounds: a read sample makes a clone that sounds read.
Step 2: the notes
Each episode is born from a single file. There's a template for each episode type. This is the article one:
---
tipo: artigo
artigo: "De sênior a principal: o trabalho que acontece entre os times"
url: "https://fghisi.com.br/2026/10/16/de-senior-a-principal/"
minutos: 12
publicar_em: 2026-10-18
---
## Why I wrote this article
## What I want listeners to keep (3 points)
## A story or example to tell out loud
## What changed in my thinking since I wrote it
## What to apply on Monday
The book template has the main ideas with my comments, where I disagree and who I'd recommend it to. The lesson template has the context, what I thought before and what I'd do differently.
Step 3: the script
The script comes from three files together: the notes, the voice guide and the prompt.
The voice guide is written once. It says how I open and close episodes, which expressions I use and which I never would, whether I explain acronyms, and includes excerpts from my articles as a style reference. It also has fixed rules: first person singular, no real examples from my team or company, no made-up data.
The structure changes with the episode type:
| Type | The middle of the episode |
|---|---|
| Book | The authors' context, three to five key ideas with my comments, criticism and practical application |
| Article | Why I wrote it, the main points told as a conversation, what changed since then and practical application |
| Lesson | The context, what I thought before, the lessons, what I'd do differently and practical application |
The opening and closing are fixed in all of them: "Hi, I'm Fernando Ghisi, and this is the fghisi podcast" and the invitation to the notes on the blog.
The prompt's rules exist because text to read and text to hear are different things:
- Short sentences, no lists, tables or parentheses.
- Numbers spelled out and acronyms explained on first mention.
- Never quote books for more than one sentence: paraphrase and comment.
- Never invent anything that isn't in the notes. When a fact is missing, mark
[VERIFICAR](to check). The pipeline refuses to generate audio while one is left. - Deliver the title, the description, the episode notes with references and a LinkedIn post draft along with it.
The script can be generated by the Claude API, in an Actions workflow, with the output forced into a tool (tool_choice) so it always comes back structured in blocks. Or it can be written in a conversation and sent along with the notes: when the workflow finds a ready roteiro.md, it doesn't call the API. I started the second way, without an Anthropic key.
The review is the human gate. The script arrives in a pull request, and that's where a factual error or a sentence that doesn't sound like me gets caught. In the first version, the text was too polished and sounded read. I rewrote the voice guide for the tone of an engineer talking to another, with casual Portuguese, questions for the listener and examples like the PR stuck in review or the Friday deploy.
Step 4: the voice
ElevenLabs gets one request per script block, on the POST /v1/text-to-speech/{voice_id} endpoint. Each request body looks like this:
{
"text": "Oi! Eu sou o Fernando Guízi, e este é o f guízi podcast.",
"model_id": "eleven_multilingual_v2",
"seed": 42,
"previous_request_ids": ["…", "…", "…"],
"voice_settings": {
"stability": 0.4,
"similarity_boost": 0.75,
"style": 0.4,
"use_speaker_boost": true
}
}
Each parameter has a reason:
previous_request_idschains the requests, up to three previous ones, so the intonation flows from one block to the next.- A fixed
seedmakes a regeneration come out close to the previous one. voice_settings: the API default favors stability and sounds monotonous. Lower stability and higher style make the speech livelier. The values live in repository variables, to tweak without touching code.- Requests of up to 1,500 characters: long requests tend to lose energy at the end.
Pronunciation got its own file, because the voice got names wrong, including mine. Each name is spelled the way it's said in Portuguese, and the swap only applies to the text sent to the voice. The transcript on the site keeps the right spelling:
{
"Ghisi": "Guízi",
"fghisi": "f guízi",
"Caleb Mellas": "Quêileb Mélas",
"deploy": "deplói",
"PR": "pê érre"
}
Before starting, the script checks how many credits are left in the month, when the key has permission to. If the episode doesn't fit, it stops without spending anything, instead of ending up half done.
Step 4, part 2: post-production
The first episode had a flaw: the loudness dropped in some stretches. The cause was single-pass normalization, which keeps changing the gain along the audio. Post-production now works like this:
- Every part at the same loudness. The script measures each part that came from ElevenLabs and applies the gain needed to reach the target.
- Joining, with short pauses between blocks and shorter ones within a block.
- Soundtrack. It opens the episode alone, ducks when I speak and closes the episode.
- Two-pass normalization, to −16 LUFS, the podcast standard. The first pass measures, the second applies a single gain.
- ID3 tags: title, author, the cover and the notice that the narration is AI-generated.
- Duration and size in bytes, which the RSS feed requires.
Two-pass normalization, in ffmpeg:
# 1st pass: measure
ffmpeg -i bruto.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -
# 2nd pass: apply a single gain with the measured values
ffmpeg -i bruto.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:linear=true:measured_I=…:measured_TP=…:measured_LRA=…:measured_thresh=…:offset=… episodio.mp3
The soundtrack ducks under the voice with a sidechain filter: the voice itself controls the music's volume.
[music][voice]sidechaincompress=threshold=0.015:ratio=10:attack=60:release=700
I measured the result. With the music underneath, the speech loudness was the same as the voice alone: −16.6 LUFS in both cases.
The soundtrack is also made with code. It's an original track in the style of 1980s pop rock: a half-time shuffle, rhythm guitar with chorus on the offbeat, a lead guitar with vibrato and bends, bass, drums and keyboards, all synthesized note by note in Python. The guitar uses the Karplus-Strong algorithm, which simulates a plucked string with a noise buffer that gets progressively smoothed. Since the track is mine, I didn't have to worry about copyright to publish it.
Step 5: publishing
The MP3s don't go into git. They live in the releases of a public repository, separate from the site. The site's repository is private, and a private release doesn't open for players. Publishing uploads two files per episode: the finished episode and the voice alone, without the soundtrack.
The site is the blog itself, generated by Eleventy. Each episode is a Markdown file with the audio address, the duration and the size. The same build produces:
/podcast/, with the cover and the list of episodes;/podcast/<episode>/, with the player, the notes and the full transcript, which helps search and accessibility;/podcast/feed.xml, the RSS feed, with the podcast tags apps expect.
Each feed item has the essentials for the apps:
<item>
<title>De sênior a principal: o trabalho entre os times</title>
<enclosure url="https://fghisi.com.br/podcast/audio/2026-10-de-senior-a-principal.mp3"
length="7532377" type="audio/mpeg"/>
<itunes:duration>474</itunes:duration>
<itunes:explicit>false</itunes:explicit>
</item>
An episode with a future date doesn't get a page or a feed item until the day. The deploy runs every morning, so episodes can be scheduled.
For now, the podcast lives only on the blog. The feed is ready for podcast apps, with a 3000 × 3000 px cover and the tags they ask for. Taking the podcast to them is the next step.
The cover, by the way, also comes from a script: the letters of the site's logo are converted to curves from the font, and the result becomes a 3000 px PNG.
Orchestration
The plan had three workflows. Two stuck, plus the deploy the site already had:
| Workflow | Trigger | What it does |
|---|---|---|
| Script | push of notas.md on an ep/* branch |
Generates the script with Claude, if it doesn't exist yet, and opens the PR |
| Audio | gerar-audio label on the PR |
Checks the token, generates the voice, assembles the episode, keeps the MP3 on the run, publishes the release, writes the page and comments the link on the PR |
| Audio | remixar label on the PR |
The same, but reuses the already published voice: redoes only the mix, without spending credits |
| Deploy | merge to main and every day at 6 a.m. |
Builds the site, downloads the release MP3s into it and publishes to Hostinger |
The repository secrets are three: the ElevenLabs key, the voice_id and a GitHub token with access only to the audio repository. The Anthropic key is optional. Voice settings and the model live in variables, which aren't secret.
The tests cover the scripts offline, with mocked APIs: front matter, block splitting, pronunciation, the [VERIFICAR] refusal, request chaining, the ffmpeg commands and the episode page. For the audio itself, I installed ffmpeg and tested with macOS's own voices recorded at different volumes.
The plan and reality
A good part of the original design survived. But some decisions changed along the way:
| In the plan | In practice | Why |
|---|---|---|
| Astro | Eleventy | It's the generator the site already uses |
| Cloudflare R2 | GitHub releases | No new account; the public repository does the job |
| Creator plan and professional clone | Starter plan and instant clone | Validate everything before paying more |
| Script always via the API | Script in a conversation or via the API | Start without another key |
| Three workflows | Two, plus the deploy | Publishing reuses the site's deploy |
| MP3 played straight from GitHub | MP3 served by the site | iPhone Safari wouldn't play it |
| Voice only | Voice with a soundtrack | It made the episode feel like a podcast |
What went wrong
Worth telling, because it's where I learned the most.
The wrong token cost three voices. The token that publishes to the audio repository pointed at the wrong repository. The voice was generated, publishing failed, and the audio was lost along with the Actions machine. Three times, and each attempt spent credits. The fix is the same as in any system: fail early. Now the workflow checks the permission before generating the voice, by creating the release as a draft, and keeps the MP3 on the run as soon as it's ready.
The fix didn't apply right away. A pull request event runs the workflow from the merge of the branch with the base, and that merge wasn't refreshed when the fix landed on develop. One attempt still ran with the old version. Lesson: update the episode branch before trying again.
Actions couldn't open pull requests. The repository setting doesn't allow it, and the script workflow failed on its last step. Now it just warns, and the PR is opened by hand.
iPhone Safari wouldn't play. GitHub serves the release file as a download, with Content-Type: application/octet-stream, and iOS Safari refuses to play it inside the page. The fix was to serve the MP3 from the site itself: during deploy, the pipeline downloads each audio file into it.
The voice sounded robotic. Much of it came from the polished script and the default settings, and was fixed. The rest is the limit of the instant clone, which copies the timbre but not the way I talk.
What it costs
| Item | Cost |
|---|---|
| ElevenLabs, Starter plan | about US$ 5 a month, with credits for about two episodes |
| ElevenLabs, Creator plan (if it grows) | about US$ 22 a month, with the professional clone |
| Script with Claude | cents per episode via the API, or nothing if written in a conversation |
| GitHub, Actions and releases | US$ 0 |
| Hosting and domain | already paid for by the site |
Precautions
- Copyright. Brazil's copyright law (Law 9,610/98, article 46, item III) allows quoting excerpts for criticism or study, to a justified extent and citing author and work. That's why the prompt forbids long quotes and asks for paraphrase and commentary. I'm not a lawyer; when in doubt, it's worth asking one.
- Transparency. The podcast page, each episode and the MP3 tags say the narration is AI-generated from my voice. The first episode opens by explaining it.
- Voice security. The ElevenLabs key lives only in the secrets, the GitHub token only sees the audio repository, and the
voice_idisn't public anywhere. Two-step verification on the ElevenLabs account is the next step. - Company. The same rule as the articles: no examples from my team or the company I work for.
What I take from it
The podcast confirmed something I'd already seen with amardoar: AI speeds things up a lot, but what makes it safe is the process. Branch, pull request, review, tests and automated deploy. The same cycle we run in teams.
And it confirmed another: the most expensive mistake is the one that shows up at the end. Moving the checks to the start of the pipeline, before the step that costs money, is the first improvement I'd make in any similar automation.
The first episode is live. If you listen, let me know what you think.
Sources
Did you like this article?
Comments
No comments yet. How about being the first?