Everyone reaches for faceswap first. It's free, it's instant, and it demos beautifully — you generate a person, you paste your person's face on, done.
Then you put it on a billboard-sized frame and the illusion dies. I spent a week going down that road before admitting the ceiling was structural, not a settings problem. Here's why the swap loses, and what the rig that replaced it actually looks like at 221 stills.
128 pixels is not a face
The popular swappers operate on a small aligned crop — in the region of 128×128 — and then blend that crop back into your image. Your output frame is 1080×1920. The maths does the rest.
Upscaling a 128px crop to fill a face that occupies 500 pixels of a vertical ad gives you the plastic look you've seen a thousand times. It survives a thumbnail. It does not survive someone watching full-screen on a phone.
And the failure isn't uniform, which is worse. It's fine straight-on and falls apart exactly where ads live:
Faceswap onto a generated double
- Soft, low-frequency face on a sharp body — reads as uncanny before anyone can say why
- Profiles collapse; there's no side-of-head data in the crop
- Occlusion breaks it — a hand, a harness strap, a ladder rung across the jaw
- Motion makes it shimmer, because each frame is blended independently
- Identity is a layer, so lighting never truly matches the scene
Character LoRA, generated natively
- The face is rendered at full resolution as part of the image
- Profiles work — if you shot profiles into the dataset
- Occlusion is composed, not patched over
- Scene lighting hits the face because it is the scene
- The seed bakes in the environment, so the frame is already correct
That last point is the one I underrated. When you animate a still, the model clings to what's in the frame. Anchor on a studio photo of your person and the video drifts back toward a grey studio backdrop. Generate them already on the roof, already in the harness, already in the storm light, and the motion stage has nothing to drift to. One shot, right setting, real face.
The honest trade
Faceswap is minutes of setup and seconds per frame. A LoRA is a 20-minute shoot, a training run and roughly 9 minutes per still. You are buying resolution and robustness with time. Below a certain output size, faceswap genuinely wins — and if your deliverable is a 400px social thumbnail, take the cheap road.
The rig: 221 stills, one flat folder
Once likeness is solved, the work becomes logistics. Fifty scenes turns into 221 once you want variations, and at 9 minutes each that's a queue, not a session. The rig is four pieces.
A prompt bank, authored in themes
50 prompts across 10 themed batches of five — hero, on-roof inspection, safety kit, wardrobe variety, expressions, lighting, camera angles, wide establishing, presenter-to-camera, props. Each entry carries its own name, seed and dimensions. Themes matter: you want coverage, not 50 rolls of the same dice.
A runner that treats the file as the state
Every scene maps to one output filename. Exists? Skip. Doesn't? Render. That single rule makes the batch resumable after a crash, restartable after an edit, and safe to run twice.
One flat output folder
No ad_ideas/, no per-theme subfolders, no clever hierarchy. Byproducts, drafts and rejects go to a scrap folder — never into the delivery folder. A batch you have to hunt through is a batch you don't use.
Contact sheets for review
Nobody opens 221 PNGs. The runner tiles each batch into a single sheet with labels, so the review is one image and a list of names to redo.
def pending(scenes, out_dir): return [s for s in ordered(scenes) if not os.path.exists(os.path.join(out_dir, s.name + ".png"))]# review output: names, not files# REJECTS.txt -> redo.py re-queues exactly those scenes, nothing else
######## PERSISTENT RENDERER — 221 scenes to go (model stays resident) ########
loading z-image-turbo q4 + character LoRA (once)…
model resident after 41s
🖼 insp_chalk_03 (seed 10412 896x1152, 9 steps) [187 left]
✅ still insp_chalk_03 (247s)
🧹 purged — compressor 4.2GB -> 0.6GB
✅ still safety_ladder_lock_02 (238s)
❌ still hero_wide_07 FAILED: allocation of 4.9GB failed — requeued
Stacking adapters instead of prompting harder
Some things a text prompt simply will not render consistently. In my case it was a specific inspection technique and a particular safety-harness rig — described in words, the model produced a different plausible interpretation every time.
The fix isn't more adjectives. It's a second small LoRA trained on that one thing, loaded alongside the person:
mflux-generate-z-image-turbo --model Tongyi-MAI/Z-Image-Turbo -q 4 \ --lora-paths person.safetensors technique.safetensors \1 --lora-scales 0.9 0.8 \2 --steps 9 --width 896 --height 1152 \ --prompt "prsn1 man, on a corrugated metal roof, overcast raking light"3
- Order matches
--lora-scalespositionally. Get them out of sync and you'll spend an hour blaming the dataset. - Identity at 0.9; the object or technique a notch lower at 0.8 so it informs the scene without steamrolling the face.
- The trigger token still leads. A stacked adapter doesn't replace the token — it adds a second thing the prompt can reach for.
Adapters compose. Think of them as small, cheap, single-purpose modules rather than one giant model you keep re-training — a new technique costs a 20-image set and an hour, not another full run on the person.
Where text prompts genuinely give up
Specific anatomical or structural geometry — a particular animal's markings, a fanned frill, an embroidered wordmark — does not arrive from adjectives, no matter how many you stack. When you hit that wall, the move is image-to-image from a reference, or a dedicated adapter. Rewriting the prompt for the fifth time is how you lose an hour.
Upscaling: the boring answer is right
I tried a 4× ESRGAN pass on the hero stills. Technically it worked — 3584×4608 out of an 896×1152 in — and the result was underwhelming, because pixel upscalers can only sharpen what's already there. They add resolution, not detail.
The better move is a low-strength image-to-image pass on the same model that made the still:
mflux-generate-z-image-turbo ... --image-path in.png --image-strength 0.6
Around 0.6 the model re-renders with real new detail — skin texture, fabric weave, edge micro-contrast — while holding identity, because the same LoRA is still loaded. Generative upscaling beats interpolation whenever you own the generator.
The line I won't cross
This is a synthetic human in advertising, so: consent is per-use, not blanket. The person in these renders agreed to this campaign, knows where the adapter lives, and can have it deleted. The renders show them doing things they actually do for work — nothing that implies a claim, an endorsement or an event that never happened.
That's not legal advice, it's the floor. The technology is a week's work; the trust is the asset.
The verdict
Faceswap is a demo. A character LoRA is a production dependency — slower, heavier, and the only one of the two that holds up at full resolution, in profile, under occlusion, in motion.
Budget for it honestly: one honest shoot, one training run, and a queue that runs while you're asleep. The dataset and config are covered here, the rendering rig survives the night because of five unglamorous rules, and the loop itself is the same shape whether it's making pictures or shipping code.
Want a brand face that scales without a call sheet every fortnight? Start with a conversation, or browse the work first.