| name | character-gallery-generator |
| description | Generate one or more /meet gallery images for an Alysse character using the full Valentina-grade pipeline: scene spec → RunPod Hub LoRA generation → visual validation → R2 upload → old-key cleanup → manifest update. Captures every lesson learned during the 22-character gallery rebuild (May 2026). TRIGGER when: user says "generate gallery image for X", "create new gallery for Y", "redo X's gallery image N", "add a new pose to Z", "regenerate X gallery", or any variation of producing character meet-page imagery. |
| origin | alysse-internal |
Character Gallery Generator — Full Pipeline
This skill captures every lesson learned generating gallery images for the 22 characters during the May 2026 rebuild. Each rule cost at least one regenerated image, a re-upload, or a user correction. Follow them verbatim.
Why this skill exists
The user spent weeks tuning the Valentina gallery to a specific aesthetic — sexy but tasteful, identity-locked, no moles, no second person, breasts and curves visible through outfits but never explicit. Replicating that across 21 more characters required learning:
- Old R2 keys must be explicitly deleted when replacing — Cloudflare R2 sets
Cache-Control: immutable, so uploading a new key does NOT remove the old one. The CDN will serve both. (This was the user's most repeated correction: "il faut faire ca depuis aya".)
- Qwen Image Edit 2511 is a VLM, not an SD model. Natural language only. SD-style weight syntax
(keyword:1.4) is ignored.
- Identity drift is real. The composite + face-ref + physicalSfw description must all agree, or the model invents a different person each scene.
- "Solo" must be enforced positively + negatively. Settings like bars, ramen counters, gyms naturally contain background people. Without explicit solo enforcement, the model adds partners.
- The Valentina rule applies to all characters: outfits must show breasts and body curves — fitted dresses, satin slips, crop tops, bikinis, lingerie hints. Boring outfits (oversized hoodies, generic blouses) produce dead images.
- No repeated poses or decors across the 22 characters. Every character needs its own unique visual identity.
- Specs file is the source of truth. Manifest in
characters.ts follows specs file filenames. They MUST match exactly.
Step 0 — Confirm context before generating
Before touching anything, verify:
-
Character config exists at apps/api/src/config/characters/{slug}.ts with these mandatory fields:
physicalSfw — natural language description (no SD weights, no measurements in SFW)
compositeUrl — R2 URL ending in composites/{slug}.png
faceRefUrl — R2 URL ending in face-refs/{slug}/01-portrait.png (or similar)
negativeExtra (optional) — per-character negative prompt additions
-
Composite + face-ref are live on R2. The generator probes both with HEAD requests before starting. If either 404s, fix R2 first.
-
Other characters' scenes, so you don't repeat poses/decors. Read scripts/character-gallery-specs.ts and skim every existing spec.
grep "prompt:" scripts/character-gallery-specs.ts | head -100
Step 1 — Write or update scene spec
Open scripts/character-gallery-specs.ts and add/edit the character's 4-scene block. The structure:
'character-slug': [
{
filename: 'gallery-{unique-scene-name}.png',
layoutSlot: 'hero',
register: 'intimate',
prompt:
"She is [setting + pose + gaze]. She is wearing [explicit garment with neckline + coverage]. Hair [explicit]. Looking [direction] with [expression].",
},
],
Rules for the scene prompt field
- Start with the setting, not the character.
"She is at...", never "Maria stands at...".
- Wardrobe must be explicit. Qwen needs the garment name + neckline + coverage.
"fitted cream silk camisole with thin spaghetti straps, soft sweetheart neckline showing collarbone, modest depth" not "a nice top".
- Apply the Valentina rule. Every scene must feature an outfit that shows the character's curves and either cleavage hint, bare shoulders, bare midriff, bare legs, or bare back. Boring outfits = boring images. Examples:
- SFW sexy: fitted slip dress, crop top + denim cutoffs, bikini, lingerie-ish robe, body-con mini dress, low-cut camisole, halter top.
- Avoid: oversized hoodies, baggy clothing, shapeless garments. The negative prompt actively penalizes these.
- Anchor to the character's world. Use locations from her bio: Brielle is a Long Beach college student → bleachers/dorm/pool/dive bar. Zoe is a retro gamer → CRT TV, N64 setup, comics. Camila is Miami Cuban → beach/balcony/Calle Ocho.
- Pose is intentional. Hero shot = front-facing, eye contact. Wide = action, looking off-frame. Tall = full-body movement or reclining. Square = close-up half-body or seated with body visible.
- Don't repeat poses or decors across characters. No two characters should both have "yoga studio" or "coffee shop sketching". Verify by greping existing prompts.
Layout slot rules
| Slot | Dimensions | Use for |
|---|
| hero | 1024×1280 | Anchor portrait, character looking at/near camera |
| wide | 1280×1024 | Action shot, environmental context |
| tall | 1024×1536 | Full-body or reclining, vertical composition |
| square | 1024×1024 | Half-body close, expressive moment |
Naming convention
gallery-{2-4-word-kebab-slug}.png. Should be unique across all 22 characters. Examples:
- ✅
gallery-bleachers-golden-hour.png
- ✅
gallery-miami-balcony-bikini.png
- ❌
gallery-portrait.png (too generic)
- ❌
gallery-img-1.png (no semantic info)
Step 2 — Generate
npx tsx scripts/generate-character-gallery.ts <slug>
This runs all 4 scenes through RunPod Hub qwen-image-edit-2511-lora ($0.025/image, ~15-25s each). Output → characters/{slug}/gallery-final/.
What the generator does (do NOT modify these without reason)
- Loads
physicalSfw, compositeUrl, faceRefUrl from the character config.
- Builds prompt:
"Realism. Same person as in reference image. {physicalSfw}. {spec.prompt} {TAIL}"
TAIL adds: camera spec (Canon R5 85mm f/1.4), SKIN_DIRECTIVE (anti-mole nuclear), solo enforcement.
- Passes composite + face-ref as input images.
- LoRA: Realism @ 0.8 strength.
- Negative prompt: anti-mole (weights up to 2.0), anti-cleavage-as-subject, anti-second-person, anti-baggy.
- Sanitizes
negativeExtra to strip "no freckles"/"no tattoos" double-negatives that conflict with pipeline rules.
If a scene fails
- Identity drift (different face): regenerate that single scene. The model has stochasticity — second roll usually fixes it.
- Two people in image: the prompt's setting (bar, restaurant, gym) leaked. Add
"Solo, completely alone, no other person visible anywhere in frame" to the spec's prompt field, regenerate.
- Moles visible: rare but happens. Regenerate. If persistent across 3 rolls, check the face-ref image itself for a mole the model is generalizing.
- Boring outfit / not sexy enough: rewrite the wardrobe sentence in the spec to be explicit about a fitted/revealing garment. Regenerate.
Step 3 — Validate (Claude visual review)
For each of the 4 generated PNGs in characters/{slug}/gallery-final/, Claude must read the image and check:
| Check | Pass criteria |
|---|
| Identity match | Same hair color/length, eye color, skin tone, face shape as composite+face-ref |
| Body type match | Matches physicalSfw (e.g. "very large breasts" or "small modest breasts") |
| Solo | No second person. Background bartenders/cooks are OK only if far/blurred |
| No moles | No moles, beauty marks, dark spots on chest, shoulders, neck, face |
| No body freckles | Freckles only on nose bridge if canon to that character |
| Outfit matches spec | Garment described in prompt is what's worn |
| Sexy/curves visible | Outfit shows curves AND at least one of: cleavage hint, bare shoulders, bare midriff, bare legs |
| No explicit nudity | Nipples covered, no genitals visible |
| Setting matches spec | Location/props from prompt are visible |
Reject and regenerate any scene that fails 2+ criteria.
What to do when you're uncertain
Ask: "Would this fit in the Valentina gallery on /meet/valentina-reyes?" If no, regenerate.
Step 4 — Upload to R2
npx tsx scripts/upload-character-gallery-r2.ts <slug>
Uploads each PNG to avatars/{slug}/{filename}.png on deepbond-images bucket. Reads filenames from CHARACTER_GALLERY_SPECS[slug] — they must match the files in characters/{slug}/gallery-final/.
Verify upload
The script prints public URLs ending in /avatars/{slug}/{filename}.png. Open one in a browser to confirm before proceeding.
Step 5 — Delete old R2 keys (CRITICAL — most-forgotten step)
If this is a replacement (the character previously had different filenames), you MUST delete the old keys. R2 sets Cache-Control: public, max-age=31536000, immutable, so leaving old keys leaves them publicly accessible on the CDN.
How to find the old keys
git log --all --oneline apps/web/src/lib/characters.ts | head -5
git show <commit>:apps/web/src/lib/characters.ts | grep -A 30 "'<slug>'" | grep "filename"
Delete them
npx tsx scripts/delete-r2-keys.ts \
avatars/<slug>/<old-filename-1>.png \
avatars/<slug>/<old-filename-2>.png \
avatars/<slug>/<old-filename-3>.png \
avatars/<slug>/<old-filename-4>.png
The script accepts any number of keys. Output should print DELETED: ... for each.
When to skip this step
Only if you are 100% sure the character had no previous gallery (i.e., this is a brand new character whose specs and uploads are happening in the same session). Even then, run the delete with the expected old filenames — it's a no-op if they don't exist on R2.
Step 6 — Update the manifest
Edit apps/web/src/lib/characters.ts. The CHARACTER_GALLERY object has one block per character slug. Replace the old filenames with the new ones (same filenames you used in the specs file).
'character-slug': [
{
filename: 'gallery-new-scene-1.png',
scene: 'Short scene description — used as the alt-text/caption',
layoutSlot: 'hero',
},
],
The filename field MUST match scripts/character-gallery-specs.ts exactly — that's what the <Image src=> resolves to on the /meet page.
The scene field is the descriptive caption shown on the page. Keep it short (one sentence, present tense, describes outfit + location). Do NOT just copy the long generator prompt — that's for the AI; the caption is for the user.
Step 7 — Visual smoke test
Open https://alysse.me/meet/{slug} (or localhost:3000/meet/{slug} if dev). Verify:
- All 4 images load (no broken images / 404s).
- Layout looks right (hero in hero slot, etc.).
- No old image is still showing (would mean Step 5 was skipped or the manifest still references it).
If an old image still appears in the browser, hard-reload (Ctrl+Shift+R). If it persists, the manifest still references an old filename — go back to Step 6.
Lessons that aren't obvious
Why the prompt starts with "Realism."
That's the LoRA trigger token for flymy_realism.safetensors. Without it, the LoRA's photorealism boost doesn't activate. The generator injects this automatically — don't add it to specs.
Why the prompt mentions the character physically every time
Qwen Image Edit 2511 is a VLM. It needs the physical description in every generation — it doesn't carry character state between runs. The generator injects physicalSfw automatically; you write the scene.
Why SKIN_DIRECTIVE is "nuclear" (positive-side mole eradication)
Early attempts only used negative prompts: (mole:1.6). That wasn't enough because Qwen's training data has so many moled chests that even strong negatives leak. The fix was a positive directive framing the scene as "luxury skincare campaign with retouching applied" — that shifts the entire generation toward smooth skin.
Why negativeExtra gets sanitized
Some characters (Emma, Aya, Lauren) include "no freckles" in their negativeExtra to protect canon nose-bridge freckles via double negative. But our central negative prompt wants to suppress chest/body freckles. The sanitizer strips "no freckles"/"no tattoos"/"no beauty marks" tokens from per-character negativeExtra so the central rule wins.
Why composites and face-refs are dual-mode (SFW + NSFW)
For NSFW image generation (not gallery — that's image.service.ts), the system uses NSFW-specific composites. The gallery generator only uses SFW composites. If you see naming like composites/aya-khalid.png vs composites/aya-khalid-nsfw.png, the gallery script uses the former.
Why old keys are immutable
R2 default for public bucket: Cache-Control: public, max-age=31536000, immutable. That means a CDN client can cache a key for 1 year. Even after you delete a key from R2, browsers/CDN edges may still have it cached for a while. The first defense is deleting the key. The second is the manifest no longer referencing it — so even if cached, nothing links to it.
Why "skye-alverra.png" is the actual composite filename for Skye Alvarez
Typo in the original R2 upload. Don't "fix" it by renaming — the character config (apps/api/src/config/characters/skye-alvarez.ts) reads that exact key. If you rename the R2 file, you'll break the chain. Leave it.
Why some characters have a face-ref called 01-portrait-clean.png
When a face-ref had a visible watermark or tattoo that polluted every generated image (Molly Ellery's case), the face-ref was regenerated cleanly and renamed. The character config points to whichever face-ref is canonical — always read the config, never assume 01-portrait.png.
Single-scene mode (regenerate just 1 of 4)
The current generator regenerates all 4 scenes for a slug. If you need to regenerate just one:
- Edit the spec file to keep only the one scene you want to regenerate.
- Run the generator (it will only produce 1 PNG).
- Restore the full 4-scene spec.
- Upload — only the regenerated file will be re-uploaded if you put it alone in
gallery-final/.
Alternative: a quick one-off script that takes (slug, filename) and runs that single spec entry. If the user is going to do this often, propose adding --scene <filename> arg to the generator.
Generating a brand-new pose for an existing character
When the user says "trouve une autre pose et change [filename]" (find a different pose and change [filename]):
- Read the current image to understand what's wrong (e.g., user disliked the body proportions, the outfit, the setting).
- Read other scenes for that character in the specs to avoid duplicating context already covered.
- Read other characters' scenes to avoid duplicating poses/decors across the gallery.
- Propose a new scene to the user OR write it directly if obvious — character's bio + sexy-Valentina-rule + unique-pose constraint should give you 1-2 strong options.
- Edit only the one scene block in
scripts/character-gallery-specs.ts.
- Generate just that scene (delete the other 3 spec entries temporarily, or modify the generator to accept a single filename).
- Validate. Upload. Delete old key for that filename only. Update manifest.
The filename should change too — if gallery-scottsdale-wine-bar.png is being replaced by a poolside scene, rename to gallery-scottsdale-poolside.png so the old key can be cleanly deleted.
Mapping the entire pipeline to a single command (mental model)
spec → generate → claude validates → upload → delete-old → manifest → smoke-test
↑ │
└─── regenerate ──────┘ (if validation fails)
Never skip:
- ❌ Skip validation → ship broken images.
- ❌ Skip delete-old → old images stay on CDN.
- ❌ Skip manifest → page still points to old filenames.
- ❌ Skip smoke-test → broken /meet page in production.
Quick recipe: 1 image for 1 character
npx tsx scripts/generate-character-gallery.ts <slug>
npx tsx scripts/upload-character-gallery-r2.ts <slug>
npx tsx scripts/delete-r2-keys.ts avatars/<slug>/<old-filename>.png
Reference files
- Generator:
scripts/generate-character-gallery.ts
- Uploader:
scripts/upload-character-gallery-r2.ts
- Deleter:
scripts/delete-r2-keys.ts
- Specs:
scripts/character-gallery-specs.ts
- Character configs:
apps/api/src/config/characters/{slug}.ts
- Manifest:
apps/web/src/lib/characters.ts
- Pose guide:
docs/seo/character-poses.md
- R2 bucket:
deepbond-images (legacy name, never renamed)
- R2 CDN:
https://pub-440a93f059fc44acb172efed4eabad62.r2.dev
- RunPod endpoint:
qwen-image-edit-2511-lora (Hub managed, $0.025/image)