Skip to content

· 15 min read

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.

The first design: I act at two points and the rest runs on its own I act at two points; the rest runs on its own me automatic set up once 1. Book notes2. Script3. Review in the pull request 4. Audio in my voice5. Post-production6. Publishing Voice cloneVoice guide and prompt I write the notesClaude writes the script and episode notes I adjust and approveElevenLabs, block by block ffmpeg: intro, -16 LUFS, ID3 tagsPublished MP3, page and RSS feed 1 to 3 h of recordingMy style, in writing
The first design of the pipeline, before any code: two steps of mine, four automatic ones and two blocks set up once.

Almost all of it was built. What changed along the way is further down, in "The plan and reality".

The flow of an episode

The flow of an episode: notes, script, review, audio and publishing NotesScriptReviewÁudioPublishing notas.md on anep/… branch Claude + voiceguide I read it aloudin the pull request ElevenLabs+ ffmpeg release, pageand RSS feed MEAIMEGITHUB ACTIONSDEPLOY TWO HUMAN STEPS: THE NOTES AND THE REVIEW. THE REST IS AUTOMATIC.
The flow of an episode: I write the notes and review the script; the rest runs on GitHub Actions.

There are six steps. Two are mine and the others run on their own:

  1. Voice cloning, done once and reused in every episode.
  2. Notes, a notas.md file I write on an ep/<date>-<topic> branch. It's the only fully manual step, and it's what guarantees the episode carries my opinion.
  3. Script, written with Claude from the notes and a voice guide.
  4. Review, in the pull request. I read the script out loud and adjust whatever doesn't sound like me.
  5. Audio: the voice is generated on ElevenLabs and assembled in ffmpeg, with consistent loudness and a soundtrack.
  6. 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:

  1. Room: a quiet room, with curtains, a rug or clothes nearby to dampen echo, and no fan running.
  2. Gear: the microphone always at the same distance, about a hand's width from the mouth.
  3. Format: WAV, 44.1 or 48 kHz, mono, no music and no aggressive noise reduction.
  4. 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_ids chains the requests, up to three previous ones, so the intonation flows from one block to the next.
  • A fixed seed makes 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 mix: the soundtrack opens the episode, ducks under the voice and closes it 0 s4 sVOICE ENDS+10 s voicevoicevoice opening:soundtrack only in the pauses, the soundtrack rises a little closing:it rises and fades UNDER THE VOICE, THE SOUNDTRACK DUCKS ON ITS OWN (SIDECHAIN). THE EPISODE ENDS AT -16 LUFS.
The mix of every episode: the soundtrack opens, stays low under the voice and closes.

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:

  1. Every part at the same loudness. The script measures each part that came from ElevenLabs and applies the gain needed to reach the target.
  2. Joining, with short pauses between blocks and shorter ones within a block.
  3. Soundtrack. It opens the episode alone, ducks when I speak and closes the episode.
  4. Two-pass normalization, to −16 LUFS, the podcast standard. The first pass measures, the second applies a single gain.
  5. ID3 tags: title, author, the cover and the notice that the narration is AI-generated.
  6. 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_id isn'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