- 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>
90 lines
2.9 KiB
Python
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}))
|