We built our own AI voice-over for a few cents a minute
Our team records every Recruitly tutorial in their own voices, and a dubbing tool used to turn those recordings into a native US or UK voice. We replaced it with our own in a day. Three days in, the team has run 52 takes and 95 minutes of finished narration through it, for a few cents a minute.
Every how-to video in the Recruitly Academy starts the same way. Someone on the team opens OpenScreen, records their screen, and talks through the feature. Most of our team is in India and most of our customers are in the UK and the US, so the narration is in Indian-accented English for an audience that mostly is not. We ran each recording through Dubverse, which re-voiced it in a native accent, and it did that job well. Then someone downloaded the result and uploaded it to our library by hand, and we did that 48 times.
When it became clear we could not count on that tool being there, I looked at what it would take to build our own. The answer was one day, and this post is how, what it costs now that we have measured it, and the handful of decisions that decided whether it survived its first morning with real users.
Everything expensive was already in the building
The reason this took a day is that I did not have to build the expensive parts. I went through the codebase before writing a line and listed what a dubbing pipeline needs against what we already run. Almost all of it was there, paid for and wired, for other reasons.
| What dubbing needs | What we already had |
|---|---|
| A video library | Our CDN file manager on Cloudflare R2, with upload, metadata and a public URL for every video. |
| A voice engine | ElevenLabs, already connected through our AI gateway for speech-to-text, so no new key and no new secret. |
| A transcript with timings | ElevenLabs Scribe on a published chain, returning every word with its start and end time. We already use it to transcribe recordings. |
| Somewhere to run ffmpeg | The document server that does our OCR and CV conversion: an i9 with 128 GB of RAM, mostly idle, with a deploy pipeline. |
| A job queue and an admin UI | The same pattern every internal tool here uses: a row in D1, a worker, a callback, a page in System Admin. |
What was missing was ffmpeg on that box, one call to a voice, and the glue between them. If you are weighing build against buy, this is the inventory to do first, because the price of building is the price of what you do not already own.
Two ways to change a voice, and we let our ears decide
Because the source and the result are the same language, there are two ways to do this, and they fail in different places. The first is to re-speak: transcribe the recording, then have a native voice read the transcript. The accent is guaranteed, because the new voice never hears the old one, and the cost is that you have to fit the new audio back onto the original timing. The second is a voice changer, speech to speech: it keeps the original timing exactly, for free, and the risk is that the source accent bleeds through, which would defeat the whole exercise.
I built both behind one endpoint and wrote a probe script that takes one real Academy video and produces both versions, so the default would be chosen by listening to them. Re-speak won, and all but two of the 52 takes since have used it.
The pipeline, and the picture we never touch
A take runs like this. The document server pulls the audio out of the video and leaves the picture alone. Scribe transcribes it by URL and returns every word with its timings. The words are grouped into lines. Each line is sent to ElevenLabs and comes back spoken in the chosen voice, several at once. The lines are placed on a silent track at the moments the originals were said, and ffmpeg joins that track to the original picture.
The decision I would defend hardest is the last step. The video stream is copied, never re-encoded. Only the audio track changes, so the picture is bit for bit what the team recorded, and joining the two costs a couple of seconds of CPU, where a re-encode would cost many times that and soften the text on screen. Across the 52 takes the average one came back in fourteen seconds.
Drift is what kills a screen recording
These are videos that say "click here". If the voice arrives half a second after the cursor, the video is useless, and a new voice never takes the same time to say a sentence as the old one did. The obvious build, speak each line and join them end to end, drifts a little more with every sentence until the narration is describing a screen that went by ten seconds ago.
So a line is placed at the moment it was originally spoken, or where the previous line actually finished, whichever is later. An overrun is absorbed at the next natural pause, and it can never push the rest of the video along with it.
The first version also sped a line up, to a ceiling of 1.3 times, when it ran past its slot. On the first morning the team heard it straight away: the opening "Hi everyone, welcome to Recruitly" sounded hurried. A US voice reading a script paced for Indian English runs ten to thirty percent longer, so that ceiling was being hit all the time. We changed the default the same day. The voice now keeps its natural pace, a line may start a fraction of a second after its cue, the job says exactly how many lines did and by how much, and when the narration outlasts the picture the last frame is held. A sentence that starts slightly late is fine, and a rushed one sounds like a machine.
The boring decisions that got it through day one
Job state lives in the database. The document server runs eight separate server processes. A job kept in memory is invisible to the next status check, which lands on a different process, so every job is a row in D1 with a heartbeat, and a job whose heartbeat stops turns into a Retry button fifteen minutes later.
The concurrency limit is a directory of lock files. A semaphore only exists inside one process and there are eight. Two takes run at once across the box, ffmpeg runs at low priority on four threads, and the OCR and CV work that live users are waiting on never notices.
The audio is assembled in Python, at exact byte offsets. The ffmpeg way is a filter graph with sixty inputs. Writing each spoken line into a silent track at a computed offset is exact, it can be unit tested without a network, and it has no mixing behaviour to be surprised by.
The gateway refuses bodies over 25 MB, and five minutes of raw audio is 26. Re-speak never uploads audio at all, because Scribe reads the video by URL. The voice changer sends compressed mono. I found this limit during the build, and it would have been a production incident on the first long video.
Voice ids are read from the account. They are fetched from ElevenLabs into a table and an admin enables the ones we use. We also ask ElevenLabs for the voices most similar to the narrator in a given video, so a recording by one of our team can be matched to a voice that suits it.
Edit two lines, pay for two lines
What the team missed most from Dubverse was the editor: the transcript beside the video, fix a word, add a pause, hear it. So the transcript became a script studio with a timeline under the video. Pauses are written as a break tag, which ElevenLabs honours, and every other tag is stripped, because a text box that reaches a speech engine must never become a way to inject markup into it.
Then I looked at the bill. Every Generate was re-speaking every line, so fixing one word in a forty-line script paid for forty lines. Now a line whose text and voice have not changed is never sent again; its audio is reused from the earlier take. Of the 1,213 lines in the takes so far, 900 were reused. Three quarters of what the team generated cost nothing the second time. The same idea powers the preview: before committing to a take, the studio speaks only the lines that changed and plays them over the video at their cues.
What it costs, measured
I did not want an estimate in this post, so these are from our own job table. ElevenLabs returns the cost of every call in a response header, and each job adds those up. A call that comes back without the header is recorded as unmetered and the job says its figure is a floor, because a missing number must never read as free.
In the first three days, four people ran 52 takes and produced 95 minutes of finished narration. The 42 metered takes covered 88 minutes and used 20,933 credits, which is 237 credits for each finished minute, retakes included. At ElevenLabs' rates that is somewhere between two and five cents a minute depending on the plan. The document server, the storage and the CDN were already paid for.
For comparison, ElevenLabs' own turnkey dubbing API is priced at $0.33 to $2.20 a minute, and dubbing SaaS products sit around one to two dollars a minute. Those products do more than we need, translation above all. We needed the same words in a different voice, and against a dollar a minute our figure is twenty to fifty times cheaper. The bigger gain is one I cannot put a price on: the finished video lands in our own library with its metadata, and nobody downloads and re-uploads anything.
What I would tell another team
- Inventory what you already own before you price the build. We owned the library, the voice engine, the transcripts and the compute. The build was three small things.
- When there are two ways to do it, build a probe and listen. An afternoon arguing about accent bleed would have settled nothing that ten minutes with two files did.
- Copy the video, never re-encode it. If only the audio changes, only the audio should be touched.
- Anchor every line to its own cue. Anything that accumulates error over the length of a video will.
- Meter from the provider's own numbers, and treat a missing number as unknown. Then use the meter, because ours is what showed that every retake was paying for the whole script.
- Put it in front of the people who use it on day one. The rushed opening line, the missing editor and the per-line menu all came from the team within hours, and all of them were built the same day.
Credit where it is due. ElevenLabs does the hard part, a voice good enough that we publish it to customers, and Dubverse showed us what a good editor for this looks like. What we built is the part in between, shaped around how our team works, and it is the same argument we made about the phone system and about putting a new model into production in a day. When the foundations are yours, the next thing is cheap.
Written for engineering teams paying by the minute for something they already own most of.



