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