modelbeast/server/operators/openrouter_image/run.py
MODELBEAST 76f065d3a6 A/B image-gen: local FLUX (mflux/MLX) + OpenRouter nano-banana operators
- flux_local: prompt->image fully on-device via mflux (schnell/dev, steps,
  quantize, seed, size). Both FLUX repos are HF-gated as of 2026-07 (schnell
  included) — clean GatedRepoError surfaces with a hint; needs owner HF token.
- openrouter_image: prompt->image via OpenRouter chat/completions with
  modalities [image,text]; parses data-URL images from the response; model
  picker for nano-banana / nano-banana-pro. Gated on OPENROUTER_API_KEY.
- settings: openrouter_key added to vault (masked/redacted/env-injected)
- scripts/install_mflux.sh; venv installed
- A/B flow: run both with the same prompt, judge in Compare mode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:31:19 +10:00

90 lines
2.9 KiB
Python

import argparse
import base64
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
ap = argparse.ArgumentParser()
ap.add_argument("--input", action="append", default=[])
ap.add_argument("--outdir", required=True)
ap.add_argument("--params", default="{}")
a = ap.parse_args()
p = json.loads(a.params)
if not p.get("prompt"):
print("ERROR: prompt is required")
sys.exit(1)
key = os.environ.get("OPENROUTER_API_KEY")
if not key:
print("ERROR: OPENROUTER_API_KEY not set — add it in Settings")
sys.exit(1)
model = p.get("model", "google/gemini-2.5-flash-image")
body = {
"model": model,
"messages": [{"role": "user", "content": p["prompt"]}],
"modalities": ["image", "text"],
}
req = urllib.request.Request(
"https://openrouter.ai/api/v1/chat/completions",
data=json.dumps(body).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
"X-Title": "MODELBEAST"},
method="POST",
)
print(f"calling {model} via OpenRouter ...", flush=True)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=300) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
print(f"ERROR {e.code}: {e.read().decode()[:1500]}")
sys.exit(1)
elapsed = round(time.time() - t0, 1)
outdir = Path(a.outdir)
(outdir / "openrouter_result.json").write_text(json.dumps(
{k: v for k, v in result.items() if k != "choices"} |
{"note": "choices omitted here; images extracted below"}, indent=2))
def data_urls(obj, acc):
"""Collect data:image/... URLs from any nesting (message.images, content parts)."""
if isinstance(obj, dict):
for v in obj.values():
data_urls(v, acc)
elif isinstance(obj, list):
for v in obj:
data_urls(v, acc)
elif isinstance(obj, str) and obj.startswith("data:image/"):
acc.append(obj)
urls: list[str] = []
data_urls(result.get("choices", []), urls)
if not urls:
text = ""
try:
text = result["choices"][0]["message"].get("content") or ""
except (KeyError, IndexError):
pass
print("ERROR: no image in response.", ("Model said: " + str(text)[:800]) if text else "")
print(json.dumps(result, indent=2)[:1500])
sys.exit(1)
outputs = []
for i, u in enumerate(urls):
header, b64 = u.split(",", 1)
ext = ".png" if "png" in header else ".webp" if "webp" in header else ".jpg"
name = f"nano_banana_{i}{ext}" if len(urls) > 1 else f"nano_banana{ext}"
(outdir / name).write_bytes(base64.b64decode(b64))
outputs.append({"path": name, "meta": {"tool": "openrouter", "model": model,
"seconds": elapsed}})
usage = result.get("usage", {})
print(f"done in {elapsed}s — {len(outputs)} image(s); usage: {json.dumps(usage)}", flush=True)
(outdir / "result.json").write_text(json.dumps({"outputs": outputs}))