stage-whisper / image_engine.py
Onur Kansoy
Initial upload
e15ad5a verified
Raw
History Blame Contribute Delete
3.32 kB
"""
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"
# Directory to store generated scene images
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
# Strip data URI prefix
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", {})
# Flux returns images in message.images[] array
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 # External URL, use directly
# Fallback: check content field
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