Product shots image gen
Open-source Claude Code skills that turn one product photo into a full set of e-commerce visuals — main images, A+ detail pages, multi-angle shoots, social posts, and ad creatives. For cross-border sellers on Amazon, Shopify, TikTok Shop.
npx -y skills add motiful/product-shots --skill product-shots-image-genAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 9 stars9 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Unified image-generation engine for the product-shots ecosystem. Dispatches to the right model family (OpenAI gpt-image-2 / Gemini gemini-3-pro-image-preview Nano Banana Pro) with one parameterised script for text-to-image and image-to-image. Primary backend: OmniMaaS / Cloubic gateway (https://api.omnimaas.com/v1); also supports any OpenAI-SDK-compatible gateway via fallback env vars. Auto-resizes oversized reference images to prevent edge-proxy timeouts. Use when the user says "generate an image", "make a picture", "create a visual", "render this", "edit this image", "做一张图", "生成图片", "改这张图" — or when invoked as the image-generation backend by another product-shots skill (main-image / detail-page / ad-creative / social-post / multi-angle). NOT a creative-direction skill: takes a prompt + optional reference images, returns a file path.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
16.7 KB, as published. Nobody here has run it
Image Gen
The image-generation engine of the product-shots ecosystem. A single, parameterised entry point: routes the request to the correct API endpoint based on model family (OpenAI's /v1/images/generations + /v1/images/edits vs Gemini's /v1/chat/completions multimodal), preprocesses reference images, and returns a saved file path.
Primary backend: OmniMaaS / Cloubic image gateway — https://api.omnimaas.com/v1. The gateway is OpenAI-SDK-compatible and unifies access to GPT image 2 and the Gemini Nano Banana family behind one auth token. See:
- GPT image 2 via OmniMaaS: https://docs.cloubic.com/docs/zh-CN/image-generation/image-openai
- Gemini via OmniMaaS: https://docs.cloubic.com/docs/zh-CN/image-generation/image-gemini
Fallback backends: any other OpenAI-SDK-compatible image gateway works — set PRODUCT_SHOTS_IMAGEGEN_BASE_URL + PRODUCT_SHOTS_IMAGEGEN_API_KEY, or keep the legacy CANVASFLOW_IMAGEGEN_* env vars for migrated installs.
Onboarding (First-Use Setup)
Before generating any image, this skill MUST verify an API gateway is configured. If env vars are missing, surface the instructions below to the user verbatim — never silently fall back to a hard-coded key, never ask the user to fork the repo, never ask them to paste a key into chat.
Detection logic
Run on every invocation. The resolution order:
OMNIMAAS_API_KEY(env var, preferred — unified Cloubic / OmniMaaS gateway)PRODUCT_SHOTS_IMAGEGEN_API_KEY(env var, generic fallback)CANVASFLOW_IMAGEGEN_API_KEY(env var, legacy compatibility)~/.product_shots_imagegen_api_key(file, chmod 600, one-line key)~/.canvasflow_imagegen_api_key(file, legacy)
If none resolve, stop and emit the onboarding message in the next section. Do NOT attempt the API call.
Onboarding message (show to user verbatim when env missing)
This skill needs an image-gateway API key before it can generate. You haven't configured one yet — here's the 30-second setup:
Option A — temporary (this terminal only):
export OMNIMAAS_API_KEY="sk-..." # your OmniMaaS / Cloubic token # optional: only set this if you have a private gateway endpoint # export OMNIMAAS_BASE_URL="https://api.omnimaas.com/v1"Option B — persistent across sessions: Add the same
exportline to your~/.zshrc(or~/.bashrc), thensource ~/.zshrc.Option C — file-based (no env vars):
echo "sk-..." > ~/.product_shots_imagegen_api_key chmod 600 ~/.product_shots_imagegen_api_keyWhere the key comes from: docs.cloubic.com — get a token from the OmniMaaS / Cloubic dashboard. The same token covers both OpenAI
gpt-image-2and Geminigemini-3-pro-image-preview(Nano Banana Pro).Any OpenAI-SDK-compatible image gateway also works — replace
OMNIMAAS_*withPRODUCT_SHOTS_IMAGEGEN_BASE_URL+PRODUCT_SHOTS_IMAGEGEN_API_KEYpointing at your gateway.Once configured, re-run the original request.
Security invariants
- Never echo the full key. When confirming detection, show only the first 8 characters (
sk-wPfH6K…). - Never write the key to logs, stdout, or any file inside the repo.
- Never instruct the user to fork the repo, edit skill source, or paste their key into chat. The key lives in env /
~/.*_api_keyfiles only. - API key is transmitted only via the
Authorization: Bearer <key>header.
Local-proxy bypass (CN users)
ClashX / Shadowsocks / similar local proxies inject ALL_PROXY=socks5://... into the environment, which breaks requests (Missing dependencies for SOCKS support). The bundled scripts/generate.py already handles this via Session.trust_env = False. If you implement a custom caller, replicate this pattern.
Engagement Principles
These rules always apply. Read them before acting.
- Auto-detect model family — never ask the user. The model name (e.g.,
gpt-image-2vsgemini-3-pro-image-preview) determines the endpoint and request shape. Caller specifies model; skill resolves family. - Default to
gemini-3-pro-image-previewwhen the caller does not specify a model. It is the strongest single-model performer (Nano Banana Pro) and supports both text-to-image and image-to-image through one endpoint. Seereferences/model-selection.md. - Reference images MUST be ≤1024px max dimension AND ≤1MB before being sent. Auto-resize otherwise. Reason: most edge proxies time out at ~100s; a 2.5MB PNG + base64 expansion + Gemini processing reliably triggers HTTP 524 at that ceiling. See
references/reference-image-handling.md. - Negative constraints are appended to the prompt as "Avoid: …", never sent as a separate parameter. No supported model exposes a true
negative_promptfield; this is by API design, not a workaround. - Aspect ratio handling diverges by family. OpenAI: translated to a pixel
sizeparam. Gemini: appended to the prompt text ("aspect ratio: X:Y") since chat-completions has no size field. Caller passes--aspect-ratio; skill handles both. - Output is always saved to disk and the path returned. Never return base64 or URL to the caller — the file on disk is the canonical artifact. Default location uses a timestamp; caller can override with
--output. - Cost and token usage are logged on every call. Caller can budget. Estimated rates: gpt-image-2 ≈ ¥0.35/image, gemini-3-pro-image-preview ≈ ¥1/image, gemini-3.1-flash-image-preview ≈ ¥0.20/image.
- Bounded retries for image-to-image; fail-fast everywhere else. Image-to-image calls retry up to 3 attempts on retryable errors (HTTP 429, 5xx, 524, connection errors, read timeouts) with exponential backoff (1s, 4s, 16s between attempts). Text-to-image calls do not retry. Auth errors (401 / 403) fail fast — never retried. See
references/error-handling.md. - API keys never reach logs or stdout. Loaded in this order:
OMNIMAAS_API_KEY(preferred — Cloubic / OmniMaaS gateway) →PRODUCT_SHOTS_IMAGEGEN_API_KEY(canonical generic) →RENDER_API_KEY(short alias) →CANVASFLOW_IMAGEGEN_API_KEY(legacy) →~/.product_shots_imagegen_api_key→~/.product_shots_render_api_key(compat) →~/.canvasflow_imagegen_api_key(legacy). Passed only via theAuthorizationheader. Never echoed. The base URL is auto-resolved (defaults tohttps://api.omnimaas.com/v1whenOMNIMAAS_API_KEYis set without an explicitOMNIMAAS_BASE_URL).
Execution Procedure
Follow this procedure exactly. Each step maps to a section of scripts/generate.py.
generate_image(prompt, model, [aspect_ratio], [negative_prompt],
[reference_images], [output_path]) → file_path
# Step 0 — Resolve API key + base URL (MUST come first)
api_key, key_source = load_api_key()
# 1. OMNIMAAS_API_KEY env var (preferred — OmniMaaS gateway)
# 2. PRODUCT_SHOTS_IMAGEGEN_API_KEY env var (canonical generic)
# 3. RENDER_API_KEY env var (short alias)
# 4. CANVASFLOW_IMAGEGEN_API_KEY env var (legacy)
# 5. ~/.product_shots_imagegen_api_key file
# 6. ~/.product_shots_render_api_key file (compat)
# 7. ~/.canvasflow_imagegen_api_key file (legacy)
# fail with clear message if none present
base_url, url_source = load_base_url()
# 1. OMNIMAAS_BASE_URL env var
# 2. PRODUCT_SHOTS_IMAGEGEN_BASE_URL env var (canonical generic)
# 3. RENDER_BASE_URL env var (short alias)
# 4. CANVASFLOW_IMAGEGEN_BASE_URL env var (legacy)
# 5. https://api.omnimaas.com/v1 (default when OMNIMAAS_API_KEY is set)
# Step 0a — Validate caller arguments + fill caller-context defaults
args = validate_args(args) # see references/parameter-spec.md
# rejects empty prompt, unknown model, invalid aspect_ratio
# auto-fills size for OpenAI from aspect_ratio
# warns when Gemini ignores --n / --size
args = lookup_caller_defaults(caller_skill, surface) | args
# caller-skill-specific defaults from parameter-spec.md §Family-Specific Defaults
# explicit args win over defaults (dict merge with args on the right)
# Step 1 — Resolve model + identify family
if not model:
model = select_default_model(use_case) # see references/model-selection.md
# use_case from caller brief: "text-overlay" / "photorealistic" /
# "creative" / "image-to-image" / "cost-sensitive" / "general" (default)
# ⚠ "text-overlay" WINS over all others — any caller whose prompt
# asks for on-image letters or digits (headlines, labels, CTAs,
# price chips, callouts) MUST pass "text-overlay" so the dispatcher
# routes to gpt-image-2. Gemini family garbles small text.
family = model_family(model) # see references/model-selection.md
# openai → {gpt-image-1, gpt-image-2, dall-e-3}
# gemini → {gemini-3-pro-image-preview, gemini-3.1-flash-image-preview, ...}
# unknown → exit with the supported model list
# Step 2 — Preprocess reference images (if any)
for img_path in reference_images:
img_path = maybe_resize(img_path, max_dim=1024, max_bytes=1MB)
# Pillow thumbnail → temp file
# passthrough if already within limits
# see references/reference-image-handling.md
# Step 3 — Compose effective prompt and size
final_prompt = compose_prompt(prompt, negative_prompt, aspect_ratio, family)
# Gemini: prompt + "(aspect ratio: X:Y)" + "Avoid: ..."
# OpenAI: prompt + "Avoid: ..." (aspect handled via size param instead)
if family == "openai":
size = OPENAI_SIZE_BY_RATIO[aspect_ratio] OR explicit --size OR "1024x1024"
# Step 4 — Dispatch
if family == "openai":
if reference_images:
response = POST <base_url>/images/edits (multipart, image[]=@file...)
else:
response = POST <base_url>/images/generations (JSON, {model, prompt, n, size})
elif family == "gemini":
response = POST <base_url>/chat/completions
body: {model, messages:[{role:"user", content:[
{type:"text", text: final_prompt},
{type:"image_url", image_url:{url: data_url_per_ref_image}}, ...
]}]}
if response.status != 200:
handle_http_error(family, response.status, response.body)
# see references/error-handling.md
# Step 5 — Parse response (family-specific)
if family == "openai":
first = response.body["data"][0]
if "b64_json" not in first and "url" not in first:
classify_response_error(family, response.body) # references/error-handling.md
image_bytes = base64_decode(first["b64_json"]) if "b64_json" in first else fetch(first["url"])
ext = "png"
elif family == "gemini":
content = response.body["choices"][0]["message"]["content"]
# content is a markdown string: 
if no "data:image/...;base64" pattern in content:
classify_response_error(family, response.body) # references/error-handling.md
image_bytes = base64_decode(extract_data_url(content))
ext = "jpeg"
# Step 6 — Save + log
out_path = output_path OR ./output-<unix_ts>.<ext>
write_bytes(out_path, image_bytes)
log(gateway_source, elapsed, file_size, total_tokens, out_path)
# Self-check
assert out_path.exists()
assert out_path.stat().st_size > 10_000 # tiny files = broken response
return out_path
TOC of Module Files
references/model-selection.md— Model catalogue (image models supported by this skill), family routing rules, decision tree for choosing the right model per use case (photorealism / text rendering / cheap-and-fast / CJK text rendering).references/parameter-spec.md— Every CLI flag ofscripts/generate.py: name, type, default, applicability per family, valid range. Single source of truth for what callers can pass — including the full API key / base URL resolution order.references/reference-image-handling.md— The 524 finding, the 1024px / 1MB ceiling, multi-reference behaviour (Gemini ≤9 images per call), passthrough rule, why we use Pillowthumbnail(preserves aspect ratio).references/error-handling.md— Common API failure modes (401 / 429 / 524 / malformed response / connection timeout) with diagnostic messages and recommended caller behaviour. The 524 → resize-and-retry pattern.
Section Index
Goal → SKILL.md intro paragraph
Engagement Principles → SKILL.md §Engagement Principles
Execution Procedure → SKILL.md §Execution Procedure
Model Selection → references/model-selection.md
Family Routing
Model Catalogue
Decision Tree
Parameter Spec → references/parameter-spec.md
CLI Flags
Aspect Ratio → OpenAI Size Mapping
API Key + Base URL Resolution
Reference Image Handling → references/reference-image-handling.md
The 524 Finding
Auto-Resize Rule
Multi-Reference Constraints
Error Handling → references/error-handling.md
Common Failure Modes
Retry Patterns
Tooling
scripts/generate.py— the parameterised entry point. Stdlib +requests+Pillowonly. No host capability requirements.scripts/setup.sh— idempotent dependency installer (Python 3 + pip + requests + Pillow). Run once before first use.- No sub-skill calls. This skill is a leaf: it does not invoke other skills.
- Callers (other product-shots specialists) invoke via shell. Resolve the script path from the host's skill directory rather than hardcoding any platform path:
or via direct Python import of the# Path examples — pick whichever your harness uses; do NOT bake one in: # Claude Code: ~/.claude/skills/product-shots-image-gen/scripts/generate.py # Codex: ~/.agents/skills/product-shots-image-gen/scripts/generate.py # Cursor: ~/.cursor/skills/product-shots-image-gen/scripts/generate.py # Windsurf: ~/.codeium/windsurf/skills/product-shots-image-gen/scripts/generate.py # Copilot: ~/.copilot/skills/product-shots-image-gen/scripts/generate.py python <skill-dir>/scripts/generate.py \ --prompt "..." --model gemini-3-pro-image-preview --output ./out.jpeggenerate_openai/generate_geminifunctions.
Caller Contract
When invoked from another skill in the product-shots ecosystem (e.g., product-shots-main-image reaching its image = generate_image(prompt) step), the caller passes:
| Field | Required | Format | Skill behaviour |
|---|---|---|---|
prompt | yes | string, ≤4000 chars | passed through (+ negative + aspect ratio appended) |
model | yes | from supported set | family-routed to correct endpoint |
aspect_ratio | no | "1:1" / "16:9" / "9:16" / "4:3" / "3:4" / "3:2" / "2:3" | OpenAI: → size; Gemini: appended to prompt |
reference_images | no | list of file paths | auto-resized to ≤1024px / ≤1MB before send |
negative_prompt | no | string | appended as "Avoid: …" |
output | no | file path | defaults to ./output-<ts>.<png|jpeg> |
Returns: file path on disk (PNG for OpenAI, JPEG for Gemini). Caller MUST treat the file as the canonical artifact.
OmniMaaS gateway setup quickstart
# 1. Get an OmniMaaS / Cloubic API token from https://docs.cloubic.com
export OMNIMAAS_API_KEY="your-omnimaas-token"
# 2. (optional) Override the base URL if you have a private endpoint
# export OMNIMAAS_BASE_URL="https://api.omnimaas.com/v1"
# 3. Install dependencies
bash scripts/setup.sh
# 4. Test
python scripts/generate.py --prompt "a red apple on wood" \
--model gemini-3-pro-image-preview \
--output /tmp/test.jpeg
When OMNIMAAS_API_KEY is set, the script auto-defaults the base URL to https://api.omnimaas.com/v1 — no additional configuration required.