DEV Community

KunStudio
KunStudio

Posted on • Originally published at dating-photos.pages.dev

Making AI dating photos that don't look AI-generated (the negative prompt matters more than the model)

Making AI dating photos that don't look AI-generated (the negative prompt matters more than the model)

I already wrote about why identity-preservation models like fal.ai's flux-pulid beat LoRA fine-tuning for keeping a face recognizable. Building AI Dating Photos on the same model surfaced a different, harder problem: keeping the skin recognizable. Identity-preserving generation solves "does this still look like the same person." It does nothing for "does this look like a real photograph," and for a dating profile picture, the second failure mode is the one that gets someone unmatched — that telltale waxy, over-smoothed, poreless AI skin is instantly recognizable to anyone who's swiped past a few AI-generated profiles already.

The two-stage pipeline

Every generation is two fal.ai calls, not one:

  1. fal-ai/flux-pulid — generates the styled photo from the reference selfie, tuned with num_inference_steps: 26 (above the model's 20-step default) and id_weight: 1.0 for a hard face lock.
  2. fal-ai/clarity-upscaler — a finishing pass with creativity: 0.3, resemblance: 1.5, run purely to reintroduce real skin texture and kill plastic/CGI artifacts, not to increase resolution as a goal in itself.

The negative prompt carried through both stages is doing most of the actual work against the "looks AI" problem: waxy skin, plastic skin, airbrushed, over-smooth skin, poreless skin, beauty filter, oversaturated sit right alongside the more expected deformed, extra fingers, blurry. Most negative-prompt lists you'll find in tutorials stop at the structural failure modes (extra limbs, wrong eye count) because those are the obviously broken outputs. The skin-texture terms exist because a structurally correct face with plastic skin is arguably the more common failure mode for a photoreal use case, and it's the one that actually kills conversion on a dating-photo product specifically.

Architecture: one style per call, by design

The Cloudflare Pages Function behind this doesn't generate a full batch of every style in one request — it generates one style per call, and the browser loops through styles client-side. That's not an accident of the code, it's a direct consequence of Cloudflare Functions' CPU/wall-time limits: flux-pulid generation plus an upscale pass, polled at 1.5s intervals with up to 60 polls, can run long enough that bundling four styles into one request risks the whole batch timing out and the user getting nothing. Splitting by style means a slow style can fail or time out without taking the other three down with it.

Payment-gated generation with a signed grant, no database

There's no order table anywhere in this flow. Checkout produces an HMAC-signed grant (payload + signature, verified with the Web Crypto crypto.subtle API already available in the Functions runtime) carrying the paid tier and an expiry timestamp. The generation endpoint verifies the signature and expiry before touching fal at all — no payment, no fal API call, so a failed or forged request never costs anything. Tier determines both which style presets are unlocked and how many images per style get generated (tier.count / tier.styles.length, capped at 12 per style regardless of tier).

What I'd tell anyone building a photoreal (not stylized) generation product

  • Write your negative prompt as if the model's default failure mode is "technically correct but obviously synthetic," not "visibly broken." For dating/headshot/portrait use cases, skin texture is the tell, not anatomy.
  • If your finishing/upscale step exists to fix a texture problem, tune its creativity low and resemblance high — you want it correcting texture, not reinterpreting the image.
  • Split long-running generation work by the smallest reasonable unit (here: one style) if you're running on a platform with hard function time limits. It's a resilience boundary, not just a UX choice.

Live tool: https://dating-photos.pages.dev/

Happy to go deeper on the negative-prompt list or the CF Functions time-limit workaround if useful — drop a comment.

Top comments (0)