Spaces:
Running on Zero
Running on Zero
File size: 2,845 Bytes
75c5414 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | """Step image generator — delegates to the deployed Modal FLUX.2 endpoint."""
from __future__ import annotations
import base64
import logging
from typing import Optional
from src import config
from src.pipeline import Recipe, RecipeStep
log = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _b64(png_bytes: bytes) -> str:
return base64.b64encode(png_bytes).decode()
def _step_prompt(visual: str, cuisine: str, n: int) -> str:
desc = visual.strip() or f"cooking step {n}"
return (
f"Top-down photo of a kitchen pan or plate showing {desc}. "
f"{cuisine} home cooking. Warm natural lighting. "
"Recipe magazine style. Photorealistic. Appetizing."
)
def _dish_prompt(visual: str, cuisine: str) -> str:
desc = visual.strip() or "the finished plated dish, garnished and beautifully presented"
return (
f"Top-down photo of a {desc} on a rustic wooden table. "
f"{cuisine} home cooking. Warm natural lighting. "
"Recipe magazine style. Photorealistic. Appetizing."
)
# ---------------------------------------------------------------------------
# Modal call
# ---------------------------------------------------------------------------
def _call_modal(prompt: str, seed: int = 42) -> Optional[bytes]:
"""Call the deployed Modal FLUX endpoint. Returns PNG bytes or None."""
try:
import modal
cls = modal.Cls.from_name(config.MODAL_APP_NAME, config.MODAL_CLS_NAME)
return cls().render_step.remote(prompt, seed=seed)
except Exception as exc:
log.warning("Modal FLUX call failed: %s", exc)
return None
# ---------------------------------------------------------------------------
# Public function
# ---------------------------------------------------------------------------
def illustrate_recipe(recipe: Recipe) -> Recipe:
"""Generate FLUX images for every step + final dish.
Mutates and returns the same Recipe with image_b64 fields populated
(or left as None when Modal is unavailable).
"""
cuisine = recipe.cuisine or "International"
# Final dish hero image
final_bytes = _call_modal(_dish_prompt(recipe.final_dish_visual, cuisine), seed=0)
if final_bytes:
recipe.final_dish_image_b64 = _b64(final_bytes)
log.info("Generated final dish image.")
# Per-step images (sequential to respect GPU limits on Modal)
for step in recipe.steps:
prompt = _step_prompt(step.visual, cuisine, step.n)
step_bytes = _call_modal(prompt, seed=step.n)
if step_bytes:
step.image_b64 = _b64(step_bytes)
log.info("Generated image for step %d.", step.n)
return recipe
|