ChatGPT Codex CLI OpenAI Skill

imagegen

Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variant

LLM Mart · 0 points · 59 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download openai-skills-skills_.system_imagegen-49f948f.zip · 33 KB
Part of openai/skills — 44 skills

Install

skills CLI npx skills add https://github.com/openai/skills/tree/main/skills/.system/imagegen
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install openai-skills@llmmart
Git git clone https://github.com/openai/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole openai/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Image Generation Skill

Generates or edits images for the current project (for example website assets, game assets, UI mockups, product mockups, wireframes, logo design, photorealistic images, or infographics).

Top-level modes and rules

This skill has exactly two top-level modes:

  • Default built-in tool mode (preferred): built-in image_gen tool for normal image generation and editing. Does not require OPENAI_API_KEY.
  • Fallback CLI mode (explicit-only): scripts/image_gen.py CLI. Use only when the user explicitly asks for the CLI path. Requires OPENAI_API_KEY.

Within the explicit CLI fallback only, the CLI exposes three subcommands:

  • generate
  • edit
  • generate-batch

Rules:

  • Use the built-in image_gen tool by default for all normal image generation and editing requests.
  • Never switch to CLI fallback automatically.
  • If the built-in tool fails or is unavailable, tell the user the CLI fallback exists and that it requires OPENAI_API_KEY. Proceed only if the user explicitly asks for that fallback.
  • If the user explicitly asks for CLI mode, use the bundled scripts/image_gen.py workflow. Do not create one-off SDK runners.
  • Never modify scripts/image_gen.py. If something is missing, ask the user before doing anything else.

Built-in save-path policy:

  • In built-in tool mode, Codex saves generated images under $CODEX_HOME/* by default.
  • Do not describe or rely on OS temp as the default built-in destination.
  • Do not describe or rely on a destination-path argument (if any) on the built-in image_gen tool. If a specific location is needed, generate first and then move or copy the selected output from $CODEX_HOME/generated_images/....
  • Save-path precedence in built-in mode:
    1. If the user names a destination, move or copy the selected output there.
    2. If the image is meant for the current project, move or copy the final selected image into the workspace before finishing.
    3. If the image is only for preview or brainstorming, render it inline; the underlying file can remain at the default $CODEX_HOME/* path.
  • Never leave a project-referenced asset only at the default $CODEX_HOME/* path.
  • Do not overwrite an existing asset unless the user explicitly asked for replacement; otherwise create a sibling versioned filename such as hero-v2.png or item-icon-edited.png.

Shared prompt guidance for both modes lives in references/prompting.md and references/sample-prompts.md.

Fallback-only docs/resources for CLI mode:

  • references/cli.md
  • references/image-api.md
  • references/codex-network.md
  • scripts/image_gen.py

When to use

  • Generate a new image (concept art, product shot, cover, website hero)
  • Generate a new image using one or more reference images for style, composition, or mood
  • Edit an existing image (inpainting, lighting or weather transformations, background replacement, object removal, compositing, transparent background)
  • Produce many assets or variants for one task

When not to use

  • Extending or matching an existing SVG/vector icon set, logo system, or illustration library inside the repo
  • Creating simple shapes, diagrams, wireframes, or icons that are better produced directly in SVG, HTML/CSS, or canvas
  • Making a small project-local asset edit when the source file already exists in an editable native format
  • Any task where the user clearly wants deterministic code-native output instead of a generated bitmap

Decision tree

Think about two separate questions:

  1. Intent: is this a new image or an edit of an existing image?
  2. Execution strategy: is this one asset or many assets/variants?

Intent:

  • If the user wants to modify an existing image while preserving parts of it, treat the request as edit.
  • If the user provides images only as references for style, composition, mood, or subject guidance, treat the request as generate.
  • If the user provides no images, treat the request as generate.

Built-in edit semantics:

  • Built-in edit mode is for images already visible in the conversation context, such as attached images or images generated earlier in the thread.
  • If the user wants to edit a local image file with the built-in tool, first load it with built-in view_image tool so the image is visible in the conversation context, then proceed with the built-in edit flow.
  • Do not promise arbitrary filesystem-path editing through the built-in tool.
  • If a local file still needs direct file-path control, masks, or other explicit CLI-only parameters, use the explicit CLI fallback only when the user asks for it.
  • For edits, preserve invariants aggressively and save non-destructively by default.

Execution strategy:

  • In the built-in default path, produce many assets or variants by issuing one image_gen call per requested asset or variant.
  • In the explicit CLI fallback path, use the CLI generate-batch subcommand only when the user explicitly chose CLI mode and needs many prompts/assets.

Assume the user wants a new image unless they clearly ask to change an existing one.

Workflow

  1. Decide the top-level mode: built-in by default, fallback CLI only if explicitly requested.
  2. Decide the intent: generate or edit.
  3. Decide whether the output is preview-only or meant to be consumed by the current project.
  4. Decide the execution strategy: single asset vs repeated built-in calls vs CLI generate-batch.
  5. Collect inputs up front: prompt(s), exact text (verbatim), constraints/avoid list, and any input images.
  6. For every input image, label its role explicitly:
    • reference image
    • edit target
    • supporting insert/style/compositing input
  7. If the edit target is only on the local filesystem and you are staying on the built-in path, inspect it with view_image first so the image is available in conversation context.
  8. If the user asked for a photo, illustration, sprite, product image, banner, or other explicitly raster-style asset, use image_gen rather than substituting SVG/HTML/CSS placeholders. If the request is for an icon, logo, or UI graphic that should match existing repo-native SVG/vector/code assets, prefer editing those directly instead.
  9. Augment the prompt based on specificity:
    • If the user's prompt is already specific and detailed, normalize it into a clear spec without adding creative requirements.
    • If the user's prompt is generic, add tasteful augmentation only when it materially improves output quality.
  10. Use the built-in image_gen tool by default.
  11. If the user explicitly chooses the CLI fallback, then and only then use the fallback-only docs for quality, input_fidelity, masks, output format, output paths, and network setup.
  12. Inspect outputs and validate: subject, style, composition, text accuracy, and invariants/avoid items.
  13. Iterate with a single targeted change, then re-check.
  14. For preview-only work, render the image inline; the underlying file may remain at the default $CODEX_HOME/generated_images/... path.
  15. For project-bound work, move or copy the selected artifact into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the default $CODEX_HOME/generated_images/... path.
  16. For batches, persist only the selected finals in the workspace unless the user explicitly asked to keep discarded variants.
  17. Always report the final saved path for any workspace-bound asset, plus the final prompt and whether the built-in tool or fallback CLI mode was used.

Prompt augmentation

Reformat user prompts into a structured, production-oriented spec. Make the user's goal clearer and more actionable, but do not blindly add detail.

Treat this as prompt-shaping guidance, not a closed schema. Use only the lines that help, and add a short extra labeled line when it materially improves clarity.

Specificity policy

Use the user's prompt specificity to decide how much augmentation is appropriate:

  • If the prompt is already specific and detailed, preserve that specificity and only normalize/structure it.
  • If the prompt is generic, you may add tasteful augmentation when it will materially improve the result.

Allowed augmentations:

  • composition or framing hints
  • polish level or intended-use hints
  • practical layout guidance
  • reasonable scene concreteness that supports the stated request

Not allowed augmentations:

  • extra characters or objects that are not implied by the request
  • brand names, slogans, palettes, or narrative beats that are not implied
  • arbitrary side-specific placement unless the surrounding layout supports it

Use-case taxonomy (exact slugs)

Classify each request into one of these buckets and keep the slug consistent across prompts and references.

Generate:

  • photorealistic-natural — candid/editorial lifestyle scenes with real texture and natural lighting.
  • product-mockup — product/packaging shots, catalog imagery, merch concepts.
  • ui-mockup — app/web interface mockups and wireframes; specify the desired fidelity.
  • infographic-diagram — diagrams/infographics with structured layout and text.
  • logo-brand — logo/mark exploration, vector-friendly.
  • illustration-story — comics, children’s book art, narrative scenes.
  • stylized-concept — style-driven concept art, 3D/stylized renders.
  • historical-scene — period-accurate/world-knowledge scenes.

Edit:

  • text-localization — translate/replace in-image text, preserve layout.
  • identity-preserve — try-on, person-in-scene; lock face/body/pose.
  • precise-object-edit — remove/replace a specific element (including interior swaps).
  • lighting-weather — time-of-day/season/atmosphere changes only.
  • background-extraction — transparent background / clean cutout.
  • style-transfer — apply reference style while changing subject/scene.
  • compositing — multi-image insert/merge with matched lighting/perspective.
  • sketch-to-render — drawing/line art to photoreal render.

Shared prompt schema

Use the following labeled spec as shared prompt scaffolding for both top-level modes:

Use case: <taxonomy slug>
Asset type: <where the asset will be used>
Primary request: <user's main prompt>
Input images: <Image 1: role; Image 2: role> (optional)
Scene/backdrop: <environment>
Subject: <main subject>
Style/medium: <photo/illustration/3D/etc>
Composition/framing: <wide/close/top-down; placement>
Lighting/mood: <lighting + mood>
Color palette: <palette notes>
Materials/textures: <surface details>
Text (verbatim): "<exact text>"
Constraints: <must keep/must avoid>
Avoid: <negative constraints>

Notes:

  • Asset type and Input images are prompt scaffolding, not dedicated CLI flags.
  • Scene/backdrop refers to the visual setting. It is not the same as the fallback CLI background parameter, which controls output transparency behavior.
  • Fallback-only execution notes such as Quality:, Input fidelity:, masks, output format, and output paths belong in the explicit CLI path only. Do not treat them as built-in image_gen tool arguments.

Augmentation rules:

  • Keep it short.
  • Add only the details needed to improve the prompt materially.
  • For edits, explicitly list invariants (change only X; keep Y unchanged).
  • If any critical detail is missing and blocks success, ask a question; otherwise proceed.

Examples

Generation example (hero image)

Use case: product-mockup
Asset type: landing page hero
Primary request: a minimal hero image of a ceramic coffee mug
Style/medium: clean product photography
Composition/framing: wide composition with usable negative space for page copy if needed
Lighting/mood: soft studio lighting
Constraints: no logos, no text, no watermark

Edit example (invariants)

Use case: precise-object-edit
Asset type: product photo background replacement
Primary request: replace only the background with a warm sunset gradient
Constraints: change only the background; keep the product and its edges unchanged; no text; no watermark

Prompting best practices

  • Structure prompt as scene/backdrop -> subject -> details -> constraints.
  • Include intended use (ad, UI mock, infographic) to set the mode and polish level.
  • Use camera/composition language for photorealism.
  • Only use SVG/vector stand-ins when the user explicitly asked for vector output or a non-image placeholder.
  • Quote exact text and specify typography + placement.
  • For tricky words, spell them letter-by-letter and require verbatim rendering.
  • For multi-image inputs, reference images by index and describe how they should be used.
  • For edits, repeat invariants every iteration to reduce drift.
  • Iterate with single-change follow-ups.
  • If the prompt is generic, add only the extra detail that will materially help.
  • If the prompt is already detailed, normalize it instead of expanding it.
  • For explicit CLI fallback only, see references/cli.md and references/image-api.md for quality, input_fidelity, masks, output format, and output-path guidance.

More principles shared by both modes: references/prompting.md. Copy/paste specs shared by both modes: references/sample-prompts.md.

Guidance by asset type

Asset-type templates (website assets, game assets, wireframes, logo) are consolidated in references/sample-prompts.md.

Fallback CLI mode only

Temp and output conventions

These conventions apply only to the explicit CLI fallback. They do not describe built-in image_gen output behavior.

  • Use tmp/imagegen/ for intermediate files (for example JSONL batches); delete them when done.
  • Write final artifacts under output/imagegen/.
  • Use --out or --out-dir to control output paths; keep filenames stable and descriptive.

Dependencies

Prefer uv for dependency management in this repo.

Required Python package:

uv pip install openai

Optional for downscaling only:

uv pip install pillow

Portability note:

  • If you are using the installed skill outside this repo, install dependencies into that environment with its package manager.
  • In uv-managed environments, uv pip install ... remains the preferred path.

Environment

  • OPENAI_API_KEY must be set for live API calls.
  • Do not ask the user for OPENAI_API_KEY when using the built-in image_gen tool.
  • Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready.

If the key is missing, give the user these steps:

  1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys
  2. Set OPENAI_API_KEY as an environment variable in their system.
  3. Offer to guide them through setting the environment variable for their OS/shell if needed.

If installation is not possible in this environment, tell the user which dependency is missing and how to install it into their active environment.

Script-mode notes

  • CLI commands + examples: references/cli.md
  • API parameter quick reference: references/image-api.md
  • Network approvals / sandbox settings for CLI mode: references/codex-network.md

Reference map

  • references/prompting.md: shared prompting principles for both modes.
  • references/sample-prompts.md: shared copy/paste prompt recipes for both modes.
  • references/cli.md: fallback-only CLI usage via scripts/image_gen.py.
  • references/image-api.md: fallback-only API/CLI parameter reference.
  • references/codex-network.md: fallback-only network/sandbox troubleshooting for CLI mode.
  • scripts/image_gen.py: fallback-only CLI implementation. Do not load or use it unless the user explicitly chooses CLI mode.
Files (skills)
  • agents
    • openai.yaml 918 B
      interface:
        display_name: "Image Gen"
        short_description: "Generate or edit images for websites, games, and more"
        icon_small: "./assets/imagegen-small.svg"
        icon_large: "./assets/imagegen.png"
        default_prompt: "Generate or edit the visual assets for this task with the built-in `image_gen` tool by default. First confirm that the task actually calls for a raster image; if the project already has SVG/vector/code-native assets and the user wants to extend or match those, do not use this skill. If the task includes reference images, treat them as references unless the user clearly wants an existing image modified. For multi-asset requests, loop built-in calls rather than treating batch as a separate top-level mode. Only use the fallback CLI if the user explicitly asks for it, and keep CLI-only controls such as `generate-batch`, `quality`, `input_fidelity`, masks, and output paths on that fallback path."
      
  • assets
    • imagegen-small.svg 2.8 KB · in bundle
    • imagegen.png 1.7 KB · in bundle
  • references
    • cli.md 6.1 KB
      # CLI reference (`scripts/image_gen.py`)
      
      This file is for the fallback CLI mode only. Read it only after the user explicitly asks to use `scripts/image_gen.py` instead of the built-in `image_gen` tool.
      
      `generate-batch` is a CLI subcommand in this fallback path. It is not a top-level mode of the skill.
      
      ## What this CLI does
      - `generate`: generate a new image from a prompt
      - `edit`: edit one or more existing images
      - `generate-batch`: run many generation jobs from a JSONL file
      
      Real API calls require **network access** + `OPENAI_API_KEY`. `--dry-run` does not.
      
      ## Quick start (works from any repo)
      Set a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`):
      
      ```
      export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
      export IMAGE_GEN="$CODEX_HOME/skills/.system/imagegen/scripts/image_gen.py"
      ```
      
      Install dependencies into that environment with its package manager. In uv-managed environments, `uv pip install ...` remains the preferred path.
      
      ## Quick start
      
      Dry-run (no API call; no network required; does not require the `openai` package):
      
      ```bash
      python "$IMAGE_GEN" generate \
        --prompt "Test" \
        --out output/imagegen/test.png \
        --dry-run
      ```
      
      Notes:
      - One-off dry-runs print the API payload and the computed output path(s).
      - Repo-local finals should live under `output/imagegen/`.
      
      Generate (requires `OPENAI_API_KEY` + network):
      
      ```bash
      python "$IMAGE_GEN" generate \
        --prompt "A cozy alpine cabin at dawn" \
        --size 1024x1024 \
        --out output/imagegen/alpine-cabin.png
      ```
      
      Edit:
      
      ```bash
      python "$IMAGE_GEN" edit \
        --image input.png \
        --prompt "Replace only the background with a warm sunset" \
        --out output/imagegen/sunset-edit.png
      ```
      
      ## Guardrails
      - Use the bundled CLI directly (`python "$IMAGE_GEN" ...`) after activating the correct environment.
      - Do **not** create one-off runners (for example `gen_images.py`) unless the user explicitly asks for a custom wrapper.
      - **Never modify** `scripts/image_gen.py`. If something is missing, ask the user before doing anything else.
      
      ## Defaults
      - Model: `gpt-image-1.5`
      - Supported model family for this CLI: GPT Image models (`gpt-image-*`)
      - Size: `1024x1024`
      - Quality: `auto`
      - Output format: `png`
      - Default one-off output path: `output/imagegen/output.png`
      - Background: unspecified unless `--background` is set
      
      ## Quality, input fidelity, and masks (CLI fallback only)
      These are explicit CLI controls. They are not built-in `image_gen` tool arguments.
      
      - `--quality` works for `generate`, `edit`, and `generate-batch`: `low|medium|high|auto`
      - `--input-fidelity` is **edit-only** and validated as `low|high`
      - `--mask` is **edit-only**
      
      Example:
      
      ```bash
      python "$IMAGE_GEN" edit \
        --image input.png \
        --prompt "Change only the background" \
        --quality high \
        --input-fidelity high \
        --out output/imagegen/background-edit.png
      ```
      
      Mask notes:
      - For multi-image edits, pass repeated `--image` flags. Their order is meaningful, so describe each image by index and role in the prompt.
      - The CLI accepts a single `--mask`.
      - Use a PNG mask when possible; the script treats mask handling as best-effort and does not perform full preflight validation beyond file checks/warnings.
      - In the edit prompt, repeat invariants (`change only the background; keep the subject unchanged`) to reduce drift.
      
      ## Output handling
      - Use `tmp/imagegen/` for temporary JSONL inputs or scratch files.
      - Use `output/imagegen/` for final outputs.
      - Reruns fail if a target file already exists unless you pass `--force`.
      - `--out-dir` changes one-off naming to `image_1.<ext>`, `image_2.<ext>`, and so on.
      - Downscaled copies use the default suffix `-web` unless you override it.
      
      ## Common recipes
      
      Generate with augmentation fields:
      
      ```bash
      python "$IMAGE_GEN" generate \
        --prompt "A minimal hero image of a ceramic coffee mug" \
        --use-case "product-mockup" \
        --style "clean product photography" \
        --composition "wide product shot with usable negative space for page copy" \
        --constraints "no logos, no text" \
        --out output/imagegen/mug-hero.png
      ```
      
      Generate + also write a downscaled copy for fast web loading:
      
      ```bash
      python "$IMAGE_GEN" generate \
        --prompt "A cozy alpine cabin at dawn" \
        --size 1024x1024 \
        --downscale-max-dim 1024 \
        --out output/imagegen/alpine-cabin.png
      ```
      
      Generate multiple prompts concurrently (async batch):
      
      ```bash
      mkdir -p tmp/imagegen output/imagegen/batch
      cat > tmp/imagegen/prompts.jsonl << 'EOF'
      {"prompt":"Cavernous hangar interior with a compact shuttle parked near the center","use_case":"stylized-concept","composition":"wide-angle, low-angle","lighting":"volumetric light rays through drifting fog","constraints":"no logos or trademarks; no watermark","size":"1536x1024"}
      {"prompt":"Gray wolf in profile in a snowy forest","use_case":"photorealistic-natural","composition":"eye-level","constraints":"no logos or trademarks; no watermark","size":"1024x1024"}
      EOF
      
      python "$IMAGE_GEN" generate-batch \
        --input tmp/imagegen/prompts.jsonl \
        --out-dir output/imagegen/batch \
        --concurrency 5
      
      rm -f tmp/imagegen/prompts.jsonl
      ```
      
      Notes:
      - `generate-batch` requires `--out-dir`.
      - generate-batch requires --out-dir.
      - Use `--concurrency` to control parallelism (default `5`).
      - Per-job overrides are supported in JSONL (for example `size`, `quality`, `background`, `output_format`, `output_compression`, `moderation`, `n`, `model`, `out`, and prompt-augmentation fields).
      - `--n` generates multiple variants for a single prompt; `generate-batch` is for many different prompts.
      - In batch mode, per-job `out` is treated as a filename under `--out-dir`.
      
      ## CLI notes
      - Supported sizes: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`.
      - Transparent backgrounds require `output_format` to be `png` or `webp`.
      - `--prompt-file`, `--output-compression`, `--moderation`, `--max-attempts`, `--fail-fast`, `--force`, and `--no-augment` are supported.
      - This CLI is intended for GPT Image models. Do not assume older non-GPT image-model behavior applies here.
      
      ## See also
      - API parameter quick reference for fallback CLI mode: `references/image-api.md`
      - Prompt examples shared across both top-level modes: `references/sample-prompts.md`
      - Network/sandbox notes for fallback CLI mode: `references/codex-network.md`
      
    • codex-network.md 1.6 KB
      # Codex network approvals / sandbox notes
      
      This file is for the fallback CLI mode only. Read it only after the user explicitly asks to use `scripts/image_gen.py`.
      
      This guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt.
      
      ## Why am I asked to approve image generation calls?
      The fallback CLI uses the OpenAI Image API, so it needs outbound network access. In many Codex setups, network access is disabled by default and/or the approval policy requires confirmation before networked commands run.
      
      ## Important note about approvals vs network
      - `--ask-for-approval never` suppresses approval prompts.
      - It does **not** by itself enable network access.
      - In `workspace-write`, network access still depends on your Codex configuration (for example `[sandbox_workspace_write] network_access = true`).
      
      ## How do I reduce repeated approval prompts?
      If you trust the repo and want fewer prompts, use a configuration or profile that both:
      - enables network for the sandbox mode you plan to use
      - sets an approval policy that matches your risk tolerance
      
      Example `~/.codex/config.toml` pattern:
      
      ```toml
      approval_policy = "on-request"
      sandbox_mode = "workspace-write"
      
      [sandbox_workspace_write]
      network_access = true
      ```
      
      If you want quieter automation after network is enabled, you can choose a stricter approval policy, but do that intentionally and with care.
      
      ## Safety note
      Enabling network and reducing approvals lowers friction, but increases risk if you run untrusted code or work in an untrusted repository.
      
    • image-api.md 2.4 KB
      # Image API quick reference
      
      This file is for the fallback CLI mode only. Use it only after the user explicitly asks to use `scripts/image_gen.py` instead of the built-in `image_gen` tool.
      
      These parameters describe the Image API and bundled CLI fallback surface. Do not assume they are normal arguments on the built-in `image_gen` tool.
      
      ## Scope
      - This fallback CLI is intended for GPT Image models (`gpt-image-1.5`, `gpt-image-1`, and `gpt-image-1-mini`).
      - The built-in `image_gen` tool and the fallback CLI do not expose the same controls.
      
      ## Endpoints
      - Generate: `POST /v1/images/generations` (`client.images.generate(...)`)
      - Edit: `POST /v1/images/edits` (`client.images.edit(...)`)
      
      ## Core parameters for GPT Image models
      - `prompt`: text prompt
      - `model`: image model
      - `n`: number of images (1-10)
      - `size`: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`
      - `quality`: `low`, `medium`, `high`, or `auto`
      - `background`: output transparency behavior (`transparent`, `opaque`, or `auto`) for generated output; this is not the same thing as the prompt's visual scene/backdrop
      - `output_format`: `png` (default), `jpeg`, `webp`
      - `output_compression`: 0-100 (jpeg/webp only)
      - `moderation`: `auto` (default) or `low`
      
      ## Edit-specific parameters
      - `image`: one or more input images. For GPT Image models, you can provide up to 16 images.
      - `mask`: optional mask image
      - `input_fidelity`: `low` (default) or `high`
      
      Model-specific note for `input_fidelity`:
      - `gpt-image-1` and `gpt-image-1-mini` preserve all input images, but the first image gets richer textures and finer details.
      - `gpt-image-1.5` preserves the first 5 input images with higher fidelity.
      
      ## Output
      - `data[]` list with `b64_json` per image
      - The bundled `scripts/image_gen.py` CLI decodes `b64_json` and writes output files for you.
      
      ## Limits and notes
      - Input images and masks must be under 50MB.
      - Use the edits endpoint when the user requests changes to an existing image.
      - Masking is prompt-guided; exact shapes are not guaranteed.
      - Large sizes and high quality increase latency and cost.
      - High `input_fidelity` can materially increase input token usage.
      - If a request fails because a specific option is unsupported by the selected GPT Image model, retry manually without that option.
      
      ## Important boundary
      - `quality`, `input_fidelity`, explicit masks, `background`, `output_format`, and related parameters are fallback-only execution controls.
      - Do not assume they are built-in `image_gen` tool arguments.
      
    • prompting.md 6.3 KB
      # Prompting best practices
      
      These prompting principles are shared by both top-level modes of the skill:
      - built-in `image_gen` tool (default)
      - explicit `scripts/image_gen.py` CLI fallback
      
      This file is about prompt structure, specificity, and iteration. Fallback-only execution controls such as `quality`, `input_fidelity`, masks, output format, and output paths live in the fallback docs.
      
      ## Contents
      - [Structure](#structure)
      - [Specificity policy](#specificity-policy)
      - [Allowed and disallowed augmentation](#allowed-and-disallowed-augmentation)
      - [Composition and layout](#composition-and-layout)
      - [Constraints and invariants](#constraints-and-invariants)
      - [Text in images](#text-in-images)
      - [Input images and references](#input-images-and-references)
      - [Iterate deliberately](#iterate-deliberately)
      - [Fallback-only execution controls](#fallback-only-execution-controls)
      - [Use-case tips](#use-case-tips)
      - [Where to find copy/paste recipes](#where-to-find-copypaste-recipes)
      
      ## Structure
      - Use a consistent order: scene/backdrop -> subject -> key details -> constraints -> output intent.
      - Include intended use (ad, UI mock, infographic) to set the level of polish.
      - For complex requests, use short labeled lines instead of one long paragraph.
      
      ## Specificity policy
      - If the user prompt is already specific and detailed, normalize it into a clean spec without adding creative requirements.
      - If the prompt is generic, you may add tasteful detail when it materially improves the output.
      - Treat examples in `sample-prompts.md` as fully-authored recipes, not as the default amount of augmentation to add to every request.
      
      ## Allowed and disallowed augmentation
      
      Allowed augmentation for generic prompts:
      - composition and framing cues
      - intended-use or polish-level hints
      - practical layout guidance
      - reasonable scene concreteness that supports the request
      
      Do not add:
      - extra characters, props, or objects that are not implied
      - brand palettes, slogans, or story beats that are not implied
      - arbitrary side-specific placement unless the surrounding layout supports it
      
      ## Composition and layout
      - Specify framing and viewpoint (close-up, wide, top-down) and placement only when it materially helps.
      - Call out negative space if the asset clearly needs room for UI or copy.
      - Avoid making left/right layout decisions unless the user or surrounding layout supports them.
      
      ## Constraints and invariants
      - State what must not change (`keep background unchanged`).
      - For edits, say `change only X; keep Y unchanged` and repeat invariants on every iteration to reduce drift.
      
      ## Text in images
      - Put literal text in quotes or ALL CAPS and specify typography (font style, size, color, placement).
      - Spell uncommon words letter-by-letter if accuracy matters.
      - For in-image copy, require verbatim rendering and no extra characters.
      
      ## Input images and references
      - Do not assume that every provided image is an edit target.
      - Label each image by index and role (`Image 1: edit target`, `Image 2: style reference`).
      - If the user provides images for style, composition, or mood guidance and does not ask to modify them, treat the request as generation with references.
      - If the user asks to preserve an existing image while changing specific parts, treat the request as an edit.
      - For compositing, describe how the images interact (`place the subject from Image 2 into Image 1`).
      
      ## Iterate deliberately
      - Start with a clean base prompt, then make small single-change edits.
      - Re-specify critical constraints when you iterate.
      - Prefer one targeted follow-up at a time over rewriting the whole prompt.
      
      ## Fallback-only execution controls
      - `quality`, `input_fidelity`, explicit masks, output format, and output paths are fallback-only execution controls.
      - Do not assume they are built-in `image_gen` tool arguments.
      - If the user explicitly chooses CLI fallback, see `references/cli.md` and `references/image-api.md` for those controls.
      
      ## Use-case tips
      Generate:
      - photorealistic-natural: Prompt as if a real photo is captured in the moment; use photography language (lens, lighting, framing); call for real texture; avoid over-stylized polish unless requested.
      - product-mockup: Describe the product/packaging and materials; ensure clean silhouette and label clarity; if in-image text is needed, require verbatim rendering and specify typography.
      - ui-mockup: Describe the target fidelity first (shippable mockup or low-fi wireframe), then focus on layout, hierarchy, and practical UI elements; avoid concept-art language.
      - infographic-diagram: Define the audience and layout flow; label parts explicitly; require verbatim text.
      - logo-brand: Keep it simple and scalable; ask for a strong silhouette and balanced negative space; avoid decorative flourishes unless requested.
      - illustration-story: Define panels or scene beats; keep each action concrete.
      - stylized-concept: Specify style cues, material finish, and rendering approach (3D, painterly, clay) without inventing new story elements.
      - historical-scene: State the location/date and required period accuracy; constrain clothing, props, and environment to match the era.
      
      Edit:
      - text-localization: Change only the text; preserve layout, typography, spacing, and hierarchy; no extra words or reflow unless needed.
      - identity-preserve: Lock identity (face, body, pose, hair, expression); change only the specified elements; match lighting and shadows.
      - precise-object-edit: Specify exactly what to remove/replace; preserve surrounding texture and lighting; keep everything else unchanged.
      - lighting-weather: Change only environmental conditions (light, shadows, atmosphere, precipitation); keep geometry, framing, and subject identity.
      - background-extraction: Request a clean cutout; crisp silhouette; no halos; preserve label text exactly; no restyling.
      - style-transfer: Specify style cues to preserve (palette, texture, brushwork) and what must change; add `no extra elements` to prevent drift.
      - compositing: Reference inputs by index; specify what moves where; match lighting, perspective, and scale; keep the base framing unchanged.
      - sketch-to-render: Preserve layout, proportions, and perspective; choose materials and lighting that support the supplied sketch without adding new elements.
      
      ## Where to find copy/paste recipes
      For copy/paste prompt specs (examples only), see `references/sample-prompts.md`. This file focuses on principles, specificity, and iteration patterns.
      
    • sample-prompts.md 14.4 KB
      # Sample prompts (copy/paste)
      
      These prompt recipes are shared across both top-level modes of the skill:
      - built-in `image_gen` tool (default)
      - explicit `scripts/image_gen.py` CLI fallback
      
      Use these as starting points. They are intentionally complete prompt recipes, not the default amount of augmentation to add to every user request.
      
      When adapting a user's prompt:
      - keep user-provided requirements
      - only add detail according to the specificity policy in `SKILL.md`
      - do not treat every example below as permission to invent extra story elements
      
      The labeled lines are prompt scaffolding, not a closed schema. `Asset type` and `Input images` are prompt-only scaffolding; the CLI does not expose them as dedicated flags.
      
      Execution details such as explicit CLI flags, `quality`, `input_fidelity`, masks, output formats, and local output paths depend on mode. Use the built-in tool by default; only apply CLI-specific controls after the user explicitly opts into fallback mode.
      
      For prompting principles (structure, specificity, invariants, iteration), see `references/prompting.md`.
      
      ## Generate
      
      ### photorealistic-natural
      ```
      Use case: photorealistic-natural
      Primary request: candid photo of an elderly sailor on a small fishing boat adjusting a net
      Scene/backdrop: coastal water with soft haze
      Subject: weathered skin with wrinkles and sun texture
      Style/medium: photorealistic candid photo
      Composition/framing: medium close-up, eye-level
      Lighting/mood: soft coastal daylight, shallow depth of field, subtle film grain
      Materials/textures: real skin texture, worn fabric, salt-worn wood
      Constraints: natural color balance; no heavy retouching; no glamorization; no watermark
      Avoid: studio polish; staged look
      ```
      
      ### product-mockup
      ```
      Use case: product-mockup
      Primary request: premium product photo of a matte black shampoo bottle with a minimal label
      Scene/backdrop: clean studio gradient from light gray to white
      Subject: single bottle centered with subtle reflection
      Style/medium: premium product photography
      Composition/framing: centered, slight three-quarter angle, generous padding
      Lighting/mood: softbox lighting, clean highlights, controlled shadows
      Materials/textures: matte plastic, crisp label printing
      Constraints: no logos or trademarks; no watermark
      ```
      
      ### ui-mockup
      ```
      Use case: ui-mockup
      Primary request: mobile app home screen for a local farmers market with vendors and daily specials
      Asset type: mobile app screen
      Style/medium: realistic product UI, not concept art
      Composition/framing: clean vertical mobile layout with clear hierarchy
      Constraints: practical layout, clear typography, no logos or trademarks, no watermark
      ```
      
      ### infographic-diagram
      ```
      Use case: infographic-diagram
      Primary request: detailed infographic of an automatic coffee machine flow
      Scene/backdrop: clean, light neutral background
      Subject: bean hopper -> grinder -> brew group -> boiler -> water tank -> drip tray
      Style/medium: clean vector-like infographic with clear callouts and arrows
      Composition/framing: vertical poster layout, top-to-bottom flow
      Text (verbatim): "Bean Hopper", "Grinder", "Brew Group", "Boiler", "Water Tank", "Drip Tray"
      Constraints: clear labels, strong contrast, no logos or trademarks, no watermark
      ```
      
      ### logo-brand
      ```
      Use case: logo-brand
      Primary request: original logo for "Field & Flour", a local bakery
      Style/medium: vector logo mark; flat colors; minimal
      Composition/framing: single centered logo on a plain background with generous padding
      Constraints: strong silhouette, balanced negative space; original design only; no gradients unless essential; no trademarks; no watermark
      ```
      
      ### illustration-story
      ```
      Use case: illustration-story
      Primary request: 4-panel comic about a pet left alone at home
      Scene/backdrop: cozy living room across panels
      Subject: pet reacting to the owner leaving, then relaxing, then returning to a composed pose
      Style/medium: comic illustration with clear panels
      Composition/framing: 4 equal-sized vertical panels, readable actions per panel
      Constraints: no text; no logos or trademarks; no watermark
      ```
      
      ### stylized-concept
      ```
      Use case: stylized-concept
      Primary request: cavernous hangar interior with tall support beams and drifting fog
      Scene/backdrop: industrial hangar interior, deep scale, light haze
      Subject: compact shuttle parked near the center
      Style/medium: cinematic concept art, industrial realism
      Composition/framing: wide-angle, low-angle
      Lighting/mood: volumetric light rays cutting through fog
      Constraints: no logos or trademarks; no watermark
      ```
      
      ### historical-scene
      ```
      Use case: historical-scene
      Primary request: outdoor crowd scene in Bethel, New York on August 16, 1969
      Scene/backdrop: open field with period-appropriate staging
      Subject: crowd in period-accurate clothing, authentic environment
      Style/medium: photorealistic photo
      Composition/framing: wide shot, eye-level
      Constraints: period-accurate details; no modern objects; no logos or trademarks; no watermark
      ```
      
      ## Asset type templates (taxonomy-aligned)
      
      ### Website assets template
      ```
      Use case: <photorealistic-natural|stylized-concept|product-mockup|infographic-diagram|ui-mockup>
      Asset type: <hero image / section illustration / blog header>
      Primary request: <short description>
      Scene/backdrop: <environment or abstract backdrop>
      Subject: <main subject>
      Style/medium: <photo/illustration/3D>
      Composition/framing: <wide/centered; note usable negative space only if needed>
      Lighting/mood: <soft/bright/neutral>
      Color palette: <brand colors or neutral>
      Constraints: <no text; no logos; no watermark; leave room for UI if needed>
      ```
      
      ### Website assets example: minimal hero background
      ```
      Use case: stylized-concept
      Asset type: landing page hero background
      Primary request: minimal abstract background with a soft gradient and subtle texture
      Style/medium: matte illustration / soft-rendered abstract background
      Composition/framing: wide composition with usable negative space for page copy
      Lighting/mood: gentle studio glow
      Color palette: restrained neutral palette
      Constraints: no text; no logos; no watermark
      ```
      
      ### Website assets example: feature section illustration
      ```
      Use case: stylized-concept
      Asset type: feature section illustration
      Primary request: simple abstract shapes suggesting connection and flow
      Scene/backdrop: subtle light-gray backdrop with faint texture
      Style/medium: flat illustration; soft shadows; restrained contrast
      Composition/framing: centered cluster; open margins for UI
      Color palette: muted neutral palette
      Constraints: no text; no logos; no watermark
      ```
      
      ### Website assets example: blog header image
      ```
      Use case: photorealistic-natural
      Asset type: blog header image
      Primary request: overhead desk scene with notebook, pen, and coffee cup
      Scene/backdrop: warm wooden tabletop
      Style/medium: photorealistic photo
      Composition/framing: wide crop with clean room for page copy
      Lighting/mood: soft morning light
      Constraints: no text; no logos; no watermark
      ```
      
      ### Game assets template
      ```
      Use case: stylized-concept
      Asset type: <game environment concept art / game character concept / game UI icon / tileable game texture>
      Primary request: <biome/scene/character/icon/material>
      Scene/backdrop: <location + set dressing> (if applicable)
      Subject: <main focal element(s)>
      Style/medium: <realistic/stylized>; <concept art / character render / UI icon / texture>
      Composition/framing: <wide/establishing/top-down>; <camera angle>; <focal point placement>
      Lighting/mood: <time of day>; <mood>; <volumetric/fog/etc>
      Constraints: no logos or trademarks; no watermark
      ```
      
      ### Game assets example: environment concept art
      ```
      Use case: stylized-concept
      Asset type: game environment concept art
      Primary request: cavernous hangar interior with tall support beams and drifting fog
      Scene/backdrop: industrial hangar interior, deep scale, light haze
      Subject: compact shuttle parked near the center
      Style/medium: cinematic concept art, industrial realism
      Composition/framing: wide-angle, low-angle
      Lighting/mood: volumetric light rays cutting through fog
      Constraints: no logos or trademarks; no watermark
      ```
      
      ### Game assets example: character concept
      ```
      Use case: stylized-concept
      Asset type: game character concept
      Primary request: desert scout character with layered travel gear
      Subject: long coat, satchel, practical travel clothing
      Style/medium: character render; stylized realism
      Composition/framing: neutral hero pose on a simple backdrop
      Constraints: no logos or trademarks; no watermark
      ```
      
      ### Game assets example: UI icon
      ```
      Use case: stylized-concept
      Asset type: game UI icon
      Primary request: round shield icon with a subtle rune pattern
      Style/medium: painted game UI icon
      Composition/framing: centered icon; generous padding; clear silhouette
      Constraints: no text; no background scene elements; no logos or trademarks; no watermark
      ```
      
      ### Game assets example: tileable texture
      ```
      Use case: stylized-concept
      Asset type: tileable game texture
      Primary request: worn sandstone blocks
      Style/medium: seamless tileable texture; PBR-ish look
      Scene/backdrop: neutral lighting reference only
      Constraints: seamless edges; no obvious focal elements; no text; no logos or trademarks; no watermark
      ```
      
      ### Wireframe template
      ```
      Use case: ui-mockup
      Asset type: website wireframe
      Primary request: <page or flow to sketch>
      Style/medium: low-fi grayscale wireframe
      Composition/framing: <landscape or portrait to match expected device>
      Subject: <sections in order; grid/columns; key labels>
      Constraints: no color; no logos; no real photos; no watermark
      ```
      
      ### Wireframe example: homepage (desktop)
      ```
      Use case: ui-mockup
      Asset type: website wireframe
      Primary request: SaaS homepage layout with clear hierarchy
      Style/medium: low-fi grayscale wireframe
      Subject: top nav; hero with headline and CTA; three feature cards; testimonial strip; pricing preview; footer
      Composition/framing: landscape desktop layout
      Constraints: label major blocks; no color; no logos; no real photos; no watermark
      ```
      
      ### Wireframe example: pricing page
      ```
      Use case: ui-mockup
      Asset type: website wireframe
      Primary request: pricing page layout with comparison table
      Style/medium: low-fi grayscale wireframe
      Subject: header; plan toggle; 3 pricing cards; comparison table; FAQ accordion; footer
      Composition/framing: desktop or tablet layout
      Constraints: label key areas; no color; no logos; no real photos; no watermark
      ```
      
      ### Wireframe example: mobile onboarding flow
      ```
      Use case: ui-mockup
      Asset type: mobile onboarding wireframe
      Primary request: three-screen mobile onboarding flow
      Style/medium: low-fi grayscale wireframe
      Subject: screen 1 headline and CTA; screen 2 feature bullets; screen 3 form fields and CTA
      Composition/framing: portrait mobile layout
      Constraints: label screens and blocks; no color; no logos; no real photos; no watermark
      ```
      
      ### Logo template
      ```
      Use case: logo-brand
      Asset type: logo concept
      Primary request: <brand idea or symbol concept>
      Style/medium: vector logo mark; flat colors; minimal
      Composition/framing: centered mark; clear silhouette; generous margin
      Color palette: <1-2 colors; high contrast>
      Text (verbatim): "<exact name>" (only if needed)
      Constraints: no gradients; no mockups; no 3D; no watermark
      ```
      
      ### Logo example: abstract symbol mark
      ```
      Use case: logo-brand
      Asset type: logo concept
      Primary request: geometric leaf symbol suggesting sustainability and growth
      Style/medium: vector logo mark; flat colors; minimal
      Composition/framing: centered mark; clear silhouette
      Color palette: deep green and off-white
      Constraints: no text unless requested; no gradients; no mockups; no 3D; no watermark
      ```
      
      ### Logo example: monogram mark
      ```
      Use case: logo-brand
      Asset type: logo concept
      Primary request: interlocking monogram of the letters "AV"
      Style/medium: vector logo mark; flat colors; minimal
      Composition/framing: centered mark; balanced spacing
      Color palette: black on white
      Constraints: no gradients; no mockups; no 3D; no watermark
      ```
      
      ### Logo example: wordmark
      ```
      Use case: logo-brand
      Asset type: logo concept
      Primary request: clean wordmark for a modern studio
      Style/medium: vector wordmark; flat colors; minimal
      Text (verbatim): "Studio North"
      Composition/framing: centered text; even letter spacing
      Constraints: no gradients; no mockups; no 3D; no watermark
      ```
      
      ## Edit
      
      ### text-localization
      ```
      Use case: text-localization
      Input images: Image 1: original infographic
      Primary request: replace "Bean Hopper", "Grinder", "Brew Group", "Boiler", "Water Tank", and "Drip Tray" with "Tolva", "Molino", "Grupo de infusión", "Caldera", "Depósito de agua", and "Bandeja de goteo"
      Constraints: change only the text; preserve layout, typography, spacing, and hierarchy; no extra words; do not alter logos or imagery
      ```
      
      ### identity-preserve
      ```
      Use case: identity-preserve
      Input images: Image 1: person photo; Image 2..N: clothing references
      Primary request: replace only the clothing with the provided garments
      Constraints: preserve face, body shape, pose, hair, expression, and identity; match lighting and shadows; keep the background unchanged; no accessories or text
      ```
      
      ### precise-object-edit
      ```
      Use case: precise-object-edit
      Input images: Image 1: room photo
      Primary request: replace only the white chairs with wooden chairs
      Constraints: preserve camera angle, room lighting, floor shadows, and surrounding objects; keep all other aspects unchanged
      ```
      
      ### lighting-weather
      ```
      Use case: lighting-weather
      Input images: Image 1: original photo
      Primary request: make it look like a winter evening with gentle snowfall
      Constraints: preserve subject identity, geometry, camera angle, and composition; change only lighting, atmosphere, and weather
      ```
      
      ### background-extraction
      ```
      Use case: background-extraction
      Input images: Image 1: product photo
      Primary request: isolate the product on a clean transparent background
      Constraints: crisp silhouette; no halos or fringing; preserve label text exactly; no restyling
      ```
      
      ### style-transfer
      ```
      Use case: style-transfer
      Input images: Image 1: style reference
      Primary request: apply Image 1's visual style to a man riding a motorcycle on a plain white backdrop
      Constraints: preserve palette, texture, and brushwork; no extra elements
      ```
      
      ### compositing
      ```
      Use case: compositing
      Input images: Image 1: base scene; Image 2: subject to insert
      Primary request: place the subject from Image 2 next to the person in Image 1
      Constraints: match lighting, perspective, and scale; keep the base framing unchanged; no extra elements
      ```
      
      ### sketch-to-render
      ```
      Use case: sketch-to-render
      Input images: Image 1: drawing
      Primary request: turn the drawing into a photorealistic image
      Constraints: preserve layout, proportions, and perspective; choose realistic materials and lighting; do not add new elements or text
      ```
      
  • scripts
    • image_gen.py 31.1 KB
      #!/usr/bin/env python3
      """Fallback CLI for explicit image generation or editing with GPT Image models.
      
      Used only when the user explicitly opts into CLI fallback mode.
      
      Defaults to gpt-image-1.5 and a structured prompt augmentation workflow.
      """
      
      from __future__ import annotations
      
      import argparse
      import asyncio
      import base64
      import json
      import os
      from pathlib import Path
      import re
      import sys
      import time
      from typing import Any, Dict, Iterable, List, Optional, Tuple
      
      from io import BytesIO
      
      DEFAULT_MODEL = "gpt-image-1.5"
      DEFAULT_SIZE = "1024x1024"
      DEFAULT_QUALITY = "auto"
      DEFAULT_OUTPUT_FORMAT = "png"
      DEFAULT_CONCURRENCY = 5
      DEFAULT_DOWNSCALE_SUFFIX = "-web"
      DEFAULT_OUTPUT_PATH = "output/imagegen/output.png"
      GPT_IMAGE_MODEL_PREFIX = "gpt-image-"
      
      ALLOWED_SIZES = {"1024x1024", "1536x1024", "1024x1536", "auto"}
      ALLOWED_QUALITIES = {"low", "medium", "high", "auto"}
      ALLOWED_BACKGROUNDS = {"transparent", "opaque", "auto", None}
      ALLOWED_INPUT_FIDELITIES = {"low", "high", None}
      
      MAX_IMAGE_BYTES = 50 * 1024 * 1024
      MAX_BATCH_JOBS = 500
      
      
      def _die(message: str, code: int = 1) -> None:
          print(f"Error: {message}", file=sys.stderr)
          raise SystemExit(code)
      
      
      def _warn(message: str) -> None:
          print(f"Warning: {message}", file=sys.stderr)
      
      
      def _dependency_hint(package: str, *, upgrade: bool = False) -> str:
          command = f"uv pip install {'-U ' if upgrade else ''}{package}"
          return (
              "Activate the repo-selected environment first, then install it with "
              f"`{command}`. If this repo uses a local virtualenv, start with "
              "`source .venv/bin/activate`; otherwise use this repo's configured shared fallback "
              "environment. If your project declares dependencies, prefer that project's normal "
              "`uv sync` flow."
          )
      
      
      def _ensure_api_key(dry_run: bool) -> None:
          if os.getenv("OPENAI_API_KEY"):
              print("OPENAI_API_KEY is set.", file=sys.stderr)
              return
          if dry_run:
              _warn("OPENAI_API_KEY is not set; dry-run only.")
              return
          _die("OPENAI_API_KEY is not set. Export it before running.")
      
      
      def _read_prompt(prompt: Optional[str], prompt_file: Optional[str]) -> str:
          if prompt and prompt_file:
              _die("Use --prompt or --prompt-file, not both.")
          if prompt_file:
              path = Path(prompt_file)
              if not path.exists():
                  _die(f"Prompt file not found: {path}")
              return path.read_text(encoding="utf-8").strip()
          if prompt:
              return prompt.strip()
          _die("Missing prompt. Use --prompt or --prompt-file.")
          return ""  # unreachable
      
      
      def _check_image_paths(paths: Iterable[str]) -> List[Path]:
          resolved: List[Path] = []
          for raw in paths:
              path = Path(raw)
              if not path.exists():
                  _die(f"Image file not found: {path}")
              if path.stat().st_size > MAX_IMAGE_BYTES:
                  _warn(f"Image exceeds 50MB limit: {path}")
              resolved.append(path)
          return resolved
      
      
      def _normalize_output_format(fmt: Optional[str]) -> str:
          if not fmt:
              return DEFAULT_OUTPUT_FORMAT
          fmt = fmt.lower()
          if fmt not in {"png", "jpeg", "jpg", "webp"}:
              _die("output-format must be png, jpeg, jpg, or webp.")
          return "jpeg" if fmt == "jpg" else fmt
      
      
      def _validate_size(size: str) -> None:
          if size not in ALLOWED_SIZES:
              _die(
                  "size must be one of 1024x1024, 1536x1024, 1024x1536, or auto for GPT image models."
              )
      
      
      def _validate_quality(quality: str) -> None:
          if quality not in ALLOWED_QUALITIES:
              _die("quality must be one of low, medium, high, or auto.")
      
      
      def _validate_background(background: Optional[str]) -> None:
          if background not in ALLOWED_BACKGROUNDS:
              _die("background must be one of transparent, opaque, or auto.")
      
      
      def _validate_input_fidelity(input_fidelity: Optional[str]) -> None:
          if input_fidelity not in ALLOWED_INPUT_FIDELITIES:
              _die("input-fidelity must be one of low or high.")
      
      
      def _validate_model(model: str) -> None:
          if not model.startswith(GPT_IMAGE_MODEL_PREFIX):
              _die(
                  "model must be a GPT Image model (for example gpt-image-1.5, gpt-image-1, or gpt-image-1-mini)."
              )
      
      
      def _validate_transparency(background: Optional[str], output_format: str) -> None:
          if background == "transparent" and output_format not in {"png", "webp"}:
              _die("transparent background requires output-format png or webp.")
      
      
      def _validate_generate_payload(payload: Dict[str, Any]) -> None:
          _validate_model(str(payload.get("model", DEFAULT_MODEL)))
          n = int(payload.get("n", 1))
          if n < 1 or n > 10:
              _die("n must be between 1 and 10")
          size = str(payload.get("size", DEFAULT_SIZE))
          quality = str(payload.get("quality", DEFAULT_QUALITY))
          background = payload.get("background")
          _validate_size(size)
          _validate_quality(quality)
          _validate_background(background)
          oc = payload.get("output_compression")
          if oc is not None and not (0 <= int(oc) <= 100):
              _die("output_compression must be between 0 and 100")
      
      
      def _build_output_paths(
          out: str,
          output_format: str,
          count: int,
          out_dir: Optional[str],
      ) -> List[Path]:
          ext = "." + output_format
      
          if out_dir:
              out_base = Path(out_dir)
              out_base.mkdir(parents=True, exist_ok=True)
              return [out_base / f"image_{i}{ext}" for i in range(1, count + 1)]
      
          out_path = Path(out)
          if out_path.exists() and out_path.is_dir():
              out_path.mkdir(parents=True, exist_ok=True)
              return [out_path / f"image_{i}{ext}" for i in range(1, count + 1)]
      
          if out_path.suffix == "":
              out_path = out_path.with_suffix(ext)
          elif output_format and out_path.suffix.lstrip(".").lower() != output_format:
              _warn(
                  f"Output extension {out_path.suffix} does not match output-format {output_format}."
              )
      
          if count == 1:
              return [out_path]
      
          return [
              out_path.with_name(f"{out_path.stem}-{i}{out_path.suffix}")
              for i in range(1, count + 1)
          ]
      
      
      def _augment_prompt(args: argparse.Namespace, prompt: str) -> str:
          fields = _fields_from_args(args)
          return _augment_prompt_fields(args.augment, prompt, fields)
      
      
      def _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optional[str]]) -> str:
          if not augment:
              return prompt
      
          sections: List[str] = []
          if fields.get("use_case"):
              sections.append(f"Use case: {fields['use_case']}")
          sections.append(f"Primary request: {prompt}")
          if fields.get("scene"):
              sections.append(f"Scene/background: {fields['scene']}")
          if fields.get("subject"):
              sections.append(f"Subject: {fields['subject']}")
          if fields.get("style"):
              sections.append(f"Style/medium: {fields['style']}")
          if fields.get("composition"):
              sections.append(f"Composition/framing: {fields['composition']}")
          if fields.get("lighting"):
              sections.append(f"Lighting/mood: {fields['lighting']}")
          if fields.get("palette"):
              sections.append(f"Color palette: {fields['palette']}")
          if fields.get("materials"):
              sections.append(f"Materials/textures: {fields['materials']}")
          if fields.get("text"):
              sections.append(f"Text (verbatim): \"{fields['text']}\"")
          if fields.get("constraints"):
              sections.append(f"Constraints: {fields['constraints']}")
          if fields.get("negative"):
              sections.append(f"Avoid: {fields['negative']}")
      
          return "\n".join(sections)
      
      
      def _fields_from_args(args: argparse.Namespace) -> Dict[str, Optional[str]]:
          return {
              "use_case": getattr(args, "use_case", None),
              "scene": getattr(args, "scene", None),
              "subject": getattr(args, "subject", None),
              "style": getattr(args, "style", None),
              "composition": getattr(args, "composition", None),
              "lighting": getattr(args, "lighting", None),
              "palette": getattr(args, "palette", None),
              "materials": getattr(args, "materials", None),
              "text": getattr(args, "text", None),
              "constraints": getattr(args, "constraints", None),
              "negative": getattr(args, "negative", None),
          }
      
      
      def _print_request(payload: dict) -> None:
          print(json.dumps(payload, indent=2, sort_keys=True))
      
      
      def _decode_and_write(images: List[str], outputs: List[Path], force: bool) -> None:
          for idx, image_b64 in enumerate(images):
              if idx >= len(outputs):
                  break
              out_path = outputs[idx]
              if out_path.exists() and not force:
                  _die(f"Output already exists: {out_path} (use --force to overwrite)")
              out_path.parent.mkdir(parents=True, exist_ok=True)
              out_path.write_bytes(base64.b64decode(image_b64))
              print(f"Wrote {out_path}")
      
      
      def _derive_downscale_path(path: Path, suffix: str) -> Path:
          if suffix and not suffix.startswith("-") and not suffix.startswith("_"):
              suffix = "-" + suffix
          return path.with_name(f"{path.stem}{suffix}{path.suffix}")
      
      
      def _downscale_image_bytes(image_bytes: bytes, *, max_dim: int, output_format: str) -> bytes:
          try:
              from PIL import Image
          except Exception:
              _die(f"Downscaling requires Pillow. {_dependency_hint('pillow')}")
      
          if max_dim < 1:
              _die("--downscale-max-dim must be >= 1")
      
          with Image.open(BytesIO(image_bytes)) as img:
              img.load()
              w, h = img.size
              scale = min(1.0, float(max_dim) / float(max(w, h)))
              target = (max(1, int(round(w * scale))), max(1, int(round(h * scale))))
      
              resized = img if target == (w, h) else img.resize(target, Image.Resampling.LANCZOS)
      
              fmt = output_format.lower()
              if fmt == "jpg":
                  fmt = "jpeg"
      
              if fmt == "jpeg":
                  if resized.mode in ("RGBA", "LA") or ("transparency" in getattr(resized, "info", {})):
                      bg = Image.new("RGB", resized.size, (255, 255, 255))
                      bg.paste(resized.convert("RGBA"), mask=resized.convert("RGBA").split()[-1])
                      resized = bg
                  else:
                      resized = resized.convert("RGB")
      
              out = BytesIO()
              resized.save(out, format=fmt.upper())
              return out.getvalue()
      
      
      def _decode_write_and_downscale(
          images: List[str],
          outputs: List[Path],
          *,
          force: bool,
          downscale_max_dim: Optional[int],
          downscale_suffix: str,
          output_format: str,
      ) -> None:
          for idx, image_b64 in enumerate(images):
              if idx >= len(outputs):
                  break
              out_path = outputs[idx]
              if out_path.exists() and not force:
                  _die(f"Output already exists: {out_path} (use --force to overwrite)")
              out_path.parent.mkdir(parents=True, exist_ok=True)
      
              raw = base64.b64decode(image_b64)
              out_path.write_bytes(raw)
              print(f"Wrote {out_path}")
      
              if downscale_max_dim is None:
                  continue
      
              derived = _derive_downscale_path(out_path, downscale_suffix)
              if derived.exists() and not force:
                  _die(f"Output already exists: {derived} (use --force to overwrite)")
              derived.parent.mkdir(parents=True, exist_ok=True)
              resized = _downscale_image_bytes(raw, max_dim=downscale_max_dim, output_format=output_format)
              derived.write_bytes(resized)
              print(f"Wrote {derived}")
      
      
      def _create_client():
          try:
              from openai import OpenAI
          except ImportError:
              _die(f"openai SDK not installed in the active environment. {_dependency_hint('openai')}")
          return OpenAI()
      
      
      def _create_async_client():
          try:
              from openai import AsyncOpenAI
          except ImportError:
              try:
                  import openai as _openai  # noqa: F401
              except ImportError:
                  _die(
                      f"openai SDK not installed in the active environment. {_dependency_hint('openai')}"
                  )
              _die(
                  "AsyncOpenAI not available in this openai SDK version. "
                  f"{_dependency_hint('openai', upgrade=True)}"
              )
          return AsyncOpenAI()
      
      
      def _slugify(value: str) -> str:
          value = value.strip().lower()
          value = re.sub(r"[^a-z0-9]+", "-", value)
          value = re.sub(r"-{2,}", "-", value).strip("-")
          return value[:60] if value else "job"
      
      
      def _normalize_job(job: Any, idx: int) -> Dict[str, Any]:
          if isinstance(job, str):
              prompt = job.strip()
              if not prompt:
                  _die(f"Empty prompt at job {idx}")
              return {"prompt": prompt}
          if isinstance(job, dict):
              if "prompt" not in job or not str(job["prompt"]).strip():
                  _die(f"Missing prompt for job {idx}")
              return job
          _die(f"Invalid job at index {idx}: expected string or object.")
          return {}  # unreachable
      
      
      def _read_jobs_jsonl(path: str) -> List[Dict[str, Any]]:
          p = Path(path)
          if not p.exists():
              _die(f"Input file not found: {p}")
          jobs: List[Dict[str, Any]] = []
          for line_no, raw in enumerate(p.read_text(encoding="utf-8").splitlines(), start=1):
              line = raw.strip()
              if not line or line.startswith("#"):
                  continue
              try:
                  item: Any
                  if line.startswith("{"):
                      item = json.loads(line)
                  else:
                      item = line
                  jobs.append(_normalize_job(item, idx=line_no))
              except json.JSONDecodeError as exc:
                  _die(f"Invalid JSON on line {line_no}: {exc}")
          if not jobs:
              _die("No jobs found in input file.")
          if len(jobs) > MAX_BATCH_JOBS:
              _die(f"Too many jobs ({len(jobs)}). Max is {MAX_BATCH_JOBS}.")
          return jobs
      
      
      def _merge_non_null(dst: Dict[str, Any], src: Dict[str, Any]) -> Dict[str, Any]:
          merged = dict(dst)
          for k, v in src.items():
              if v is not None:
                  merged[k] = v
          return merged
      
      
      def _job_output_paths(
          *,
          out_dir: Path,
          output_format: str,
          idx: int,
          prompt: str,
          n: int,
          explicit_out: Optional[str],
      ) -> List[Path]:
          out_dir.mkdir(parents=True, exist_ok=True)
          ext = "." + output_format
      
          if explicit_out:
              base = Path(explicit_out)
              if base.suffix == "":
                  base = base.with_suffix(ext)
              elif base.suffix.lstrip(".").lower() != output_format:
                  _warn(
                      f"Job {idx}: output extension {base.suffix} does not match output-format {output_format}."
                  )
              base = out_dir / base.name
          else:
              slug = _slugify(prompt[:80])
              base = out_dir / f"{idx:03d}-{slug}{ext}"
      
          if n == 1:
              return [base]
          return [
              base.with_name(f"{base.stem}-{i}{base.suffix}")
              for i in range(1, n + 1)
          ]
      
      
      def _extract_retry_after_seconds(exc: Exception) -> Optional[float]:
          # Best-effort: openai SDK errors vary by version. Prefer a conservative fallback.
          for attr in ("retry_after", "retry_after_seconds"):
              val = getattr(exc, attr, None)
              if isinstance(val, (int, float)) and val >= 0:
                  return float(val)
          msg = str(exc)
          m = re.search(r"retry[- ]after[:= ]+([0-9]+(?:\\.[0-9]+)?)", msg, re.IGNORECASE)
          if m:
              try:
                  return float(m.group(1))
              except Exception:
                  return None
          return None
      
      
      def _is_rate_limit_error(exc: Exception) -> bool:
          name = exc.__class__.__name__.lower()
          if "ratelimit" in name or "rate_limit" in name:
              return True
          msg = str(exc).lower()
          return "429" in msg or "rate limit" in msg or "too many requests" in msg
      
      
      def _is_transient_error(exc: Exception) -> bool:
          if _is_rate_limit_error(exc):
              return True
          name = exc.__class__.__name__.lower()
          if "timeout" in name or "timedout" in name or "tempor" in name:
              return True
          msg = str(exc).lower()
          return "timeout" in msg or "timed out" in msg or "connection reset" in msg
      
      
      async def _generate_one_with_retries(
          client: Any,
          payload: Dict[str, Any],
          *,
          attempts: int,
          job_label: str,
      ) -> Any:
          last_exc: Optional[Exception] = None
          for attempt in range(1, attempts + 1):
              try:
                  return await client.images.generate(**payload)
              except Exception as exc:
                  last_exc = exc
                  if not _is_transient_error(exc):
                      raise
                  if attempt == attempts:
                      raise
                  sleep_s = _extract_retry_after_seconds(exc)
                  if sleep_s is None:
                      sleep_s = min(60.0, 2.0**attempt)
                  print(
                      f"{job_label} attempt {attempt}/{attempts} failed ({exc.__class__.__name__}); retrying in {sleep_s:.1f}s",
                      file=sys.stderr,
                  )
                  await asyncio.sleep(sleep_s)
          raise last_exc or RuntimeError("unknown error")
      
      
      async def _run_generate_batch(args: argparse.Namespace) -> int:
          jobs = _read_jobs_jsonl(args.input)
          out_dir = Path(args.out_dir)
      
          base_fields = _fields_from_args(args)
          base_payload = {
              "model": args.model,
              "n": args.n,
              "size": args.size,
              "quality": args.quality,
              "background": args.background,
              "output_format": args.output_format,
              "output_compression": args.output_compression,
              "moderation": args.moderation,
          }
      
          if args.dry_run:
              for i, job in enumerate(jobs, start=1):
                  prompt = str(job["prompt"]).strip()
                  fields = _merge_non_null(base_fields, job.get("fields", {}))
                  # Allow flat job keys as well (use_case, scene, etc.)
                  fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})
                  augmented = _augment_prompt_fields(args.augment, prompt, fields)
      
                  job_payload = dict(base_payload)
                  job_payload["prompt"] = augmented
                  job_payload = _merge_non_null(job_payload, {k: job.get(k) for k in base_payload.keys()})
                  job_payload = {k: v for k, v in job_payload.items() if v is not None}
      
                  _validate_generate_payload(job_payload)
                  effective_output_format = _normalize_output_format(job_payload.get("output_format"))
                  _validate_transparency(job_payload.get("background"), effective_output_format)
                  job_payload["output_format"] = effective_output_format
      
                  n = int(job_payload.get("n", 1))
                  outputs = _job_output_paths(
                      out_dir=out_dir,
                      output_format=effective_output_format,
                      idx=i,
                      prompt=prompt,
                      n=n,
                      explicit_out=job.get("out"),
                  )
                  downscaled = None
                  if args.downscale_max_dim is not None:
                      downscaled = [
                          str(_derive_downscale_path(p, args.downscale_suffix)) for p in outputs
                      ]
                  _print_request(
                      {
                          "endpoint": "/v1/images/generations",
                          "job": i,
                          "outputs": [str(p) for p in outputs],
                          "outputs_downscaled": downscaled,
                          **job_payload,
                      }
                  )
              return 0
      
          client = _create_async_client()
          sem = asyncio.Semaphore(args.concurrency)
      
          any_failed = False
      
          async def run_job(i: int, job: Dict[str, Any]) -> Tuple[int, Optional[str]]:
              nonlocal any_failed
              prompt = str(job["prompt"]).strip()
              job_label = f"[job {i}/{len(jobs)}]"
      
              fields = _merge_non_null(base_fields, job.get("fields", {}))
              fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})
              augmented = _augment_prompt_fields(args.augment, prompt, fields)
      
              payload = dict(base_payload)
              payload["prompt"] = augmented
              payload = _merge_non_null(payload, {k: job.get(k) for k in base_payload.keys()})
              payload = {k: v for k, v in payload.items() if v is not None}
      
              n = int(payload.get("n", 1))
              _validate_generate_payload(payload)
              effective_output_format = _normalize_output_format(payload.get("output_format"))
              _validate_transparency(payload.get("background"), effective_output_format)
              payload["output_format"] = effective_output_format
              outputs = _job_output_paths(
                  out_dir=out_dir,
                  output_format=effective_output_format,
                  idx=i,
                  prompt=prompt,
                  n=n,
                  explicit_out=job.get("out"),
              )
              try:
                  async with sem:
                      print(f"{job_label} starting", file=sys.stderr)
                      started = time.time()
                      result = await _generate_one_with_retries(
                          client,
                          payload,
                          attempts=args.max_attempts,
                          job_label=job_label,
                      )
                      elapsed = time.time() - started
                      print(f"{job_label} completed in {elapsed:.1f}s", file=sys.stderr)
                  images = [item.b64_json for item in result.data]
                  _decode_write_and_downscale(
                      images,
                      outputs,
                      force=args.force,
                      downscale_max_dim=args.downscale_max_dim,
                      downscale_suffix=args.downscale_suffix,
                      output_format=effective_output_format,
                  )
                  return i, None
              except Exception as exc:
                  any_failed = True
                  print(f"{job_label} failed: {exc}", file=sys.stderr)
                  if args.fail_fast:
                      raise
                  return i, str(exc)
      
          tasks = [asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1)]
      
          try:
              await asyncio.gather(*tasks)
          except Exception:
              for t in tasks:
                  if not t.done():
                      t.cancel()
              raise
      
          return 1 if any_failed else 0
      
      
      def _generate_batch(args: argparse.Namespace) -> None:
          exit_code = asyncio.run(_run_generate_batch(args))
          if exit_code:
              raise SystemExit(exit_code)
      
      
      def _generate(args: argparse.Namespace) -> None:
          prompt = _read_prompt(args.prompt, args.prompt_file)
          prompt = _augment_prompt(args, prompt)
      
          payload = {
              "model": args.model,
              "prompt": prompt,
              "n": args.n,
              "size": args.size,
              "quality": args.quality,
              "background": args.background,
              "output_format": args.output_format,
              "output_compression": args.output_compression,
              "moderation": args.moderation,
          }
          payload = {k: v for k, v in payload.items() if v is not None}
      
          output_format = _normalize_output_format(args.output_format)
          _validate_transparency(args.background, output_format)
          payload["output_format"] = output_format
          output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir)
          downscaled = None
          if args.downscale_max_dim is not None:
              downscaled = [str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths]
      
          if args.dry_run:
              _print_request(
                  {
                      "endpoint": "/v1/images/generations",
                      "outputs": [str(p) for p in output_paths],
                      "outputs_downscaled": downscaled,
                      **payload,
                  }
              )
              return
      
          print(
              "Calling Image API (generation). This can take up to a couple of minutes.",
              file=sys.stderr,
          )
          started = time.time()
          client = _create_client()
          result = client.images.generate(**payload)
          elapsed = time.time() - started
          print(f"Generation completed in {elapsed:.1f}s.", file=sys.stderr)
      
          images = [item.b64_json for item in result.data]
          _decode_write_and_downscale(
              images,
              output_paths,
              force=args.force,
              downscale_max_dim=args.downscale_max_dim,
              downscale_suffix=args.downscale_suffix,
              output_format=output_format,
          )
      
      
      def _edit(args: argparse.Namespace) -> None:
          prompt = _read_prompt(args.prompt, args.prompt_file)
          prompt = _augment_prompt(args, prompt)
      
          image_paths = _check_image_paths(args.image)
          mask_path = Path(args.mask) if args.mask else None
          if mask_path:
              if not mask_path.exists():
                  _die(f"Mask file not found: {mask_path}")
              if mask_path.suffix.lower() != ".png":
                  _warn(f"Mask should be a PNG with an alpha channel: {mask_path}")
              if mask_path.stat().st_size > MAX_IMAGE_BYTES:
                  _warn(f"Mask exceeds 50MB limit: {mask_path}")
      
          payload = {
              "model": args.model,
              "prompt": prompt,
              "n": args.n,
              "size": args.size,
              "quality": args.quality,
              "background": args.background,
              "output_format": args.output_format,
              "output_compression": args.output_compression,
              "input_fidelity": args.input_fidelity,
              "moderation": args.moderation,
          }
          payload = {k: v for k, v in payload.items() if v is not None}
      
          output_format = _normalize_output_format(args.output_format)
          _validate_transparency(args.background, output_format)
          payload["output_format"] = output_format
          _validate_input_fidelity(args.input_fidelity)
          output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir)
          downscaled = None
          if args.downscale_max_dim is not None:
              downscaled = [str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths]
      
          if args.dry_run:
              payload_preview = dict(payload)
              payload_preview["image"] = [str(p) for p in image_paths]
              if mask_path:
                  payload_preview["mask"] = str(mask_path)
              _print_request(
                  {
                      "endpoint": "/v1/images/edits",
                      "outputs": [str(p) for p in output_paths],
                      "outputs_downscaled": downscaled,
                      **payload_preview,
                  }
              )
              return
      
          print(
              f"Calling Image API (edit) with {len(image_paths)} image(s).",
              file=sys.stderr,
          )
          started = time.time()
          client = _create_client()
      
          with _open_files(image_paths) as image_files, _open_mask(mask_path) as mask_file:
              request = dict(payload)
              request["image"] = image_files if len(image_files) > 1 else image_files[0]
              if mask_file is not None:
                  request["mask"] = mask_file
              result = client.images.edit(**request)
      
          elapsed = time.time() - started
          print(f"Edit completed in {elapsed:.1f}s.", file=sys.stderr)
          images = [item.b64_json for item in result.data]
          _decode_write_and_downscale(
              images,
              output_paths,
              force=args.force,
              downscale_max_dim=args.downscale_max_dim,
              downscale_suffix=args.downscale_suffix,
              output_format=output_format,
          )
      
      
      def _open_files(paths: List[Path]):
          return _FileBundle(paths)
      
      
      def _open_mask(mask_path: Optional[Path]):
          if mask_path is None:
              return _NullContext()
          return _SingleFile(mask_path)
      
      
      class _NullContext:
          def __enter__(self):
              return None
      
          def __exit__(self, exc_type, exc, tb):
              return False
      
      
      class _SingleFile:
          def __init__(self, path: Path):
              self._path = path
              self._handle = None
      
          def __enter__(self):
              self._handle = self._path.open("rb")
              return self._handle
      
          def __exit__(self, exc_type, exc, tb):
              if self._handle:
                  try:
                      self._handle.close()
                  except Exception:
                      pass
              return False
      
      
      class _FileBundle:
          def __init__(self, paths: List[Path]):
              self._paths = paths
              self._handles: List[object] = []
      
          def __enter__(self):
              self._handles = [p.open("rb") for p in self._paths]
              return self._handles
      
          def __exit__(self, exc_type, exc, tb):
              for handle in self._handles:
                  try:
                      handle.close()
                  except Exception:
                      pass
              return False
      
      
      def _add_shared_args(parser: argparse.ArgumentParser) -> None:
          parser.add_argument("--model", default=DEFAULT_MODEL)
          parser.add_argument("--prompt")
          parser.add_argument("--prompt-file")
          parser.add_argument("--n", type=int, default=1)
          parser.add_argument("--size", default=DEFAULT_SIZE)
          parser.add_argument("--quality", default=DEFAULT_QUALITY)
          parser.add_argument("--background")
          parser.add_argument("--output-format")
          parser.add_argument("--output-compression", type=int)
          parser.add_argument("--moderation")
          parser.add_argument("--out", default=DEFAULT_OUTPUT_PATH)
          parser.add_argument("--out-dir")
          parser.add_argument("--force", action="store_true")
          parser.add_argument("--dry-run", action="store_true")
          parser.add_argument("--augment", dest="augment", action="store_true")
          parser.add_argument("--no-augment", dest="augment", action="store_false")
          parser.set_defaults(augment=True)
      
          # Prompt augmentation hints
          parser.add_argument("--use-case")
          parser.add_argument("--scene")
          parser.add_argument("--subject")
          parser.add_argument("--style")
          parser.add_argument("--composition")
          parser.add_argument("--lighting")
          parser.add_argument("--palette")
          parser.add_argument("--materials")
          parser.add_argument("--text")
          parser.add_argument("--constraints")
          parser.add_argument("--negative")
      
          # Post-processing (optional): generate an additional downscaled copy for fast web loading.
          parser.add_argument("--downscale-max-dim", type=int)
          parser.add_argument("--downscale-suffix", default=DEFAULT_DOWNSCALE_SUFFIX)
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(
              description="Fallback CLI for explicit image generation or editing via GPT Image models"
          )
          subparsers = parser.add_subparsers(dest="command", required=True)
      
          gen_parser = subparsers.add_parser("generate", help="Create a new image")
          _add_shared_args(gen_parser)
          gen_parser.set_defaults(func=_generate)
      
          batch_parser = subparsers.add_parser(
              "generate-batch",
              help="Generate multiple prompts concurrently (JSONL input)",
          )
          _add_shared_args(batch_parser)
          batch_parser.add_argument("--input", required=True, help="Path to JSONL file (one job per line)")
          batch_parser.add_argument("--concurrency", type=int, default=DEFAULT_CONCURRENCY)
          batch_parser.add_argument("--max-attempts", type=int, default=3)
          batch_parser.add_argument("--fail-fast", action="store_true")
          batch_parser.set_defaults(func=_generate_batch)
      
          edit_parser = subparsers.add_parser("edit", help="Edit an existing image")
          _add_shared_args(edit_parser)
          edit_parser.add_argument("--image", action="append", required=True)
          edit_parser.add_argument("--mask")
          edit_parser.add_argument("--input-fidelity")
          edit_parser.set_defaults(func=_edit)
      
          args = parser.parse_args()
          if args.n < 1 or args.n > 10:
              _die("--n must be between 1 and 10")
          if getattr(args, "concurrency", 1) < 1 or getattr(args, "concurrency", 1) > 25:
              _die("--concurrency must be between 1 and 25")
          if getattr(args, "max_attempts", 3) < 1 or getattr(args, "max_attempts", 3) > 10:
              _die("--max-attempts must be between 1 and 10")
          if args.output_compression is not None and not (0 <= args.output_compression <= 100):
              _die("--output-compression must be between 0 and 100")
          if args.command == "generate-batch" and not args.out_dir:
              _die("generate-batch requires --out-dir")
          if getattr(args, "downscale_max_dim", None) is not None and args.downscale_max_dim < 1:
              _die("--downscale-max-dim must be >= 1")
      
          _validate_size(args.size)
          _validate_quality(args.quality)
          _validate_background(args.background)
          _validate_model(args.model)
          _ensure_api_key(args.dry_run)
      
          args.func(args)
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • LICENSE.txt 10.5 KB
    Apache License
    Version 2.0, January 2004
    http://www.apache.org/licenses/
    
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    
    1. Definitions.
    
       "License" shall mean the terms and conditions for use, reproduction,
       and distribution as defined by Sections 1 through 9 of this document.
    
       "Licensor" shall mean the copyright owner or entity authorized by
       the copyright owner that is granting the License.
    
       "Legal Entity" shall mean the union of the acting entity and all
       other entities that control, are controlled by, or are under common
       control with that entity. For the purposes of this definition,
       "control" means (i) the power, direct or indirect, to cause the
       direction or management of such entity, whether by contract or
       otherwise, or (ii) ownership of fifty percent (50%) or more of the
       outstanding shares, or (iii) beneficial ownership of such entity.
    
       "You" (or "Your") shall mean an individual or Legal Entity
       exercising permissions granted by this License.
    
       "Source" form shall mean the preferred form for making modifications,
       including but not limited to software source code, documentation
       source, and configuration files.
    
       "Object" form shall mean any form resulting from mechanical
       transformation or translation of a Source form, including but
       not limited to compiled object code, generated documentation,
       and conversions to other media types.
    
       "Work" shall mean the work of authorship, whether in Source or
       Object form, made available under the License, as indicated by a
       copyright notice that is included in or attached to the work
       (an example is provided in the Appendix below).
    
       "Derivative Works" shall mean any work, whether in Source or Object
       form, that is based on (or derived from) the Work and for which the
       editorial revisions, annotations, elaborations, or other modifications
       represent, as a whole, an original work of authorship. For the purposes
       of this License, Derivative Works shall not include works that remain
       separable from, or merely link (or bind by name) to the interfaces of,
       the Work and Derivative Works thereof.
    
       "Contribution" shall mean any work of authorship, including
       the original version of the Work and any modifications or additions
       to that Work or Derivative Works thereof, that is intentionally
       submitted to Licensor for inclusion in the Work by the copyright owner
       or by an individual or Legal Entity authorized to submit on behalf of
       the copyright owner. For the purposes of this definition, "submitted"
       means any form of electronic, verbal, or written communication sent
       to the Licensor or its representatives, including but not limited to
       communication on electronic mailing lists, source code control systems,
       and issue tracking systems that are managed by, or on behalf of, the
       Licensor for the purpose of discussing and improving the Work, but
       excluding communication that is conspicuously marked or otherwise
       designated in writing by the copyright owner as "Not a Contribution."
    
       "Contributor" shall mean Licensor and any individual or Legal Entity
       on behalf of whom a Contribution has been received by Licensor and
       subsequently incorporated within the Work.
    
    2. Grant of Copyright License. Subject to the terms and conditions of
       this License, each Contributor hereby grants to You a perpetual,
       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
       copyright license to reproduce, prepare Derivative Works of,
       publicly display, publicly perform, sublicense, and distribute the
       Work and such Derivative Works in Source or Object form.
    
    3. Grant of Patent License. Subject to the terms and conditions of
       this License, each Contributor hereby grants to You a perpetual,
       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
       (except as stated in this section) patent license to make, have made,
       use, offer to sell, sell, import, and otherwise transfer the Work,
       where such license applies only to those patent claims licensable
       by such Contributor that are necessarily infringed by their
       Contribution(s) alone or by combination of their Contribution(s)
       with the Work to which such Contribution(s) was submitted. If You
       institute patent litigation against any entity (including a
       cross-claim or counterclaim in a lawsuit) alleging that the Work
       or a Contribution incorporated within the Work constitutes direct
       or contributory patent infringement, then any patent licenses
       granted to You under this License for that Work shall terminate
       as of the date such litigation is filed.
    
    4. Redistribution. You may reproduce and distribute copies of the
       Work or Derivative Works thereof in any medium, with or without
       modifications, and in Source or Object form, provided that You
       meet the following conditions:
    
       (a) You must give any other recipients of the Work or
           Derivative Works a copy of this License; and
    
       (b) You must cause any modified files to carry prominent notices
           stating that You changed the files; and
    
       (c) You must retain, in the Source form of any Derivative Works
           that You distribute, all copyright, patent, trademark, and
           attribution notices from the Source form of the Work,
           excluding those notices that do not pertain to any part of
           the Derivative Works; and
    
       (d) If the Work includes a "NOTICE" text file as part of its
           distribution, then any Derivative Works that You distribute must
           include a readable copy of the attribution notices contained
           within such NOTICE file, excluding those notices that do not
           pertain to any part of the Derivative Works, in at least one
           of the following places: within a NOTICE text file distributed
           as part of the Derivative Works; within the Source form or
           documentation, if provided along with the Derivative Works; or,
           within a display generated by the Derivative Works, if and
           wherever such third-party notices normally appear. The contents
           of the NOTICE file are for informational purposes only and
           do not modify the License. You may add Your own attribution
           notices within Derivative Works that You distribute, alongside
           or as an addendum to the NOTICE text from the Work, provided
           that such additional attribution notices cannot be construed
           as modifying the License.
    
       You may add Your own copyright statement to Your modifications and
       may provide additional or different license terms and conditions
       for use, reproduction, or distribution of Your modifications, or
       for any such Derivative Works as a whole, provided Your use,
       reproduction, and distribution of the Work otherwise complies with
       the conditions stated in this License.
    
    5. Submission of Contributions. Unless You explicitly state otherwise,
       any Contribution intentionally submitted for inclusion in the Work
       by You to the Licensor shall be under the terms and conditions of
       this License, without any additional terms or conditions.
       Notwithstanding the above, nothing herein shall supersede or modify
       the terms of any separate license agreement you may have executed
       with Licensor regarding such Contributions.
    
    6. Trademarks. This License does not grant permission to use the trade
       names, trademarks, service marks, or product names of the Licensor,
       except as required for reasonable and customary use in describing the
       origin of the Work and reproducing the content of the NOTICE file.
    
    7. Disclaimer of Warranty. Unless required by applicable law or
       agreed to in writing, Licensor provides the Work (and each
       Contributor provides its Contributions) on an "AS IS" BASIS,
       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
       implied, including, without limitation, any warranties or conditions
       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
       PARTICULAR PURPOSE. You are solely responsible for determining the
       appropriateness of using or redistributing the Work and assume any
       risks associated with Your exercise of permissions under this License.
    
    8. Limitation of Liability. In no event and under no legal theory,
       whether in tort (including negligence), contract, or otherwise,
       unless required by applicable law (such as deliberate and grossly
       negligent acts) or agreed to in writing, shall any Contributor be
       liable to You for damages, including any direct, indirect, special,
       incidental, or consequential damages of any character arising as a
       result of this License or out of the use or inability to use the
       Work (including but not limited to damages for loss of goodwill,
       work stoppage, computer failure or malfunction, or any and all
       other commercial damages or losses), even if such Contributor
       has been advised of the possibility of such damages.
    
    9. Accepting Warranty or Additional Liability. While redistributing
       the Work or Derivative Works thereof, You may choose to offer,
       and charge a fee for, acceptance of support, warranty, indemnity,
       or other liability obligations and/or rights consistent with this
       License. However, in accepting such obligations, You may act only
       on Your own behalf and on Your sole responsibility, not on behalf of
       any other Contributor, and only if You agree to indemnify,
       defend, and hold each Contributor harmless for any liability
       incurred by, or claims asserted against, such Contributor by reason
       of your accepting any such warranty or additional liability.
    
    END OF TERMS AND CONDITIONS
    
    APPENDIX: How to apply the Apache License to your work.
    
       To apply the Apache License to your work, attach the following
       boilerplate notice, with the fields enclosed by brackets "[]"
       replaced with your own identifying information. (Don\'t include
       the brackets!)  The text should be enclosed in the appropriate
       comment syntax for the file format. We also recommend that a
       file or class name and description of purpose be included on the
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
    
    Copyright [yyyy] [name of copyright owner]
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    
  • SKILL.md 15.8 KB
    ---
    name: "imagegen"
    description: "Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas."
    ---
    
    # Image Generation Skill
    
    Generates or edits images for the current project (for example website assets, game assets, UI mockups, product mockups, wireframes, logo design, photorealistic images, or infographics).
    
    ## Top-level modes and rules
    
    This skill has exactly two top-level modes:
    
    - **Default built-in tool mode (preferred):** built-in `image_gen` tool for normal image generation and editing. Does not require `OPENAI_API_KEY`.
    - **Fallback CLI mode (explicit-only):** `scripts/image_gen.py` CLI. Use only when the user explicitly asks for the CLI path. Requires `OPENAI_API_KEY`.
    
    Within the explicit CLI fallback only, the CLI exposes three subcommands:
    
    - `generate`
    - `edit`
    - `generate-batch`
    
    Rules:
    - Use the built-in `image_gen` tool by default for all normal image generation and editing requests.
    - Never switch to CLI fallback automatically.
    - If the built-in tool fails or is unavailable, tell the user the CLI fallback exists and that it requires `OPENAI_API_KEY`. Proceed only if the user explicitly asks for that fallback.
    - If the user explicitly asks for CLI mode, use the bundled `scripts/image_gen.py` workflow. Do not create one-off SDK runners.
    - Never modify `scripts/image_gen.py`. If something is missing, ask the user before doing anything else.
    
    Built-in save-path policy:
    - In built-in tool mode, Codex saves generated images under `$CODEX_HOME/*` by default.
    - Do not describe or rely on OS temp as the default built-in destination.
    - Do not describe or rely on a destination-path argument (if any) on the built-in `image_gen` tool. If a specific location is needed, generate first and then move or copy the selected output from `$CODEX_HOME/generated_images/...`.
    - Save-path precedence in built-in mode:
      1. If the user names a destination, move or copy the selected output there.
      2. If the image is meant for the current project, move or copy the final selected image into the workspace before finishing.
      3. If the image is only for preview or brainstorming, render it inline; the underlying file can remain at the default `$CODEX_HOME/*` path.
    - Never leave a project-referenced asset only at the default `$CODEX_HOME/*` path.
    - Do not overwrite an existing asset unless the user explicitly asked for replacement; otherwise create a sibling versioned filename such as `hero-v2.png` or `item-icon-edited.png`.
    
    Shared prompt guidance for both modes lives in `references/prompting.md` and `references/sample-prompts.md`.
    
    Fallback-only docs/resources for CLI mode:
    - `references/cli.md`
    - `references/image-api.md`
    - `references/codex-network.md`
    - `scripts/image_gen.py`
    
    ## When to use
    - Generate a new image (concept art, product shot, cover, website hero)
    - Generate a new image using one or more reference images for style, composition, or mood
    - Edit an existing image (inpainting, lighting or weather transformations, background replacement, object removal, compositing, transparent background)
    - Produce many assets or variants for one task
    
    ## When not to use
    - Extending or matching an existing SVG/vector icon set, logo system, or illustration library inside the repo
    - Creating simple shapes, diagrams, wireframes, or icons that are better produced directly in SVG, HTML/CSS, or canvas
    - Making a small project-local asset edit when the source file already exists in an editable native format
    - Any task where the user clearly wants deterministic code-native output instead of a generated bitmap
    
    ## Decision tree
    
    Think about two separate questions:
    
    1. **Intent:** is this a new image or an edit of an existing image?
    2. **Execution strategy:** is this one asset or many assets/variants?
    
    Intent:
    - If the user wants to modify an existing image while preserving parts of it, treat the request as **edit**.
    - If the user provides images only as references for style, composition, mood, or subject guidance, treat the request as **generate**.
    - If the user provides no images, treat the request as **generate**.
    
    Built-in edit semantics:
    - Built-in edit mode is for images already visible in the conversation context, such as attached images or images generated earlier in the thread.
    - If the user wants to edit a local image file with the built-in tool, first load it with built-in `view_image` tool so the image is visible in the conversation context, then proceed with the built-in edit flow.
    - Do not promise arbitrary filesystem-path editing through the built-in tool.
    - If a local file still needs direct file-path control, masks, or other explicit CLI-only parameters, use the explicit CLI fallback only when the user asks for it.
    - For edits, preserve invariants aggressively and save non-destructively by default.
    
    Execution strategy:
    - In the built-in default path, produce many assets or variants by issuing one `image_gen` call per requested asset or variant.
    - In the explicit CLI fallback path, use the CLI `generate-batch` subcommand only when the user explicitly chose CLI mode and needs many prompts/assets.
    
    Assume the user wants a new image unless they clearly ask to change an existing one.
    
    ## Workflow
    1. Decide the top-level mode: built-in by default, fallback CLI only if explicitly requested.
    2. Decide the intent: `generate` or `edit`.
    3. Decide whether the output is preview-only or meant to be consumed by the current project.
    4. Decide the execution strategy: single asset vs repeated built-in calls vs CLI `generate-batch`.
    5. Collect inputs up front: prompt(s), exact text (verbatim), constraints/avoid list, and any input images.
    6. For every input image, label its role explicitly:
       - reference image
       - edit target
       - supporting insert/style/compositing input
    7. If the edit target is only on the local filesystem and you are staying on the built-in path, inspect it with `view_image` first so the image is available in conversation context.
    8. If the user asked for a photo, illustration, sprite, product image, banner, or other explicitly raster-style asset, use `image_gen` rather than substituting SVG/HTML/CSS placeholders. If the request is for an icon, logo, or UI graphic that should match existing repo-native SVG/vector/code assets, prefer editing those directly instead.
    9. Augment the prompt based on specificity:
       - If the user's prompt is already specific and detailed, normalize it into a clear spec without adding creative requirements.
       - If the user's prompt is generic, add tasteful augmentation only when it materially improves output quality.
    10. Use the built-in `image_gen` tool by default.
    11. If the user explicitly chooses the CLI fallback, then and only then use the fallback-only docs for quality, `input_fidelity`, masks, output format, output paths, and network setup.
    12. Inspect outputs and validate: subject, style, composition, text accuracy, and invariants/avoid items.
    13. Iterate with a single targeted change, then re-check.
    14. For preview-only work, render the image inline; the underlying file may remain at the default `$CODEX_HOME/generated_images/...` path.
    15. For project-bound work, move or copy the selected artifact into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the default `$CODEX_HOME/generated_images/...` path.
    16. For batches, persist only the selected finals in the workspace unless the user explicitly asked to keep discarded variants.
    17. Always report the final saved path for any workspace-bound asset, plus the final prompt and whether the built-in tool or fallback CLI mode was used.
    
    ## Prompt augmentation
    
    Reformat user prompts into a structured, production-oriented spec. Make the user's goal clearer and more actionable, but do not blindly add detail.
    
    Treat this as prompt-shaping guidance, not a closed schema. Use only the lines that help, and add a short extra labeled line when it materially improves clarity.
    
    ### Specificity policy
    
    Use the user's prompt specificity to decide how much augmentation is appropriate:
    
    - If the prompt is already specific and detailed, preserve that specificity and only normalize/structure it.
    - If the prompt is generic, you may add tasteful augmentation when it will materially improve the result.
    
    Allowed augmentations:
    - composition or framing hints
    - polish level or intended-use hints
    - practical layout guidance
    - reasonable scene concreteness that supports the stated request
    
    Not allowed augmentations:
    - extra characters or objects that are not implied by the request
    - brand names, slogans, palettes, or narrative beats that are not implied
    - arbitrary side-specific placement unless the surrounding layout supports it
    
    ## Use-case taxonomy (exact slugs)
    
    Classify each request into one of these buckets and keep the slug consistent across prompts and references.
    
    Generate:
    - photorealistic-natural — candid/editorial lifestyle scenes with real texture and natural lighting.
    - product-mockup — product/packaging shots, catalog imagery, merch concepts.
    - ui-mockup — app/web interface mockups and wireframes; specify the desired fidelity.
    - infographic-diagram — diagrams/infographics with structured layout and text.
    - logo-brand — logo/mark exploration, vector-friendly.
    - illustration-story — comics, children’s book art, narrative scenes.
    - stylized-concept — style-driven concept art, 3D/stylized renders.
    - historical-scene — period-accurate/world-knowledge scenes.
    
    Edit:
    - text-localization — translate/replace in-image text, preserve layout.
    - identity-preserve — try-on, person-in-scene; lock face/body/pose.
    - precise-object-edit — remove/replace a specific element (including interior swaps).
    - lighting-weather — time-of-day/season/atmosphere changes only.
    - background-extraction — transparent background / clean cutout.
    - style-transfer — apply reference style while changing subject/scene.
    - compositing — multi-image insert/merge with matched lighting/perspective.
    - sketch-to-render — drawing/line art to photoreal render.
    
    ## Shared prompt schema
    
    Use the following labeled spec as shared prompt scaffolding for both top-level modes:
    
    ```text
    Use case: <taxonomy slug>
    Asset type: <where the asset will be used>
    Primary request: <user's main prompt>
    Input images: <Image 1: role; Image 2: role> (optional)
    Scene/backdrop: <environment>
    Subject: <main subject>
    Style/medium: <photo/illustration/3D/etc>
    Composition/framing: <wide/close/top-down; placement>
    Lighting/mood: <lighting + mood>
    Color palette: <palette notes>
    Materials/textures: <surface details>
    Text (verbatim): "<exact text>"
    Constraints: <must keep/must avoid>
    Avoid: <negative constraints>
    ```
    
    Notes:
    - `Asset type` and `Input images` are prompt scaffolding, not dedicated CLI flags.
    - `Scene/backdrop` refers to the visual setting. It is not the same as the fallback CLI `background` parameter, which controls output transparency behavior.
    - Fallback-only execution notes such as `Quality:`, `Input fidelity:`, masks, output format, and output paths belong in the explicit CLI path only. Do not treat them as built-in `image_gen` tool arguments.
    
    Augmentation rules:
    - Keep it short.
    - Add only the details needed to improve the prompt materially.
    - For edits, explicitly list invariants (`change only X; keep Y unchanged`).
    - If any critical detail is missing and blocks success, ask a question; otherwise proceed.
    
    ## Examples
    
    ### Generation example (hero image)
    ```text
    Use case: product-mockup
    Asset type: landing page hero
    Primary request: a minimal hero image of a ceramic coffee mug
    Style/medium: clean product photography
    Composition/framing: wide composition with usable negative space for page copy if needed
    Lighting/mood: soft studio lighting
    Constraints: no logos, no text, no watermark
    ```
    
    ### Edit example (invariants)
    ```text
    Use case: precise-object-edit
    Asset type: product photo background replacement
    Primary request: replace only the background with a warm sunset gradient
    Constraints: change only the background; keep the product and its edges unchanged; no text; no watermark
    ```
    
    ## Prompting best practices
    - Structure prompt as scene/backdrop -> subject -> details -> constraints.
    - Include intended use (ad, UI mock, infographic) to set the mode and polish level.
    - Use camera/composition language for photorealism.
    - Only use SVG/vector stand-ins when the user explicitly asked for vector output or a non-image placeholder.
    - Quote exact text and specify typography + placement.
    - For tricky words, spell them letter-by-letter and require verbatim rendering.
    - For multi-image inputs, reference images by index and describe how they should be used.
    - For edits, repeat invariants every iteration to reduce drift.
    - Iterate with single-change follow-ups.
    - If the prompt is generic, add only the extra detail that will materially help.
    - If the prompt is already detailed, normalize it instead of expanding it.
    - For explicit CLI fallback only, see `references/cli.md` and `references/image-api.md` for `quality`, `input_fidelity`, masks, output format, and output-path guidance.
    
    More principles shared by both modes: `references/prompting.md`.
    Copy/paste specs shared by both modes: `references/sample-prompts.md`.
    
    ## Guidance by asset type
    Asset-type templates (website assets, game assets, wireframes, logo) are consolidated in `references/sample-prompts.md`.
    
    ## Fallback CLI mode only
    
    ### Temp and output conventions
    These conventions apply only to the explicit CLI fallback. They do not describe built-in `image_gen` output behavior.
    - Use `tmp/imagegen/` for intermediate files (for example JSONL batches); delete them when done.
    - Write final artifacts under `output/imagegen/`.
    - Use `--out` or `--out-dir` to control output paths; keep filenames stable and descriptive.
    
    ### Dependencies
    Prefer `uv` for dependency management in this repo.
    
    Required Python package:
    ```bash
    uv pip install openai
    ```
    
    Optional for downscaling only:
    ```bash
    uv pip install pillow
    ```
    
    Portability note:
    - If you are using the installed skill outside this repo, install dependencies into that environment with its package manager.
    - In uv-managed environments, `uv pip install ...` remains the preferred path.
    
    ### Environment
    - `OPENAI_API_KEY` must be set for live API calls.
    - Do not ask the user for `OPENAI_API_KEY` when using the built-in `image_gen` tool.
    - Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready.
    
    If the key is missing, give the user these steps:
    1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys
    2. Set `OPENAI_API_KEY` as an environment variable in their system.
    3. Offer to guide them through setting the environment variable for their OS/shell if needed.
    
    If installation is not possible in this environment, tell the user which dependency is missing and how to install it into their active environment.
    
    ### Script-mode notes
    - CLI commands + examples: `references/cli.md`
    - API parameter quick reference: `references/image-api.md`
    - Network approvals / sandbox settings for CLI mode: `references/codex-network.md`
    
    ## Reference map
    - `references/prompting.md`: shared prompting principles for both modes.
    - `references/sample-prompts.md`: shared copy/paste prompt recipes for both modes.
    - `references/cli.md`: fallback-only CLI usage via `scripts/image_gen.py`.
    - `references/image-api.md`: fallback-only API/CLI parameter reference.
    - `references/codex-network.md`: fallback-only network/sandbox troubleshooting for CLI mode.
    - `scripts/image_gen.py`: fallback-only CLI implementation. Do not load or use it unless the user explicitly chooses CLI mode.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related