| """Modal FLUX client for comic-panel image generation. |
| |
| If FLUX_URL is set, POSTs the image prompt to the Modal FLUX endpoint and |
| returns a base64 PNG. Otherwise β or on any failure/timeout β returns None, |
| and the frontend renders the composed chibi-comic fallback instead. Mirrors the |
| warm / cold-timeout pattern in llm.py. |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import threading |
| import time |
|
|
| import requests |
|
|
| from .trace import trace |
|
|
| |
| |
| FLUX_TIMEOUT = float(os.environ.get("BDS_FLUX_TIMEOUT", "30")) |
| FLUX_COLD_TIMEOUT = float(os.environ.get("BDS_FLUX_COLD_TIMEOUT", "90")) |
| _warmed = False |
|
|
|
|
| def flux_available() -> bool: |
| return bool(os.environ.get("FLUX_URL")) |
|
|
|
|
| def _headers() -> dict: |
| return {"Authorization": "Bearer " + os.environ.get("FLUX_TOKEN", "")} |
|
|
|
|
| def warm() -> None: |
| """Boot the FLUX container in the background (page load / new game).""" |
| if not flux_available() or _warmed: |
| return |
| threading.Thread(target=_warm_ping, daemon=True).start() |
|
|
|
|
| def _warm_ping() -> None: |
| try: |
| requests.post(os.environ["FLUX_URL"], json={"warmup": True}, |
| headers=_headers(), timeout=FLUX_COLD_TIMEOUT + 60) |
| trace("flux", "warmup ping done") |
| except requests.RequestException: |
| pass |
|
|
|
|
| def generate_comic(prompt: str) -> str | None: |
| """Return a base64 PNG for the comic, or None β composed fallback.""" |
| if not flux_available() or not prompt: |
| return None |
| global _warmed |
| timeout = FLUX_TIMEOUT if _warmed else FLUX_COLD_TIMEOUT |
| t0 = time.time() |
| try: |
| resp = requests.post(os.environ["FLUX_URL"], json={"prompt": prompt}, |
| headers=_headers(), timeout=timeout) |
| ms = int((time.time() - t0) * 1000) |
| if resp.status_code == 200: |
| body = resp.json() |
| img = body.get("image_b64") |
| if img: |
| _warmed = True |
| trace("flux", f"comic LIVE {ms}ms ({len(img)}b)") |
| return img |
| trace("flux", f"comic not-ok {ms}ms: " |
| f"{str(body.get('error', body))[:120]}") |
| else: |
| trace("flux", f"comic HTTP {resp.status_code} {ms}ms") |
| except requests.Timeout: |
| trace("flux", f"comic TIMEOUT after {timeout}s") |
| except requests.RequestException as exc: |
| trace("flux", f"comic transport error: {str(exc)[:120]}") |
| return None |
|
|