How to Make UGC Video Ads at Scale: One Clip, Fifty Hook Variants

You shot one good clip. Now the media buyer wants fifty versions of it, each with a different opening line, three aspect ratios, and filenames that match the ad sets in Ads Manager. The shoot took an afternoon. The variants will take a week if you open an editor.
Quick answer: A UGC video ad test is won in the variant matrix, not the shoot. Take one base clip and render it once per cell of a hook text × CTA × aspect ratio grid, changing exactly one variable per row so the results are readable. In FFmpeg that's acropto reframe, thendrawboxanddrawtextto stamp the hook, batched from a CSV so every output filename maps back to an ad set ID. If you'd rather not install an encoder, hold a render queue open, or watch a workflow time out on file 90 of 150, send the base clip and the hook list to FFmpeg Micro as one API call and pull the finished variants back.
The expensive part of UGC ads is assembly, not generation
Conventional wisdom says the bottleneck in UGC video ads is getting the footage: find creators, brief them, wait for delivery. That's mostly true the first month. But once you have five clips that convert at all, the cost stops being production and becomes combinatorics. Five base clips, ten hooks, three placements is 150 renders, and every one of them is the same twenty seconds of video with different pixels in the top 260 rows.
Every AI avatar tool on the market sells you the first half, the generation. Almost nobody ships the second half, the assembly pipeline that turns one approved clip into a controlled test. That gap is where the week goes.
The mechanism that fixes it isn't a faster editor. It's never opening an editor: treat the base clip as an immutable master and treat each variant as a deterministic function of a row in a spreadsheet.
Design the variant matrix before you render anything
A variant matrix is a table where each row is one output file and each column is one thing you're allowed to change. Write it first, in a spreadsheet, and the render step becomes mechanical.
| base | hook | cta | ratio | adset | output |
|---|---|---|---|---|---|
| c04 | h07 | cta2 | 9x16 | 120215 | ugc_c04_h07_cta2_9x16.mp4 |
| c04 | h08 | cta2 | 9x16 | 120216 | ugc_c04_h08_cta2_9x16.mp4 |
| c04 | h09 | cta2 | 9x16 | 120217 | ugc_c04_h09_cta2_9x16.mp4 |
Three rows, one changing column. That's a hook test. The moment a row changes both the hook and the ratio, the row stops being a test and becomes a guess, because a 9:16 variant on Reels and a 4:5 variant in the Facebook feed get different placements, different audiences, and different CPMs before the copy has a chance to matter.
Keep the hooks themselves in files, not in the spreadsheet. hooks/h07.txt holds the literal text, including line breaks. This matters more than it sounds like it should, and the pitfalls section explains why.
Reframe first, then stamp the hook
Order of operations decides whether the pipeline works. Reframe the master to the target aspect ratio, then draw text on the reframed canvas. Do it the other way around and the 4:5 crop slices your hook in half.
From a 1080x1920 vertical master, the three common placements are pure crops and one pad:
# 4:5 (1080x1350) for the Facebook and Instagram feed
crop=1080:1350:0:285
# 1:1 (1080x1080)
crop=1080:1080:0:420
# 16:9 (1920x1080), padded because there is no 16:9 hiding in a vertical frame
scale=-2:1080,pad=1920:1080:(ow-iw)/2:0:color=black
Now the hook. A colored bar plus centered text is the format that survives sound-off scrolling, and it's two filters:
ffmpeg -y -i masters/c04.mp4 -filter_complex \
"[0:v]crop=1080:1350:0:285,\
drawbox=x=0:y=0:w=iw:h=260:color=0xFACC15@1:t=fill,\
drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:\
textfile=hooks/h07.txt:fontcolor=0x111111:fontsize=64:line_spacing=12:\
x=(w-text_w)/2:y=(260-text_h)/2,\
drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:\
textfile=ctas/cta2.txt:fontcolor=white:fontsize=52:box=1:boxcolor=black@0.55:\
boxborderw=18:x=(w-text_w)/2:y=h-260:enable='gte(t,16)'[v]" \
-map "[v]" -map 0:a -c:v libx264 -crf 20 -preset veryfast -pix_fmt yuv420p \
-c:a copy out/ugc_c04_h07_cta2_4x5.mp4
The second drawtext is the CTA, and enable='gte(t,16)' holds it back until the last few seconds of a twenty second clip. That's the whole variant. Everything else in the file is byte-identical intent from the same master, which is exactly what makes the test readable.
Two placement rules worth encoding once. TikTok's right rail and caption area covers roughly the bottom fifth of the frame, so a CTA at y=h-260 on a 1350 tall canvas sits above it. And the top bar has to clear the status area on 9:16, which is why 260 pixels of bar starting at y=0 works on a 1920 tall canvas but crowds a 1080 square.
Batch the renders and name files after the ad set
Render every variant from the master, never from another variant. Chaining a 1:1 render off a 4:5 render stacks two lossy encodes on the same footage, and by the third generation the yellow hook bar has visible mosquito noise around the letters.
A CSV and a loop is the entire batch layer:
tail -n +2 matrix.csv | while IFS=, read -r base hook cta ratio adset out; do
case "$ratio" in
9x16) RF="crop=1080:1920:0:0" ;;
4x5) RF="crop=1080:1350:0:285" ;;
1x1) RF="crop=1080:1080:0:420" ;;
esac
ffmpeg -y -i "masters/$base.mp4" -filter_complex \
"[0:v]$RF,drawbox=x=0:y=0:w=iw:h=260:color=0xFACC15@1:t=fill,\
drawtext=fontfile=$FONT:textfile=hooks/$hook.txt:fontcolor=0x111111:\
fontsize=64:x=(w-text_w)/2:y=(260-text_h)/2[v]" \
-map "[v]" -map 0:a -c:v libx264 -crf 20 -preset veryfast \
-pix_fmt yuv420p -c:a copy "out/$out"
echo "$adset,$out" >> upload_manifest.csv
done
The manifest is the part people skip and then regret. When 150 files land in a folder called out/, the only thing standing between you and a mislabeled test is a filename that encodes base, hook, CTA, and ratio in a fixed order, plus a two column map from ad set ID to file.
On a laptop, a twenty second 1080x1920 clip at -preset veryfast -crf 20 re-encodes in a few seconds, so 150 variants is roughly ten to fifteen minutes of serial CPU time. That's fine on your own machine and not fine inside an n8n or Make run, where a fifteen minute synchronous step is a timeout with extra steps. If the matrix lives in a spreadsheet and the renders should happen without a machine of yours staying awake, Google Sheets works well as the queue and FFmpeg Micro runs the render as one API call per row, with no encoder to install and no long-running job to babysit. The Text Hook Variants blueprint is the click-through version of the command above: upload one video, give it up to three opening hooks, get three variants back on a colored bar. Use it to sanity-check the format before you wire the full matrix.
Pitfalls that break the batch on file 12
Most failures here are text handling, not video. FFmpeg's drawtext treats :, ', %, and \ as syntax, so a hook like I tried it for 30 days: here's what happened will either error out or silently render wrong. Using textfile= instead of text= sidesteps all of it, which is why hooks live in files.
The other recurring failures:
- Missing fonts in containers.
fontfilepointing at a path that exists on your Mac and not in the image gives you "Cannot find a valid font" on every row. Ship the TTF inside the image and reference an absolute path. The font and fontconfig layer is usually the reason an FFmpeg container that "works locally" doesn't. - Missing
-pix_fmt yuv420p. Meta's uploader and QuickTime both reject or misplay yuv444p output from some sources. One flag, no downside for ad delivery. - Emoji and non-Latin hooks. DejaVu Sans Bold has no emoji glyphs, so the character renders as a blank box in every variant using that hook. Swap in a font with the coverage you need, or drop emoji from the hook set.
- Word wrap.
drawtextdoesn't wrap. Long hooks run off the frame edge, so put the line breaks in the text file yourself and cap hooks at two lines. If you need per-word timing or animated text rather than a static bar,drawtextis the wrong filter and ASS subtitles are the right one. - Silent audio drift.
-c:a copyis correct when you never touch the audio. Add a music bed to some rows and not others and you've introduced a second variable into a hook test.
When this pipeline is the wrong tool
The stamp-and-batch approach assumes the footage is fixed and the message is variable. If your test needs a different actor, a different script read, or a different room, no filter chain produces that, and you're looking at an AI avatar generation product or another shoot. Assembly can't create performance it wasn't given.
Two other boundaries. If a variant needs keyframed motion, animated lower thirds, or per-word timing, that's a motion graphics job and a template rendering service fits better than a filter chain. And if the whole test is six files, open your editor. The pipeline pays off somewhere around thirty outputs, when the cost of naming files by hand exceeds the cost of writing the CSV.
FAQ
How many UGC video variants do I need for a readable hook test?
A hook test needs enough spend per variant to separate signal from noise, which in practice means fewer variants than most people build. Six to ten hooks against one base clip, each in a single aspect ratio, gives you a clean read; thirty hooks against one clip splits budget so thin that nothing reaches significance. Add base clips before you add hooks.
Can I change the hook without re-uploading the base clip?
Keeping one immutable master on disk or in object storage means every variant is generated from that one file, so changing a hook only re-renders the output, never the source. This is why the loop above reads masters/$base.mp4 every time instead of chaining renders. It also means a new hook set costs you a render, not an upload.
What aspect ratios should UGC video ads ship in?
UGC video ads generally ship in three ratios: 9:16 for TikTok, Reels, and Stories, 4:5 for the Facebook and Instagram feed, and 1:1 where a square still performs. Shoot the master at 1080x1920 and every one of those is a crop, so you never need a second shoot for a placement. A 16:9 version has to be padded and usually looks like padding, so treat it as a last resort rather than a default.
Does re-rendering the same clip 50 times hurt quality?
Re-rendering from the same master fifty times produces fifty single-generation files, all of equal quality, because each render starts from the original footage. Quality loss only compounds when you render a variant from another variant. At -crf 20 on a twenty second social clip, one encode is visually transparent, and encoding once for every platform beats exporting per network anyway.
Why does my drawtext command fail with an escaping error?
FFmpeg's drawtext filter parses colons, apostrophes, backslashes, and percent signs inside the text= value as filter syntax, so any real ad copy will eventually break the command. Writing the copy into a .txt file and passing textfile=hooks/h07.txt avoids the parser entirely, and it makes hooks reviewable in version control.
If your matrix is already in a spreadsheet and the only thing missing is a renderer that won't time out at file 90, sign up free and run the first batch against the API. The video ads use case page has the rest of the assembly steps, from watermarking to platform-ready delivery.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Fix "moov atom not found" on Uploaded Videos in Your Pipeline
"moov atom not found" almost never means a corrupt video. It means a truncated upload or a Range request that missed the trailing moov box. Check and fix it.

Skip the Media Service: Video Processing API for a SaaS Product
A video processing API for a SaaS product beats a media microservice: real TCO of FFmpeg on Lambda vs Fargate, plus a one-call endpoint you ship today.

Put Two Videos Side by Side with FFmpeg: hstack, vstack, xstack
The ffmpeg side by side recipe that actually works: hstack needs matching heights, vstack matching widths, and shortest=1 stops the short clip freezing.
Skip the command line
The Text Hook Variants blueprint runs the same job for you: upload, done.
Run it (free)