| 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() |