Add reference-anchored concept generation
Full build / Import + headless tests (push) Successful in 49s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-03 14:59:53 +01:00
1 parent 5b306b1c52
commit e21ff30783
8 files changed
+305 -31

No files matched your search

+109 -16
View File
@@ -21,8 +21,10 @@ import os
import random
import stat
import sys
import re
import urllib.error
import urllib.request
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
@@ -373,19 +375,122 @@ def _api_key() -> str:
return key
CHAT_COMPLETIONS_URL = "https://openrouter.ai/api/v1/chat/completions"
_MIME_BY_SUFFIX = {".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".webp": "image/webp"}
def _openrouter_headers() -> dict:
return {
"Authorization": f"Bearer {_api_key()}",
"Content-Type": "application/json",
"HTTP-Referer": "https://git.allucanget.biz/zwitschi/security-game",
"X-Title": "Gig Line",
}
def _openrouter_post(req: urllib.request.Request) -> dict:
try:
with urllib.request.urlopen(req, timeout=300) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")
sys.exit(f"error: OpenRouter returned HTTP {exc.code}: {detail}")
except urllib.error.URLError as exc:
sys.exit(f"error: could not reach OpenRouter: {exc.reason}")
def _data_url(path) -> str:
"""Encode a local image file as a base64 `data:` URL for multimodal input."""
path = Path(path)
if not path.is_file():
sys.exit(f"error: reference image not found: {path}")
mime = _MIME_BY_SUFFIX.get(path.suffix.lower())
if not mime:
sys.exit(f"error: unsupported reference image type: {path.suffix}")
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:{mime};base64,{encoded}"
def _decode_chat_image(payload: dict) -> Image.Image:
"""Pull the returned image out of a chat-completions response.
Models return either `message.images[].image_url.url` (a `data:` URL or an
https URL) or a `data:` URL embedded in the message content.
"""
message = (payload.get("choices") or [{}])[0].get("message", {})
urls = []
for item in message.get("images") or []:
if isinstance(item, dict):
urls.append(item.get("image_url", {}).get("url", ""))
if not urls:
content = message.get("content") or ""
match = re.search(r"data:image/[a-z]+;base64,[A-Za-z0-9+/=]+", str(content))
if match:
urls.append(match.group(0))
if not urls:
sys.exit(f"error: OpenRouter returned no image data: {payload}")
url = urls[0]
if url.startswith("data:"):
raw = base64.b64decode(url.split(",", 1)[1])
else:
try:
with urllib.request.urlopen(urllib.request.Request(url), timeout=120) as resp:
raw = resp.read()
except urllib.error.URLError as exc:
sys.exit(f"error: could not download returned image: {exc.reason}")
try:
img = Image.open(io.BytesIO(raw))
img.load()
except Exception as exc: # noqa: BLE001 - surface any decode failure clearly
sys.exit(f"error: could not decode returned image: {exc}")
return img
def _generate_image_with_reference(prompt: str, model: str, reference_path) -> Image.Image:
"""Image-to-image request: the reference photo anchors the composition."""
body = {
"model": model,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": _data_url(reference_path)}},
],
}],
}
req = urllib.request.Request(
CHAT_COMPLETIONS_URL,
data=json.dumps(body).encode("utf-8"),
headers=_openrouter_headers(),
method="POST",
)
return _decode_chat_image(_openrouter_post(req))
def generate_image(prompt: str, model: str, size: int,
transparent: bool = False,
aspect_ratio: str = "1:1") -> Image.Image:
aspect_ratio: str = "1:1",
reference_path=None) -> Image.Image:
"""Call the OpenRouter image API and return the decoded PIL image.
Requests a square image at `size` px or a role-shaped image at the matching
resolution tier and decodes `data[].b64_json`. Seedream's square requests
retain the existing GEN_SIZE floor. When `transparent` is true, the API is
asked for a transparent PNG background.
When `reference_path` points at an image file, the request switches to the
multimodal chat endpoint and sends the file as a base64 data URL next to the
prompt (image-to-image guidance). Size/aspect parameters do not apply on
that endpoint; the caller crops or rescales the returned frame as usual.
Only licence-cleared reference photos may be passed (see
`scene_concepts._reference_source`).
"""
size = max(1, int(size))
if model == DEFAULT_IMAGE_MODEL:
if model == DEFAULT_IMAGE_MODEL and reference_path is None:
size = max(GEN_SIZE, size)
if reference_path is not None:
return _generate_image_with_reference(prompt, model, reference_path)
body = {
"model": model,
"prompt": prompt,
@@ -405,22 +510,10 @@ def generate_image(prompt: str, model: str, size: int,
req = urllib.request.Request(
OPENROUTER_IMAGES_URL,
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {_api_key()}",
"Content-Type": "application/json",
"HTTP-Referer": "https://git.allucanget.biz/zwitschi/security-game",
"X-Title": "Gig Line",
},
headers=_openrouter_headers(),
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=300) as resp:
payload = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")
sys.exit(f"error: OpenRouter returned HTTP {exc.code}: {detail}")
except urllib.error.URLError as exc:
sys.exit(f"error: could not reach OpenRouter: {exc.reason}")
payload = _openrouter_post(req)
data = payload.get("data") or []
if not data or not data[0].get("b64_json"):