Skip to content
Skills

Ballet Tights Body-Type Visualization

Author

Giovanni Lu

Date Published

Body-type references: A slim/petite (left), B curvy plus-size (right), heads cropped

Problem

The client wanted to see how their product would look on models with different body types. The existing Taobao listing shows one slim model, and the listing graphics are in Chinese.

Action

  • Captured the Taobao listing. After the user logged in to Taobao, an automated browser session on that logged-in profile captured the item page:
    • a full-page screenshot for reference;
    • a read-only scan of the page to list every listing image and its position;
    • download original from image server instead of cropping from screenshot to retain image quality.
  • Sampled the existing photos. Sorted the seller's 28 original listing images into model photos (18), text graphics (8) and images needing no change (2).
  • Defined the target body types. Took the two body references from a client-supplied photo: A is slim/petite (left) and B is curvy plus-size, roughly US 14 to 16 (right). Each reference is used only for body proportions. Face, clothing and setting are ignored.
Body-type references: A (left), B (right)
  • Compared image models in parallel. Generated body-type variations of 4 representative shots (close-up, full-length, cover, text graphic) on 5 image models at once: nano-banana-2, nano-banana-pro, gemini-3-pro, gemini-3.1-flash and gpt-image-2. Showed the side-by-side results to the client, who picked nano-banana-2 as the best match for their taste.
One shot re-rendered with body B on each model
Close-up shot rendered with body B on each model, original at the far left. nano-banana-pro and both Gemini models zoomed out to show the upper back. gpt-image-2 kept the framing but barely changed the body. nano-banana-2 kept the original crop with a visible body change.
  • Locked what must not change. Each prompt fixed the product (colour, texture, waistband), pose and framing. Only the model's body changed. On images with page layout, only the photo area was regenerated and pasted back, so margins and headings stayed pixel-identical. Chinese text was translated to English in place.

Result

  • Full run with the winning model: two body-type versions (slim and curvy plus-size) of every model photo, plus English versions of all text graphics.
  • QA: checked every image against its original. Re-rolled the ~1 in 8 that drifted (changed pose, recoloured garment, invented words) and kept the best attempt.
  • Final package: a complete 28-image listing per body type in the original order, plus a stitched preview of the full product page for each.
Original vs generated, one example per image category
One example per category. Model photos get the new body with pose, product and framing unchanged. Text graphics are translated with the layout unchanged. Images with no Chinese text and no model are used as-is.
  • Cost: ~$20 total. Time: ~1 hour.
  • Reusable: packaged as the listing-bodyswap skill, so the next listing is a config file plus one run.

Skill

listing-bodyswap

Re-render a product listing's model photos with other body types and translate its text, without changing the product, pose or layout. The next listing is a config file plus one run.

Download

listing-bodyswap.zip

Download

SKILL.md

---
name: listing-bodyswap
description: Use when user wants e-commerce product-listing photos (Taobao/Tmall/1688/AliExpress/Shopify, any seller page) re-rendered with a different model body shape (curvy, plus-size, petite, "a body like this photo") while keeping the product, pose, framing and composition identical, and/or wants the listing's text translated to English in place. Triggers on "swap the model's body", "localize the product images". Covers pulling the seller's original images, body references, per-image framing locks, picking a image model, the inset trick that keeps page layout pixel-identical, retries, QA contact sheets, and assembling per-body final folders + a stitched listing page.
---

# listing-bodyswap

Re-render a product listing's model photos with other body types and translate its
text, without changing the product, pose or layout. A filled-in config is in
`examples/project.example.json`.

## Prerequisites

| Need | Detail / check |
|---|---|
| **Image model API key** | Provider is picked from the model name: `gemini-*` → `GEMINI_API_KEY`; `gpt-image-*` → `OPENAI_API_KEY`; anything else → an OpenAI-compatible chat endpoint with image output (default OpenRouter) via `CHAT_IMAGE_API_KEY`. Override base URLs with `GEMINI_BASE_URL` / `OPENAI_BASE_URL` / `CHAT_IMAGE_BASE_URL`. |
| **`uv`** | All scripts run as `uv run -q --with requests --with pillow --with numpy python …`. No venv is needed. |
| **Source images** | The seller's **original** listing images, saved as `src/NN.png` in listing order (00 = cover). Not screenshots (see step 1). |
| **Body reference photo(s)** | One clear photo per body type, e.g. a phone photo of real people. Crop each person into `ref/body_<X>.jpg` and apply EXIF transpose first (`ImageOps.exif_transpose`). HEIC opens via `--with pillow-heif`. |
| If listing requires logged in scraping | Try remote browser with user session (browser.agent-everywhere.com). Some sites have strong anti-bot and must be run locally in a logged-in web-cli Chrome profile (code in Agent Everywhere github repo). **try to limit to read-only DOM only.** minimal clicks, scrolls or navigation. |

## Expected output

```
<project>/
  project.json                 # all per-listing data (framing, translations, bodies)
  src/NN.png                   # originals            ref/body_X.jpg   # body refs
  out/<model>/{id}{X}.png      # body X versions, same pixel size as src
  out/<model>/{id}T.png        # translated text-only graphics
  out/<model>/_attempts/       # every superseded attempt (for picking the best)
  final/body_X/NN.png          # COMPLETE listing per body: swapped photos + translated
                               #   graphics + untouched images, original numbering
  final/listing_body_X.jpg     # the description (01..last) stitched, 1200px wide
  qa_*.jpg                     # original|generated contact sheets
```
For each body: every model photo has the new build, with the same pose, crop, product
colour, background and page margins. No source-language text is left anywhere, and
English that was already there plus logos are unchanged. Expect roughly 1 re-roll per 8
renders.

Known limits to tell the user up front:
- Outputs are resized to the source's pixel size. If the model returns 1K, fine texture
  ends up slightly softer than the original (set `"image_size": "2K"` where supported).
- A reference body close to the original model's gives a near-identical image. Point
  this out and suggest a more distinct body type.

## Workflow

Scripts are in this skill's `scripts/` directory. Every script takes `--project DIR`.

### 1. Get the originals (do not screenshot)
- Read image URLs from the item page's DOM (`img` `src`/`data-src` plus
  `getBoundingClientRect()` for order). Strip CDN resize suffixes to get the full-size file,
  check its width, and save it as `src/NN.png` in page order.
- Chrome full-page screenshots garble content past ~16384px. Never cut sources from them.
- Put marketplace boilerplate (price notices, etc.) in `exclude`.

### 2. Classify every image → `project.json`
Make a contact sheet of `src/` and read it. Put each id in one bucket:
- `full_imgs`: the photo fills the whole image (edge to edge, or with a caption band).
  The whole image is re-rendered and its text translated in the same call.
- `inset_imgs`: the photo sits on a white page with margins or headings. **Always prefer
  this bucket for those images.** Full-canvas renders drifted the page margins. Here only
  the auto-detected photo rectangle is re-rendered and pasted back, so the page stays
  pixel-identical. If the page itself has text to translate, also list the id in
  `page_text`; a `T` translation of the page is made and the photo is pasted into that.
- `text_only`: graphics with no model (fabric, size chart, after-sales notes).
  Translation only.
- `keep`: nothing to change. `exclude`: not part of the output.

Copy `examples/project.example.json`, then fill in:
- **`framing`/`inset_framing` (the most important field).** For each photo, describe
  precisely what's in frame: crop edges, viewpoint, facing direction, where each hand is,
  and what is NOT in frame. Without this the models zoom out, reframe close-ups, or turn
  the model to face the camera.
- **`text`.** Read each image (crop and zoom in on small text) and list every
  source-language string → English, keeping lengths similar. Leave English alone. Set
  `source_language` if it isn't Chinese.
- **`bodies`.** One entry per target body: a reference image and a concrete description.
- **`model`.** The default image model (step 3).
- **`product`.** One sentence naming exactly what must not change (colour, texture,
  construction lines, fit).

Check the plan with `gen.py --project P --list` and
`gen.py --project P --jobs 13B --print-prompt`.

### 3. Pick the model (cheap bake-off first)
Run 3 to 4 representative jobs (a close-up, a full-length, one text graphic) on 2 to 3 models:
`gen.py --project P --models gemini-3.1-flash-image-preview,gpt-image-1 --jobs 00B,13B,04T`.
Show the user side-by-side sheets before the full run. Observed behaviour:

| model | framing | body change | notes |
|---|---|---|---|
| gemini-3.1-flash-image (nano-banana-2) | good | moderate to strong, natural | Best overall, fast (~10s), best text size match |
| gemini-3-pro-image | worst | strong | Turns poses around, adds divider lines |
| nano-banana-pro | reframes | strong | Darkens garment colours |
| gpt-image | best | subtle | Safety filter rejects some lingerie/tights shots, slow |

### 4. Full run
`gen.py --project P --par 8`. Existing outputs are skipped, and each job retries 4× on
errors. `--force --jobs X,Y` re-rolls specific jobs; the previous output moves to
`_attempts/`.

### 5. QA (always do this; roughly 1 in 8 renders drifts)
- `qa.py --project P pairs qa_B1.jpg B 00 01 03 …`, then Read the sheet. Do the same for
  each body and for `T`. Look for:
  - pose changes (lifted leg, moved arms)
  - camera angle or viewpoint changes
  - garment recolour (e.g. waistband turning pink)
  - body change too weak
  - margins shifted
- Proof-read text at full size with
  `qa.py --project P crop z.png out/<m>/27T.png x0 y0 x1 y1`. The models invent words
  and sometimes leave source text in. Also check `keep` images for stray source text.
- Re-roll bad jobs 2 to 3× with `--force`. Compare with
  `qa.py --project P tries t.jpg 14pB`, then copy the best `_attempts/<job>_n.png` back to
  `out/<m>/<job>.png`.
- If a text graphic still won't come out clean, fall back to a PIL paint-over and
  typeset the `text` strings.

### 6. Assemble
`compose.py --project P`. This pastes the insets back, builds `final/body_X/` (falling
back to the original for anything missing, and saying so), and writes
`final/listing_body_X.jpg`. Open `final/` for the user, and summarise the retries plus
the known limits.

## Gotchas
- The cover (00) is often square with a caption band. Treat it as `full_imgs` with
  `text`.
- Keep garment colours that differ between shots. Never normalise them.
- Images whose text sits *over* the photo (banner, logo watermark) must be `full_imgs`.
  State in the framing that the logo "must stay exactly as it is".
- `photo_rects.json` caches detected rects. Delete an entry if the detection was wrong
  (it looks for the largest non-white block).