From 0e1886dde54aef60bf4d98a838462ff1d5bfd17f Mon Sep 17 00:00:00 2001 From: chenpipi0807 <144689950+chenpipi0807@users.noreply.github.com> Date: Fri, 26 Jun 2026 18:56:56 +0800 Subject: [PATCH] Add OpenAI gpt-image-2 image backend New --model openai branch in asset_gen.py with low/medium/high quality, abstract-size or raw WIDTHxHEIGHT pixel sizing, ceil'd per-size/quality cost table, and b64_json output (url fallback). Docs and OPENAI_API_KEY setup updated. Co-Authored-By: Claude Opus 4.6 --- setup.md | 1 + shared/skills/godogen/asset-gen.md | 35 +++- shared/skills/godogen/tools/asset_gen.py | 180 +++++++++++++++++-- shared/skills/godogen/tools/requirements.txt | 1 + 4 files changed, 202 insertions(+), 15 deletions(-) diff --git a/setup.md b/setup.md index 0becbe37..52c27729 100644 --- a/setup.md +++ b/setup.md @@ -230,6 +230,7 @@ Set in environment: - `GOOGLE_API_KEY` — Gemini image generation - `XAI_API_KEY` — xAI Grok image/video generation +- `OPENAI_API_KEY` — OpenAI gpt-image-2 image generation (requires API org verification) - `TRIPO3D_API_KEY` — image-to-3D conversion ## Post-Task Telegram Push (optional) diff --git a/shared/skills/godogen/asset-gen.md b/shared/skills/godogen/asset-gen.md index 5de16b67..7e2486f7 100644 --- a/shared/skills/godogen/asset-gen.md +++ b/shared/skills/godogen/asset-gen.md @@ -1,6 +1,6 @@ # Asset Generator -Generate PNG images (Gemini or xAI Grok) and GLB 3D models (Tripo3D) from text prompts. +Generate PNG images (Gemini, xAI Grok, or OpenAI gpt-image-2) and GLB 3D models (Tripo3D) from text prompts. ## Models @@ -8,12 +8,14 @@ Generate PNG images (Gemini or xAI Grok) and GLB 3D models (Tripo3D) from text p |-------|------|------|----------| | `gemini-3.1-flash-image-preview` | `--model gemini` | 5-15¢ (by size) | Precise prompt following — references, characters, backgrounds, 3D refs | | `grok-imagine-image` | `--model grok` | 2¢ | High-quality but imprecise — textures, simple objects, item kits | +| `gpt-image-2` | `--model openai` | 1-85¢ (by size+quality) | In-image text rendering (incl. CJK), strong world knowledge, detailed layered prompts | **When to use which:** - **Gemini** — reference images, character design, 3D model references, animated sprite refs/poses, backgrounds with precise layout. Gemini costs more but reliably produces what you described. - **Grok** — textures, simple objects, item kits, props, simple scenic backgrounds (sky, clouds, abstract). Produces high-quality (even photographic) output but often defaults to common interpretations instead of following specific instructions. Great when exact prompt adherence doesn't matter. +- **OpenAI gpt-image-2** — anything needing readable text baked into the image (signs, posters, UI mockups, labels — including CJK), real-world/cultural knowledge, or long layered prompts. Cost varies widely by size and `--quality`. Cannot produce transparent backgrounds — run rembg downstream. -Default is `grok`. Switch to `gemini` when precision matters. +Default is `grok`. Switch to `gemini` for precision, or `openai` when in-image text or world knowledge matters. ### Gemini sizes and costs @@ -24,6 +26,23 @@ Default is `grok`. Switch to `gemini` when precision matters. | `2K` | 10¢ | | `4K` | 15¢ | +### OpenAI gpt-image-2 sizes, quality, and costs + +`--quality` is `low` / `medium` / `high` (default `medium`). Cost spread between `low` and `high` at the same size can be ~35×. + +`--size` accepts either an abstract size (mapped via `--aspect-ratio` orientation) or a raw `WIDTHxHEIGHT` pixel size. Pixel sizes must be 16-multiples, max edge 3840, long:short ≤ 3:1, total pixels 655,360–8,294,400. + +| `--size` + `--aspect-ratio` | Pixel size | low | medium | high | +|------|------|------|------|------| +| `1K` 1:1 | 1024×1024 | 1¢ | 6¢ | 22¢ | +| `1K` 16:9 / 3:2 | 1536×1024 | 1¢ | 5¢ | 17¢ | +| `1K` 9:16 / 2:3 | 1024×1536 | 1¢ | 5¢ | 17¢ | +| `2K` 1:1 | 2048×2048 | 3¢ | 22¢ | 85¢ | +| `2K` 16:9 | 2048×1152 | 2¢ | 12¢ | 48¢ | +| `4K` 16:9 | 3840×2160 | 3¢ | 16¢ | 41¢ | + +Costs are rounded up to whole cents. `512` and `4K` square are unsupported (outside pixel limits). For an exact size, pass e.g. `--size 1536x1024`. + ## CLI Reference Tools live at `${GODOGEN_SKILL_DIR}/tools/`. Run from the project root. @@ -37,14 +56,17 @@ python3 ${GODOGEN_SKILL_DIR}/tools/asset_gen.py image \ --prompt "the full prompt" -o assets/img/car.png ``` -`--model` (default `grok`): `grok` (2¢), `gemini` (5-15¢ by size) -`--size` (default `1K`): Grok: `1K`, `2K`. Gemini: `512`, `1K`, `2K`, `4K`. -`--aspect-ratio` (default `1:1`): varies by backend — both support `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3` +`--model` (default `grok`): `grok` (2¢), `gemini` (5-15¢ by size), `openai` (gpt-image-2, 1-85¢ by size+quality) +`--size` (default `1K`): Grok: `1K`, `2K`. Gemini: `512`, `1K`, `2K`, `4K`. OpenAI: abstract (`1K`/`2K`/`4K`, mapped via `--aspect-ratio`) or raw `WIDTHxHEIGHT` (e.g. `1536x1024`). +`--quality` (default `medium`, **OpenAI only**): `low`, `medium`, `high`. Ignored by gemini/grok. +`--aspect-ratio` (default `1:1`): varies by backend — both support `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`. For OpenAI abstract sizes it picks orientation; ignored when `--size` is `WIDTHxHEIGHT`. Typical combos: - `--model gemini --size 1K` — reference images, character sprites, 3D refs (7¢) - `--model gemini --size 2K --aspect-ratio 16:9` — backgrounds, title screens (10¢) - `--model grok` — textures, simple objects, item kits (2¢) +- `--model openai --quality high` — posters/UI/signage with readable in-image text (22¢ at 1K 1:1) +- `--model openai --size 1536x1024 --quality medium` — exact-pixel layout, e.g. video cover (5¢) ### Remove background @@ -220,6 +242,9 @@ result=$(python3 ${GODOGEN_SKILL_DIR}/tools/asset_gen.py image --prompt "..." -o | Image | --model gemini --size 1K | 7 cents | References, characters, 3D refs | | Image | --model gemini --size 2K | 10 cents | Backgrounds, title screens | | Image | --model gemini --size 4K | 15 cents | Large maps, panoramas | +| Image | --model openai --quality medium (1K) | 5-6 cents | In-image text, world knowledge | +| Image | --model openai --quality high (1K) | 17-22 cents | High-fidelity text/posters | +| Image | --model openai --quality high --size 2K | 48-85 cents | Large high-fidelity text/layout | | GLB | default | 30 cents | v3.1, 30k face cap, standard texture + PBR | | GLB | hd | 60 cents | v3.1, detailed geometry + HD texture + PBR | | Rig | biped | 25 cents | one-time per character, on top of the GLB cost | diff --git a/shared/skills/godogen/tools/asset_gen.py b/shared/skills/godogen/tools/asset_gen.py index 0afa056b..2c48db48 100644 --- a/shared/skills/godogen/tools/asset_gen.py +++ b/shared/skills/godogen/tools/asset_gen.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Asset Generator CLI - creates images (Gemini / xAI Grok) and GLBs (Tripo3D). +"""Asset Generator CLI - creates images (Gemini / xAI Grok / OpenAI gpt-image-2) and GLBs (Tripo3D). Subcommands: - image Generate a PNG from a prompt (Gemini 5-15¢ or Grok 2¢) + image Generate a PNG from a prompt (Gemini 5-15¢, Grok 2¢, or OpenAI gpt-image-2) video Generate MP4 video from prompt + reference image (5¢/sec, Grok) glb Convert a PNG to a static GLB (30¢ default, 60¢ hd) rig Convert a PNG to a rigged biped GLB (preset + 25¢) @@ -16,6 +16,8 @@ import base64 import io import json +import math +import re import sys from pathlib import Path @@ -23,6 +25,7 @@ import xai_sdk from google import genai from google.genai import types +from openai import OpenAI from PIL import Image from tripo3d import ( @@ -117,6 +120,37 @@ def result_json(ok: bool, path: str | None = None, cost_cents: int = 0, error: s "2:1", "1:2", "19.5:9", "9:19.5", "20:9", "9:20", "auto", ] +OPENAI_MODEL = "gpt-image-2-2026-04-21" # fixed snapshot (avoids float-alias drift) +OPENAI_QUALITIES = ["low", "medium", "high"] + +# Maps the abstract (--size, orientation) to a recommended gpt-image-2 pixel size. +# OpenAI needs absolute pixels; orientation is derived from --aspect-ratio. +# 512 and 4K-square are intentionally absent (below min / above max pixel limits). +OPENAI_SIZE_MAP = { + ("1K", "square"): "1024x1024", + ("1K", "landscape"): "1536x1024", + ("1K", "portrait"): "1024x1536", + ("2K", "square"): "2048x2048", + ("2K", "landscape"): "2048x1152", + ("2K", "portrait"): "1152x2048", + ("4K", "landscape"): "3840x2160", + ("4K", "portrait"): "2160x3840", +} + +# Documented per-size, per-quality cost in cents (OpenAI calculator estimates). +# Sizes not listed are scaled from 1024x1024 by pixel ratio in _openai_cost_cents. +OPENAI_SIZE_COSTS = { + "1024x1024": {"low": 0.6, "medium": 5.3, "high": 21.1}, + "1536x1024": {"low": 0.5, "medium": 4.1, "high": 16.5}, + "1024x1536": {"low": 0.5, "medium": 4.1, "high": 16.5}, + "3840x2160": {"low": 2.4, "medium": 16.0, "high": 41.0}, + "2160x3840": {"low": 2.4, "medium": 16.0, "high": 41.0}, +} + +OPENAI_MAX_EDGE = 3840 +OPENAI_MIN_PIXELS = 655_360 +OPENAI_MAX_PIXELS = 8_294_400 + ALL_SIZES = ["512", "1K", "2K", "4K"] ALL_ASPECT_RATIOS = sorted(set(GEMINI_ASPECT_RATIOS + GROK_ASPECT_RATIOS)) @@ -212,15 +246,134 @@ def _generate_grok(args, output: Path, cost: int): result_json(True, path=str(output), cost_cents=cost) +def _aspect_orientation(aspect_ratio: str) -> str: + """Classify an aspect ratio string as square / landscape / portrait.""" + try: + w, h = (float(x) for x in aspect_ratio.split(":")) + except ValueError: + return "square" + if w > h: + return "landscape" + if h > w: + return "portrait" + return "square" + + +def _validate_openai_pixels(width: int, height: int): + """Enforce gpt-image-2 size constraints. Exit with error JSON on violation.""" + errs = [] + if width % 16 or height % 16: + errs.append("both dimensions must be multiples of 16") + if max(width, height) > OPENAI_MAX_EDGE: + errs.append(f"max edge is {OPENAI_MAX_EDGE}px") + short, long = sorted((width, height)) + if long > short * 3: + errs.append("aspect ratio long:short must be <= 3:1") + pixels = width * height + if pixels < OPENAI_MIN_PIXELS or pixels > OPENAI_MAX_PIXELS: + errs.append(f"total pixels must be {OPENAI_MIN_PIXELS}-{OPENAI_MAX_PIXELS} (got {pixels})") + if errs: + result_json(False, error="Invalid gpt-image-2 size: " + "; ".join(errs)) + sys.exit(1) + + +def _resolve_openai_size(size: str, aspect_ratio: str) -> str: + """Resolve a gpt-image-2 'WxH' size from a pixel passthrough or abstract size+aspect.""" + m = re.fullmatch(r"(\d+)x(\d+)", size) + if m: + width, height = int(m.group(1)), int(m.group(2)) + _validate_openai_pixels(width, height) + return f"{width}x{height}" + + orientation = _aspect_orientation(aspect_ratio) + key = (size, orientation) + if key not in OPENAI_SIZE_MAP: + combos = ", ".join(f"{s}+{o}" for s, o in OPENAI_SIZE_MAP) + result_json(False, error=( + f"gpt-image-2 has no mapping for size {size} + aspect {aspect_ratio} ({orientation}). " + f"Pass a WIDTHxHEIGHT size, or use one of: {combos}." + )) + sys.exit(1) + return OPENAI_SIZE_MAP[key] + + +def _openai_cost_cents(openai_size: str, quality: str) -> int: + """Cost in whole cents (ceil), from the documented table or pixel-scaled fallback.""" + table = OPENAI_SIZE_COSTS.get(openai_size) + if table and quality in table: + cents = table[quality] + else: + width, height = (int(x) for x in openai_size.split("x")) + base = OPENAI_SIZE_COSTS["1024x1024"][quality] + cents = base * (width * height) / (1024 * 1024) + return math.ceil(cents) + + +def _openai_image_bytes(item) -> bytes: + """Extract PNG bytes from an OpenAI image response item (b64_json or url fallback).""" + b64 = getattr(item, "b64_json", None) + if b64: + return base64.b64decode(b64) + url = getattr(item, "url", None) + if url: + dl = requests.get(url, timeout=120) + dl.raise_for_status() + return dl.content + raise RuntimeError("No image data in OpenAI response") + + +def _generate_openai(args, output: Path, cost: int, openai_size: str): + client = OpenAI() + try: + if args.image: + ref_path = Path(args.image) + if not ref_path.exists(): + result_json(False, error=f"Reference image not found: {ref_path}") + sys.exit(1) + with open(ref_path, "rb") as f: + resp = client.images.edit( + model=OPENAI_MODEL, + image=f, + prompt=args.prompt, + size=openai_size, + quality=args.quality, + n=1, + ) + else: + resp = client.images.generate( + model=OPENAI_MODEL, + prompt=args.prompt, + size=openai_size, + quality=args.quality, + n=1, + response_format="b64_json", + ) + data = _openai_image_bytes(resp.data[0]) + # Re-encode as real PNG (decode + save normalizes format) + img = Image.open(io.BytesIO(data)) + img.save(output, format="PNG") + except Exception as e: + result_json(False, error=str(e)) + sys.exit(1) + + print(f"Saved: {output}", file=sys.stderr) + record_spend(cost, "openai") + result_json(True, path=str(output), cost_cents=cost) + + def cmd_image(args): backend = args.model size = args.size + openai_size = None if backend == "gemini": if size not in GEMINI_SIZES: result_json(False, error=f"Gemini does not support size {size}. Use: {', '.join(GEMINI_SIZES)}") sys.exit(1) cost = GEMINI_COSTS[size] + elif backend == "openai": + openai_size = _resolve_openai_size(size, args.aspect_ratio) + cost = _openai_cost_cents(openai_size, args.quality) else: if size not in GROK_SIZES: result_json(False, error=f"Grok does not support size {size}. Use: {', '.join(GROK_SIZES)}") @@ -231,13 +384,18 @@ def cmd_image(args): output = Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) - label = f"{backend} {size} {args.aspect_ratio}" + if backend == "openai": + label = f"openai {openai_size} {args.quality}" + else: + label = f"{backend} {size} {args.aspect_ratio}" if args.image: label += " (image-to-image)" print(f"Generating image ({label})...", file=sys.stderr) if backend == "gemini": _generate_gemini(args, output, cost) + elif backend == "openai": + _generate_openai(args, output, cost, openai_size) else: _generate_grok(args, output, cost) @@ -578,17 +736,19 @@ def cmd_set_budget(args): def main(): - parser = argparse.ArgumentParser(description="Asset Generator — images (Gemini / xAI Grok) and GLBs (Tripo3D)") + parser = argparse.ArgumentParser(description="Asset Generator — images (Gemini / xAI Grok / OpenAI gpt-image-2) and GLBs (Tripo3D)") sub = parser.add_subparsers(dest="command", required=True) - p_img = sub.add_parser("image", help="Generate a PNG image (Gemini 5-15¢ or Grok 2¢)") + p_img = sub.add_parser("image", help="Generate a PNG image (Gemini 5-15¢, Grok 2¢, or OpenAI gpt-image-2)") p_img.add_argument("--prompt", required=True, help="Full image generation prompt") - p_img.add_argument("--model", choices=["gemini", "grok"], default="grok", - help="Backend: grok (2¢, fast, simple images) or gemini (5-15¢, precise prompt following). Default: grok.") - p_img.add_argument("--size", choices=ALL_SIZES, default="1K", - help="Resolution. Grok: 1K, 2K. Gemini: 512, 1K, 2K, 4K. Default: 1K.") + p_img.add_argument("--model", choices=["gemini", "grok", "openai"], default="grok", + help="Backend: grok (2¢, fast, simple), gemini (5-15¢, precise), or openai gpt-image-2 (varies by size/quality, strong text rendering). Default: grok.") + p_img.add_argument("--size", default="1K", + help="Resolution. Grok: 1K, 2K. Gemini: 512, 1K, 2K, 4K. OpenAI: abstract (1K/2K/4K, mapped via --aspect-ratio) or a WIDTHxHEIGHT pixel size (16-multiples, max edge 3840). Default: 1K.") + p_img.add_argument("--quality", choices=OPENAI_QUALITIES, default="medium", + help="OpenAI only: low/medium/high (35x cost spread). Ignored by gemini/grok. Default: medium.") p_img.add_argument("--aspect-ratio", choices=ALL_ASPECT_RATIOS, default="1:1", - help="Aspect ratio. Default: 1:1") + help="Aspect ratio. For OpenAI abstract sizes it selects orientation (square/landscape/portrait); ignored when --size is WIDTHxHEIGHT. Default: 1:1") p_img.add_argument("--image", default=None, help="Reference image for image-to-image edit") p_img.add_argument("-o", "--output", required=True, help="Output PNG path") p_img.set_defaults(func=cmd_image) diff --git a/shared/skills/godogen/tools/requirements.txt b/shared/skills/godogen/tools/requirements.txt index 32ab11af..7cf8a597 100644 --- a/shared/skills/godogen/tools/requirements.txt +++ b/shared/skills/godogen/tools/requirements.txt @@ -1,5 +1,6 @@ xai-sdk google-genai +openai requests numpy pillow