Install
$ agentstack add skill-motiful-product-shots-product-shots-image-gen ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
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): > ``bash > 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 export line to your ~/.zshrc (or ~/.bashrc), then source ~/.zshrc. > > **Option C — file-based (no env vars):** > `bash > echo "sk-..." > ~/.product_shots_imagegen_api_key > chmod 600 ~/.product_shots_imagegen_api_key > ` > > **Where the key comes from:** [docs.cloubic.com](https://docs.cloubic.com) — get a token from the OmniMaaS / Cloubic dashboard. The same token covers both OpenAI gpt-image-2 and Gemini gemini-3-pro-image-preview (Nano Banana Pro). > > Any OpenAI-SDK-compatible image gateway also works — replace OMNIMAAS* with PRODUCTSHOTSIMAGEGENBASEURL + PRODUCTSHOTSIMAGEGENAPI_KEY` pointing 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: Bearerheader.
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 /images/edits (multipart, image[]=@file...)
else:
response = POST /images/generations (JSON, {model, prompt, n, size})
elif family == "gemini":
response = POST /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-.
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:
`` # 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 /scripts/generate.py \ --prompt "..." --model gemini-3-pro-image-preview --output ./out.jpeg ` or via direct Python import of the generateopenai / generategemini` functions.
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-. |
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.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: motiful
- Source: motiful/product-shots
- License: MIT
- Homepage: https://github.com/motiful/product-shots
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.