Skill: laolujava/image-generation-gpt (version 1.0.0)

--- name: best-image-generation description: High quality AI image generation via the WellAPI gpt-image-2 model. Supports text-to-image and image editing (image-to-image). version: 1.0.2 tags: [image, image-generation, image-editing, ai, wellapi, gpt-image, text-to-image, image-to-image] homepage: https://wellapi.ai metadata: {"openclaw": {"emoji": "🎨", "requires": {"env": ["WELLAPI_API_KEY"]}, "primaryEnv": "WELLAPI_API_KEY"}} --- # WellAPI gpt-image-2 Generate and edit images via the WellAPI `gpt-image-2` model (OpenAI-compatible). The API returns image bytes inline as base64 (`data[i].b64_json`) — no polling, no URL download. ## API Endpoints - Base: `https://wellapi.ai/v1` - Text-to-image: `POST /images/generations` — `application/json` - Image edit / image-to-image: `POST /images/edits` — `multipart/form-data` Authentication: `Authorization: Bearer <WELLAPI_API_KEY>` header. ## Request — `/images/generations` (text-to-image) Content-Type: `application/json` | Field | Type | Required | Notes | |---|---|---|---| | `model` | string | ✅ | e.g. `gpt-image-2` | | `prompt` | string | ✅ | Image description, **max 1000 chars** | | `n` | integer | ✅ | Number of images, **1–10** | | `size` | string | optional | See size table below; default `auto` | | `quality` | string | optional | `low` / `medium` / `high` / `auto` (default `auto`) | | `format` | string | optional | `png` / `jpeg` / `webp` (default `png`) | Example body: ```json { "model": "gpt-image-2", "prompt": "大海", "n": 1, "size": "1024x1024", "quality": "low", "format": "jpeg" } ``` ## Request — `/images/edits` (image-to-image / editing) Content-Type: `multipart/form-data` | Field | Type | Required | Notes | |---|---|---|---| | `image` | file (repeatable) | ✅ | One or more input images. **Up to 16 images, total ≤ 50MB.** | | `prompt` | string | ✅ | Edit description | | `mask` | file | optional | A PNG with fully transparent regions marking the edit area. Applied to the first `image` if multiple are sent. Must be valid PNG, **< 4MB**, same dimensions as the image. | | `model` | string | optional | `gpt-image-1`, `gpt-image-1-all`, `flux-kontext-pro`, `flux-kontext-max`, `gpt-image-2`, `gpt-image-2-all`. Default in this skill: `gpt-image-2`. | | `n` | string | optional | `"1"` – `"10"` | | `size` | string | optional | See size table | | `quality` | string | optional | `low` / `medium` / `high` / `auto` (default `auto`) | | `format` | string | optional | `png` / `jpeg` / `webp` | | `background` | string | optional | `opaque` / `auto` / `transparent`. `auto` lets the model pick. | | `moderation` | string | optional | `low` / `auto` (default). `low` = less restrictive filtering (gpt-image-1 family). | ## `size` values | Value | Description | |---|---| | `1024x1024` | Square | | `1536x1024` | Landscape | | `1024x1536` | Portrait | | `2048x2048` | 2K square | | `2048x1152` | 2K landscape | | `3840x2160` | 4K landscape | | `2160x3840` | 4K portrait | | `auto` | Default — model chooses | **Strict size rules** (when picking a custom size): 1. Longest side ≤ `3840px` 2. Both width and height must be **multiples of 16** 3. `max(w, h) / min(w, h) ≤ 3:1` 4. Total pixels: **655,360 ≤ w*h ≤ 8,294,400** ## Response (both endpoints) Synchronous JSON — no polling: ```json { "created": 1778236581, "background": "opaque", "data": [ { "b64_json": "iVBORw0KGgo..." } ], "output_format": "png", "quality": "low", "size": "1024x1024", "usage": { "input_tokens": 8, "input_tokens_details": { "image_tokens": 0, "text_tokens": 8 }, "output_tokens": 196, "total_tokens": 204 } } ``` **Each `data[i].b64_json` is the full image as a base64 string.** Decode and write to disk. ## Output 1. Base64-decode each `data[i].b64_json` into bytes. 2. Save as `wellapi-<TIMESTAMP>.<ext>` where `<ext>` matches `response.output_format` (or the requested `format`, fallback `png`). 3. If multiple images returned, append `-1`, `-2`, … to the filename. 4. Print `MEDIA:<absolute_path>` (one line per image) for OpenClaw auto-attach. **CRITICAL SECURITY:** Before passing an output filename to shell commands, sanitize: - Strip shell metacharacters: `tr -cd 'A-Za-z0-9._-'` - Enforce valid extension (`.png`, `.jpg`, `.jpeg`, `.webp`) - Fallback to `wellapi-<timestamp>.png` if empty ## Reference Implementations | Platform | File | |---|---| | Python (all platforms, zero deps) | `{baseDir}/references/python.md` | | PowerShell 5.1+ (Windows) | `{baseDir}/references/powershell.md` | | curl + bash (Unix/macOS) | `{baseDir}/references/curl_heredoc.md` | ## API Key - `WELLAPI_API_KEY` env var (required) — sent as `Authorization: Bearer <key>` - Get key: https://wellapi.ai - If the environment variable is unset, follow **First-run onboarding** below. ### First-run onboarding (no API key found) When no key can be located (env var unset, no cached key on disk), **before making any API call** show the user this short prompt verbatim. Translate to the user's language if appropriate. **Do not** expand it into multiple "options / methods", **do not** show shell commands, **do not** ask follow-up questions like "do you already have a key": > 请粘贴你的 WellAPI API Key。 如果还没有，请前往 https://wellapi.ai/register?channel=c_qqn3vdvc 注册后领取免费 API Key。 > > Please paste your WellAPI API Key. If you don't have one yet, register at https://wellapi.ai/register?channel=c_qqn3vdvc to get a free key. After the user pastes a key, use it for the current request and proceed straight to image generation. Do not lecture the user about env vars, shells, or persistence unless they explicitly ask how to save it. ## Triggers - Chinese: "高质量生图：xxx" / "编辑图片：xxx" - English: "best image: xxx" / "edit image: xxx" Treat the text after the colon as `prompt`, default `size=auto`, `quality=auto`, `format=png`, `n=1`, and generate immediately. For image editing, the user provides one or more local image file paths along with the prompt; submit them as repeated `image` form fields to `/images/edits`. ## Notes - Response is **synchronous** — no task ID, no polling. - Print `MEDIA:<absolute_path>` for OC auto-attach — one line per generated image. - `quality: high` and larger `size` values may incur extra charges. - `format` controls the encoding of the returned base64 bytes; the file extension should match. - Up to **16** reference images per edit request, total **≤ 50MB**. - `mask` requires PNG ≤ 4MB, same WxH as the image it applies to.

path: CHANGELOG.md
content: # Changelog

All notable changes to **best-image-generation** are documented here.

Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [1.0.2] — 2026-05-16

### Added
- `README.md` with quick-start, trigger table, and output protocol.
- `CHANGELOG.md`.
- `examples/basic.md` — minimal text-to-image walkthrough.
- `examples/advanced.md` — multi-image edit, mask inpainting, and 4K size selection.
- `version` and `tags` fields in `SKILL.md` frontmatter for better ClawHub search ranking.

### Changed
- No behavioral changes — purely documentation & metadata polish for the ClawHub listing.

---

## [1.0.1] — 2026-05-16

### Fixed
- Filename sanitization step before shell interpolation (defense against prompt-injected filenames).
- Extension whitelist enforcement (`.png / .jpg / .jpeg / .webp`).

---

## [1.0.0] — 2026-05-16

### Added
- Initial public release.
- Text-to-image via `POST /v1/images/generations` (`application/json`).
- Image edit / image-to-image via `POST /v1/images/edits` (`multipart/form-data`), including optional mask for inpainting and up to 16 reference images per call.
- Synchronous base64 response handling — `data[i].b64_json` decoded and written to disk as `wellapi-<TIMESTAMP>.<ext>`.
- `MEDIA:<absolute_path>` stdout protocol for OpenClaw auto-attach.
- Reference implementations:
  - `references/python.md` (zero-dependency, all platforms)
  - `references/powershell.md` (Windows, PS 5.1+)
  - `references/curl_heredoc.md` (Unix/macOS)
- Chinese & English triggers (`高质量生图：…` / `编辑图片：…` / `best image: …` / `edit image: …`).
- Size presets from `1024×1024` through 4K (`3840×2160`, `2160×3840`), with custom-size validation rules.
- Quality / format / background / moderation knobs surfaced on the edit endpoint.


path: README.md
content: # best-image-generation

> 🎨 High-quality AI image generation & editing for OpenClaw, powered by [WellAPI](https://wellapi.ai) `gpt-image-2`.

A drop-in OpenClaw skill that gives your agent the ability to:

- **Text-to-image** — generate images from a natural-language prompt.
- **Image editing / image-to-image** — modify one or more local images with a prompt (optional mask for inpainting).
- Resolutions from `1024×1024` up to **4K** (`3840×2160` / `2160×3840`).
- Synchronous responses — base64 bytes come back inline, no polling, no URL fetch.
- Auto-attach generated images into your OpenClaw conversation via the `MEDIA:<path>` line protocol.

---

## Quick start

### 1. Install the skill

From an OpenClaw-enabled client (Claude Code / OpenClaw CLI):

```bash
clawhub install best-image-generation
```

Or drop this folder under your OpenClaw skills directory.

### 2. Set your API key

The skill reads its credential from a single environment variable:

```bash
# macOS / Linux
export WELLAPI_API_KEY="sk-..."

# Windows (PowerShell)
$env:WELLAPI_API_KEY = "sk-..."
```

Get a key from <https://wellapi.ai>. If the env var is not set, the skill will prompt for it before running.

### 3. Trigger it

| Intent | Chinese trigger | English trigger |
|---|---|---|
| Text-to-image | `高质量生图：一只在月球上读报的柴犬` | `best image: a shiba inu reading a newspaper on the moon` |
| Image edit | `编辑图片：把背景换成赛博朋克霓虹街道` (+ 附图) | `edit image: replace background with cyberpunk neon street` (+ attach files) |

Text after the colon becomes the `prompt`. Defaults: `size=auto`, `quality=auto`, `format=png`, `n=1`.

---

## What's in the box

```
image-generation-gpt/
├── SKILL.md              # Skill definition + frontmatter (ClawHub manifest)
├── README.md             # You are here
├── CHANGELOG.md          # Version history
├── _meta.json            # ClawHub publish metadata
├── examples/
│   ├── basic.md          # Simple text-to-image walkthrough
│   └── advanced.md       # Multi-image edit + mask + 4K
└── references/
    ├── python.md         # Python 3 reference implementation (zero deps)
    ├── powershell.md     # PowerShell 5.1+ reference (Windows)
    └── curl_heredoc.md   # curl + bash reference (Unix/macOS)
```

The agent picks the reference implementation that matches the host platform.

---

## API surface (summary)

Full details live in [SKILL.md](./SKILL.md). Cheat sheet:

| Endpoint | Method | Content-Type | Purpose |
|---|---|---|---|
| `https://wellapi.ai/v1/images/generations` | POST | `application/json` | Text-to-image |
| `https://wellapi.ai/v1/images/edits` | POST | `multipart/form-data` | Image edit / img2img |

Common knobs:

- `model` — defaults to `gpt-image-2`. Edit endpoint also accepts `gpt-image-1`, `flux-kontext-pro`, `flux-kontext-max`, etc.
- `n` — 1–10 images per call.
- `size` — `1024x1024` / `1536x1024` / `1024x1536` / `2048x2048` / `2048x1152` / `3840x2160` / `2160x3840` / `auto`.
- `quality` — `low` / `medium` / `high` / `auto`.
- `format` — `png` / `jpeg` / `webp`.

Custom sizes must obey: longest side ≤ 3840px, both dims multiple of 16, aspect ratio ≤ 3:1, total pixels in `[655 360, 8 294 400]`.

---

## Output protocol

Every generated image is:

1. Base64-decoded from `response.data[i].b64_json`.
2. Written to `wellapi-<TIMESTAMP>.<ext>` (suffixed `-1`, `-2`, … when `n > 1`).
3. Announced on stdout as `MEDIA:<absolute_path>` — OpenClaw auto-attaches it to the chat.

Filenames are sanitized (`tr -cd 'A-Za-z0-9._-'`) before any shell interpolation. Extension is validated against `.png / .jpg / .jpeg / .webp`.

---

## Requirements

- An OpenClaw-compatible host (Claude Code / OpenClaw CLI / clawd).
- `WELLAPI_API_KEY` environment variable.
- One of: Python 3.6+ **or** PowerShell 5.1+ **or** `curl` — the skill picks whichever is available.

No SDK install required. The reference implementations rely only on the language's standard library.

---

## Limits & cost notes

- **Prompt** ≤ 1000 characters.
- **Edit input**: up to **16** reference images per request, total ≤ **50 MB**.
- **Mask**: PNG only, < 4 MB, same dimensions as the image it applies to.
- `quality: high` and large `size` values incur higher per-image cost — see <https://wellapi.ai> pricing.

---

## Links

- Skill manifest: [SKILL.md](./SKILL.md)
- Changelog: [CHANGELOG.md](./CHANGELOG.md)
- Examples: [examples/basic.md](./examples/basic.md), [examples/advanced.md](./examples/advanced.md)
- Provider: <https://wellapi.ai>
- Issues / feedback: file via the ClawHub skill page.

---

## License

Published on ClawHub under **MIT-0**. You may use, modify, and redistribute this skill freely, including commercially. Attribution not required.


path: _meta.json
content: ownerId: kn74p4xy6sja0199cea53anecs81kqjs
slug: best-image-generation
version: 1.0.2
publishedAt: 1771684570437

path: references/curl_heredoc.md
content: # curl + bash (Unix/macOS, no dependencies)

> Requires `curl` (preinstalled on macOS/Linux) and one of `python3`/`base64`/`openssl` for base64 decoding (try in that order — at least one is always available).

Run in a single shell call (avoid relying on exported variables persisting across tool calls).

## Common preamble — API key, sanitized filename, JSON-escape helper

```bash
# Prompt user if env var is not set
if [ -z "$WELLAPI_API_KEY" ]; then
  read -r -p "Enter WELLAPI_API_KEY: " WELLAPI_API_KEY
fi
[REDACTED_SECRET]"
[ -z "$API_KEY" ] && { echo "Error: WELLAPI_API_KEY not provided." >&2; exit 2; }

# Map FORMAT → extension
ext_for_format() {
  case "${1,,}" in
    jpeg|jpg) printf .jpg ;;
    webp)     printf .webp ;;
    *)        printf .png ;;
  esac
}

# CRITICAL: sanitize any filename used in shell
sanitize_name() {
  local raw="$1" fallback="$2"
  local clean
  clean=$(echo "$raw" | tr -cd 'A-Za-z0-9._-')
  if [ -z "$clean" ]; then clean="$fallback"; fi
  if [[ ! "$clean" =~ \.(png|jpg|jpeg|webp)$ ]]; then clean="$clean.png"; fi
  printf '%s' "$clean"
}

# Robust JSON string escape (python3 if available, else minimal sed)
json_escape() {
  python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1" 2>/dev/null \
    || printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
}

# Decode first b64_json from a JSON response into a file.
decode_to_file() {
  local json="$1" out="$2"
  if command -v python3 >/dev/null 2>&1; then
    printf '%s' "$json" | python3 -c '
import json, sys, base64
d = json.load(sys.stdin)
if "data" not in d or not d["data"]:
    sys.stderr.write("No image data: %r\n" % d); sys.exit(1)
b64 = d["data"][0]["b64_json"]
sys.stdout.buffer.write(base64.b64decode(b64))
' > "$out"
    return $?
  fi
  local b64
  b64=$(printf '%s' "$json" | sed -n 's/.*"b64_json"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
  [ -z "$b64" ] && { echo "Error: no b64_json. Response: $json" >&2; return 1; }
  if command -v base64 >/dev/null 2>&1; then
    printf '%s' "$b64" | base64 -d > "$out" 2>/dev/null \
      || printf '%s' "$b64" | base64 -D > "$out" 2>/dev/null
    [ -s "$out" ] && return 0
  fi
  if command -v openssl >/dev/null 2>&1; then
    printf '%s' "$b64" | openssl base64 -d -A > "$out"
    return $?
  fi
  echo "Error: no base64 decoder available (need python3, base64, or openssl)" >&2
  return 1
}
```

## Text-to-image — `/images/generations`

Body: `application/json`. Required: `model`, `prompt` (≤1000 chars), `n` (1–10).

```bash
MODEL="gpt-image-2"
PROMPT="大海"           # ≤ 1000 chars
SIZE="1024x1024"        # 1024x1024 | 1536x1024 | 1024x1536 | 2048x2048 | 2048x1152 | 3840x2160 | 2160x3840 | auto
QUALITY="low"           # low | medium | high | auto
FORMAT="jpeg"           # png | jpeg | webp
N=1                     # 1..10

EXT=$(ext_for_format "$FORMAT")
OUT_FILE=$(sanitize_name "wellapi-$(date +%Y%m%d-%H%M%S)$EXT" "wellapi-$(date +%s).png")

PROMPT_JSON=$(json_escape "$PROMPT")

RESP=$(curl -sS -X POST "https://wellapi.ai/v1/images/generations" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<WELL_END
{"model":"$MODEL","prompt":$PROMPT_JSON,"n":$N,"size":"$SIZE","quality":"$QUALITY","format":"$FORMAT"}
WELL_END
)

if ! decode_to_file "$RESP" "$OUT_FILE"; then
  echo "Error: decode failed. Raw (truncated): ${RESP:0:500}" >&2
  exit 1
fi

FULL_PATH="$(cd "$(dirname "$OUT_FILE")" && pwd)/$(basename "$OUT_FILE")"
echo "MEDIA:$FULL_PATH"
```

> For `n > 1`: parse `data[*].b64_json` (use python3) and write each to `wellapi-<ts>-1.<ext>`, `-2.<ext>`, … with one `MEDIA:` line per file.

## Image edit (image-to-image) — `/images/edits`

Body: `multipart/form-data`. `image` is **required** (repeatable, up to **16 files, total ≤ 50MB**).
Optional: `mask` (PNG ≤ 4MB, same WxH as image), `background` (`opaque`/`auto`/`transparent`), `moderation` (`low`/`auto`), `n`/`size`/`quality`/`format`.

```bash
MODEL="gpt-image-2"      # also: gpt-image-1, gpt-image-1-all, gpt-image-2-all, flux-kontext-pro, flux-kontext-max
PROMPT="将他们合并在一个图片里面"
SIZE="1024x1536"
QUALITY="auto"
FORMAT="png"
N=1
BACKGROUND=""            # opaque | auto | transparent (optional)
MODERATION=""            # low | auto (optional)

INPUT_IMAGES=( "./例子.png" "./场景2.png" )   # up to 16, total ≤ 50MB
MASK=""                                       # optional PNG, ≤ 4MB

# Validate inputs
[ "${#INPUT_IMAGES[@]}" -gt 16 ] && { echo "Error: >16 images." >&2; exit 2; }
TOTAL=0
for p in "${INPUT_IMAGES[@]}"; do
  [ -f "$p" ] || { echo "Error: image not found: $p" >&2; exit 2; }
  SZ=$(wc -c <"$p" | tr -d ' ')
  TOTAL=$((TOTAL + SZ))
done
[ "$TOTAL" -gt 52428800 ] && { echo "Error: total > 50MB ($TOTAL bytes)." >&2; exit 2; }

if [ -n "$MASK" ]; then
  [ -f "$MASK" ] || { echo "Error: mask not found: $MASK" >&2; exit 2; }
  MASK_SZ=$(wc -c <"$MASK" | tr -d ' ')
  [ "$MASK_SZ" -gt 4194304 ] && { echo "Error: mask > 4MB." >&2; exit 2; }
fi

EXT=$(ext_for_format "$FORMAT")
OUT_FILE=$(sanitize_name "wellapi-$(date +%Y%m%d-%H%M%S)$EXT" "wellapi-edit-$(date +%s).png")

# Build curl form args
CURL_ARGS=(-sS -X POST "https://wellapi.ai/v1/images/edits"
  -H "Authorization: Bearer $API_KEY"
  -F "model=$MODEL"
  -F "prompt=$PROMPT"
  -F "n=$N"
  -F "size=$SIZE"
  -F "quality=$QUALITY"
  -F "format=$FORMAT")

[ -n "$BACKGROUND" ] && CURL_ARGS+=(-F "background=$BACKGROUND")
[ -n "$MODERATION" ] && CURL_ARGS+=(-F "moderation=$MODERATION")

for p in "${INPUT_IMAGES[@]}"; do
  CURL_ARGS+=(-F "image=@${p}")
done
[ -n "$MASK" ] && CURL_ARGS+=(-F "mask=@${MASK}")

RESP=$(curl "${CURL_ARGS[@]}")

if ! decode_to_file "$RESP" "$OUT_FILE"; then
  echo "Error: decode failed. Raw (truncated): ${RESP:0:500}" >&2
  exit 1
fi

FULL_PATH="$(cd "$(dirname "$OUT_FILE")" && pwd)/$(basename "$OUT_FILE")"
echo "MEDIA:$FULL_PATH"
```

## Notes

- **Synchronous** — no polling.
- `data[i].b64_json` is the full image bytes, base64-encoded. Decode and save.
- For `n > 1`, iterate `data[*]` and emit one `MEDIA:` line per file (`-1`, `-2`, … suffixes).
- Always print `MEDIA:<absolute_path>` so OpenClaw auto-attaches the result.
- Size strict rules: multiples of 16, max side 3840, ratio ≤ 3:1, total pixels in [655360, 8294400].


path: references/powershell.md
content: # PowerShell fallback (Windows, no Python needed)

> Requires PowerShell 5.1+ (pre-installed on all Windows). Save the script below as `generate.ps1`.

## Usage

```powershell
# Text-to-image
powershell -File generate.ps1 -Prompt "a cute cat" -Size 1024x1024 -Quality low -Format jpeg

# Image edit (image-to-image)
powershell -File generate.ps1 -Prompt "add a hat" -Image .\cat.png -Format png

# Image edit with mask + transparent background
powershell -File generate.ps1 -Prompt "replace sky" -Image .\scene.png -Mask .\mask.png -Background transparent

# Multiple reference images (up to 16, total ≤ 50MB)
powershell -File generate.ps1 -Prompt "combine these" -Image .\a.png,.\b.png,.\c.png
```

## Parameters

- `-Prompt` (required, max 1000 chars)
- `-Size` — preset (`1024x1024`, `1536x1024`, `1024x1536`, `2048x2048`, `2048x1152`, `3840x2160`, `2160x3840`, `auto`) or `WxH` (multiples of 16, max side 3840, ratio ≤ 3:1, total pixels 655360–8294400)
- `-Quality` — `low`/`medium`/`high`/`auto` (default `auto`)
- `-Format` — `png`/`jpeg`/`webp`
- `-N` — 1–10
- `-Model` — default `gpt-image-2`
- `-Image` — local file path(s) for edit (switches to `/images/edits`)
- `-Mask` — optional PNG mask for edit (≤ 4MB)
- `-Background` — `opaque`/`auto`/`transparent` (edit only)
- `-Moderation` — `low`/`auto` (edit only)

```powershell
<#
.SYNOPSIS
    Generate / edit images via WellAPI gpt-image-2 (Windows PowerShell fallback).
.DESCRIPTION
    Zero external dependencies. Uses Invoke-RestMethod (PS 5.1+).
    Response is synchronous; data[].b64_json is base64-decoded to a local file.
    Outputs MEDIA:<path> for OpenClaw auto-attach (one line per image).
#>
param(
    [string]$[REDACTED_SECRET],
    [Parameter(Mandatory)][string]$Prompt,
    [string]$Model = "gpt-image-2",
    [string]$Size = "auto",
    [ValidateSet("low", "medium", "high", "auto")]
    [string]$Quality = "auto",
    [ValidateSet("png", "jpeg", "webp")]
    [string]$Format = "png",
    [int]$N = 1,
    [string[]]$Image,
    [string]$Mask,
    [ValidateSet("opaque", "auto", "transparent")]
    [string]$Background,
    [ValidateSet("low", "auto")]
    [string]$Moderation,
    [string]$Out,
    [switch]$VerboseLog
)

$ErrorActionPreference = "Stop"

$MaxEditImages     = 16
$MaxEditTotalBytes = 50MB
$MaxMaskBytes      = 4MB
$MaxPromptChars    = 1000

if (-not $ApiKey) { $[REDACTED_SECRET] "Enter WELLAPI_API_KEY" }
if (-not $ApiKey) { Write-Error "WELLAPI_API_KEY not provided."; exit 2 }

if ($Prompt.Length -gt $MaxPromptChars) {
    Write-Error "Prompt exceeds $MaxPromptChars chars."; exit 2
}
if ($N -lt 1 -or $N -gt 10) { Write-Error "-N must be 1..10."; exit 2 }

function Test-Size([string]$s) {
    $presets = @(
        "1024x1024","1536x1024","1024x1536",
        "2048x2048","2048x1152",
        "3840x2160","2160x3840","auto"
    )
    if ($presets -contains $s) { return }
    if ($s -notmatch '^(\d+)x(\d+)$') {
        throw "Invalid -Size '$s'. Use a preset or WxH."
    }
    $w = [int]$Matches[1]; $h = [int]$Matches[2]
    if ($w % 16 -ne 0 -or $h % 16 -ne 0) { throw "-Size $s: w/h must be multiples of 16." }
    if ([Math]::Max($w, $h) -gt 3840) { throw "-Size $s: longest side > 3840." }
    $ratio = [Math]::Max($w, $h) / [Math]::Min($w, $h)
    if ($ratio -gt 3.0) { throw "-Size ${s}: ratio $ratio exceeds 3:1." }
    $total = $w * $h
    if ($total -lt 655360 -or $total -gt 8294400) {
        throw "-Size ${s}: total pixels $total outside [655360, 8294400]."
    }
}
try { Test-Size $Size } catch { Write-Error $_; exit 2 }

$isEdit = [bool]($Image -and $Image.Count -gt 0)

if ($isEdit) {
    if ($Image.Count -gt $MaxEditImages) {
        Write-Error "Maximum $MaxEditImages reference images allowed."; exit 2
    }
    $totalBytes = 0
    foreach ($p in $Image) {
        if (-not (Test-Path -LiteralPath $p -PathType Leaf)) {
            Write-Error "Image file not found: $p"; exit 2
        }
        $totalBytes += (Get-Item -LiteralPath $p).Length
    }
    if ($totalBytes -gt $MaxEditTotalBytes) {
        Write-Error "Total image size $totalBytes bytes exceeds 50MB limit."; exit 2
    }
    if ($Mask) {
        if (-not (Test-Path -LiteralPath $Mask -PathType Leaf)) {
            Write-Error "Mask file not found: $Mask"; exit 2
        }
        if ((Get-Item -LiteralPath $Mask).Length -gt $MaxMaskBytes) {
            Write-Error "Mask exceeds 4MB limit."; exit 2
        }
    }
} else {
    if ($Mask -or $Background -or $Moderation) {
        Write-Warning "-Mask/-Background/-Moderation only apply to image edits; ignored."
    }
}

$apiBase = "https://wellapi.ai/v1"
$headers = @{
    "Authorization" = "Bearer $ApiKey"
    "User-Agent"    = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    "Accept"        = "application/json"
}

if ($VerboseLog) {
    $mode = if ($isEdit) { "edit (image-to-image)" } else { "text-to-image" }
    Write-Host "Mode: $mode"
    Write-Host "Model=$Model size=$Size quality=$Quality format=$Format n=$N"
}

function Get-ExtForFormat([string]$fmt) {
    switch ($fmt.ToLower()) {
        "jpeg" { ".jpg" }
        "jpg"  { ".jpg" }
        "webp" { ".webp" }
        default { ".png" }
    }
}

function Get-MimeForFile([string]$path) {
    switch ([System.IO.Path]::GetExtension($path).ToLower()) {
        ".png"  { "image/png" }
        ".jpg"  { "image/jpeg" }
        ".jpeg" { "image/jpeg" }
        ".webp" { "image/webp" }
        ".gif"  { "image/gif" }
        default { "application/octet-stream" }
    }
}

try {
    if ($isEdit) {
        $boundary = "----WellAPIBoundary$([guid]::NewGuid().ToString('N'))"
        $LF  = "`r`n"
        $enc = [System.Text.Encoding]::UTF8
        $ms  = New-Object System.IO.MemoryStream

        function Write-Field([string]$name, [string]$value) {
            $part = "--$boundary$LF" +
                    "Content-Disposition: form-data; name=`"$name`"$LF$LF" +
                    "$value$LF"
            $bytes = $enc.GetBytes($part)
            $ms.Write($bytes, 0, $bytes.Length)
        }

        function Write-File([string]$name, [string]$path) {
            $fileName = [System.IO.Path]::GetFileName($path)
            $mime     = Get-MimeForFile $path
            $head = "--$boundary$LF" +
                    "Content-Disposition: form-data; name=`"$name`"; filename=`"$fileName`"$LF" +
                    "Content-Type: $mime$LF$LF"
            $hb = $enc.GetBytes($head); $ms.Write($hb, 0, $hb.Length)
            $fb = [System.IO.File]::ReadAllBytes($path); $ms.Write($fb, 0, $fb.Length)
            $tb = $enc.GetBytes($LF); $ms.Write($tb, 0, $tb.Length)
        }

        Write-Field "model"   $Model
        Write-Field "prompt"  $Prompt
        Write-Field "n"       "$N"
        Write-Field "size"    $Size
        Write-Field "quality" $Quality
        Write-Field "format"  $Format
        if ($Background) { Write-Field "background" $Background }
        if ($Moderation) { Write-Field "moderation" $Moderation }

        foreach ($p in $Image) {
            $full = (Resolve-Path -LiteralPath $p).Path
            Write-File "image" $full
        }
        if ($Mask) {
            $maskFull = (Resolve-Path -LiteralPath $Mask).Path
            Write-File "mask" $maskFull
        }

        $tail = $enc.GetBytes("--$boundary--$LF")
        $ms.Write($tail, 0, $tail.Length)

        $bodyBytes = $ms.ToArray()
        $ms.Dispose()

        $headers["Content-Type"] = "multipart/form-data; boundary=$boundary"
        $resp = Invoke-RestMethod -Uri "$apiBase/images/edits" -Method POST `
            -Headers $headers -Body $bodyBytes -TimeoutSec 300
    }
    else {
        $body = @{
            model   = $Model
            prompt  = $Prompt
            n       = $N
            size    = $Size
            quality = $Quality
            format  = $Format
        } | ConvertTo-Json -Compress -Depth 5

        $resp = Invoke-RestMethod -Uri "$apiBase/images/generations" -Method POST `
            -Headers $headers -Body $body -ContentType "application/json" -TimeoutSec 300
    }
}
catch {
    Write-Error "API request failed: $_"
    exit 1
}

if (-not $resp.data -or $resp.data.Count -eq 0) {
    Write-Error "No image data in response: $($resp | ConvertTo-Json -Compress -Depth 5)"
    exit 1
}

$outputFormat = if ($resp.output_format) { $resp.output_format } else { $Format }
$ext   = Get-ExtForFormat $outputFormat
$total = $resp.data.Count

for ($i = 0; $i -lt $total; $i++) {
    $item = $resp.data[$i]
    if (-not $item.b64_json) { Write-Error "Missing b64_json in data[$i]"; exit 1 }

    if ($Out) {
        $rootBase = [System.IO.Path]::GetFileNameWithoutExtension($Out)
        $rootDir  = [System.IO.Path]::GetDirectoryName($Out)
        $givenExt = [System.IO.Path]::GetExtension($Out)
        $useExt   = if ($givenExt) { $givenExt } else { $ext }
        $name = if ($total -gt 1) { "$rootBase-$($i + 1)$useExt" } else { "$rootBase$useExt" }
        $outFile = if ($rootDir) { Join-Path $rootDir $name } else { $name }
    } else {
        $ts = Get-Date -Format "yyyyMMdd-HHmmss-fff"
        $suffix = if ($total -gt 1) { "-$($i + 1)" } else { "" }
        $outFile = "wellapi-$ts$suffix$ext"
    }
    $outFile = [System.IO.Path]::GetFullPath($outFile)

    try {
        $bytes = [System.Convert]::FromBase64String($item.b64_json)
        [System.IO.File]::WriteAllBytes($outFile, $bytes)
    }
    catch {
        Write-Error "Failed to decode/save image $($i + 1): $_"; exit 1
    }

    if ($VerboseLog) { Write-Host "Saved: $outFile ($($bytes.Length) bytes)" }
    Write-Output "MEDIA:$outFile"
}

exit 0
```


path: references/python.md
content: # Python reference (all platforms, zero dependencies)

> Requires Python 3.6+. Uses only stdlib (`urllib`, `json`, `base64`, `uuid`).

Save the script below as `generate.py`.

## Usage

```bash
# Text-to-image
python3 generate.py --prompt "a cute cat" --size 1024x1024 --quality low --format jpeg

# Image edit (image-to-image) — pass one or more local image paths
python3 generate.py --prompt "add a hat" --image ./cat.png --image ./hat.png --format png

# Image edit with mask
python3 generate.py --prompt "replace sky with sunset" --image ./scene.png --mask ./mask.png

# Use a different edit model
python3 generate.py --prompt "..." --image ./input.png --model flux-kontext-max
```

## Options

- `--prompt` — required, **max 1000 chars**
- `--size` — `1024x1024`, `1536x1024`, `1024x1536`, `2048x2048`, `2048x1152`, `3840x2160`, `2160x3840`, `auto`, or custom `WxH` (W,H multiples of 16; max side 3840; ratio ≤ 3:1; total pixels 655360–8294400)
- `--quality` — `low`, `medium`, `high`, `auto` (default `auto`)
- `--format` — `png`, `jpeg`, `webp`
- `--n` — number of images, **1–10** (default 1)
- `--model` — default `gpt-image-2`; for edits also accepts `gpt-image-1`, `gpt-image-1-all`, `gpt-image-2-all`, `flux-kontext-pro`, `flux-kontext-max`
- `--image PATH` — local image for editing (repeat, up to 16, total ≤ 50MB); switches to `/images/edits`
- `--mask PATH` — optional PNG mask for editing (≤ 4MB, same WxH as first image)
- `--background` — `opaque`, `auto`, `transparent` (edit only)
- `--moderation` — `low`, `auto` (edit only)

```python
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import base64
import datetime as _dt
import json
import mimetypes
import os
import re
import sys
import urllib.error
import urllib.request
import uuid


API_BASE = "https://wellapi.ai/v1"
DEFAULT_MODEL = "gpt-image-2"
EDIT_MODELS = {
    "gpt-image-1", "gpt-image-1-all",
    "gpt-image-2", "gpt-image-2-all",
    "flux-kontext-pro", "flux-kontext-max",
}
PRESET_SIZES = {
    "1024x1024", "1536x1024", "1024x1536",
    "2048x2048", "2048x1152",
    "3840x2160", "2160x3840",
    "auto",
}
MAX_EDIT_IMAGES = 16
MAX_EDIT_TOTAL_BYTES = 50 * 1024 * 1024  # 50 MB
MAX_MASK_BYTES = 4 * 1024 * 1024         # 4 MB
MAX_PROMPT_CHARS = 1000

_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
       "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")


def _validate_size(size: str) -> None:
    if size in PRESET_SIZES:
        return
    m = re.fullmatch(r"(\d+)x(\d+)", size)
    if not m:
        raise ValueError(f"Invalid --size '{size}'. Use a preset (e.g. 1024x1024, auto) or WxH.")
    w, h = int(m.group(1)), int(m.group(2))
    if w % 16 or h % 16:
        raise ValueError(f"--size {size}: width and height must be multiples of 16.")
    if max(w, h) > 3840:
        raise ValueError(f"--size {size}: longest side must be ≤ 3840.")
    ratio = max(w, h) / min(w, h)
    if ratio > 3.0 + 1e-9:
        raise ValueError(f"--size {size}: ratio {ratio:.2f} exceeds 3:1.")
    total = w * h
    if total < 655_360 or total > 8_294_400:
        raise ValueError(f"--size {size}: total pixels {total} outside [655360, 8294400].")


def _post_json(url: str, api_key: str, payload: dict, timeout_s: int = 300) -> dict:
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "User-Agent": _UA,
        "Accept": "application/json",
    }
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            body = resp.read()
    except urllib.error.HTTPError as e:
        err_body = e.read()
        raise RuntimeError(f"HTTP {e.code} {e.reason}: {err_body.decode('utf-8', 'replace')}") from None
    except urllib.error.URLError as e:
        raise RuntimeError(f"Network error: {e.reason}") from None
    try:
        return json.loads(body.decode("utf-8"))
    except Exception:
        raise RuntimeError(f"Non-JSON response: {body[:200]!r}") from None


def _post_multipart(url: str, api_key: str, fields: list, files: list, timeout_s: int = 300) -> dict:
    """fields: list of (name, str_value); files: list of (name, filename, bytes, content_type)."""
    boundary = "----WellAPIBoundary" + uuid.uuid4().hex
    parts = []
    for name, value in fields:
        parts.append(f"--{boundary}".encode())
        parts.append(f'Content-Disposition: form-data; name="{name}"'.encode())
        parts.append(b"")
        parts.append(str(value).encode("utf-8"))
    for name, filename, content, ctype in files:
        parts.append(f"--{boundary}".encode())
        parts.append(
            f'Content-Disposition: form-data; name="{name}"; filename="{filename}"'.encode("utf-8")
        )
        parts.append(f"Content-Type: {ctype}".encode())
        parts.append(b"")
        parts.append(content)
    parts.append(f"--{boundary}--".encode())
    parts.append(b"")
    body = b"\r\n".join(parts)

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "User-Agent": _UA,
        "Accept": "application/json",
        "Content-Length": str(len(body)),
    }
    req = urllib.request.Request(url, data=body, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            resp_body = resp.read()
    except urllib.error.HTTPError as e:
        err_body = e.read()
        raise RuntimeError(f"HTTP {e.code} {e.reason}: {err_body.decode('utf-8', 'replace')}") from None
    except urllib.error.URLError as e:
        raise RuntimeError(f"Network error: {e.reason}") from None
    try:
        return json.loads(resp_body.decode("utf-8"))
    except Exception:
        raise RuntimeError(f"Non-JSON response: {resp_body[:200]!r}") from None


def _ext_for_format(fmt: str) -> str:
    fmt = (fmt or "").lower()
    if fmt in ("jpg", "jpeg"):
        return ".jpg"
    if fmt == "webp":
        return ".webp"
    return ".png"


def _default_out_file(ext: str, index: int = 0, total: int = 1) -> str:
    ts = _dt.datetime.now().strftime("%Y%m%d-%H%M%S-%f")
    suffix = f"-{index + 1}" if total > 1 else ""
    return f"wellapi-{ts}{suffix}{ext}"


def _resolve_api_key(cli_value: str | None) -> str:
    key = (cli_value or os.getenv("WELLAPI_API_KEY") or "").strip()
    if not key and sys.stdin.isatty():
        try:
            key = input("Enter WELLAPI_API_KEY: ").strip()
        except EOFError:
            key = ""
    return key


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(description="Generate / edit images via WellAPI gpt-image-2.")
    parser.add_argument("--api-key", default=None, help="WellAPI key (or set WELLAPI_API_KEY).")
    parser.add_argument("--prompt", required=True, help="Image prompt (max 1000 chars).")
    parser.add_argument("--model", default=DEFAULT_MODEL,
                        help=f"Model name (default {DEFAULT_MODEL}).")
    parser.add_argument("--size", default="auto", help="Image size (preset or WxH).")
    parser.add_argument("--quality", default="auto",
                        choices=["low", "medium", "high", "auto"],
                        help="Image quality (default auto).")
    parser.add_argument("--format", dest="fmt", default="png",
                        choices=["png", "jpeg", "webp"], help="Output format (default png).")
    parser.add_argument("--n", type=int, default=1, help="Number of images (1-10).")
    parser.add_argument("--image", action="append", default=None,
                        help="Local image file path for edit. Repeat for multiple (up to 16, total ≤ 50MB).")
    parser.add_argument("--mask", default=None,
                        help="Optional PNG mask for edit (≤ 4MB, same WxH as first image).")
    parser.add_argument("--background", default=None,
                        choices=["opaque", "auto", "transparent"],
                        help="Edit only: background handling.")
    parser.add_argument("--moderation", default=None,
                        choices=["low", "auto"],
                        help="Edit only: content moderation level.")
    parser.add_argument("--out", default=None,
                        help="Output filename (single image) or prefix (multi-image).")
    parser.add_argument("--verbose", action="store_true", help="Print debug info.")
    args = parser.parse_args(argv)

    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: WELLAPI_API_KEY not provided.", file=sys.stderr)
        return 2

    if len(args.prompt) > MAX_PROMPT_CHARS:
        print(f"Error: prompt exceeds {MAX_PROMPT_CHARS} chars.", file=sys.stderr)
        return 2

    if args.n < 1 or args.n > 10:
        print("Error: --n must be between 1 and 10.", file=sys.stderr)
        return 2

    try:
        _validate_size(args.size)
    except ValueError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 2

    is_edit = bool(args.image)
    if is_edit:
        if len(args.image) > MAX_EDIT_IMAGES:
            print(f"Error: max {MAX_EDIT_IMAGES} reference images allowed.", file=sys.stderr)
            return 2
        total_bytes = 0
        image_payloads = []
        for p in args.image:
            if not os.path.isfile(p):
                print(f"Error: image file not found: {p}", file=sys.stderr)
                return 2
            with open(p, "rb") as f:
                content = f.read()
            total_bytes += len(content)
            ctype, _ = mimetypes.guess_type(p)
            if not ctype:
                ctype = "application/octet-stream"
            image_payloads.append((os.path.basename(p), content, ctype))
        if total_bytes > MAX_EDIT_TOTAL_BYTES:
            print(f"Error: total image size {total_bytes} bytes exceeds 50MB limit.", file=sys.stderr)
            return 2

        mask_payload = None
        if args.mask:
            if not os.path.isfile(args.mask):
                print(f"Error: mask file not found: {args.mask}", file=sys.stderr)
                return 2
            with open(args.mask, "rb") as f:
                mask_bytes = f.read()
            if len(mask_bytes) > MAX_MASK_BYTES:
                print("Error: mask exceeds 4MB limit.", file=sys.stderr)
                return 2
            mask_payload = (os.path.basename(args.mask), mask_bytes, "image/png")

        if args.model not in EDIT_MODELS:
            print(f"Warning: model '{args.model}' not in known edit-model list; sending anyway.",
                  file=sys.stderr)
    else:
        if args.mask or args.background or args.moderation:
            print("Note: --mask/--background/--moderation only apply to image edits; ignored.",
                  file=sys.stderr)

    if args.verbose:
        mode = "edit (image-to-image)" if is_edit else "text-to-image"
        print(f"Mode: {mode}")
        print(f"Model={args.model} size={args.size} quality={args.quality} "
              f"format={args.fmt} n={args.n}")

    if is_edit:
        fields = [
            ("model", args.model),
            ("prompt", args.prompt),
            ("n", str(args.n)),
            ("size", args.size),
            ("quality", args.quality),
            ("format", args.fmt),
        ]
        if args.background:
            fields.append(("background", args.background))
        if args.moderation:
            fields.append(("moderation", args.moderation))

        files = []
        for fname, content, ctype in image_payloads:
            files.append(("image", fname, content, ctype))
        if mask_payload:
            files.append(("mask", mask_payload[0], mask_payload[1], mask_payload[2]))

        resp = _post_multipart(f"{API_BASE}/images/edits", api_key, fields, files)
    else:
        payload = {
            "model": args.model,
            "prompt": args.prompt,
            "n": args.n,
            "size": args.size,
            "quality": args.quality,
            "format": args.fmt,
        }
        resp = _post_json(f"{API_BASE}/images/generations", api_key, payload)

    data = resp.get("data") or []
    if not data:
        raise RuntimeError(f"No image data in response: {resp}")

    output_format = (resp.get("output_format") or args.fmt or "png").lower()
    ext = _ext_for_format(output_format)
    total = len(data)

    for i, item in enumerate(data):
        b64 = item.get("b64_json")
        if not b64:
            raise RuntimeError(f"Missing b64_json in data[{i}]: {item}")
        try:
            img_bytes = base64.b64decode(b64)
        except Exception as e:
            raise RuntimeError(f"Invalid base64 in data[{i}]: {e}") from None

        if args.out:
            base = args.out
            root, given_ext = os.path.splitext(base)
            if total > 1:
                out_file = f"{root}-{i + 1}{given_ext or ext}"
            else:
                out_file = base if given_ext else base + ext
        else:
            out_file = _default_out_file(ext, i, total)

        out_file = os.path.abspath(out_file)
        with open(out_file, "wb") as f:
            f.write(img_bytes)

        if args.verbose:
            print(f"Saved: {out_file} ({len(img_bytes)} bytes)")
        print(f"MEDIA:{out_file}")

    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main(sys.argv[1:]))
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        raise SystemExit(1)
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)