Tie back the Joke Shop curtain bays and add the describe_image vision tool
This commit is contained in:
1 parent
e7d733bc93
commit
e4065c2068
6 files changed
+160
-15
No files matched your search
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ask an OpenRouter vision model to describe an image (free-form).
|
||||
|
||||
Complements the structured judges (`scene_concepts.py realism --judge`,
|
||||
`screen_designs.py judge`): those return scores/verdicts, this one returns an
|
||||
open-ended textual observation for exactly the question asked. Used to review
|
||||
captures and references when the agent harness itself cannot display images.
|
||||
|
||||
Usage:
|
||||
python3 tools/describe_image.py <image> [--question "..."] [--model ...]
|
||||
python3 tools/describe_image.py <image> <image2> [--question "..."] # compare
|
||||
|
||||
Requires OPENROUTER_API_KEY (a gitignored `.env` in the repo root holds it;
|
||||
source with `set -a && . ./.env && set +a`, or the tool reads `.env` directly).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
import openrouter_judge # noqa: E402
|
||||
|
||||
CHAT_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
DEFAULT_QUESTION = (
|
||||
"Describe this image as an art director reviewing a game screenshot: the "
|
||||
"subject, composition, materials, lighting, silhouette, and any obvious "
|
||||
"visual defects or things that read as flat, broken or out of place. Be "
|
||||
"concrete and specific."
|
||||
)
|
||||
|
||||
|
||||
def _load_env_file() -> None:
|
||||
env = ROOT / ".env"
|
||||
if not env.is_file():
|
||||
return
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
os.environ.setdefault(key.strip(), value.strip().strip("\"'"))
|
||||
|
||||
|
||||
def _data_url(path: Path) -> str:
|
||||
return "data:image/png;base64," + base64.b64encode(Path(path).read_bytes()).decode("ascii")
|
||||
|
||||
|
||||
def _call(body: dict, timeout: int = 180) -> dict:
|
||||
key = os.environ.get("OPENROUTER_API_KEY", "").strip()
|
||||
if not key:
|
||||
raise ValueError("OPENROUTER_API_KEY is not set")
|
||||
req = urllib.request.Request(
|
||||
CHAT_URL, data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://git.allucanget.biz/zwitschi/security-game",
|
||||
"X-Title": "Gig Line"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise ValueError(f"OpenRouter HTTP {exc.code}: {exc.read().decode('utf-8', 'replace')[:300]}")
|
||||
|
||||
|
||||
def build_body(model: str, images: list[Path], question: str) -> dict:
|
||||
content: list[dict] = [{"type": "text", "text": question}]
|
||||
for path in images:
|
||||
content.append({"type": "image_url", "image_url": {"url": _data_url(path)}})
|
||||
return {"model": model, "messages": [{"role": "user", "content": content}],
|
||||
"temperature": 0}
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("images", nargs="+", help="image file(s) to send")
|
||||
ap.add_argument("--question", default=DEFAULT_QUESTION,
|
||||
help="question to ask (default: a full art-director description)")
|
||||
ap.add_argument("--model", default="auto",
|
||||
help="explicit model, or 'auto' for the cheapest image-capable model")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
_load_env_file()
|
||||
images = [Path(p) for p in args.images]
|
||||
for path in images:
|
||||
if not path.is_file():
|
||||
print(f"error: not a file: {path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
models = openrouter_judge.resolve_models(args.model)
|
||||
if len(images) > 1:
|
||||
args.question = args.question + (
|
||||
f"\n\nI sent {len(images)} images in order. Compare them and describe the "
|
||||
"differences, especially anything that changed or regressed.")
|
||||
try:
|
||||
model, payload = openrouter_judge.call_with_fallback(
|
||||
models, lambda m: build_body(m, images, args.question), _call)
|
||||
except ValueError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
text = payload["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
text = "unreadable model response"
|
||||
if isinstance(text, list):
|
||||
text = "".join(p.get("text", "") for p in text if isinstance(p, dict))
|
||||
print(f"[model {model}]")
|
||||
print(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user