StoryForge / image_model.py
Claude
refactor: clean dead files, canonical module names, visible error recovery
e83a7fe unverified
Raw
History Blame Contribute Delete
1.92 kB
"""
image_model.py — Image generation with optional character consistency.
Uses Modal GPU (Ghibli-Diffusion + IP-Adapter) when Modal credentials are set,
falls back to local CPU pipeline otherwise.
"""
import io
import os
from functools import lru_cache
MODAL_APP = os.getenv("MODAL_APP_NAME", "storyforge")
_STEPS_LOCAL = 10
_SIZE_LOCAL = 384
def _modal_ready() -> bool:
return bool(os.getenv("MODAL_TOKEN_ID") and os.getenv("MODAL_TOKEN_SECRET"))
_NEGATIVE = (
"border, frame, box, square, panel, grid, letterbox, vignette, "
"text, watermark, signature, logo, blur, dark, ugly, deformed"
)
def generate_image(prompt: str, reference_bytes: bytes = None) -> bytes:
"""Return PNG bytes. reference_bytes enables IP-Adapter character consistency."""
if _modal_ready():
try:
import modal
ImageModel = modal.Cls.from_name(MODAL_APP, "ImageModel")
return ImageModel().generate.remote(prompt, reference_bytes, _NEGATIVE)
except Exception as e:
import traceback
print(f"[image_model] Modal call failed: {e}")
traceback.print_exc()
return None
return _generate_local(prompt)
@lru_cache(maxsize=1)
def _load_local():
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"nitrosocke/Ghibli-Diffusion",
torch_dtype=torch.float32,
safety_checker=None,
requires_safety_checker=False,
).to("cpu")
pipe.set_progress_bar_config(disable=True)
return pipe
def _generate_local(prompt: str) -> bytes:
pipe = _load_local()
result = pipe(
prompt,
num_inference_steps=_STEPS_LOCAL,
guidance_scale=6.0,
height=_SIZE_LOCAL,
width=_SIZE_LOCAL,
)
buf = io.BytesIO()
result.images[0].save(buf, format="PNG")
return buf.getvalue()