Step 2: Contact Sheet Workflow
Upload the fashion editorial and product image. Generate 8 distinct angle variations with product consistency from only 2 Nano Banana generations.
About
This ComfyUI workflow builds a fast, repeatable contact-sheet generator for fashion products. You provide two inputs—a fashion editorial reference and a clean product image—and the graph produces eight distinct angle variations while preserving product identity. The pipeline pivots on Nano Banana Pro for image generation and uses a small, consistent set of prompts to drive variety without drifting from the product look. Outputs are compiled into a vertical 9:16 contact sheet at 4K resolution (2160x3840) for quick review or social/merchandising handoff.
Technically, the graph uses LoadImage to bring in your editorial reference and product packshot. A PrimitiveStringMultiline node holds the eight angle directives (you can edit these to change viewpoints), while a PrimitiveNode stores simple numeric settings (such as the number of angles or seeds). GeminiImage2Node (configured for gemini-3-pro-image-preview) can be used as a helper to analyze the editorial reference or produce caption text that feeds into your prompt block. Two essentials nodes (UUIDs 68c58c16-e698-45a4-97f9-54ae8eb9dee9 and 487baed2-dc27-49c3-a6b2-0bf45a53e64d from comfyui_essentials) handle image collation and layout with BatchImagesNode assembling the eight results, RegexReplace cleaning file names, and SaveImage writing a single 8-up contact sheet plus optional individual frames.
The result is a compact, reliable step that expands only two Nano Banana generations into eight consistent angles.
What you provide
- Reading the workflow
Runs in ComfyUI. This workflow can't run on this page. Open it in Comfy Cloud with everything loaded, or download it for your own ComfyUI.
Try in CloudInside the workflow
A read-only preview of the node graph. Open it in Comfy Cloud or download it to edit the nodes.
Loading the graph
Open full-size workflow preview- Author
- Rob
Requires custom nodes
- Reading the workflow
Use this workflow from code
Generated from the workflow graph and inputs when possible; stored examples are shown otherwise. Review credentials and endpoints before running code, which may incur charges.
What you need
- Runs on
- Comfy Cloud
- Key
- COMFY_API_KEY
Needs a paid Cloud plan and credits.
# Python 3.10+: pip install comfy-sdk==0.4.0
import os
from comfy_sdk import Comfy
api_key = os.environ.get("COMFY_API_KEY", "").strip()
if not api_key: raise RuntimeError("Set COMFY_API_KEY")
os.environ.setdefault("COMFY_BASE_URL", "https://cloud.comfy.org")
client = Comfy(api_key=api_key)
workflow = client.workflows.from_json({
"16": {
"class_type": "LoadImage",
"inputs": {
"image": "contact_sheet_app_input_1.png"
},
"_meta": {
"title": "Load Character Image"
}
},
"26": {
"class_type": "LoadImage",
"inputs": {
"image": "brown_moisturizer_bottle.jpg"
},
"_meta": {
"title": "Load Product Image"
}
},
"30": {
"class_type": "GeminiImage2Node",
"inputs": {
"aspect_ratio": "9:16",
"images": [
"229",
0
],
"model": "gemini-3-pro-image-preview",
"prompt": [
"248",
0
],
"resolution": "4K",
"response_modalities": "IMAGE+TEXT",
"seed": 586933274482026,
"system_prompt": "You are an expert image-generation engine. You must ALWAYS produce an image.\nInterpret all user input—regardless of format, intent, or abstraction—as literal visual directives for image composition.\nIf a prompt is conversational or lacks specific visual details, you must creatively invent a concrete visual scenario that depicts the concept.\nPrioritize generating the visual representation above any text, formatting, or conversational requests."
},
"_meta": {
"title": "Nano Banana Pro (Google Gemini Image)"
}
},
"34": {
"class_type": "PrimitiveStringMultiline",
"inputs": {
"value": "Analyze the input images and silently inventory all fashion-critical details: the subject(s) from image 1, exact wardrobe pieces, facial features, materials, colors, textures, accessories, hair, makeup, body proportions, environment, set geometry, light direction, and shadow quality.\nAll wardrobe, styling, hair, makeup, lighting, environment, and color grade must remain 100% unchanged across all frames. The character from image 1 is holding the product from image 2, maintain exact representation of the product, text legibility and relative size as seen in image 1.\nDo not add or remove anything (unless specified in the prompt).\nDo not generate with visible gutters/padding between grid images.\n \nYour visible output must be:\n \nOne 2×2 grid image (4 frames).\n \nThen a keyframe breakdown for each frame.\n \nEach frame must represent a resting point after a dramatic camera move — only describe the final camera position and what the subject is doing, never the motion itself.\n \nThe four frames must be spatially dynamic, non-linear, and visually distinct.\n \nRequired 4-Frame Shot List \n{grid_descriptions}\n\n \nContinuity & Technical Requirements\n \nMaintain perfect wardrobe fidelity in every frame: exact garment type, silhouette, material, color, texture, stitching, accessories, closures, jewelry, shoes, hair, and makeup.\n \nEnvironment, textures, and lighting must remain consistent.\n \nDepth of field shifts naturally with focal length (deep for distant shots, shallow for close/detail shots).\n \nPhoto real textures and physically plausible light behavior required.\n \nFrames must feel like different camera placements within the same scene, not different scenes.\n \nAll keyframes must be the exact same aspect ratio, and exactly 4 keyframes should be output. Maintain the exact visual style in all keyframes, where the image is shot on fuji velvia film with a hard flash, the light is concentrated on the subject and fades slightly toward the edges of the frame. The image is over exposed showing significant film grain and is oversaturated. The skin appears shiny (almost oily), and there are harsh white reflections on the glasses frames.\n \nOutput Format\nA) 2×2 Contact Sheet Image (Mandatory)"
},
"_meta": {
"title": "Prompt Template V1"
}
},
"61": {
"class_type": "GeminiImage2Node",
"inputs": {
"aspect_ratio": "9:16",
"images": [
"230",
0
],
"model": "gemini-3-pro-image-preview",
"prompt": [
"255",
0
],
"resolution": "4K",
"response_modalities": "IMAGE+TEXT",
"seed": 363592989122759,
"system_prompt": "You are an expert image-generation engine. You must ALWAYS produce an image.\nInterpret all user input—regardless of format, intent, or abstraction—as literal visual directives for image composition.\nIf a prompt is conversational or lacks specific visual details, you must creatively invent a concrete visual scenario that depicts the concept.\nPrioritize generating the visual representation above any text, formatting, or conversational requests."
},
"_meta": {
"title": "Nano Banana Pro (Google Gemini Image)"
}
},
"62": {
"class_type": "PrimitiveStringMultiline",
"inputs": {
"value": "Analyze the input images and inventory all fashion-critical details: the subject(s) from image 1, exact wardrobe pieces, facial features, materials, colors, textures, accessories, hair, makeup, body proportions, environment, set geometry, light direction, and shadow quality.\nAll wardrobe, styling, hair, makeup, lighting, environment, and color grade must remain 100% unchanged across all frames. The character from image 1 is holding the product from image 2, maintain exact representation of the product, text legibility and relative size as seen in image 1.\nDo not add or remove anything (unless specified in the prompt).\nDo not generate with visible gutters/padding between grid images.\n \nYour visible output must be:\n \nOne 2×2 grid (4 frames).\n \nThen a keyframe breakdown for each frame.\n \nEach frame must represent a resting point after a dramatic camera move — only describe the final camera position and what the subject is doing, never the motion itself.\n \nThe four frames must be spatially dynamic, non-linear, and visually distinct.\n \nRequired 4-Frame Shot List \n{grid_descriptions}\n \nContinuity & Technical Requirements\n \nMaintain perfect wardrobe fidelity in every frame: exact garment type, silhouette, material, color, texture, stitching, accessories, closures, jewelry, shoes, hair, and makeup.\n \nEnvironment, textures, and lighting must remain consistent.\n \nDepth of field shifts naturally with focal length (deep for distant shots, shallow for close/detail shots).\n \nPhoto real textures and physically plausible light behavior required.\n \nFrames must feel like different camera placements within the same scene, not different scenes.\n \nAll keyframes must be the exact same aspect ratio, and exactly 4 keyframes should be output. Maintain the exact visual style in all keyframes, where the image is shot on fuji velvia film with a hard flash, the light is concentrated on the subject and fades slightly toward the edges of the frame. The image is over exposed showing significant film grain and is oversaturated. The skin appears shiny (almost oily), and there are harsh white reflections on the glasses frames.\n \nOutput Format\nA) 2×2 Contact Sheet Image (Mandatory)"
},
"_meta": {
"title": "Prompt Template V2"
}
},
"88": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_5",
"images": [
"228:264",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"89": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_6",
"images": [
"228:265",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"90": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_7",
"images": [
"228:268",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"91": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_8",
"images": [
"228:269",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"229": {
"class_type": "BatchImagesNode",
"inputs": {
"images.image0": [
"16",
0
],
"images.image1": [
"26",
0
]
},
"_meta": {
"title": "Batch Images"
}
},
"230": {
"class_type": "BatchImagesNode",
"inputs": {
"images.image0": [
"16",
0
],
"images.image1": [
"26",
0
]
},
"_meta": {
"title": "Batch Images"
}
},
"239": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_1",
"images": [
"278:274",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"240": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_2",
"images": [
"278:275",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"241": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_3",
"images": [
"278:276",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"242": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI-image_4",
"images": [
"278:277",
0
]
},
"_meta": {
"title": "Save Image"
}
},
"245": {
"class_type": "PrimitiveStringMultiline",
"inputs": {
"value": "1. High-Fashion Beauty Portrait (Close, Editorial, Intimate)\n \nCamera positioned very close to the subject's face, slightly above or slightly below eye level, using an elegant offset angle that enhances bone structure and highlights key wardrobe elements near the neckline. Shallow depth of field, flawless texture rendering, and a sculptural fashion-forward composition.\n \n2. High-Angle Three-Quarter Frame\n \nCamera positioned overhead but off-center, capturing the subject from a diagonal downward angle.\nThis frame should create strong shape abstraction and reveal wardrobe details from above.\n \n3. Low-Angle Oblique Full-Body Frame\n \nCamera positioned low to the ground and angled obliquely toward the subject.\nThis elongates the silhouette, emphasizes footwear, and creates a dramatic perspective distinct from Frames 1 and 2.\n \n4. Side-On Compression Frame (Long Lens)\n \nCamera placed far to one side of the subject, using a tighter focal length to compress space.\nThe subject appears in clean profile or near-profile, showcasing garment structure in a flattened, editorial manner."
},
"_meta": {
"title": "Prompt Grid Images V1"
}
},
"248": {
"class_type": "RegexReplace",
"inputs": {
"case_insensitive": True,
"count": 0,
"dotall": False,
"multiline": False,
"regex_pattern": "{grid_descriptions}\n",
"replace": [
"245",
0
],
"string": [
"34",
0
]
},
"_meta": {
"title": "Replace Text (Regex)"
}
},
"249": {
"class_type": "PrimitiveStringMultiline",
"inputs": {
"value": " 1. Intimate Close Portrait From an Unexpected Height\n \nCamera positioned very close to the subject's face (or upper torso) but slightly above or below eye level.\nThe angle should feel fashion-editorial, not conventional — offset, elegant, and expressive.\n \n2. Extreme Detail Frame From a Non-Intuitive Angle\n \nCamera positioned extremely close to a wardrobe detail, accessory, or texture, but from an unusual spatial direction (e.g., from below, from behind, from the side of a neckline).\nThis must be a striking, abstract, editorial detail frame.\n\n3. Extreme depth perception of the subject holding item (INTRODUCE A NEW ITEM IF PROMPTED)\n\nA dynamic, high-energy photograph shot from an extreme low-angle worm’s-eye view using forced perspective. The man is thrusting the product from image 2 toward the camera lens, making the device appear larger than life in the foreground. His free hand is raised in a casual peace sign. \n\n4. Extreme Low Angle Shot from Perspective of the Subject's Shoe.\n\nCamera positioned extremely low with main focus on the characters shoe in the immediate foreground. Extreme foreshortening makes the sneaker in the immediate foreground appear massive and highly detailed, dominating the bottom half of the frame, while the subject's body tapers upward towards a smaller head. Infer from the rest of the outfit what shoe the subject is wearing."
},
"_meta": {
"title": "Prompt Grid Images V2"
}
},
"255": {
"class_type": "RegexReplace",
"inputs": {
"case_insensitive": True,
"count": 0,
"dotall": False,
"multiline": False,
"regex_pattern": "{grid_descriptions}\n",
"replace": [
"249",
0
],
"string": [
"62",
0
]
},
"_meta": {
"title": "Replace Text (Regex)"
}
},
"228:17": {
"class_type": "GetImageSize",
"inputs": {
"image": [
"61",
0
]
},
"_meta": {
"title": "Get Image Size"
}
},
"228:226": {
"class_type": "SimpleMath+",
"inputs": {
"a": [
"228:17",
1
],
"b": 0,
"value": "a / 2"
},
"_meta": {
"title": "HEIGHT"
}
},
"228:264": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"228:226",
0
],
"image": [
"61",
0
],
"position": "top-left",
"width": [
"228:83",
0
],
"x_offset": 0,
"y_offset": 0
},
"_meta": {
"title": "Crop Grid 1"
}
},
"228:265": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"228:226",
0
],
"image": [
"61",
0
],
"position": "top-left",
"width": [
"228:83",
0
],
"x_offset": [
"228:83",
0
],
"y_offset": 0
},
"_meta": {
"title": "Crop Grid 2"
}
},
"228:268": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"228:226",
0
],
"image": [
"61",
0
],
"position": "top-left",
"width": [
"228:83",
0
],
"x_offset": 0,
"y_offset": [
"228:87",
0
]
},
"_meta": {
"title": "Crop Grid 3"
}
},
"228:269": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"228:226",
0
],
"image": [
"61",
0
],
"position": "top-left",
"width": [
"228:83",
0
],
"x_offset": [
"228:83",
0
],
"y_offset": [
"228:87",
0
]
},
"_meta": {
"title": "Crop Grid 4"
}
},
"228:83": {
"class_type": "SimpleMath+",
"inputs": {
"a": [
"228:17",
0
],
"b": 0,
"value": "a / 2"
},
"_meta": {
"title": "WIDTH"
}
},
"228:87": {
"class_type": "SimpleMath+",
"inputs": {
"a": [
"228:226",
0
],
"b": 0,
"value": "a + (b/2)"
},
"_meta": {
"title": "Row 2 - Y val"
}
},
"278:270": {
"class_type": "SimpleMath+",
"inputs": {
"a": [
"278:272",
1
],
"b": 0,
"value": "a / 2"
},
"_meta": {
"title": "HEIGHT"
}
},
"278:271": {
"class_type": "SimpleMath+",
"inputs": {
"a": [
"278:272",
0
],
"b": 0,
"value": "a / 2"
},
"_meta": {
"title": "WIDTH"
}
},
"278:272": {
"class_type": "GetImageSize",
"inputs": {
"image": [
"30",
0
]
},
"_meta": {
"title": "Get Image Size"
}
},
"278:273": {
"class_type": "SimpleMath+",
"inputs": {
"a": [
"278:270",
0
],
"b": 0,
"value": "a + (b/2)"
},
"_meta": {
"title": "Row 2 - Y val"
}
},
"278:274": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"278:270",
0
],
"image": [
"30",
0
],
"position": "top-left",
"width": [
"278:271",
0
],
"x_offset": 0,
"y_offset": 0
},
"_meta": {
"title": "Crop Grid 1"
}
},
"278:275": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"278:270",
0
],
"image": [
"30",
0
],
"position": "top-left",
"width": [
"278:271",
0
],
"x_offset": [
"278:271",
0
],
"y_offset": 0
},
"_meta": {
"title": "Crop Grid 2"
}
},
"278:276": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"278:270",
0
],
"image": [
"30",
0
],
"position": "top-left",
"width": [
"278:271",
0
],
"x_offset": 0,
"y_offset": [
"278:273",
0
]
},
"_meta": {
"title": "Crop Grid 3"
}
},
"278:277": {
"class_type": "ImageCrop+",
"inputs": {
"height": [
"278:270",
0
],
"image": [
"30",
0
],
"position": "top-left",
"width": [
"278:271",
0
],
"x_offset": [
"278:271",
0
],
"y_offset": [
"278:273",
0
]
},
"_meta": {
"title": "Crop Grid 4"
}
}
})
# run() also passes api_key to the partner nodes in this workflow.
job = client.run(workflow, api_key=api_key)Questions & answers
Edit the PrimitiveStringMultiline node. Replace or reorder the angle directives to match the views you need. The workflow will generate one image per line, then assemble all eight into the contact sheet.
No. GeminiImage2Node (gemini-3-pro-image-preview) is an optional helper for deriving captions or angle hints from the editorial reference. You can rely entirely on the manual directives in PrimitiveStringMultiline if you prefer full control.
Consistency comes from using the same product image input and a tightly scoped set of prompts with Nano Banana Pro. Keep the product image clean and well-lit, avoid overly creative modifiers, and reuse a fixed seed if you want stricter repeatability.
Yes. BatchImagesNode passes each frame to SaveImage. You can keep both the 8-up grid and the individual frames, or disable the grid output path if you only want separate images.