repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | #!/usr/bin/env python3 |
| #2 | import argparse |
| #3 | import base64 |
| #4 | import datetime as dt |
| #5 | import json |
| #6 | import os |
| #7 | import random |
| #8 | import re |
| #9 | import sys |
| #10 | import urllib.error |
| #11 | import urllib.request |
| #12 | from pathlib import Path |
| #13 | |
| #14 | |
| #15 | def slugify(text: str) -> str: |
| #16 | text = text.lower().strip() |
| #17 | text = re.sub(r"[^a-z0-9]+", "-", text) |
| #18 | text = re.sub(r"-{2,}", "-", text).strip("-") |
| #19 | return text or "image" |
| #20 | |
| #21 | |
| #22 | def default_out_dir() -> Path: |
| #23 | now = dt.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") |
| #24 | preferred = Path.home() / "Projects" / "tmp" |
| #25 | base = preferred if preferred.is_dir() else Path("./tmp") |
| #26 | base.mkdir(parents=True, exist_ok=True) |
| #27 | return base / f"openai-image-gen-{now}" |
| #28 | |
| #29 | |
| #30 | def pick_prompts(count: int) -> list[str]: |
| #31 | subjects = [ |
| #32 | "a lobster astronaut", |
| #33 | "a brutalist lighthouse", |
| #34 | "a cozy reading nook", |
| #35 | "a cyberpunk noodle shop", |
| #36 | "a Vienna street at dusk", |
| #37 | "a minimalist product photo", |
| #38 | "a surreal underwater library", |
| #39 | ] |
| #40 | styles = [ |
| #41 | "ultra-detailed studio photo", |
| #42 | "35mm film still", |
| #43 | "isometric illustration", |
| #44 | "editorial photography", |
| #45 | "soft watercolor", |
| #46 | "architectural render", |
| #47 | "high-contrast monochrome", |
| #48 | ] |
| #49 | lighting = [ |
| #50 | "golden hour", |
| #51 | "overcast soft light", |
| #52 | "neon lighting", |
| #53 | "dramatic rim light", |
| #54 | "candlelight", |
| #55 | "foggy atmosphere", |
| #56 | ] |
| #57 | prompts: list[str] = [] |
| #58 | for _ in range(count): |
| #59 | prompts.append( |
| #60 | f"{random.choice(styles)} of {random.choice(subjects)}, {random.choice(lighting)}" |
| #61 | ) |
| #62 | return prompts |
| #63 | |
| #64 | |
| #65 | def get_model_defaults(model: str) -> tuple[str, str]: |
| #66 | """Return (default_size, default_quality) for the given model.""" |
| #67 | if model == "dall-e-2": |
| #68 | # quality will be ignored |
| #69 | return ("1024x1024", "standard") |
| #70 | elif model == "dall-e-3": |
| #71 | return ("1024x1024", "standard") |
| #72 | else: |
| #73 | # GPT image or future models |
| #74 | return ("1024x1024", "high") |
| #75 | |
| #76 | |
| #77 | def request_images( |
| #78 | api_key: str, |
| #79 | prompt: str, |
| #80 | model: str, |
| #81 | size: str, |
| #82 | quality: str, |
| #83 | background: str = "", |
| #84 | output_format: str = "", |
| #85 | style: str = "", |
| #86 | ) -> dict: |
| #87 | url = "https://api.openai.com/v1/images/generations" |
| #88 | args = { |
| #89 | "model": model, |
| #90 | "prompt": prompt, |
| #91 | "size": size, |
| #92 | "n": 1, |
| #93 | } |
| #94 | |
| #95 | # Quality parameter - dall-e-2 doesn't accept this parameter |
| #96 | if model != "dall-e-2": |
| #97 | args["quality"] = quality |
| #98 | |
| #99 | # Note: response_format no longer supported by OpenAI Images API |
| #100 | # dall-e models now return URLs by default |
| #101 | |
| #102 | if model.startswith("gpt-image"): |
| #103 | if background: |
| #104 | args["background"] = background |
| #105 | if output_format: |
| #106 | args["output_format"] = output_format |
| #107 | |
| #108 | if model == "dall-e-3" and style: |
| #109 | args["style"] = style |
| #110 | |
| #111 | body = json.dumps(args).encode("utf-8") |
| #112 | req = urllib.request.Request( |
| #113 | url, |
| #114 | method="POST", |
| #115 | headers={ |
| #116 | "Authorization": f"Bearer {api_key}", |
| #117 | "Content-Type": "application/json", |
| #118 | }, |
| #119 | data=body, |
| #120 | ) |
| #121 | try: |
| #122 | with urllib.request.urlopen(req, timeout=300) as resp: |
| #123 | return json.loads(resp.read().decode("utf-8")) |
| #124 | except urllib.error.HTTPError as e: |
| #125 | payload = e.read().decode("utf-8", errors="replace") |
| #126 | raise RuntimeError(f"OpenAI Images API failed ({e.code}): {payload}") from e |
| #127 | |
| #128 | |
| #129 | def write_gallery(out_dir: Path, items: list[dict]) -> None: |
| #130 | thumbs = "\n".join( |
| #131 | [ |
| #132 | f""" |
| #133 | <figure> |
| #134 | <a href="{it["file"]}"><img src="{it["file"]}" loading="lazy" /></a> |
| #135 | <figcaption>{it["prompt"]}</figcaption> |
| #136 | </figure> |
| #137 | """.strip() |
| #138 | for it in items |
| #139 | ] |
| #140 | ) |
| #141 | html = f"""<!doctype html> |
| #142 | <meta charset="utf-8" /> |
| #143 | <title>openai-image-gen</title> |
| #144 | <style> |
| #145 | :root {{ color-scheme: dark; }} |
| #146 | body {{ margin: 24px; font: 14px/1.4 ui-sans-serif, system-ui; background: #0b0f14; color: #e8edf2; }} |
| #147 | h1 {{ font-size: 18px; margin: 0 0 16px; }} |
| #148 | .grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }} |
| #149 | figure {{ margin: 0; padding: 12px; border: 1px solid #1e2a36; border-radius: 14px; background: #0f1620; }} |
| #150 | img {{ width: 100%; height: auto; border-radius: 10px; display: block; }} |
| #151 | figcaption {{ margin-top: 10px; color: #b7c2cc; }} |
| #152 | code {{ color: #9cd1ff; }} |
| #153 | </style> |
| #154 | <h1>openai-image-gen</h1> |
| #155 | <p>Output: <code>{out_dir.as_posix()}</code></p> |
| #156 | <div class="grid"> |
| #157 | {thumbs} |
| #158 | </div> |
| #159 | """ |
| #160 | (out_dir / "index.html").write_text(html, encoding="utf-8") |
| #161 | |
| #162 | |
| #163 | def main() -> int: |
| #164 | ap = argparse.ArgumentParser(description="Generate images via OpenAI Images API.") |
| #165 | ap.add_argument("--prompt", help="Single prompt. If omitted, random prompts are generated.") |
| #166 | ap.add_argument("--count", type=int, default=8, help="How many images to generate.") |
| #167 | ap.add_argument("--model", default="gpt-image-1", help="Image model id.") |
| #168 | ap.add_argument("--size", default="", help="Image size (e.g. 1024x1024, 1536x1024). Defaults based on model if not specified.") |
| #169 | ap.add_argument("--quality", default="", help="Image quality (e.g. high, standard). Defaults based on model if not specified.") |
| #170 | ap.add_argument("--background", default="", help="Background transparency (GPT models only): transparent, opaque, or auto.") |
| #171 | ap.add_argument("--output-format", default="", help="Output format (GPT models only): png, jpeg, or webp.") |
| #172 | ap.add_argument("--style", default="", help="Image style (dall-e-3 only): vivid or natural.") |
| #173 | ap.add_argument("--out-dir", default="", help="Output directory (default: ./tmp/openai-image-gen-<ts>).") |
| #174 | args = ap.parse_args() |
| #175 | |
| #176 | api_key = (os.environ.get("OPENAI_API_KEY") or "").strip() |
| #177 | if not api_key: |
| #178 | print("Missing OPENAI_API_KEY", file=sys.stderr) |
| #179 | return 2 |
| #180 | |
| #181 | # Apply model-specific defaults if not specified |
| #182 | default_size, default_quality = get_model_defaults(args.model) |
| #183 | size = args.size or default_size |
| #184 | quality = args.quality or default_quality |
| #185 | |
| #186 | count = args.count |
| #187 | if args.model == "dall-e-3" and count > 1: |
| #188 | print(f"Warning: dall-e-3 only supports generating 1 image at a time. Reducing count from {count} to 1.", file=sys.stderr) |
| #189 | count = 1 |
| #190 | |
| #191 | out_dir = Path(args.out_dir).expanduser() if args.out_dir else default_out_dir() |
| #192 | out_dir.mkdir(parents=True, exist_ok=True) |
| #193 | |
| #194 | prompts = [args.prompt] * count if args.prompt else pick_prompts(count) |
| #195 | |
| #196 | # Determine file extension based on output format |
| #197 | if args.model.startswith("gpt-image") and args.output_format: |
| #198 | file_ext = args.output_format |
| #199 | else: |
| #200 | file_ext = "png" |
| #201 | |
| #202 | items: list[dict] = [] |
| #203 | for idx, prompt in enumerate(prompts, start=1): |
| #204 | print(f"[{idx}/{len(prompts)}] {prompt}") |
| #205 | res = request_images( |
| #206 | api_key, |
| #207 | prompt, |
| #208 | args.model, |
| #209 | size, |
| #210 | quality, |
| #211 | args.background, |
| #212 | args.output_format, |
| #213 | args.style, |
| #214 | ) |
| #215 | data = res.get("data", [{}])[0] |
| #216 | image_b64 = data.get("b64_json") |
| #217 | image_url = data.get("url") |
| #218 | if not image_b64 and not image_url: |
| #219 | raise RuntimeError(f"Unexpected response: {json.dumps(res)[:400]}") |
| #220 | |
| #221 | filename = f"{idx:03d}-{slugify(prompt)[:40]}.{file_ext}" |
| #222 | filepath = out_dir / filename |
| #223 | if image_b64: |
| #224 | filepath.write_bytes(base64.b64decode(image_b64)) |
| #225 | else: |
| #226 | try: |
| #227 | urllib.request.urlretrieve(image_url, filepath) |
| #228 | except urllib.error.URLError as e: |
| #229 | raise RuntimeError(f"Failed to download image from {image_url}: {e}") from e |
| #230 | |
| #231 | items.append({"prompt": prompt, "file": filename}) |
| #232 | |
| #233 | (out_dir / "prompts.json").write_text(json.dumps(items, indent=2), encoding="utf-8") |
| #234 | write_gallery(out_dir, items) |
| #235 | print(f"\nWrote: {(out_dir / 'index.html').as_posix()}") |
| #236 | return 0 |
| #237 | |
| #238 | |
| #239 | if __name__ == "__main__": |
| #240 | raise SystemExit(main()) |
| #241 |