stage-whisper / image_generator.py
Onur Kansoy
Update: Fixed AI Playwright generation logic with Gemma, resolved UI input box freezing, and added dynamic voice assignment for new characters.
39f0786 verified
Raw
History Blame Contribute Delete
3.32 kB
"""
image_generator.py — Generates theatrical scene images via FLUX.2-pro on OpenRouter.
OpenRouter image generation uses the /api/v1/chat/completions endpoint
with modalities: ["image"], returning base64-encoded images.
"""
import os
import base64
import requests
from pathlib import Path
from PIL import Image
import io
_PROMPT_PREFIX = "theatrical stage scene, dramatic lighting, cinematic, "
_MODEL = "black-forest-labs/flux.2-pro"
_OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
class ImageGenerator:
"""Generates 16:9 theatrical scene images via FLUX on OpenRouter."""
def __init__(self, api_key: str):
self.api_key = api_key
if not self.api_key:
print("[ImageGenerator] WARNING: api_key is empty — image generation will fail.")
def generate(self, prompt: str, output_path: str) -> str:
"""
Generate a 16:9 theatrical scene image and save as PNG to output_path.
Returns output_path on success.
Raises RuntimeError on failure.
"""
if not self.api_key:
raise RuntimeError("OpenRouter API key not configured.")
full_prompt = _PROMPT_PREFIX + prompt
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": _MODEL,
"messages": [
{"role": "user", "content": full_prompt}
],
"modalities": ["image"],
"image_config": {
"aspect_ratio": "16:9",
},
}
response = requests.post(_OPENROUTER_URL, headers=headers, json=payload, timeout=120)
if response.status_code != 200:
raise RuntimeError(
f"OpenRouter image API error {response.status_code}: {response.text[:300]}"
)
data = response.json()
# Extract image from response
# OpenRouter returns images in choices[0].message.images or as base64 data URL
image_bytes = None
try:
message = data["choices"][0]["message"]
# Try message.images list first
if message.get("images"):
img_url = message["images"][0]["image_url"]["url"]
# Fall back to message.content as data URL
elif isinstance(message.get("content"), str) and message["content"].startswith("data:image"):
img_url = message["content"]
else:
raise RuntimeError(f"No image found in response: {str(data)[:400]}")
# Decode base64 data URL
if img_url.startswith("data:image"):
header, b64data = img_url.split(",", 1)
image_bytes = base64.b64decode(b64data)
else:
# It's a URL — fetch it
r = requests.get(img_url, timeout=60)
r.raise_for_status()
image_bytes = r.content
except (KeyError, IndexError) as e:
raise RuntimeError(f"Unexpected response format: {e}\n{str(data)[:400]}")
# Save as PNG
img = Image.open(io.BytesIO(image_bytes))
img.save(output_path, format="PNG")
return output_path