Skip to content

Local AI

Training a Likeness LoRA on a Real Person for Ads

25 Photos, One Trigger Token, Zero Studio Days

📸🧬🪪

The brief: fifty ad stills of the same real human — the bloke who fronts the client's ads — on roofs, in a claims office, in three wardrobes and four lighting conditions. Shooting that properly is a multi-day production with weather risk, a safety officer and a photographer's invoice.

What it actually took: twenty minutes with a phone, twenty-five photos, and a training run that finished while I slept.

This is the practical version — the shot list, the captions, the config that worked, and the two things that still don't.

01 shoot~25 phone stills, 20 min
02 captiontrigger token + what varies
03 trainrank-8 LoRA, overnight
04 generate9 steps, 896×1152
05 compositereal logo in post

Before any of this

You are training a model on a real person's face. Get explicit, informed, written consent — for training, for the specific ads, and for how long the adapter lives. Say where the weights are stored and who can run them. Mine sits on one encrypted local box and never touches a hosted service. If you can't be bothered doing that part, don't do the rest of it.

The dataset is 90% of the outcome

I'll save you the hyperparameter rabbit hole: twenty minutes with a phone beats any model choice you can make downstream. A rank-16 adapter on a bad dataset loses to a rank-8 adapter on a good one, every time.

Good means varied. The failure mode isn't "not enough photos", it's twenty-five near-identical frames — same angle, same top, same window light. Train on that and the model learns a photograph, not a person. Ask it for a three-quarter view and it hands you the front view with a slight lean and someone else's jaw.

So the shoot is a checklist, not a vibe:

  • Ten tight face close-ups

    Head and shoulders fill the frame at about 1–1.5m. Front neutral, front smiling, mid-sentence with the mouth open, chin up, chin down, ¾ left neutral and smiling, ¾ right neutral and smiling, and one looking off-camera.

  • Two full profiles

    Left and right. Skip these and every profile you generate later will be an invention.

  • Six upper-body frames

    Waist-up so the model learns build as well as face — and deliberately across different tops, so identity doesn't fuse to one shirt.

  • Four lighting repeats

    The same front close-up in window light, open shade, overcast and indoor. This is what stops every render looking like it happened at 11am on one Tuesday.

  • Capture settings that matter

    Highest resolution, portrait orientation, tap-to-focus on the face every frame. Beauty filters off. Portrait-mode blur off — fake bokeh teaches the model a smeared edge where the head meets the world. Natural light, face the window, no harsh overhead sun, no cap, no sunglasses, plain uncluttered background, nobody else in frame.

    Captions: name what varies, hide what shouldn't

    Every image gets a sidecar .txt with the same base name. This is where most first attempts quietly go wrong, because captioning is not describing — it's deciding what stays controllable and what gets baked in.

    # dataset/p_01.txt
    prsn1 man, a young clean-shaven man with short brown hair, front view,
    wearing a navy polo shirt, plain grey studio background, photorealistic
    
    # dataset/p_02.txt
    prsn1 man, a young clean-shaven man with short brown hair, three-quarter
    left view, wearing a navy polo shirt, plain grey studio background, photorealistic
    

    Three rules are doing the work here:

    1. A rare trigger token leads every caption. prsn1 man — a string with no prior meaning in the model. It becomes the handle you pull at generation time. Use a real name and you fight everything the base model already believes about that name; use "man" alone and you've just retrained the concept of men.

    2. Caption what you want to vary later. View angle, wardrobe, background. Naming them teaches the model these are dials, not part of the person.

    3. Don't caption what must be inseparable. The specific face never gets described beyond generic scaffolding, so identity has nowhere to attach except the trigger token. Same reason the client logo on the chest is deliberately not in the captions — I want the face bound to the token, not the branding.

    The config that worked

    Base
    Z-Image-Turbo6B, distilled, runs local
    Rank
    8attn + feed-forward
    Blocks
    0–30all of them
    Epochs
    40batch size 1
    LR
    1e-4AdamW
    Quantize
    4-bitfits 36GB
    Train res
    512pxgenerate higher
    Checkpoint
    every 50pick, don't hope
    {
      "model": "z-image-turbo",
      "data": "dataset",
      "seed": 42,
      "quantize": 4,
      "max_resolution": 512,
      "low_ram": true,
      "training_loop": { "num_epochs": 40, "batch_size": 1,
                         "timestep_low": 4, "timestep_high": 9 },
      "optimizer": { "name": "AdamW", "learning_rate": 1e-4 },
      "checkpoint": { "save_frequency": 50, "output_path": "output" },
      "lora_layers": { "targets": [
        { "module_path": "layers.{block}.attention.to_q",     "blocks": {"start":0,"end":30}, "rank": 8 },
        { "module_path": "layers.{block}.attention.to_k",     "blocks": {"start":0,"end":30}, "rank": 8 },
        { "module_path": "layers.{block}.attention.to_v",     "blocks": {"start":0,"end":30}, "rank": 8 },
        { "module_path": "layers.{block}.attention.to_out.0", "blocks": {"start":0,"end":30}, "rank": 8 },
        { "module_path": "layers.{block}.feed_forward.w1",    "blocks": {"start":0,"end":30}, "rank": 8 },
        { "module_path": "layers.{block}.feed_forward.w2",    "blocks": {"start":0,"end":30}, "rank": 8 },
        { "module_path": "layers.{block}.feed_forward.w3",    "blocks": {"start":0,"end":30}, "rank": 8 }
      ] }
    }
    

    Two choices worth defending. Rank 8, not 32 — for a single face, a low rank is a feature: it lacks the capacity to memorise your grey studio wall, so it's forced to learn the thing that's consistent across all 25 frames, which is the face. Overfitting shows up as every render inheriting the same background and pose.

    Train at 512, generate at 896×1152. Identity is low-frequency information; it transfers up. Training at generation resolution costs multiples more memory for a face that doesn't get more recognisable.

    And save_frequency: 50 matters because the last checkpoint is not automatically the best one. Late checkpoints drift toward "identical to the training photos". Generate the same test prompt from three or four checkpoints, pick by eye, keep that one.

    Generating: the setup you lock and never touch

    mflux-generate-z-image-turbo --model Tongyi-MAI/Z-Image-Turbo -q 4 \
      --lora-paths person_lora.safetensors --lora-scales 0.9 \
      --steps 9 --width 896 --height 1152 --seed 1 \
      --prompt "prsn1 man, a young clean-shaven roof inspector with short brown hair, \
    plain navy polo, documentary realism, high detail" \
      --output stills/smoke_test.png
    

    Trigger token first, always. LoRA scale 0.9 — at 1.0 it starts dragging the training photos' framing along with the face; below about 0.7 it stops looking like him.

    Nine steps because the base is a distilled turbo model. If you're used to 30–50 steps, that number looks wrong; it isn't, and cranking it mostly buys render minutes.

    When a render comes back wrong, the fix is almost always one of three things — and they're distinguishable by how it's wrong:

    The render doesn't look like them. Now what?
    generic face

    Identity isn't landing at all.

    Check the trigger token actually leads the prompt, then raise LoRA scale toward 0.9. Below ~0.7 the adapter is barely participating.

    too like the photos

    Same framing, same background, same shirt in every render.

    Overfit. Drop the scale to ~0.8 and go back an earlier checkpoint — the last one is rarely the best one.

    only wrong in profile

    Head-on is perfect, three-quarter and side views are someone else.

    Not a settings problem — a dataset hole. No amount of tuning invents an angle you never shot.

    What still doesn't work

    The logo on the chest is a smudge. All 25 frames show the client's mark at roughly the same distance and angle, so the model learned a blurry rectangle rather than an embroidered object on curved fabric. Generated renders produce convincing garble. The fix is either a second LoRA trained on dense multi-angle close-ups of the garment itself, or — what actually ships — compositing the real vector lockup in post. Assume the wordmark never survives at ad scale. The icon might.

    Toolchains move under you. Mid-project the trainer retired support for the base model family I'd started on. The dataset, captions and shot list all survived; only the config's module_path targets changed. That's the argument for treating your dataset as the asset and the config as disposable.

    Honest cost

    Roughly 9 minutes per still on a 36GB M4 Max at 896×1216. Fifty stills is a night's work, not a coffee break — which is exactly why it feeds an overnight loop rather than a person sitting there clicking.

    The verdict

    A likeness LoRA is not a shortcut around a photoshoot — it's a shortcut around the eleventh photoshoot. You still need one honest twenty-minute session with real light and a real person who has agreed to it. After that, every new scenario costs render time instead of a call sheet.

    Get the dataset varied, get the captions deliberate, keep the rank low, and check your checkpoints. The rest is the boring rig around it: why this beats faceswap and how the batch actually renders, and the hardening rules that keep a 50-still queue alive until morning.

    Thinking about doing this for a brand face? Ask me the awkward questions first — or see the rest of what we build.

    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.