lulluna / illustrate.py
mbkv's picture
Initial deployment β€” Lulluna bedtime story weaver
0daff5d
Raw
History Blame Contribute Delete
2.82 kB
"""
Lulluna β€” Illustration Engine (SD Turbo, ~860 M params)
Two usage modes:
1. Direct import β€” called from app.py on HF Spaces (unified env)
2. Subprocess β€” called by app.py on local dev (separate .venv_tts)
python illustrate.py <reads JSON prompt from stdin, writes base64 PNG JSON to stdout>
The diffusers pipeline is cached after first load so repeated illustration
requests within the same process (import mode) don't reload the weights each time.
"""
import sys
import os
import json
import base64
import io
import torch
from diffusers import AutoPipelineForText2Image
from PIL import Image
# Device: MPS on Apple Silicon, CUDA on NVIDIA (HF Spaces GPU), CPU fallback.
DEVICE = (
"mps" if torch.backends.mps.is_available() else
"cuda" if torch.cuda.is_available() else
"cpu"
)
DTYPE = torch.float16 if DEVICE in ("mps", "cuda") else torch.float32
MODEL_ID = "stabilityai/sd-turbo"
STEPS = int(os.getenv("ILLUS_STEPS", "4"))
WIDTH = int(os.getenv("ILLUS_WIDTH", "512"))
HEIGHT = int(os.getenv("ILLUS_HEIGHT", "512"))
STYLE_SUFFIX = (
", children's book illustration, watercolor painting, "
"soft pastel colors, cute, whimsical, storybook art style, "
"warm lighting, no text"
)
# Module-level cache β€” populated on first call, reused for all subsequent calls.
_pipe: "AutoPipelineForText2Image | None" = None
def _get_pipe() -> "AutoPipelineForText2Image":
global _pipe
if _pipe is None:
_pipe = AutoPipelineForText2Image.from_pretrained(
MODEL_ID,
torch_dtype=DTYPE,
variant="fp16" if DTYPE == torch.float16 else None,
)
_pipe.to(DEVICE)
_pipe.set_progress_bar_config(disable=True)
return _pipe
def generate(prompt: str) -> dict:
"""Generate a storybook illustration for *prompt*. Returns {image: base64, mime: image/png}."""
pipe = _get_pipe()
full_prompt = prompt.strip() + STYLE_SUFFIX
image: Image.Image = pipe(
prompt=full_prompt,
num_inference_steps=STEPS,
guidance_scale=0.0, # SD Turbo is distilled β€” no CFG needed
width=WIDTH,
height=HEIGHT,
).images[0]
buf = io.BytesIO()
image.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode()
return {"image": b64, "mime": "image/png"}
if __name__ == "__main__":
# Subprocess mode: read JSON prompt from stdin, write JSON to stdout.
data = json.loads(sys.stdin.read().strip())
prompt = data.get("prompt", "")
if not prompt:
print("ERROR: empty prompt", file=sys.stderr)
sys.exit(1)
try:
result = generate(prompt)
print(json.dumps(result), end="")
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)