| """ |
| Image Engine — Generates scene images via OpenRouter using black-forest-labs/flux.2-pro. |
| Saves images as compressed JPEG files to avoid huge base64 data URIs. |
| """ |
|
|
| import os |
| import re |
| import io |
| import base64 |
| import requests |
| from PIL import Image |
| from dotenv import load_dotenv |
|
|
| load_dotenv() |
|
|
| OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "") |
| MODEL_ID = "black-forest-labs/flux.2-pro" |
| OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" |
|
|
| |
| IMG_DIR = os.path.join(os.path.dirname(__file__), "generated_scenes") |
| os.makedirs(IMG_DIR, exist_ok=True) |
|
|
| _img_counter = 0 |
|
|
|
|
| def _save_as_jpeg(data_uri: str) -> str: |
| """Convert a base64 data URI (PNG) to a compressed JPEG file. Returns file path.""" |
| global _img_counter |
| _img_counter += 1 |
|
|
| |
| b64_data = data_uri.split(",", 1)[1] if "," in data_uri else data_uri |
| raw = base64.b64decode(b64_data) |
|
|
| img = Image.open(io.BytesIO(raw)).convert("RGB") |
| path = os.path.join(IMG_DIR, f"scene_{_img_counter}.jpg") |
| img.save(path, "JPEG", quality=72) |
|
|
| size_kb = os.path.getsize(path) / 1024 |
| print(f"[ImageEngine] Saved JPEG ({size_kb:.0f} KB): {path}") |
| return path |
|
|
|
|
| def generate_scene_image(prompt: str) -> str | None: |
| """ |
| Generate a scene image from a text prompt via OpenRouter chat/completions. |
| Returns a local file path to a JPEG or None on failure. |
| """ |
| headers = { |
| "Authorization": f"Bearer {OPENROUTER_API_KEY}", |
| "Content-Type": "application/json", |
| } |
|
|
| full_prompt = ( |
| f"A cinematic wide-angle theatrical stage scene, dramatic lighting, " |
| f"rich colors, film still quality: {prompt}" |
| ) |
|
|
| payload = { |
| "model": MODEL_ID, |
| "messages": [ |
| {"role": "user", "content": full_prompt} |
| ], |
| "modalities": ["image"], |
| } |
|
|
| try: |
| print(f"[ImageEngine] Generating image: {prompt[:60]}...") |
| resp = requests.post( |
| OPENROUTER_URL, |
| headers=headers, |
| json=payload, |
| timeout=60, |
| ) |
|
|
| if resp.status_code != 200: |
| print(f"[ImageEngine] API Error ({resp.status_code}): {resp.text[:200]}") |
| return None |
|
|
| data = resp.json() |
| message = data.get("choices", [{}])[0].get("message", {}) |
|
|
| |
| images = message.get("images", []) |
| if images: |
| img = images[0] |
| if isinstance(img, dict): |
| url = img.get("image_url", {}).get("url") or img.get("url") |
| if url and url.startswith("data:image"): |
| return _save_as_jpeg(url) |
| elif url: |
| return url |
|
|
| |
| content = message.get("content") |
| if content: |
| if content.startswith("data:image"): |
| return _save_as_jpeg(content) |
| md_match = re.search(r'!\[.*?\]\((.*?)\)', content) |
| if md_match: |
| return md_match.group(1) |
|
|
| print(f"[ImageEngine] Could not extract image from response.") |
| return None |
|
|
| except Exception as e: |
| print(f"[ImageEngine] Error: {e}") |
| return None |
|
|