Product Image Variations

View as Markdown
1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2# SPDX-License-Identifier: Apache-2.0
3# /// script
4# requires-python = ">=3.10"
5# dependencies = [
6# "data-designer",
7# ]
8# ///
9"""Product Image Variation Recipe
10
11Generate a base apparel-on-person catalog image, then create inclusive fashion
12catalog variations with an image-to-image model through ImageContext. Use this pattern
13for e-commerce apparel variants, fit and styling coverage, marketplace
14thumbnails, lookbook imagery, and creative QA workflows.
15
16For real product images, replace the `base_product_image` generation column with
17a seed dataset column containing your product image paths, URLs, or base64 data,
18then point `ImageContext(column_name=...)` at that seed column.
19
20Prerequisites:
21 - An image-generation provider key for a model that supports image-to-image
22 editing through the chat-completions route. The defaults use OpenRouter
23 and Gemini 3.1 Flash Image Preview, so set OPENROUTER_API_KEY before running.
24
25Run:
26 uv run product_image_variations.py --num-records 5
27"""
28
29from __future__ import annotations
30
31import argparse
32from pathlib import Path
33
34import data_designer.config as dd
35from data_designer.interface import DataDesigner, DatasetCreationResults
36
37DEFAULT_MODEL_PROVIDER = "openrouter"
38DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview"
39DEFAULT_MODEL_ALIAS = "product-image-model"
40
41
42def build_model_configs(
43 *,
44 model_provider: str,
45 model_id: str,
46 model_alias: str,
47 image_size: str,
48 aspect_ratio: str,
49 max_parallel_requests: int,
50) -> list[dd.ModelConfig]:
51 """Build an image model config for text-to-image and image-to-image generation."""
52 return [
53 dd.ModelConfig(
54 alias=model_alias,
55 model=model_id,
56 provider=model_provider,
57 inference_parameters=dd.ImageInferenceParams(
58 extra_body={
59 "modalities": ["image", "text"],
60 "image_config": {
61 "aspect_ratio": aspect_ratio,
62 "image_size": image_size,
63 },
64 },
65 max_parallel_requests=max_parallel_requests,
66 ),
67 skip_health_check=True,
68 )
69 ]
70
71
72def add_category(
73 config_builder: dd.DataDesignerConfigBuilder,
74 name: str,
75 values: list[str],
76 weights: list[float] | None = None,
77) -> None:
78 """Add a categorical sampler column."""
79 config_builder.add_column(
80 dd.SamplerColumnConfig(
81 name=name,
82 sampler_type=dd.SamplerType.CATEGORY,
83 params=dd.CategorySamplerParams(values=values, weights=weights),
84 )
85 )
86
87
88def add_visual_variation_id(config_builder: dd.DataDesignerConfigBuilder) -> None:
89 """Add a unique row-level key that discourages duplicate image generations."""
90 config_builder.add_column(
91 dd.SamplerColumnConfig(
92 name="visual_variation_id",
93 sampler_type=dd.SamplerType.UUID,
94 params=dd.UUIDSamplerParams(prefix="apparel-", short_form=True),
95 )
96 )
97
98
99def build_config(
100 *,
101 model_provider: str = DEFAULT_MODEL_PROVIDER,
102 model_id: str = DEFAULT_MODEL_ID,
103 model_alias: str = DEFAULT_MODEL_ALIAS,
104 image_size: str = "1K",
105 aspect_ratio: str = "3:4",
106 max_parallel_requests: int = 10,
107) -> dd.DataDesignerConfigBuilder:
108 """Build an apparel product image variation pipeline."""
109 model_configs = build_model_configs(
110 model_provider=model_provider,
111 model_id=model_id,
112 model_alias=model_alias,
113 image_size=image_size,
114 aspect_ratio=aspect_ratio,
115 max_parallel_requests=max_parallel_requests,
116 )
117 config_builder = dd.DataDesignerConfigBuilder(model_configs=model_configs)
118 add_visual_variation_id(config_builder)
119
120 add_category(
121 config_builder,
122 "apparel_item",
123 [
124 "organic cotton crewneck t-shirt",
125 "lightweight denim jacket",
126 "water-resistant rain jacket",
127 "relaxed-fit hoodie",
128 "wide-leg linen trousers",
129 "ribbed knit cardigan",
130 "quilted puffer vest",
131 "stretch woven workwear shirt",
132 "ankle-length everyday dress",
133 "adaptive zip-front jacket",
134 ],
135 )
136
137 add_category(
138 config_builder,
139 "base_colorway",
140 [
141 "matte black",
142 "warm white",
143 "sage green",
144 "deep navy",
145 "brushed silver",
146 "terracotta",
147 "soft lavender",
148 "charcoal gray",
149 "sunflower yellow",
150 "denim blue",
151 ],
152 )
153
154 add_category(
155 config_builder,
156 "base_view",
157 [
158 "front-facing standing full-body catalog photo with one synthetic adult model",
159 "three-quarter standing full-body catalog photo with one synthetic adult model",
160 "side-angle standing full-body catalog photo with one synthetic adult model",
161 "walking-pose full-body catalog photo with one synthetic adult model",
162 ],
163 )
164
165 add_category(
166 config_builder,
167 "base_model_profile",
168 [
169 "young adult Black model with an athletic build",
170 "middle-aged East Asian model with an average build",
171 "older Latine model with a petite build",
172 "young adult South Asian model with a plus-size build",
173 "middle-aged Middle Eastern model with a tall build",
174 "young adult Indigenous model with a broad-shouldered build",
175 "older White model with a curvy build",
176 "young adult multiracial model with a slender build",
177 ],
178 )
179
180 add_category(
181 config_builder,
182 "variation_goal",
183 [
184 "inclusive e-commerce catalog image on a clean white background",
185 "lifestyle lookbook image in an everyday urban setting",
186 "fit-guide image showing garment drape and silhouette",
187 "seasonal campaign image for cool-weather layering",
188 "adaptive-fashion catalog image emphasizing ease of wear",
189 "single-model adult age-inclusive catalog image",
190 "social media campaign image with bold colored backdrop",
191 "editorial fashion image with soft premium lighting",
192 ],
193 )
194
195 add_category(
196 config_builder,
197 "edit_scene_delta",
198 [
199 "move from a neutral studio catalog reference into an outdoor urban lifestyle scene",
200 "move from a neutral studio catalog reference into a warm home entryway lookbook scene",
201 "move from a neutral studio catalog reference into a bold color-block campaign set",
202 "move from a neutral studio catalog reference into a smart-casual workplace corridor scene",
203 "move from a neutral studio catalog reference into a weekend park lookbook scene",
204 "move from a neutral studio catalog reference into a premium editorial studio set with draped fabric",
205 "move from a neutral studio catalog reference into a clean fit-guide scene with a new full-body pose",
206 "move from a neutral studio catalog reference into a seasonal layering scene with visible outerwear styling",
207 ],
208 )
209
210 add_category(
211 config_builder,
212 "model_age_group",
213 [
214 "young adult model",
215 "middle-aged adult model",
216 "older adult model",
217 "senior adult model",
218 ],
219 )
220
221 add_category(
222 config_builder,
223 "model_ethnicity",
224 [
225 "Black or African diaspora model",
226 "East Asian model",
227 "South Asian model",
228 "Latine model",
229 "Middle Eastern or North African model",
230 "Indigenous model",
231 "Pacific Islander model",
232 "White or European model",
233 "multiracial model",
234 ],
235 )
236
237 add_category(
238 config_builder,
239 "body_type",
240 [
241 "petite build",
242 "tall build",
243 "plus-size build",
244 "athletic build",
245 "broad-shouldered build",
246 "curvy build",
247 "slender build",
248 "average build",
249 ],
250 )
251
252 add_category(
253 config_builder,
254 "accessibility_context",
255 [
256 "standing model without visible mobility aids",
257 "model with no specific accessibility cue",
258 "standing model in a relaxed catalog pose",
259 "model walking naturally without visible mobility aids",
260 "model seated on a simple studio stool",
261 "model leaning lightly against a studio block",
262 "model holding a small neutral accessory",
263 "seated model using a wheelchair",
264 "model with a visible prosthetic limb",
265 "model using forearm crutches",
266 ],
267 weights=[1.6, 1.6, 1.4, 1.4, 0.9, 0.9, 0.9, 0.2, 0.2, 0.2],
268 )
269
270 add_category(
271 config_builder,
272 "styling_context",
273 [
274 "pure white seamless catalog background",
275 "soft neutral studio backdrop",
276 "outdoor morning city street",
277 "modern home entryway",
278 "adult campus casual setting",
279 "workplace smart-casual setting",
280 "weekend park setting",
281 "minimal geometric studio set",
282 ],
283 )
284
285 standing_compositions = [
286 "front-facing full-body catalog pose with the entire person visible",
287 "three-quarter full-body pose with the entire person visible",
288 "side-angle full-body pose with clear garment silhouette",
289 ]
290 seated_compositions = [
291 "single seated full-body pose showing garment fit with the whole body visible",
292 ]
293 wheelchair_compositions = [
294 "single seated full-body pose showing garment fit with the whole body and wheelchair visible",
295 ]
296 walking_compositions = [
297 "single walking full-body pose with natural garment movement",
298 ]
299 prosthetic_compositions = [
300 "standing full-body pose with visible prosthetic limb and clear garment fit",
301 "single walking full-body pose with visible prosthetic limb and natural garment movement",
302 ]
303 crutch_compositions = [
304 "standing full-body pose with forearm crutches visible and clear garment fit",
305 "single walking full-body pose with forearm crutches visible and natural garment movement",
306 ]
307 flexible_compositions = standing_compositions + seated_compositions + walking_compositions
308 config_builder.add_column(
309 dd.SamplerColumnConfig(
310 name="composition",
311 sampler_type=dd.SamplerType.SUBCATEGORY,
312 params=dd.SubcategorySamplerParams(
313 category="accessibility_context",
314 values={
315 "standing model without visible mobility aids": standing_compositions,
316 "model with no specific accessibility cue": flexible_compositions,
317 "standing model in a relaxed catalog pose": standing_compositions,
318 "model walking naturally without visible mobility aids": walking_compositions,
319 "model seated on a simple studio stool": seated_compositions,
320 "model leaning lightly against a studio block": standing_compositions,
321 "model holding a small neutral accessory": standing_compositions,
322 "seated model using a wheelchair": wheelchair_compositions,
323 "model with a visible prosthetic limb": prosthetic_compositions,
324 "model using forearm crutches": crutch_compositions,
325 },
326 ),
327 )
328 )
329
330 add_category(
331 config_builder,
332 "lighting",
333 [
334 "softbox studio lighting",
335 "natural window light",
336 "bright catalog lighting",
337 "warm golden-hour lighting",
338 "soft overcast outdoor light",
339 ],
340 )
341
342 config_builder.add_column(
343 dd.ImageColumnConfig(
344 name="base_product_image",
345 prompt=BASE_PRODUCT_IMAGE_PROMPT,
346 model_alias=model_alias,
347 )
348 )
349
350 config_builder.add_column(
351 dd.ImageColumnConfig(
352 name="product_variant_image",
353 prompt=PRODUCT_VARIATION_PROMPT,
354 model_alias=model_alias,
355 multi_modal_context=[dd.ImageContext(column_name="base_product_image")],
356 )
357 )
358
359 return config_builder
360
361
362def create_dataset(
363 config_builder: dd.DataDesignerConfigBuilder,
364 *,
365 num_records: int,
366 dataset_name: str,
367 artifact_path: Path | str | None = None,
368) -> DatasetCreationResults:
369 data_designer = DataDesigner(artifact_path=artifact_path)
370 data_designer.validate(config_builder)
371 return data_designer.create(config_builder, num_records=num_records, dataset_name=dataset_name)
372
373
374BASE_PRODUCT_IMAGE_PROMPT = """\
375Create a synthetic apparel catalog reference photo of a person wearing a {{ base_colorway }} {{ apparel_item }}.
376
377Image requirements:
378- Visual variation ID, for internal diversity only: {{ visual_variation_id }}
379- View: {{ base_view }}
380- Base model profile: {{ base_model_profile }}
381- Background: clean neutral studio background
382- Lighting: soft catalog lighting
383- Use exactly one synthetic adult model in neutral catalog styling.
384- Output must use vertical portrait framing, with a 3:4 portrait composition that is taller than it is wide.
385- The garment should be centered, worn naturally, fully visible, and isolated enough to edit later.
386- The frame must be a full-body image: show the model from head to toe with feet visible and comfortable margins around the body.
387- Show fabric texture, seams, silhouette, cuffs, closures, pockets, fit, drape, and other garment details when relevant.
388- Keep the model presentation neutral, fully clothed, non-sexualized, and commercially appropriate.
389- Do not include extra people, duplicate bodies, mannequins, cropped bodies, close-up crops, landscape frames, square frames, real brand logos, real trademarks, watermarks, price tags, text overlays, celebrity likenesses, or real people.
390- Use a plausible invented garment design with consistent shape, color, fit, and material details.
391- Generate exactly one final image for this row. Do not return alternate versions, a grid, a pair of examples, a before/after image, or multiple panels. Use the visual variation ID only as an internal diversity key; never render it as text.
392"""
393
394
395PRODUCT_VARIATION_PROMPT = """\
396Edit the provided apparel-on-person catalog image into a new inclusive commercial fashion image.
397
398Variation requirements:
399- Visual variation ID, for internal diversity only: {{ visual_variation_id }}
400- Variation goal: {{ variation_goal }}
401- Required edit delta: {{ edit_scene_delta }}
402- Model age group: {{ model_age_group }}
403- Model ethnicity: {{ model_ethnicity }}
404- Body type: {{ body_type }}
405- Accessibility context: {{ accessibility_context }}
406- Styling context: {{ styling_context }}
407- Composition: {{ composition }}
408- Lighting: {{ lighting }}
409
410Use the provided image as a garment reference, not as a full-shot template.
411Preserve the garment's core identity from the reference image: same apparel
412item, same primary colorway, same fabric cues, same silhouette, same fit
413behavior, and same distinctive design details. Do not preserve the original
414person, original face, original hair, original stance, original camera angle,
415or original plain studio background unless that exact choice is requested by
416the sampled controls.
417
418Create a visibly different commercial variant. The final image should make at
419least three obvious changes from the reference photo: a different synthetic
420adult model, a different full-body pose or body orientation, a different
421setting or background, and a different lighting or campaign style. Change the
422person wearing the garment to match the requested synthetic adult model
423background, age group, body type, and accessibility context. Represent the
424adult model respectfully and without stereotypes. Every generated person must
425clearly be 18 or older.
426
427The edited output must show exactly one person. The final image must be a
428vertical 3:4 portrait full-body catalog image that is taller than it is wide:
429show the full head-to-toe body with feet visible, or the full seated body and
430full mobility aid when the accessibility context calls for one. Do not crop at
431the face, waist, knees, ankles, hands, or garment hem. Do not create landscape
432frames, square frames, group shots, mirrored duplicates, before/after
433composites, multiple models, mannequins, or background bystanders.
434
435Follow the required edit delta when changing the surrounding scene, styling,
436pose, background, and lighting. Keep the result realistic and commercially
437usable. Do not add real brand logos, real trademarks, watermarks, price tags,
438text overlays, sexualized styling, swimwear, underwear, lingerie, sheer
439clothing, or revealing poses.
440Generate exactly one final edited image for this row. Do not return alternate
441versions, a grid, a pair of examples, a before/after image, or multiple panels.
442Use the visual variation ID only as an internal diversity key; never render it
443as text.
444"""
445
446
447def parse_args() -> argparse.Namespace:
448 parser = argparse.ArgumentParser(description="Generate product image variations with image-to-image editing.")
449 parser.add_argument("--num-records", type=int, default=5, help="Number of product variation rows to generate.")
450 parser.add_argument("--dataset-name", default="product-image-variations", help="Output dataset name.")
451 parser.add_argument("--artifact-path", type=Path, default=None, help="Optional Data Designer artifact directory.")
452 parser.add_argument("--model-provider", default=DEFAULT_MODEL_PROVIDER, help="Image model provider name.")
453 parser.add_argument("--model-id", default=DEFAULT_MODEL_ID, help="Provider model ID.")
454 parser.add_argument("--model-alias", default=DEFAULT_MODEL_ALIAS, help="Alias used by image columns.")
455 parser.add_argument("--image-size", default="1K", help="OpenRouter image size tier, such as 1K, 2K, or 4K.")
456 parser.add_argument("--aspect-ratio", default="3:4", help="Provider-specific aspect ratio value.")
457 parser.add_argument("--max-parallel-requests", type=int, default=10, help="Maximum parallel image requests.")
458 return parser.parse_args()
459
460
461def main() -> None:
462 args = parse_args()
463 config_builder = build_config(
464 model_provider=args.model_provider,
465 model_id=args.model_id,
466 model_alias=args.model_alias,
467 image_size=args.image_size,
468 aspect_ratio=args.aspect_ratio,
469 max_parallel_requests=args.max_parallel_requests,
470 )
471 results = create_dataset(
472 config_builder,
473 num_records=args.num_records,
474 dataset_name=args.dataset_name,
475 artifact_path=args.artifact_path,
476 )
477 dataset = results.load_dataset()
478 print(f"Generated {len(dataset)} product variation rows.")
479 print(f"Dataset artifacts: {results.artifact_storage.base_dataset_path}")
480
481
482if __name__ == "__main__":
483 main()