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 | """ | |
| 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 | |