File size: 7,339 Bytes
fd0b9df
 
 
 
 
 
 
53940d1
 
 
 
fd0b9df
53940d1
 
fd0b9df
 
 
53940d1
fd0b9df
 
 
 
 
9e11bdb
1bcd3f8
9e11bdb
 
1bcd3f8
 
 
 
 
 
 
9e11bdb
fd0b9df
 
53940d1
fd0b9df
 
53940d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd0b9df
a2af9f2
 
 
 
 
 
 
 
fd0b9df
53940d1
 
 
 
4b81168
 
 
 
53940d1
4b81168
3051f78
 
 
 
 
 
 
 
4b81168
 
 
 
 
 
fd0b9df
4b81168
fd0b9df
4b81168
fd0b9df
 
9e11bdb
fd0b9df
 
 
 
 
 
 
 
 
 
 
a057332
3311574
 
 
 
 
 
fd0b9df
 
3311574
a057332
fd0b9df
 
8b3b570
 
 
 
 
fd0b9df
3311574
 
 
 
fd0b9df
8b3b570
fd0b9df
 
 
 
 
53940d1
fd0b9df
 
4b81168
53940d1
 
4b81168
 
53940d1
 
 
 
 
 
 
 
 
 
 
940cec7
 
 
 
fd0b9df
 
53940d1
 
 
 
fd0b9df
9e11bdb
 
 
4b81168
53940d1
4b81168
fd0b9df
4b81168
53940d1
4b81168
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import random
import os
import torch

# Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
if not hasattr(torch, "xpu"):
    class _MockXPU:
        is_available    = staticmethod(lambda: False)
        device_count    = staticmethod(lambda: 0)
        empty_cache     = staticmethod(lambda: None)
        manual_seed     = staticmethod(lambda seed: None)
        reset_peak_memory_stats = staticmethod(lambda: None)
        max_memory_allocated    = staticmethod(lambda: 0)
        synchronize     = staticmethod(lambda: None)
    torch.xpu = _MockXPU()

# ---------------------------------------------------------------------------
# Detect HuggingFace Spaces ZeroGPU
# ---------------------------------------------------------------------------

IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
if IS_HF_SPACE:
    import spaces
    import sys

    # Suppress Python 3.10 asyncio GC bug: BaseEventLoop.__del__ crashes with
    # "Invalid file descriptor: -1" when a loop is collected by the GC.
    _orig_unraisable = sys.unraisablehook
    def _unraisable_hook(args):
        if args.exc_type is ValueError and "Invalid file descriptor" in str(args.exc_value):
            return  # silently drop β€” harmless Python 3.10 GC bug
        _orig_unraisable(args)
    sys.unraisablehook = _unraisable_hook

    print("HF Space detected β€” model will load on first GPU call and stay cached in memory.")

# ---------------------------------------------------------------------------
# Emoji β†’ descriptive text maps (fed into FLUX prompt)
# ---------------------------------------------------------------------------

ANIMAL_MAP: dict[str, str] = {
    "🐢": "puppy",        "🐱": "kitten",       "🐰": "bunny",
    "🦊": "baby fox",     "🐼": "baby panda",   "🐨": "baby koala",
    "🦁": "baby lion",    "🐯": "baby tiger",   "🐸": "baby frog",
    "🐧": "baby penguin", "πŸ¦‹": "butterfly",    "πŸ¦„": "unicorn",
}

PLACE_MAP: dict[str, str] = {
    "🌊": "on a sunny beach with ocean waves",
    "πŸ”οΈ": "on a snowy mountain top",
    "🌸": "in a cherry blossom garden",
    "🌈": "under a rainbow",
    "πŸŒ™": "on a glowing crescent moon",
    "⭐": "surrounded by sparkling stars",
    "🌴": "on a tropical island",
    "🏑": "in a cosy cottage garden",
    "🌺": "in a field of tropical flowers",
    "πŸ„": "in an enchanted mushroom forest",
}

# Shared style suffix β€” must stay in sync with STYLE in scripts/generate_dataset.py.
# This exact string appears in every training caption so the LoRA learns it as a trigger.
NUMZOO_STYLE = (
    "kawaii children's book illustration, pastel anime art style, "
    "soft painterly lighting, detailed rich background with warm fairy lights, "
    "cozy magical atmosphere, cute chibi character with big sparkling eyes, "
    "soft pastel color palette, highly detailed scene, no text"
)

# ---------------------------------------------------------------------------
# Prompt builder
# ---------------------------------------------------------------------------

def build_prompt(animals: list[str], places: list[str]) -> str:
    # Use ALL selected animals and places (up to 3 each)
    animal_list = animals[:3] if animals else [random.choice(list(ANIMAL_MAP))]
    place_list  = places[:3]  if places  else [random.choice(list(PLACE_MAP))]

    # Build animal text: "puppy", "puppy and bunny", "puppy, bunny and kitten"
    animal_names = [ANIMAL_MAP.get(a, "bunny") for a in animal_list]
    if len(animal_names) == 1:
        animal_text = animal_names[0]
    elif len(animal_names) == 2:
        animal_text = f"{animal_names[0]} and {animal_names[1]}"
    else:
        animal_text = f"{animal_names[0]}, {animal_names[1]} and {animal_names[2]}"

    # Build place text: "in a garden", "in a garden and under a rainbow", ...
    place_texts = [PLACE_MAP.get(p, "in a magical garden") for p in place_list]
    if len(place_texts) == 1:
        place_text = place_texts[0]
    elif len(place_texts) == 2:
        place_text = f"{place_texts[0]} and {place_texts[1]}"
    else:
        place_text = f"{place_texts[0]}, {place_texts[1]} and {place_texts[2]}"

    return f"A cute {animal_text} {place_text}, {NUMZOO_STYLE}"

# ---------------------------------------------------------------------------
# Pipeline loader (cached globally β€” survives between ZeroGPU calls)
# ---------------------------------------------------------------------------

_pipe = None

def get_pipeline():
    global _pipe
    if _pipe is not None:
        return _pipe

    from diffusers import FluxPipeline

    hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")

    use_mps  = (not IS_HF_SPACE) and (not torch.cuda.is_available()) and torch.backends.mps.is_available()
    # MPS has incomplete bfloat16 support β€” float16 is faster and uses half the memory there.
    dtype = torch.float16 if use_mps else torch.bfloat16

    print(f"Loading FLUX.1-schnell pipeline… (token={'set' if hf_token else 'NOT SET'}, dtype={dtype})")
    _pipe = FluxPipeline.from_pretrained(
        "black-forest-labs/FLUX.1-schnell",
        torch_dtype=dtype,
        token=hf_token,
    )

    if IS_HF_SPACE:
        # ZeroGPU always has CUDA β€” don't rely on torch.cuda.is_available()
        # which can return False outside the @spaces.GPU context
        _pipe = _pipe.to("cuda")
    elif torch.cuda.is_available():
        _pipe = _pipe.to("cuda")
    elif use_mps:
        # Sequential CPU offload avoids MPS OOM: each transformer block is moved
        # to MPS one at a time, then immediately returned to CPU.
        _pipe.enable_sequential_cpu_offload()
    else:
        _pipe = _pipe.to("cpu")

    print("Pipeline ready.")
    return _pipe

# ---------------------------------------------------------------------------
# Core generation (always wrapped in try/except)
# ---------------------------------------------------------------------------

def _generate(animals: list[str], places: list[str]):
    try:
        pipe   = get_pipeline()
        prompt = build_prompt(animals, places)
        print(f"Generating | prompt: {prompt}")

        result = pipe(
            prompt=prompt,
            num_inference_steps=4,
            guidance_scale=0.0,
            height=512,
            width=512,
        )
        return result.images[0], prompt

    except Exception as e:
        import traceback
        print(f"[image_generator] ❌ generation failed: {e}")
        print(traceback.format_exc())
        return None, str(e)


# ---------------------------------------------------------------------------
# Public API β€” two versions depending on environment
# ---------------------------------------------------------------------------

if IS_HF_SPACE:
    # duration=120: covers first-run model download (~30s) + load (~10s) + generate (~5s).
    # On subsequent calls _pipe is already loaded so only ~5s of GPU time is used.
    @spaces.GPU(duration=120)
    def generate_reward_image(animals: list[str], places: list[str]):
        """Generate reward image on HF Spaces ZeroGPU."""
        return _generate(animals, places)
else:
    def generate_reward_image(animals: list[str], places: list[str]):
        """Generate reward image locally."""
        return _generate(animals, places)