Product Image Variations
Download Recipe
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 11 Generate a base apparel-on-person catalog image, then create inclusive fashion 12 catalog variations with an image-to-image model through ImageContext. Use this pattern 13 for e-commerce apparel variants, fit and styling coverage, marketplace 14 thumbnails, lookbook imagery, and creative QA workflows. 15 16 For real product images, replace the `base_product_image` generation column with 17 a seed dataset column containing your product image paths, URLs, or base64 data, 18 then point `ImageContext(column_name=...)` at that seed column. 19 20 Prerequisites: 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 25 Run: 26 uv run product_image_variations.py --num-records 5 27 """ 28 29 from __future__ import annotations 30 31 import argparse 32 from pathlib import Path 33 34 import data_designer.config as dd 35 from data_designer.interface import DataDesigner, DatasetCreationResults 36 37 DEFAULT_MODEL_PROVIDER = "openrouter" 38 DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview" 39 DEFAULT_MODEL_ALIAS = "product-image-model" 40 41 42 def 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 72 def 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 88 def 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 99 def 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 362 def 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 374 BASE_PRODUCT_IMAGE_PROMPT = """\ 375 Create a synthetic apparel catalog reference photo of a person wearing a {{ base_colorway }} {{ apparel_item }}. 376 377 Image 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 395 PRODUCT_VARIATION_PROMPT = """\ 396 Edit the provided apparel-on-person catalog image into a new inclusive commercial fashion image. 397 398 Variation 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 410 Use the provided image as a garment reference, not as a full-shot template. 411 Preserve the garment's core identity from the reference image: same apparel 412 item, same primary colorway, same fabric cues, same silhouette, same fit 413 behavior, and same distinctive design details. Do not preserve the original 414 person, original face, original hair, original stance, original camera angle, 415 or original plain studio background unless that exact choice is requested by 416 the sampled controls. 417 418 Create a visibly different commercial variant. The final image should make at 419 least three obvious changes from the reference photo: a different synthetic 420 adult model, a different full-body pose or body orientation, a different 421 setting or background, and a different lighting or campaign style. Change the 422 person wearing the garment to match the requested synthetic adult model 423 background, age group, body type, and accessibility context. Represent the 424 adult model respectfully and without stereotypes. Every generated person must 425 clearly be 18 or older. 426 427 The edited output must show exactly one person. The final image must be a 428 vertical 3:4 portrait full-body catalog image that is taller than it is wide: 429 show the full head-to-toe body with feet visible, or the full seated body and 430 full mobility aid when the accessibility context calls for one. Do not crop at 431 the face, waist, knees, ankles, hands, or garment hem. Do not create landscape 432 frames, square frames, group shots, mirrored duplicates, before/after 433 composites, multiple models, mannequins, or background bystanders. 434 435 Follow the required edit delta when changing the surrounding scene, styling, 436 pose, background, and lighting. Keep the result realistic and commercially 437 usable. Do not add real brand logos, real trademarks, watermarks, price tags, 438 text overlays, sexualized styling, swimwear, underwear, lingerie, sheer 439 clothing, or revealing poses. 440 Generate exactly one final edited image for this row. Do not return alternate 441 versions, a grid, a pair of examples, a before/after image, or multiple panels. 442 Use the visual variation ID only as an internal diversity key; never render it 443 as text. 444 """ 445 446 447 def 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 461 def 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 482 if __name__ == "__main__": 483 main()