Add reference-anchored concept generation
This commit is contained in:
1 parent
5b306b1c52
commit
e21ff30783
8 files changed
+305
-31
No files matched your search
+109
-16
@@ -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"):
|
||||
|
||||
Reference in new issue
Block a user