DEV Community

xiaodong Zhang
xiaodong Zhang

Posted on

Batch-generating 100 product images is easy. Making them look like one set is not

Most people assume the hard part of batch image generation is the volume. A hundred products means a hundred calls, which sounds tedious.

It is not the hard part. A loop solves that and machines do not get bored.

The hard part is that those hundred images have to look like one photographer shot them on one day in one studio. Same background. Light from the same direction. Same camera angle. Products at the same scale in frame. Shadows falling the same way.

Miss by a little and the storefront reads as cheap.


The pattern

The skill I have been testing (batch-image, from an MIT-licensed library) solves this with one structural idea:

prompt = [SPEC SEGMENT — byte-identical across the entire batch]
       + [VARIABLE SEGMENT — rewritten per SKU]
Enter fullscreen mode Exit fullscreen mode

The mental model that made it click for me: you are building a studio, not writing prompts. Backdrop paper, key light, fill light, camera on a tripod at 45°, everything taped to the floor. Once it is built you never touch it. Products get carried in, shot, carried out.

The spec segment is the studio.

Variable segment — different every call:

电商商拍图。图1 是商品:米白色罗纹高领针织毛衣,修身版型。
商品的颜色、材质纹理、款式细节必须与图1完全一致。
Enter fullscreen mode Exit fullscreen mode

Spec segment — never changes, not one character:

放置在同一套统一视觉里:纯米白色摄影棚背景,柔和顶光加左侧补光,
45 度视角,画面下方留出统一的商品投影,构图与留白在整组图中保持一致。
深色商品需加右侧轮廓光勾边,避免与背景糊在一起。
商品在画面中的占比统一为约 65%,边距一致。
真实商业产品摄影,无文字无水印。
Enter fullscreen mode Exit fullscreen mode

Does it hold?

I picked six products chosen to be as awkward as possible — light, dark, reflective, textile:

cream ribbed sweater · black leather ankle boots · black leather chain bag · silver steel watch · black wool bucket hat · rust wool scarf

Then ran the spec segment six times without touching a character, swapping only the variable part.

It holds. Background reads identically across all six. Light comes from upper-left in every frame. Shadows fall lower-right. Products occupy roughly the same share of frame with even margins.

The interesting case is the three black items. They do not blend into the off-white background — because of one clause I had put in the spec segment on the documentation's advice:

深色商品需加右侧轮廓光勾边,避免与背景糊在一起。
(dark products need a rim light on the right edge so they don't
 merge with the background)
Enter fullscreen mode Exit fullscreen mode

That single sentence is doing real work. Without it, dark leather against off-white loses its silhouette.


The negative control

The docs carry a heavy warning: change one character of the spec segment and the entire batch must be re-run.

Easy to test. I regenerated two of the six with a different spec — dark grey concrete background, strong side-backlight, straight-on eye-level camera — and mixed them back into the contact sheet.

You can pick them out instantly. There is no ambiguity, no "if you look closely." Two different studios in one grid.

Which is why freezing matters operationally: any edit to the spec invalidates everything generated before it. That is a strong argument for the workflow order below.


Orchestration

dlazy runs one task per invocation, so concurrency comes from the shell. The documented sweet spot is 4–6 lanes; higher risks rate limiting.

Manifest is a three-column CSV:

sku,image,desc
SKU001,products/sweater.jpg,米白色罗纹高领针织毛衣,修身版型
SKU002,products/boots.jpg,黑色皮质粗跟踝靴,侧拉链设计
Enter fullscreen mode Exit fullscreen mode

Worker with retry and backoff:

SPEC='...'   # the frozen block
export SPEC

run_one() {
  IFS=, read -r SKU IMG DESC <<< "$1"
  for attempt in 1 2 3; do
    dlazy seedream-5.0 \
      --prompt "电商商拍图。图1 是商品:${DESC}。商品的颜色、材质纹理、款式细节必须与图1完全一致。${SPEC}" \
      --images "$IMG" --size 1:1 --resolution 2k \
      --save "out/${SKU}.jpg" >/dev/null 2>&1 && break
    sleep $((attempt * 10))
  done
  [ -f "out/${SKU}.jpg" ] && echo "${SKU},ok" || echo "${SKU},fail"
}
export -f run_one

xargs -P 5 -I{} bash -c 'run_one "{}"' < manifest.csv | tee report.csv
awk -F, '{c[$2]++} END{for(k in c) print k, c[k]}' report.csv
Enter fullscreen mode Exit fullscreen mode

For large SKU counts, submit with --no-wait, collect the generateIds, and poll:

GID=$(dlazy seedream-5.0 --no-wait ... \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["task"]["generateId"])')
echo "$SKU,$GID" >> tasks.csv
# later
while IFS=, read -r SKU GID; do dlazy status "$GID" --wait; done < tasks.csv
Enter fullscreen mode Exit fullscreen mode

The gotcha nobody documents

Building English prompts for this, every call failed:

{
  "code": "too_big",
  "maximum": 500,
  "path": ["prompt"],
  "message": "expected string to have <=500 characters"
}
Enter fullscreen mode Exit fullscreen mode

There is a hard 500-character cap on the prompt, and English burns through characters far faster than Chinese does. My first spec segment — written as a natural, comfortably-worded paragraph — blew straight past it.

Trimmed version, 418 characters, output quality unaffected:

Unified visual set: off-white studio background, soft top light with left
fill, 45-degree angle, consistent shadow below, identical composition and
margins across the set. Dark products get a right rim light. Product fills
~65% of frame. Real product photography, no text, no watermark.
Enter fullscreen mode Exit fullscreen mode

Terse instructions work fine. Polite full sentences just eat budget you do not have.

Worth knowing: this cap belongs to the image models. I later ran a ~1,000-character prompt against claude-sonnet-5 in the same CLI with no complaint. So do not go trimming prompts for text models on the assumption the limit is global.


Workflow order, and why it is not optional

Because any spec edit forces a full re-run, the sequence matters more than usual:

  1. Get one SKU working end to end. Confirms the prompt direction.
  2. Sample five. Pick edge cases deliberately: darkest, lightest, most reflective, largest, smallest. My six-product set was chosen exactly this way, which is how the rim-light clause got validated before full volume.
  3. Price it. --dry-run prints parameters and a credit estimate without executing.
  4. Then run everything.

Skipping to step 4 does not get you 100 images. It gets you the same mistake made 100 times, and then you fix the spec and pay for it again.


Cost notes

seedream-5.0, ~5 credits per image. 100 SKUs lands around 500 credits.

  • Batch scenarios reportedly drop per-image compute to ~70%
  • 2k is enough for a listing; 4k only for print
  • Do not use --batch N — it multiplies cost, and your volume already comes from SKU count
  • If a handful of SKUs need extreme fidelity, re-run only those on gpt-image-2 (~6× cost). Never mix two models inside one batch, or you have just broken the consistency you paid for

Troubleshooting, condensed

Symptom Cause Fix
Batch looks inconsistent Spec edited mid-run Freeze it, re-run the batch
Dark product merges with background No rim light Add the rim-light clause, re-run
Product scale fluctuates Scale unconstrained Add "~65% of frame, consistent margins"
Shadow directions vary Shadow unconstrained Add "shadow falls to the lower right"
Mass failures partway Concurrency too high Drop to 4–5 lanes, add backoff

Note that the first four all end in "and re-run the batch." That is the whole argument for one-then-five-then-everything.


MIT licensed, 19 skills, works with Claude Code / Codex / Cursor:

npx skills add https://github.com/dlazyai/ecommerce-skills --all
Enter fullscreen mode Exit fullscreen mode

Repo: github.com/dlazyai/ecommerce-skills


If you are running this at real volume I would be curious whether anyone has wired the failure queue into a re-run with an escalated model automatically, rather than eyeballing the report.

Top comments (0)