commit 683bfe09febe10b5cac45a2d98b396e59e46e747 Author: zlei9 Date: Sun Mar 29 14:25:10 2026 +0800 Initial commit with translated description diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..95d4300 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,35 @@ +--- +name: openai-image-gen +description: "通过OpenAI Images API批量生成图像。" +--- + +# OpenAI Image Gen + +Generate a handful of “random but structured” prompts and render them via OpenAI Images API. + +## Setup + +- Needs env: `OPENAI_API_KEY` + +## Run + +From any directory (outputs to `~/Projects/tmp/...` when present; else `./tmp/...`): + +```bash +python3 ~/Projects/agent-scripts/skills/openai-image-gen/scripts/gen.py +open ~/Projects/tmp/openai-image-gen-*/index.html +``` + +Useful flags: + +```bash +python3 ~/Projects/agent-scripts/skills/openai-image-gen/scripts/gen.py --count 16 --model gpt-image-1.5 +python3 ~/Projects/agent-scripts/skills/openai-image-gen/scripts/gen.py --prompt "ultra-detailed studio photo of a lobster astronaut" --count 4 +python3 ~/Projects/agent-scripts/skills/openai-image-gen/scripts/gen.py --size 1536x1024 --quality high --out-dir ./out/images +``` + +## Output + +- `*.png` images +- `prompts.json` (prompt ↔ file mapping) +- `index.html` (thumbnail gallery) diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..6d60069 --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26", + "slug": "openai-image-gen", + "version": "1.0.1", + "publishedAt": 1767652000401 +} \ No newline at end of file diff --git a/scripts/gen.py b/scripts/gen.py new file mode 100644 index 0000000..18f4409 --- /dev/null +++ b/scripts/gen.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + +import argparse +import base64 +import datetime as _dt +import json +import os +import random +import re +import sys +import time +import urllib.error +import urllib.request + + +def _stamp() -> str: + return _dt.datetime.now().strftime("%Y-%m-%d-%H%M%S") + + +def _slug(text: str, max_len: int = 60) -> str: + s = text.lower() + s = re.sub(r"[^a-z0-9]+", "-", s).strip("-") + return (s[:max_len] or "image").strip("-") + + +def _default_out_dir() -> str: + projects_tmp = os.path.expanduser("~/Projects/tmp") + if os.path.isdir(projects_tmp): + return os.path.join(projects_tmp, f"openai-image-gen-{_stamp()}") + return os.path.join(os.getcwd(), "tmp", f"openai-image-gen-{_stamp()}") + + +def _api_url() -> str: + base = ( + os.environ.get("OPENAI_BASE_URL") + or os.environ.get("OPENAI_API_BASE") + or "https://api.openai.com" + ).rstrip("/") + if base.endswith("/v1"): + return f"{base}/images/generations" + return f"{base}/v1/images/generations" + + +def _random_prompts(count: int) -> list[str]: + subjects = [ + "a lobster piloting a vintage scooter", + "a raccoon librarian in a tiny art-deco library", + "a glass whale floating above a desert", + "a moss-covered robot tending a bonsai garden", + "a candlelit map room with impossible staircases", + "a retro-futurist diner on the moon at dusk", + "a hummingbird made of stained glass", + "a porcelain teapot city in the clouds", + "a midnight train station built inside a giant clock", + "a tiny submarine exploring a glowing kelp forest", + "a baroque observatory with brass telescopes and fog", + "a koi pond shaped like a circuit board", + ] + styles = [ + "ultra-detailed studio photo", + "35mm film still", + "risograph poster", + "oil painting on linen", + "watercolor with ink linework", + "isometric diorama", + "mid-century editorial illustration", + "high-end product shot", + ] + lighting = [ + "softbox lighting", + "golden hour", + "neon rim light", + "overcast diffuse light", + "candlelight with deep shadows", + "dramatic chiaroscuro", + ] + palettes = [ + "copper + teal + cream", + "cobalt + vermilion + bone", + "sage + sand + charcoal", + "magenta + midnight blue + silver", + ] + + random.shuffle(subjects) + prompts: list[str] = [] + for i in range(count): + subj = subjects[i % len(subjects)] + prompts.append( + f"{random.choice(styles)} of {subj}. " + f"Lighting: {random.choice(lighting)}. " + f"Palette: {random.choice(palettes)}. " + "Crisp, no text, no watermark." + ) + return prompts + + +def _post_json(url: str, api_key: str, payload: dict, timeout_s: int) -> dict: + body = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, + data=body, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=timeout_s) as resp: + raw = resp.read() + except urllib.error.HTTPError as e: + raw = e.read() + try: + data = json.loads(raw.decode("utf-8", errors="replace")) + except Exception: + raise SystemExit(f"OpenAI HTTP {e.code}: {raw[:300]!r}") + raise SystemExit(f"OpenAI HTTP {e.code}: {json.dumps(data, indent=2)[:1200]}") + except Exception as e: + raise SystemExit(f"request failed: {e}") + + try: + return json.loads(raw) + except Exception: + raise SystemExit(f"invalid JSON response: {raw[:300]!r}") + + +def _write_index(out_dir: str, items: list[dict]) -> None: + html = [ + "", + "", + "", + "openai-image-gen", + "", + "

openai-image-gen

", + ] + for it in items: + html.append("
") + html.append(f"") + html.append(f"
{it['prompt']}
") + html.append("
") + with open(os.path.join(out_dir, "index.html"), "w", encoding="utf-8") as f: + f.write("\n".join(html)) + + +def main(argv: list[str]) -> int: + p = argparse.ArgumentParser( + prog="openai-image-gen", + description="Generate a batch of images via OpenAI Images API (random prompts by default).", + ) + p.add_argument("--count", type=int, default=8) + p.add_argument("--model", default="gpt-image-1.5") + p.add_argument("--size", default="1024x1024") + p.add_argument("--quality", default="high") + p.add_argument("--timeout", type=int, default=180, help="per-request timeout (seconds)") + p.add_argument("--sleep", type=float, default=0.2, help="pause between requests (seconds)") + p.add_argument("--out-dir", default=None) + p.add_argument("--api-key", default=None) + p.add_argument("--prompt", action="append", default=None, help="repeatable; overrides random prompts") + p.add_argument("--dry-run", action="store_true", help="print prompts + exit (no API calls)") + args = p.parse_args(argv) + + api_key = args.api_key or os.environ.get("OPENAI_API_KEY") + if not api_key: + print("missing OPENAI_API_KEY (or --api-key)", file=sys.stderr) + return 2 + + out_dir = args.out_dir or _default_out_dir() + os.makedirs(out_dir, exist_ok=True) + + prompts = args.prompt if args.prompt else _random_prompts(args.count) + if args.dry_run: + for i, pr in enumerate(prompts, 1): + print(f"{i:02d} {pr}") + print(f"out_dir={out_dir}") + return 0 + + url = _api_url() + items: list[dict] = [] + + for i, prompt in enumerate(prompts, 1): + payload = { + "model": args.model, + "prompt": prompt, + "size": args.size, + "quality": args.quality, + "n": 1, + "response_format": "b64_json", + } + data = _post_json(url=url, api_key=api_key, payload=payload, timeout_s=args.timeout) + b64 = (data.get("data") or [{}])[0].get("b64_json") + if not b64: + raise SystemExit(f"unexpected response: {json.dumps(data, indent=2)[:1200]}") + + png = base64.b64decode(b64) + filename = f"{i:02d}-{_slug(prompt)}.png" + path = os.path.join(out_dir, filename) + with open(path, "wb") as f: + f.write(png) + + items.append( + { + "file": filename, + "prompt": prompt, + "model": args.model, + "size": args.size, + "quality": args.quality, + } + ) + print(f"wrote {filename}") + if args.sleep > 0: + time.sleep(args.sleep) + + with open(os.path.join(out_dir, "prompts.json"), "w", encoding="utf-8") as f: + json.dump(items, f, indent=2, ensure_ascii=False) + + _write_index(out_dir, items) + print(f"out_dir={out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))