Originally published at ffmpeg-micro.com
You've got an ElevenLabs API key generating polished AI narration in seconds. But now you need that voiceover merged into a video file, and you're staring at FFmpeg documentation wondering how to get -filter_complex amix working without blowing up your audio levels.
If you're running serverless (Vercel, AWS Lambda, Cloud Functions), you can't even install FFmpeg. And if you're on a VPS, you'll spend more time debugging audio sync and codec mismatches than building your actual product.
Quick answer: Generate voiceover audio with the ElevenLabs API, then merge it into video with one FFmpeg Micro API call. No FFmpeg binary needed. Works from any language or automation tool that can make HTTP requests.
The pipeline: text to narrated video
The workflow has two steps:
- ElevenLabs converts your script text to an MP3 audio file
- FFmpeg Micro merges that MP3 into your source video, replacing or mixing with the original audio
Both are REST APIs. No binaries, no containers, no local processing. The entire pipeline runs with two HTTP calls.
Step 1: Generate voiceover with ElevenLabs
Call the ElevenLabs text-to-speech endpoint to generate narration. You'll get back an MP3 file that you host somewhere accessible via URL (an S3 bucket, Supabase Storage, or any public URL works).
curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM" \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "FFmpeg Micro lets you process video with a single API call. No installation, no infrastructure.",
"model_id": "eleven_multilingual_v2"
}' \
--output voiceover.mp3
Upload the resulting voiceover.mp3 to any publicly accessible URL. For this tutorial, assume it's at https://storage.example.com/voiceover.mp3.
If you're using FFmpeg Micro's upload flow, you can use the presigned upload endpoint to get a GCS URL directly:
# 1. Get a presigned upload URL
curl -X POST "https://api.ffmpeg-micro.com/v1/upload/presigned-url" \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "voiceover.mp3", "contentType": "audio/mpeg", "fileSize": 45000}'
# 2. Upload the file to the returned URL
curl -X PUT "<uploadUrl from response>" \
-H "Content-Type: audio/mpeg" \
--data-binary @voiceover.mp3
# 3. Confirm the upload
curl -X POST "https://api.ffmpeg-micro.com/v1/upload/confirm" \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "<filename from presign response>", "fileSize": 45000}'
Step 2: Merge voiceover into video with FFmpeg Micro
Now merge the audio into your video. FFmpeg Micro's transcode endpoint takes multiple inputs, so you pass the video as the first input and the voiceover audio as the second:
curl -X POST "https://api.ffmpeg-micro.com/v1/transcodes" \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
{"url": "https://storage.example.com/source-video.mp4"},
{"url": "https://storage.example.com/voiceover.mp3"}
],
"outputFormat": "mp4",
"options": [
{"option": "-c:v", "argument": "copy"},
{"option": "-c:a", "argument": "aac"},
{"option": "-b:a", "argument": "192k"},
{"option": "-map", "argument": "0:v:0"},
{"option": "-map", "argument": "1:a:0"},
{"option": "-shortest", "argument": ""}
]
}'
This maps the video stream from the first input (0:v:0) and the audio stream from the second input (1:a:0). The -shortest flag trims the output to whichever track ends first, so a 60-second video with a 45-second voiceover produces a 45-second result.
The -c:v copy flag skips video re-encoding entirely, which makes the operation fast. Only the audio gets encoded to AAC.
Mixing voiceover with background audio
If your video already has music or ambient audio and you want to layer the voiceover on top (instead of replacing it), use FFmpeg's amix filter:
curl -X POST "https://api.ffmpeg-micro.com/v1/transcodes" \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
{"url": "https://storage.example.com/source-video.mp4"},
{"url": "https://storage.example.com/voiceover.mp3"}
],
"outputFormat": "mp4",
"options": [
{"option": "-filter_complex", "argument": "[0:a][1:a]amix=inputs=2:duration=first:dropout_transition=2[aout]"},
{"option": "-map", "argument": "0:v:0"},
{"option": "-map", "argument": "[aout]"},
{"option": "-c:v", "argument": "copy"},
{"option": "-c:a", "argument": "aac"}
]
}'
The amix filter combines both audio tracks. duration=first keeps the output the same length as the original video. dropout_transition=2 adds a 2-second fade when the shorter track ends, preventing an abrupt cut.
Node.js: the full pipeline in one script
Here's the complete flow in Node.js, from ElevenLabs text-to-speech to merged video:
const ELEVENLABS_KEY = process.env.ELEVENLABS_API_KEY;
const FFMPEG_KEY = process.env.FFMPEG_MICRO_API_KEY;
async function createNarratedVideo(text: string, videoUrl: string) {
// 1. Generate voiceover with ElevenLabs
const voiceRes = await fetch(
'https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM',
{
method: 'POST',
headers: {
'xi-api-key': ELEVENLABS_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
model_id: 'eleven_multilingual_v2',
}),
}
);
// Upload the audio to your storage and get a public URL
const audioBuffer = await voiceRes.arrayBuffer();
const audioUrl = await uploadToStorage(audioBuffer); // your upload logic
// 2. Merge audio + video with FFmpeg Micro
const transcodeRes = await fetch(
'https://api.ffmpeg-micro.com/v1/transcodes',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${FFMPEG_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: [{ url: videoUrl }, { url: audioUrl }],
outputFormat: 'mp4',
options: [
{ option: '-c:v', argument: 'copy' },
{ option: '-c:a', argument: 'aac' },
{ option: '-map', argument: '0:v:0' },
{ option: '-map', argument: '1:a:0' },
{ option: '-shortest', argument: '' },
],
}),
}
);
const job = await transcodeRes.json();
// 3. Poll until done
let status = job.status;
while (status === 'queued' || status === 'processing') {
await new Promise((r) => setTimeout(r, 3000));
const pollRes = await fetch(
`https://api.ffmpeg-micro.com/v1/transcodes/${job.id}`,
{ headers: { 'Authorization': `Bearer ${FFMPEG_KEY}` } }
);
const updated = await pollRes.json();
status = updated.status;
}
// 4. Get the download URL
const dlRes = await fetch(
`https://api.ffmpeg-micro.com/v1/transcodes/${job.id}/download`,
{ headers: { 'Authorization': `Bearer ${FFMPEG_KEY}` } }
);
const { url } = await dlRes.json();
return url;
}
Common pitfalls
Audio and video length mismatch. If your voiceover is longer than the video, the extra audio gets cut (with -shortest). If you need the video to loop to match longer narration, you'll need to loop the video input separately first.
Forgetting -map flags. Without explicit stream mapping, FFmpeg tries to auto-select streams. When you have two inputs, this often picks the wrong audio track. Always specify -map when merging separate audio and video files.
ElevenLabs rate limits. The free ElevenLabs tier has a 10,000 character monthly limit. For production pipelines processing dozens of videos, you'll need a paid plan. Batch your TTS calls and cache generated audio to avoid redundant API usage.
Audio level imbalance with amix. The amix filter normalizes volume by default, which can make both tracks quieter. If you need the voiceover louder than the background, add volume adjustment: [1:a]volume=1.5[vo];[0:a][vo]amix=inputs=2....
CLI comparison
If you had FFmpeg installed locally, the merge command would be:
ffmpeg -i video.mp4 -i voiceover.mp3 \
-c:v copy -c:a aac -map 0:v:0 -map 1:a:0 \
-shortest output.mp4
Same operation. The FFmpeg Micro API just wraps this into a REST call so you don't need FFmpeg installed.
FAQ
Can I use voices other than the default in ElevenLabs?
Yes. Replace the voice ID in the URL (21m00Tcm4TlvDq8ikWAM is "Rachel"). ElevenLabs has dozens of preset voices and supports custom voice cloning. Any voice that produces an audio file works with this pipeline.
What audio formats does FFmpeg Micro accept?
FFmpeg Micro accepts MP3, WAV, FLAC, and AAC audio files as inputs. ElevenLabs outputs MP3 by default, which works without any conversion.
Can I add subtitles from the voiceover text too?
Yes. FFmpeg Micro has a /v1/transcribe endpoint that generates SRT files from audio using Whisper. You can chain the pipeline: generate voiceover, transcribe it to SRT, then burn subtitles into the video in a second transcode call.
How long does the merge take?
With -c:v copy (no video re-encoding), a merge typically completes in under 10 seconds regardless of video length. If you use video re-encoding options, expect roughly real-time processing speed.
Last verified: July 2026. Code examples tested against FFmpeg Micro API v1 and ElevenLabs API v1.
Top comments (0)