> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/datadesigner/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/datadesigner/_mcp/server.

# Product Image Variations

> Use image-to-image generation to create inclusive apparel catalog variants while preserving garment identity.

[Download the complete recipe script](https://github.com/NVIDIA-NeMo/DataDesigner/blob/main/fern/assets/recipes/image_generation/product_image_variations.py)

```python
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "data-designer",
# ]
# ///
"""Product Image Variation Recipe

Generate a base apparel-on-person catalog image, then create inclusive fashion
catalog variations with an image-to-image model through ImageContext. Use this pattern
for e-commerce apparel variants, fit and styling coverage, marketplace
thumbnails, lookbook imagery, and creative QA workflows.

For real product images, replace the `base_product_image` generation column with
a seed dataset column containing your product image paths, URLs, or base64 data,
then point `ImageContext(column_name=...)` at that seed column.

Prerequisites:
    - An image-generation provider key for a model that supports image-to-image
      editing through the chat-completions route. The defaults use OpenRouter
      and Gemini 3.1 Flash Image Preview, so set OPENROUTER_API_KEY before running.

Run:
    uv run product_image_variations.py --num-records 5
"""

from __future__ import annotations

import argparse
from pathlib import Path

import data_designer.config as dd
from data_designer.interface import DataDesigner, DatasetCreationResults

DEFAULT_MODEL_PROVIDER = "openrouter"
DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview"
DEFAULT_MODEL_ALIAS = "product-image-model"


def build_model_configs(
    *,
    model_provider: str,
    model_id: str,
    model_alias: str,
    image_size: str,
    aspect_ratio: str,
    max_parallel_requests: int,
) -> list[dd.ModelConfig]:
    """Build an image model config for text-to-image and image-to-image generation."""
    return [
        dd.ModelConfig(
            alias=model_alias,
            model=model_id,
            provider=model_provider,
            inference_parameters=dd.ImageInferenceParams(
                extra_body={
                    "modalities": ["image", "text"],
                    "image_config": {
                        "aspect_ratio": aspect_ratio,
                        "image_size": image_size,
                    },
                },
                max_parallel_requests=max_parallel_requests,
            ),
            skip_health_check=True,
        )
    ]


def add_category(
    config_builder: dd.DataDesignerConfigBuilder,
    name: str,
    values: list[str],
    weights: list[float] | None = None,
) -> None:
    """Add a categorical sampler column."""
    config_builder.add_column(
        dd.SamplerColumnConfig(
            name=name,
            sampler_type=dd.SamplerType.CATEGORY,
            params=dd.CategorySamplerParams(values=values, weights=weights),
        )
    )


def add_visual_variation_id(config_builder: dd.DataDesignerConfigBuilder) -> None:
    """Add a unique row-level key that discourages duplicate image generations."""
    config_builder.add_column(
        dd.SamplerColumnConfig(
            name="visual_variation_id",
            sampler_type=dd.SamplerType.UUID,
            params=dd.UUIDSamplerParams(prefix="apparel-", short_form=True),
        )
    )


def build_config(
    *,
    model_provider: str = DEFAULT_MODEL_PROVIDER,
    model_id: str = DEFAULT_MODEL_ID,
    model_alias: str = DEFAULT_MODEL_ALIAS,
    image_size: str = "1K",
    aspect_ratio: str = "3:4",
    max_parallel_requests: int = 10,
) -> dd.DataDesignerConfigBuilder:
    """Build an apparel product image variation pipeline."""
    model_configs = build_model_configs(
        model_provider=model_provider,
        model_id=model_id,
        model_alias=model_alias,
        image_size=image_size,
        aspect_ratio=aspect_ratio,
        max_parallel_requests=max_parallel_requests,
    )
    config_builder = dd.DataDesignerConfigBuilder(model_configs=model_configs)
    add_visual_variation_id(config_builder)

    add_category(
        config_builder,
        "apparel_item",
        [
            "organic cotton crewneck t-shirt",
            "lightweight denim jacket",
            "water-resistant rain jacket",
            "relaxed-fit hoodie",
            "wide-leg linen trousers",
            "ribbed knit cardigan",
            "quilted puffer vest",
            "stretch woven workwear shirt",
            "ankle-length everyday dress",
            "adaptive zip-front jacket",
        ],
    )

    add_category(
        config_builder,
        "base_colorway",
        [
            "matte black",
            "warm white",
            "sage green",
            "deep navy",
            "brushed silver",
            "terracotta",
            "soft lavender",
            "charcoal gray",
            "sunflower yellow",
            "denim blue",
        ],
    )

    add_category(
        config_builder,
        "base_view",
        [
            "front-facing standing full-body catalog photo with one synthetic adult model",
            "three-quarter standing full-body catalog photo with one synthetic adult model",
            "side-angle standing full-body catalog photo with one synthetic adult model",
            "walking-pose full-body catalog photo with one synthetic adult model",
        ],
    )

    add_category(
        config_builder,
        "base_model_profile",
        [
            "young adult Black model with an athletic build",
            "middle-aged East Asian model with an average build",
            "older Latine model with a petite build",
            "young adult South Asian model with a plus-size build",
            "middle-aged Middle Eastern model with a tall build",
            "young adult Indigenous model with a broad-shouldered build",
            "older White model with a curvy build",
            "young adult multiracial model with a slender build",
        ],
    )

    add_category(
        config_builder,
        "variation_goal",
        [
            "inclusive e-commerce catalog image on a clean white background",
            "lifestyle lookbook image in an everyday urban setting",
            "fit-guide image showing garment drape and silhouette",
            "seasonal campaign image for cool-weather layering",
            "adaptive-fashion catalog image emphasizing ease of wear",
            "single-model adult age-inclusive catalog image",
            "social media campaign image with bold colored backdrop",
            "editorial fashion image with soft premium lighting",
        ],
    )

    add_category(
        config_builder,
        "edit_scene_delta",
        [
            "move from a neutral studio catalog reference into an outdoor urban lifestyle scene",
            "move from a neutral studio catalog reference into a warm home entryway lookbook scene",
            "move from a neutral studio catalog reference into a bold color-block campaign set",
            "move from a neutral studio catalog reference into a smart-casual workplace corridor scene",
            "move from a neutral studio catalog reference into a weekend park lookbook scene",
            "move from a neutral studio catalog reference into a premium editorial studio set with draped fabric",
            "move from a neutral studio catalog reference into a clean fit-guide scene with a new full-body pose",
            "move from a neutral studio catalog reference into a seasonal layering scene with visible outerwear styling",
        ],
    )

    add_category(
        config_builder,
        "model_age_group",
        [
            "young adult model",
            "middle-aged adult model",
            "older adult model",
            "senior adult model",
        ],
    )

    add_category(
        config_builder,
        "model_ethnicity",
        [
            "Black or African diaspora model",
            "East Asian model",
            "South Asian model",
            "Latine model",
            "Middle Eastern or North African model",
            "Indigenous model",
            "Pacific Islander model",
            "White or European model",
            "multiracial model",
        ],
    )

    add_category(
        config_builder,
        "body_type",
        [
            "petite build",
            "tall build",
            "plus-size build",
            "athletic build",
            "broad-shouldered build",
            "curvy build",
            "slender build",
            "average build",
        ],
    )

    add_category(
        config_builder,
        "accessibility_context",
        [
            "standing model without visible mobility aids",
            "model with no specific accessibility cue",
            "standing model in a relaxed catalog pose",
            "model walking naturally without visible mobility aids",
            "model seated on a simple studio stool",
            "model leaning lightly against a studio block",
            "model holding a small neutral accessory",
            "seated model using a wheelchair",
            "model with a visible prosthetic limb",
            "model using forearm crutches",
        ],
        weights=[1.6, 1.6, 1.4, 1.4, 0.9, 0.9, 0.9, 0.2, 0.2, 0.2],
    )

    add_category(
        config_builder,
        "styling_context",
        [
            "pure white seamless catalog background",
            "soft neutral studio backdrop",
            "outdoor morning city street",
            "modern home entryway",
            "adult campus casual setting",
            "workplace smart-casual setting",
            "weekend park setting",
            "minimal geometric studio set",
        ],
    )

    standing_compositions = [
        "front-facing full-body catalog pose with the entire person visible",
        "three-quarter full-body pose with the entire person visible",
        "side-angle full-body pose with clear garment silhouette",
    ]
    seated_compositions = [
        "single seated full-body pose showing garment fit with the whole body visible",
    ]
    wheelchair_compositions = [
        "single seated full-body pose showing garment fit with the whole body and wheelchair visible",
    ]
    walking_compositions = [
        "single walking full-body pose with natural garment movement",
    ]
    prosthetic_compositions = [
        "standing full-body pose with visible prosthetic limb and clear garment fit",
        "single walking full-body pose with visible prosthetic limb and natural garment movement",
    ]
    crutch_compositions = [
        "standing full-body pose with forearm crutches visible and clear garment fit",
        "single walking full-body pose with forearm crutches visible and natural garment movement",
    ]
    flexible_compositions = standing_compositions + seated_compositions + walking_compositions
    config_builder.add_column(
        dd.SamplerColumnConfig(
            name="composition",
            sampler_type=dd.SamplerType.SUBCATEGORY,
            params=dd.SubcategorySamplerParams(
                category="accessibility_context",
                values={
                    "standing model without visible mobility aids": standing_compositions,
                    "model with no specific accessibility cue": flexible_compositions,
                    "standing model in a relaxed catalog pose": standing_compositions,
                    "model walking naturally without visible mobility aids": walking_compositions,
                    "model seated on a simple studio stool": seated_compositions,
                    "model leaning lightly against a studio block": standing_compositions,
                    "model holding a small neutral accessory": standing_compositions,
                    "seated model using a wheelchair": wheelchair_compositions,
                    "model with a visible prosthetic limb": prosthetic_compositions,
                    "model using forearm crutches": crutch_compositions,
                },
            ),
        )
    )

    add_category(
        config_builder,
        "lighting",
        [
            "softbox studio lighting",
            "natural window light",
            "bright catalog lighting",
            "warm golden-hour lighting",
            "soft overcast outdoor light",
        ],
    )

    config_builder.add_column(
        dd.ImageColumnConfig(
            name="base_product_image",
            prompt=BASE_PRODUCT_IMAGE_PROMPT,
            model_alias=model_alias,
        )
    )

    config_builder.add_column(
        dd.ImageColumnConfig(
            name="product_variant_image",
            prompt=PRODUCT_VARIATION_PROMPT,
            model_alias=model_alias,
            multi_modal_context=[dd.ImageContext(column_name="base_product_image")],
        )
    )

    return config_builder


def create_dataset(
    config_builder: dd.DataDesignerConfigBuilder,
    *,
    num_records: int,
    dataset_name: str,
    artifact_path: Path | str | None = None,
) -> DatasetCreationResults:
    data_designer = DataDesigner(artifact_path=artifact_path)
    data_designer.validate(config_builder)
    return data_designer.create(config_builder, num_records=num_records, dataset_name=dataset_name)


BASE_PRODUCT_IMAGE_PROMPT = """\
Create a synthetic apparel catalog reference photo of a person wearing a {{ base_colorway }} {{ apparel_item }}.

Image requirements:
- Visual variation ID, for internal diversity only: {{ visual_variation_id }}
- View: {{ base_view }}
- Base model profile: {{ base_model_profile }}
- Background: clean neutral studio background
- Lighting: soft catalog lighting
- Use exactly one synthetic adult model in neutral catalog styling.
- Output must use vertical portrait framing, with a 3:4 portrait composition that is taller than it is wide.
- The garment should be centered, worn naturally, fully visible, and isolated enough to edit later.
- The frame must be a full-body image: show the model from head to toe with feet visible and comfortable margins around the body.
- Show fabric texture, seams, silhouette, cuffs, closures, pockets, fit, drape, and other garment details when relevant.
- Keep the model presentation neutral, fully clothed, non-sexualized, and commercially appropriate.
- 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.
- Use a plausible invented garment design with consistent shape, color, fit, and material details.
- 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.
"""


PRODUCT_VARIATION_PROMPT = """\
Edit the provided apparel-on-person catalog image into a new inclusive commercial fashion image.

Variation requirements:
- Visual variation ID, for internal diversity only: {{ visual_variation_id }}
- Variation goal: {{ variation_goal }}
- Required edit delta: {{ edit_scene_delta }}
- Model age group: {{ model_age_group }}
- Model ethnicity: {{ model_ethnicity }}
- Body type: {{ body_type }}
- Accessibility context: {{ accessibility_context }}
- Styling context: {{ styling_context }}
- Composition: {{ composition }}
- Lighting: {{ lighting }}

Use the provided image as a garment reference, not as a full-shot template.
Preserve the garment's core identity from the reference image: same apparel
item, same primary colorway, same fabric cues, same silhouette, same fit
behavior, and same distinctive design details. Do not preserve the original
person, original face, original hair, original stance, original camera angle,
or original plain studio background unless that exact choice is requested by
the sampled controls.

Create a visibly different commercial variant. The final image should make at
least three obvious changes from the reference photo: a different synthetic
adult model, a different full-body pose or body orientation, a different
setting or background, and a different lighting or campaign style. Change the
person wearing the garment to match the requested synthetic adult model
background, age group, body type, and accessibility context. Represent the
adult model respectfully and without stereotypes. Every generated person must
clearly be 18 or older.

The edited output must show exactly one person. The final image must be a
vertical 3:4 portrait full-body catalog image that is taller than it is wide:
show the full head-to-toe body with feet visible, or the full seated body and
full mobility aid when the accessibility context calls for one. Do not crop at
the face, waist, knees, ankles, hands, or garment hem. Do not create landscape
frames, square frames, group shots, mirrored duplicates, before/after
composites, multiple models, mannequins, or background bystanders.

Follow the required edit delta when changing the surrounding scene, styling,
pose, background, and lighting. Keep the result realistic and commercially
usable. Do not add real brand logos, real trademarks, watermarks, price tags,
text overlays, sexualized styling, swimwear, underwear, lingerie, sheer
clothing, or revealing poses.
Generate exactly one final edited 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.
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate product image variations with image-to-image editing.")
    parser.add_argument("--num-records", type=int, default=5, help="Number of product variation rows to generate.")
    parser.add_argument("--dataset-name", default="product-image-variations", help="Output dataset name.")
    parser.add_argument("--artifact-path", type=Path, default=None, help="Optional Data Designer artifact directory.")
    parser.add_argument("--model-provider", default=DEFAULT_MODEL_PROVIDER, help="Image model provider name.")
    parser.add_argument("--model-id", default=DEFAULT_MODEL_ID, help="Provider model ID.")
    parser.add_argument("--model-alias", default=DEFAULT_MODEL_ALIAS, help="Alias used by image columns.")
    parser.add_argument("--image-size", default="1K", help="OpenRouter image size tier, such as 1K, 2K, or 4K.")
    parser.add_argument("--aspect-ratio", default="3:4", help="Provider-specific aspect ratio value.")
    parser.add_argument("--max-parallel-requests", type=int, default=10, help="Maximum parallel image requests.")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    config_builder = build_config(
        model_provider=args.model_provider,
        model_id=args.model_id,
        model_alias=args.model_alias,
        image_size=args.image_size,
        aspect_ratio=args.aspect_ratio,
        max_parallel_requests=args.max_parallel_requests,
    )
    results = create_dataset(
        config_builder,
        num_records=args.num_records,
        dataset_name=args.dataset_name,
        artifact_path=args.artifact_path,
    )
    dataset = results.load_dataset()
    print(f"Generated {len(dataset)} product variation rows.")
    print(f"Dataset artifacts: {results.artifact_storage.base_dataset_path}")


if __name__ == "__main__":
    main()
```