Skip to content

Local AI

Faceswap Lost to a LoRA: The 221-Still Render Rig

128 Pixels Is Not a Face

🎭⚖️🖼️

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.

swapped face crop128px
natively generated896×1152

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.

  • the entire resumability contractpython
    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
    par_pipeline.log
    ######## 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:

    stacking two adapters in one rendershell
    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
    1. Order matches --lora-scales positionally. Get them out of sync and you'll spend an hour blaming the dataset.
    2. Identity at 0.9; the object or technique a notch lower at 0.8 so it informs the scene without steamrolling the face.
    3. 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.

    Let us make some quick suggestions?

    Please provide your full name.
    Please provide your phone number.
    Please provide a valid phone number.
    Please provide your email address.
    Please provide a valid email address.
    Please provide your brand name or website.
    Please provide your brand name or website.