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)
|