Update app.py
Browse files
app.py
CHANGED
|
@@ -2,352 +2,227 @@
|
|
| 2 |
try:
|
| 3 |
import spaces
|
| 4 |
SPACES_AVAILABLE = True
|
| 5 |
-
print("
|
| 6 |
except ImportError:
|
| 7 |
SPACES_AVAILABLE = False
|
| 8 |
-
print("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
import sys
|
| 11 |
import os
|
| 12 |
import re
|
| 13 |
-
import
|
| 14 |
import gc
|
| 15 |
-
import logging
|
| 16 |
import time
|
| 17 |
-
import
|
|
|
|
| 18 |
import threading
|
| 19 |
-
from datetime import datetime
|
| 20 |
|
| 21 |
import torch
|
| 22 |
-
from PIL import Image
|
| 23 |
import gradio as gr
|
| 24 |
from huggingface_hub import hf_hub_download, HfApi, CommitOperationAdd
|
| 25 |
-
from huggingface_hub.utils import HfHubHTTPError
|
| 26 |
|
| 27 |
logging.basicConfig(
|
| 28 |
level=logging.INFO,
|
| 29 |
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 30 |
handlers=[logging.StreamHandler(sys.stdout)],
|
| 31 |
)
|
| 32 |
-
logger = logging.getLogger("
|
| 33 |
-
logger.info("
|
| 34 |
-
|
| 35 |
-
if hasattr(sys.stdout, "reconfigure"):
|
| 36 |
-
sys.stdout.reconfigure(encoding="utf-8")
|
| 37 |
-
if hasattr(sys.stderr, "reconfigure"):
|
| 38 |
-
sys.stderr.reconfigure(encoding="utf-8")
|
| 39 |
|
|
|
|
|
|
|
|
|
|
| 40 |
BACKUP_DATASET_ID = "JustinD/generation-images"
|
| 41 |
-
LAST_GEN_TIME = {}
|
| 42 |
-
COOLDOWN_SECONDS = 12
|
| 43 |
LOG_VISITOR_UPLOADS = os.environ.get("LOG_VISITOR_UPLOADS", "0").strip().lower() in {"1", "true", "yes"}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
# Newer Diffusers Asian NSFW / realism chefs (2025-2026)
|
| 52 |
-
DIFFUSERS_MODELS = {
|
| 53 |
-
"WAI NSFW Illustrious v150": "John6666/wai-nsfw-illustrious-sdxl-v150-sdxl",
|
| 54 |
-
"Janku v5 NSFW Illustrious": "John6666/janku-v5-nsfw-trained-noobai-rou-wei-illustrious-xl-v50-sdxl",
|
| 55 |
-
"YAAI WAI NSFW Illustrious": "John6666/yaai-yet-another-anime-illustrious-model-full-fine-tune-based-on-wai-nsfw-v20-sdxl",
|
| 56 |
-
"PornWorks Real Porn Illustrious": "John6666/porn-works-real-porn-photo-realistic-nsfw-sdxl-pony-illustrious-chekpoint-illustrious-sdxl",
|
| 57 |
-
"Hardcore Asian Cosplay Illustrious": "John6666/hardcore-asian-cosplay-porn-il-v11-sdxl",
|
| 58 |
-
"PornMaster Pro Noob Illustrious": "John6666/pornmaster-pro-noobv15vae-sdxl",
|
| 59 |
-
"PornMaster Anime Illustrious": "John6666/pornmaster-anime-illustrious-v2-sdxl",
|
| 60 |
-
"Pornzilla Hentai Illustrious": "John6666/pornzilla-hentai-v45-sdxl",
|
| 61 |
-
"Reed NSFW Illustrious v3": "John6666/reed-nsfw-illustrious-sdxl-v30-il-sdxl",
|
| 62 |
-
"Unearthly NSFW Illustrious": "John6666/unearthly-nsfw-illustrious-sdxl-20250610b-sdxl",
|
| 63 |
-
"Perfection Asian Illustrious NSFW": "John6666/perfection-asian-ilxl-illustrious-xl-nsfw-sfw-checkpoint-v10-sdxl",
|
| 64 |
-
"Ramthrusts NSFW Illustrious": "John6666/ramthrusts-nsfw-red-shift-mix-v199-sdxl",
|
| 65 |
-
"Lustify NSFW GGWP v7": "John6666/lustify-sdxl-nsfw-checkpoint-ggwp-v7-sdxl",
|
| 66 |
-
"Lustify NSFW Endgame": "John6666/lustify-sdxl-nsfwsfw-endgame-sdxl",
|
| 67 |
-
"Lustify NSFW One Last Time": "John6666/lustify-sdxl-nsfw-checkpoint-olt-one-last-time-sdxl",
|
| 68 |
-
"Spicy Realism NSFW Mix v3": "John6666/spicy-realism-nsfw-mix-v30-sdxl",
|
| 69 |
-
"Fucktastic Real Porn Pony v5.1": "John6666/fucktastic-real-checkpoint-pony-pdxl-porn-realistic-nsfw-sfw-51-sdxl",
|
| 70 |
-
"OhMySister Beautiful Asian Woman": "John6666/oms-oh-my-sister-beautiful-asian-woman-v10-sdxl",
|
| 71 |
-
"Asian E-Girl NSFW XL": "John6666/asian-egirl-xl-realistic-uwu-egirl-tattoo-model-sdxl-nsfw-v10-sdxl",
|
| 72 |
-
}
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# LoRA pantry from known SDXL repos. Fail soft if download/load breaks.
|
| 79 |
-
LORA_NONE = "None"
|
| 80 |
-
LORA_REPOS = {
|
| 81 |
-
LORA_NONE: None,
|
| 82 |
-
"sexy-slider (Soft Domme heat)": ("ntc-ai/SDXL-LoRA-slider.sexy", "sexy.safetensors"),
|
| 83 |
-
"oily-bodystocking": ("Muapi/oily-bodystocking-sdxl-and-pony", "oily-bodystocking-sdxl-and-pony.safetensors"),
|
| 84 |
-
"super-skinny-body": (
|
| 85 |
-
"Muapi/super-skinny-body-for-pony-illustrious-sdxl-and-sd1.5",
|
| 86 |
-
"super-skinny-body-for-pony-illustrious-sdxl-and-sd1.5.safetensors",
|
| 87 |
-
),
|
| 88 |
-
"glass-body": ("Muapi/transparent-glass-body-lora-1.5-sdxl", "transparent-glass-body-lora-1.5-sdxl.safetensors"),
|
| 89 |
}
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
#
|
| 93 |
-
#
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
STACK_FUCKTASTIC_REAL = "Fucktastic × Real Porn"
|
| 108 |
-
STACK_EGIRL_LUSTIFY = "Asian E-Girl × Lustify"
|
| 109 |
-
STACK_QUAD_LUSTIFY_REAL_WAI_FACE = "Quad: Lustify → Real → WAI → Face"
|
| 110 |
-
STACK_QUAD_HARDCORE_LUSTIFY_ILLU_REAL = "Quad: Hardcore → Lustify → Illustrious → Real"
|
| 111 |
-
STACK_QUAD_ANIME_REAL_WAI_LUSTIFY = "Quad: Anime Desire → Real → WAI → Lustify"
|
| 112 |
-
STACK_QUAD_EGIRL_LUSTIFY_WAI_FACE = "Quad: E-Girl → Lustify → WAI → Face"
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def _two(chef_a, chef_b, lora, blend_b):
|
| 116 |
-
return {
|
| 117 |
-
"chef_a": chef_a,
|
| 118 |
-
"chef_b": chef_b,
|
| 119 |
-
"chef_c": None,
|
| 120 |
-
"chef_d": None,
|
| 121 |
-
"lora": lora,
|
| 122 |
-
"blend_b": float(blend_b),
|
| 123 |
-
"blend_c": 0.50,
|
| 124 |
-
"blend_d": 0.45,
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def _quad(chef_a, chef_b, chef_c, chef_d, lora, blend_b, blend_c, blend_d):
|
| 129 |
-
return {
|
| 130 |
-
"chef_a": chef_a,
|
| 131 |
-
"chef_b": chef_b,
|
| 132 |
-
"chef_c": chef_c,
|
| 133 |
-
"chef_d": chef_d,
|
| 134 |
-
"lora": lora,
|
| 135 |
-
"blend_b": float(blend_b),
|
| 136 |
-
"blend_c": float(blend_c),
|
| 137 |
-
"blend_d": float(blend_d),
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
STACKS = {
|
| 142 |
-
STACK_PHOTO_ILLUSTRIOUS: _two(
|
| 143 |
-
"PornWorks Real Porn Illustrious", "WAI NSFW Illustrious v150", LORA_NONE, 0.50
|
| 144 |
-
),
|
| 145 |
-
STACK_HARDCORE_FACE: _two(
|
| 146 |
-
"Hardcore Asian Cosplay Illustrious", "OhMySister Beautiful Asian Woman", LORA_NONE, 0.60
|
| 147 |
-
),
|
| 148 |
-
STACK_ANIME_REAL: _two(
|
| 149 |
-
"PornMaster Anime Illustrious", "PornWorks Real Porn Illustrious", LORA_NONE, 0.55
|
| 150 |
-
),
|
| 151 |
-
STACK_AI_AI_AI: _two(
|
| 152 |
-
"Lustify NSFW GGWP v7", "WAI NSFW Illustrious v150", "sexy-slider (Soft Domme heat)", 0.55
|
| 153 |
-
),
|
| 154 |
-
STACK_SOFT_DOMME: _two(
|
| 155 |
-
"Lustify NSFW GGWP v7", "Lustify NSFW Endgame", "sexy-slider (Soft Domme heat)", 0.62
|
| 156 |
-
),
|
| 157 |
-
STACK_MIRROR_HOTEL: _two(
|
| 158 |
-
"PornWorks Real Porn Illustrious", "WAI NSFW Illustrious v150", LORA_NONE, 0.48
|
| 159 |
-
),
|
| 160 |
-
STACK_NEON_ALLEY: _two(
|
| 161 |
-
"Hardcore Asian Cosplay Illustrious", "Lustify NSFW GGWP v7", LORA_NONE, 0.58
|
| 162 |
-
),
|
| 163 |
-
STACK_LUSTIFY_REAL: _two(
|
| 164 |
-
"Lustify NSFW GGWP v7", "PornWorks Real Porn Illustrious", LORA_NONE, 0.52
|
| 165 |
-
),
|
| 166 |
-
STACK_ANIME_ILLUSTRIOUS: _two(
|
| 167 |
-
"PornMaster Anime Illustrious", "WAI NSFW Illustrious v150", LORA_NONE, 0.55
|
| 168 |
-
),
|
| 169 |
-
STACK_HARDCORE_LUSTIFY: _two(
|
| 170 |
-
"Hardcore Asian Cosplay Illustrious", "Lustify NSFW One Last Time", LORA_NONE, 0.57
|
| 171 |
-
),
|
| 172 |
-
STACK_FACE_ILLUSTRIOUS: _two(
|
| 173 |
-
"OhMySister Beautiful Asian Woman", "WAI NSFW Illustrious v150", LORA_NONE, 0.50
|
| 174 |
-
),
|
| 175 |
-
STACK_SPICY_WAI: _two(
|
| 176 |
-
"Spicy Realism NSFW Mix v3", "WAI NSFW Illustrious v150", LORA_NONE, 0.53
|
| 177 |
-
),
|
| 178 |
-
STACK_FUCKTASTIC_REAL: _two(
|
| 179 |
-
"Fucktastic Real Porn Pony v5.1", "PornWorks Real Porn Illustrious", LORA_NONE, 0.54
|
| 180 |
-
),
|
| 181 |
-
STACK_EGIRL_LUSTIFY: _two(
|
| 182 |
-
"Asian E-Girl NSFW XL", "Lustify NSFW GGWP v7", "sexy-slider (Soft Domme heat)", 0.56
|
| 183 |
-
),
|
| 184 |
-
STACK_QUAD_LUSTIFY_REAL_WAI_FACE: _quad(
|
| 185 |
-
"Lustify NSFW GGWP v7",
|
| 186 |
-
"PornWorks Real Porn Illustrious",
|
| 187 |
-
"WAI NSFW Illustrious v150",
|
| 188 |
-
"OhMySister Beautiful Asian Woman",
|
| 189 |
-
LORA_NONE,
|
| 190 |
-
0.55,
|
| 191 |
-
0.50,
|
| 192 |
-
0.42,
|
| 193 |
-
),
|
| 194 |
-
STACK_QUAD_HARDCORE_LUSTIFY_ILLU_REAL: _quad(
|
| 195 |
-
"Hardcore Asian Cosplay Illustrious",
|
| 196 |
-
"Lustify NSFW GGWP v7",
|
| 197 |
-
"WAI NSFW Illustrious v150",
|
| 198 |
-
"PornWorks Real Porn Illustrious",
|
| 199 |
-
LORA_NONE,
|
| 200 |
-
0.55,
|
| 201 |
-
0.50,
|
| 202 |
-
0.45,
|
| 203 |
-
),
|
| 204 |
-
STACK_QUAD_ANIME_REAL_WAI_LUSTIFY: _quad(
|
| 205 |
-
"PornMaster Anime Illustrious",
|
| 206 |
-
"PornWorks Real Porn Illustrious",
|
| 207 |
-
"WAI NSFW Illustrious v150",
|
| 208 |
-
"Lustify NSFW GGWP v7",
|
| 209 |
-
LORA_NONE,
|
| 210 |
-
0.55,
|
| 211 |
-
0.50,
|
| 212 |
-
0.48,
|
| 213 |
-
),
|
| 214 |
-
STACK_QUAD_EGIRL_LUSTIFY_WAI_FACE: _quad(
|
| 215 |
-
"Asian E-Girl NSFW XL",
|
| 216 |
-
"Lustify NSFW GGWP v7",
|
| 217 |
-
"WAI NSFW Illustrious v150",
|
| 218 |
-
"OhMySister Beautiful Asian Woman",
|
| 219 |
-
"sexy-slider (Soft Domme heat)",
|
| 220 |
-
0.55,
|
| 221 |
-
0.50,
|
| 222 |
-
0.42,
|
| 223 |
-
),
|
| 224 |
}
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
#
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
"prefix":
|
| 234 |
-
|
| 235 |
-
),
|
| 236 |
-
"suffix": (
|
| 237 |
-
"no bra"
|
| 238 |
-
),
|
| 239 |
-
},
|
| 240 |
-
"1": {
|
| 241 |
-
"prefix": (
|
| 242 |
-
""
|
| 243 |
-
),
|
| 244 |
-
"suffix": (
|
| 245 |
-
""
|
| 246 |
-
),
|
| 247 |
-
},
|
| 248 |
-
"2": {
|
| 249 |
-
"prefix": (
|
| 250 |
-
"nice big tits"
|
| 251 |
-
),
|
| 252 |
-
"suffix": (
|
| 253 |
-
"small top"
|
| 254 |
-
),
|
| 255 |
},
|
| 256 |
-
"
|
| 257 |
-
"
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
"suffix": (
|
| 261 |
-
"art, messy art"
|
| 262 |
-
),
|
| 263 |
},
|
| 264 |
-
"
|
| 265 |
-
"
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
"suffix": (
|
| 269 |
-
"no bra"
|
| 270 |
-
),
|
| 271 |
},
|
| 272 |
-
"
|
| 273 |
-
"
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
),
|
| 277 |
-
"suffix": (
|
| 278 |
-
"hard sexy smile"
|
| 279 |
-
),
|
| 280 |
},
|
| 281 |
-
"
|
| 282 |
-
"
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
),
|
| 286 |
-
"suffix": (
|
| 287 |
-
""
|
| 288 |
-
),
|
| 289 |
},
|
| 290 |
-
"
|
| 291 |
-
"
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
),
|
| 295 |
-
"suffix": (
|
| 296 |
-
"turtleneck pulled up over breasts, hard glam filthy under knit, "
|
| 297 |
-
"breasts out under sweater"
|
| 298 |
-
),
|
| 299 |
},
|
| 300 |
}
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
)
|
| 326 |
|
| 327 |
BASE_NEGATIVES = (
|
| 328 |
"child, loli, underage, teen, young teen, barely legal, "
|
| 329 |
"18, eighteen, 19, nineteen, "
|
| 330 |
-
"man, men, male, boy, guy, guys, masculine, "
|
| 331 |
"old, elderly, aged, grandma, wrinkly, "
|
| 332 |
"fat, chubby, obese, overweight, belly fat, thick thighs, thick body, "
|
| 333 |
"beard, mustache, penis, dick, balls, "
|
| 334 |
-
"
|
| 335 |
-
"blurry, lowres, watermark, text, censored, ugly"
|
| 336 |
-
"text"
|
| 337 |
)
|
| 338 |
|
| 339 |
-
# Sex Wildcard heat: adult East/SE Asian women 21+ only, ethnicity rotates per line
|
| 340 |
-
RANDOM_PROMPTS = [
|
| 341 |
-
"art",
|
| 342 |
-
]
|
| 343 |
-
|
| 344 |
FORBIDDEN_KEYWORDS = []
|
| 345 |
if os.path.exists("restricted_words.txt"):
|
| 346 |
try:
|
| 347 |
with open("restricted_words.txt", "r", encoding="utf-8") as f:
|
| 348 |
FORBIDDEN_KEYWORDS = [
|
| 349 |
-
line.strip().lower()
|
| 350 |
-
for line in f
|
| 351 |
if line.strip() and not line.strip().startswith("#")
|
| 352 |
]
|
| 353 |
except Exception as e:
|
|
@@ -360,56 +235,127 @@ AGE_PATTERN = re.compile(
|
|
| 360 |
r"1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19)"
|
| 361 |
r"\s*[-\s]?\s*(year|yr|y\/o|yo)\b"
|
| 362 |
)
|
|
|
|
| 363 |
|
| 364 |
|
| 365 |
def check_prompt_safety(prompt: str):
|
| 366 |
-
clean = prompt.lower().replace("-", " ").replace("_", " ")
|
| 367 |
for word in FORBIDDEN_KEYWORDS:
|
| 368 |
if word in clean:
|
| 369 |
return False, word
|
| 370 |
-
match = AGE_PATTERN.search(clean)
|
| 371 |
if match:
|
| 372 |
return False, match.group(0)
|
| 373 |
return True, None
|
| 374 |
|
| 375 |
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
|
|
|
|
|
|
|
|
|
| 403 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
def should_log_upload(username) -> bool:
|
| 405 |
-
|
| 406 |
-
return False
|
| 407 |
-
return True
|
| 408 |
|
| 409 |
|
| 410 |
def async_log_upload(pil_img, seed, model_name, prompt, username, width, height, steps, cfg_scale):
|
| 411 |
if not should_log_upload(username):
|
| 412 |
-
logger.info("
|
| 413 |
return
|
| 414 |
hf_token = os.environ.get("HF_TOKEN")
|
| 415 |
if not username or not str(username).strip():
|
|
@@ -445,413 +391,183 @@ def async_log_upload(pil_img, seed, model_name, prompt, username, width, height,
|
|
| 445 |
CommitOperationAdd(path_in_repo=f"data/{filename_base}.webp", path_or_fileobj=raw),
|
| 446 |
]
|
| 447 |
api.create_commit(
|
| 448 |
-
repo_id=BACKUP_DATASET_ID,
|
| 449 |
-
repo_type="dataset",
|
| 450 |
-
operations=ops,
|
| 451 |
commit_message=f"gen for {username} @ {timestamp}",
|
| 452 |
)
|
| 453 |
-
logger.info(f"
|
| 454 |
return
|
| 455 |
except Exception as e:
|
| 456 |
logger.error(f"log fail {attempt}: {e}")
|
| 457 |
time.sleep(1.5 * attempt)
|
| 458 |
|
| 459 |
|
|
|
|
|
|
|
|
|
|
| 460 |
_pipe = None
|
| 461 |
-
|
| 462 |
|
| 463 |
|
| 464 |
def _hf_token():
|
| 465 |
return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
|
| 466 |
|
| 467 |
|
| 468 |
-
def
|
| 469 |
-
global _pipe,
|
| 470 |
-
if _pipe is not None:
|
| 471 |
-
try:
|
| 472 |
-
del _pipe
|
| 473 |
-
except Exception:
|
| 474 |
-
pass
|
| 475 |
_pipe = None
|
| 476 |
-
|
| 477 |
gc.collect()
|
| 478 |
if torch.cuda.is_available():
|
| 479 |
torch.cuda.empty_cache()
|
| 480 |
|
| 481 |
|
| 482 |
-
def
|
| 483 |
-
|
| 484 |
-
if not
|
| 485 |
-
return False
|
| 486 |
-
spec = LORA_REPOS.get(lora_name)
|
| 487 |
-
if not spec:
|
| 488 |
-
logger.warning(f"LoRA '{lora_name}' not in LORA_REPOS; skipping")
|
| 489 |
-
return False
|
| 490 |
-
lora_repo, lora_file = spec
|
| 491 |
-
try:
|
| 492 |
-
logger.info(f"Loading LoRA {lora_name} from {lora_repo}/{lora_file}")
|
| 493 |
-
lora_path = hf_hub_download(repo_id=lora_repo, filename=lora_file, token=_hf_token())
|
| 494 |
-
pipe.load_lora_weights(lora_path)
|
| 495 |
-
logger.info(f"✅ LoRA applied: {lora_name}")
|
| 496 |
-
return True
|
| 497 |
-
except Exception as e:
|
| 498 |
-
logger.error(f"⚠️ LoRA load failed for {lora_name}: {e} (continuing chef chain alone)")
|
| 499 |
-
return False
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
def load_sdxl_pipe(model_name: str, lora_name: str = LORA_NONE):
|
| 503 |
-
"""Load one Adult SDXL chef. Unloads previous. Optional LoRA after load."""
|
| 504 |
-
global _pipe, _loaded_key
|
| 505 |
-
key = f"{model_name}::{lora_name}"
|
| 506 |
-
if _loaded_key == key and _pipe is not None:
|
| 507 |
return _pipe
|
|
|
|
| 508 |
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
unload_all()
|
| 512 |
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
| 513 |
-
logger.info(f"Loading
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
use_safetensors=True,
|
| 519 |
-
)
|
| 520 |
-
else:
|
| 521 |
-
repo = MODEL_REPOS.get(model_name, HF_REPO_ID)
|
| 522 |
-
path = hf_hub_download(repo_id=repo, filename=model_name, token=_hf_token())
|
| 523 |
-
pipe = StableDiffusionXLPipeline.from_single_file(
|
| 524 |
-
path,
|
| 525 |
-
torch_dtype=dtype,
|
| 526 |
-
use_safetensors=True,
|
| 527 |
-
)
|
| 528 |
-
if hasattr(pipe, "scheduler"):
|
| 529 |
-
pipe.scheduler = EulerDiscreteScheduler.from_config(
|
| 530 |
-
pipe.scheduler.config,
|
| 531 |
-
timestep_spacing="trailing",
|
| 532 |
-
)
|
| 533 |
if torch.cuda.is_available():
|
| 534 |
pipe.to("cuda")
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
_apply_lora(pipe, lora_name)
|
| 539 |
-
_pipe = pipe
|
| 540 |
-
_loaded_key = key
|
| 541 |
return pipe
|
| 542 |
|
| 543 |
|
| 544 |
-
def
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
|
| 550 |
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
return None
|
| 557 |
-
return s
|
| 558 |
|
| 559 |
|
| 560 |
-
def
|
| 561 |
try:
|
| 562 |
-
|
| 563 |
except (TypeError, ValueError):
|
| 564 |
-
|
| 565 |
-
return
|
| 566 |
|
| 567 |
|
| 568 |
-
def
|
| 569 |
-
"""
|
| 570 |
-
"""
|
| 571 |
-
a = _norm_chef(cfg.get("chef_a"))
|
| 572 |
-
b = _norm_chef(cfg.get("chef_b"))
|
| 573 |
-
c = _norm_chef(cfg.get("chef_c"))
|
| 574 |
-
d = _norm_chef(cfg.get("chef_d"))
|
| 575 |
-
if not a:
|
| 576 |
-
raise ValueError("Make a required")
|
| 577 |
-
chefs = [a]
|
| 578 |
-
blends = []
|
| 579 |
-
if b:
|
| 580 |
-
chefs.append(b)
|
| 581 |
-
blends.append(_clamp_blend(cfg.get("blend_b", 0.55)))
|
| 582 |
-
if c:
|
| 583 |
-
chefs.append(c)
|
| 584 |
-
blends.append(_clamp_blend(cfg.get("blend_c", 0.50)))
|
| 585 |
-
if d:
|
| 586 |
-
chefs.append(d)
|
| 587 |
-
blends.append(_clamp_blend(cfg.get("blend_d", 0.45)))
|
| 588 |
-
lora = cfg.get("lora") or LORA_NONE
|
| 589 |
-
return chefs, blends, lora
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
def _make_img2img(pipe):
|
| 593 |
-
from diffusers import StableDiffusionXLImg2ImgPipeline
|
| 594 |
-
|
| 595 |
-
img2img = StableDiffusionXLImg2ImgPipeline(
|
| 596 |
-
vae=pipe.vae,
|
| 597 |
-
text_encoder=pipe.text_encoder,
|
| 598 |
-
text_encoder_2=pipe.text_encoder_2,
|
| 599 |
-
tokenizer=pipe.tokenizer,
|
| 600 |
-
tokenizer_2=pipe.tokenizer_2,
|
| 601 |
-
unet=pipe.unet,
|
| 602 |
-
scheduler=pipe.scheduler,
|
| 603 |
-
)
|
| 604 |
-
if torch.cuda.is_available():
|
| 605 |
-
img2img = img2img.to("cuda")
|
| 606 |
-
return img2img
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
def run_chef_pipeline(
|
| 610 |
-
stack_label,
|
| 611 |
-
cfg,
|
| 612 |
-
enhanced,
|
| 613 |
-
neg,
|
| 614 |
-
seeds,
|
| 615 |
-
use_steps,
|
| 616 |
-
remix_steps,
|
| 617 |
-
cfg_scale,
|
| 618 |
-
width,
|
| 619 |
-
height,
|
| 620 |
-
device,
|
| 621 |
-
):
|
| 622 |
-
"""
|
| 623 |
-
"""
|
| 624 |
-
chefs, blends, lora = resolve_chef_chain(cfg)
|
| 625 |
-
last_idx = len(chefs) - 1
|
| 626 |
-
lora_tag = lora if lora and lora != LORA_NONE else "no-LoRA"
|
| 627 |
-
|
| 628 |
-
# Pass A: txt2img (never LoRA on A unless A is the only chef, which should not happen)
|
| 629 |
-
lora_a = lora if last_idx == 0 else LORA_NONE
|
| 630 |
-
pipe_a = load_sdxl_pipe(chefs[0], lora_a)
|
| 631 |
-
current = []
|
| 632 |
-
for s in seeds:
|
| 633 |
-
gen = torch.Generator(device=device).manual_seed(int(s))
|
| 634 |
-
out = pipe_a(
|
| 635 |
-
prompt=enhanced,
|
| 636 |
-
negative_prompt=neg,
|
| 637 |
-
num_inference_steps=use_steps,
|
| 638 |
-
guidance_scale=float(cfg_scale),
|
| 639 |
-
width=int(width),
|
| 640 |
-
height=int(height),
|
| 641 |
-
generator=gen,
|
| 642 |
-
).images[0]
|
| 643 |
-
current.append((out, s))
|
| 644 |
-
|
| 645 |
-
letter_names = ["A", "B", "C", "D"]
|
| 646 |
-
chain_bits = [f"A:{chefs[0]}"]
|
| 647 |
-
|
| 648 |
-
# Passes B/C/D: img2img remix, unload between, LoRA only on last chef
|
| 649 |
-
for pass_i, chef_name in enumerate(chefs[1:], start=1):
|
| 650 |
-
unload_all()
|
| 651 |
-
strength = blends[pass_i - 1]
|
| 652 |
-
lora_here = lora if pass_i == last_idx else LORA_NONE
|
| 653 |
-
pipe_n = load_sdxl_pipe(chef_name, lora_here)
|
| 654 |
-
img2img = _make_img2img(pipe_n)
|
| 655 |
-
remixed = []
|
| 656 |
-
seed_offset = 17 * pass_i
|
| 657 |
-
for img, s in current:
|
| 658 |
-
gen = torch.Generator(device=device).manual_seed(int(s) + seed_offset)
|
| 659 |
-
out = img2img(
|
| 660 |
-
image=img,
|
| 661 |
-
prompt=enhanced,
|
| 662 |
-
negative_prompt=neg,
|
| 663 |
-
strength=float(strength),
|
| 664 |
-
num_inference_steps=remix_steps,
|
| 665 |
-
guidance_scale=float(cfg_scale),
|
| 666 |
-
generator=gen,
|
| 667 |
-
).images[0]
|
| 668 |
-
remixed.append((out, s))
|
| 669 |
-
current = remixed
|
| 670 |
-
letter = letter_names[pass_i] if pass_i < len(letter_names) else f"P{pass_i}"
|
| 671 |
-
chain_bits.append(f"{letter}:{chef_name}@{strength:.2f}")
|
| 672 |
-
del img2img
|
| 673 |
-
gc.collect()
|
| 674 |
-
if torch.cuda.is_available():
|
| 675 |
-
torch.cuda.empty_cache()
|
| 676 |
-
|
| 677 |
-
chain = f"{stack_label} | {' → '.join(chain_bits)} | {lora_tag}"
|
| 678 |
-
results = []
|
| 679 |
-
for out, s in current:
|
| 680 |
-
label = f"{stack_label} · seed {s}"
|
| 681 |
-
results.append((out, s, chain, label))
|
| 682 |
-
return results
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
def normalize_selected_stacks(selected):
|
| 686 |
-
if not selected:
|
| 687 |
-
return list(DEFAULT_STACKS)
|
| 688 |
-
if isinstance(selected, str):
|
| 689 |
-
selected = [selected]
|
| 690 |
-
out = [s for s in selected if s in STACKS]
|
| 691 |
-
return out if out else list(DEFAULT_STACKS)
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
def clamp_batch_for_stacks(n_stacks, batch):
|
| 695 |
-
"""Cap stacks × batch to avoid ZeroGPU timeout. Prefer batch=1 when 3+ stacks."""
|
| 696 |
-
batch = int(max(1, min(3, int(batch))))
|
| 697 |
-
warn = ""
|
| 698 |
-
if n_stacks * batch > MAX_STACK_BATCH_PRODUCT:
|
| 699 |
-
if n_stacks >= 3:
|
| 700 |
-
batch = 1
|
| 701 |
-
warn = f"Batch forced to 1 (selected {n_stacks} stacks; stacks×batch cap is {MAX_STACK_BATCH_PRODUCT})."
|
| 702 |
-
else:
|
| 703 |
-
batch = max(1, MAX_STACK_BATCH_PRODUCT // max(1, n_stacks))
|
| 704 |
-
warn = f"Batch reduced to {batch} (selected {n_stacks} stacks; stacks×batch cap is {MAX_STACK_BATCH_PRODUCT})."
|
| 705 |
-
return batch, warn
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
def advanced_cfg_from_ui(chef_a, chef_b, chef_c, chef_d, lora_name, blend_b, blend_c, blend_d):
|
| 709 |
-
return {
|
| 710 |
-
"chef_a": _norm_chef(chef_a) or STACKS[DEFAULT_STACK]["chef_a"],
|
| 711 |
-
"chef_b": _norm_chef(chef_b) or STACKS[DEFAULT_STACK]["chef_b"],
|
| 712 |
-
"chef_c": _norm_chef(chef_c),
|
| 713 |
-
"chef_d": _norm_chef(chef_d),
|
| 714 |
-
"lora": lora_name if lora_name else LORA_NONE,
|
| 715 |
-
"blend_b": _clamp_blend(blend_b, 0.55),
|
| 716 |
-
"blend_c": _clamp_blend(blend_c, 0.50),
|
| 717 |
-
"blend_d": _clamp_blend(blend_d, 0.45),
|
| 718 |
-
}
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
@spaces.GPU(duration=360) if SPACES_AVAILABLE else (lambda f: f)
|
| 722 |
-
def generate(
|
| 723 |
-
prompt,
|
| 724 |
-
styles,
|
| 725 |
-
selected_stacks,
|
| 726 |
-
chef_a,
|
| 727 |
-
chef_b,
|
| 728 |
-
chef_c,
|
| 729 |
-
chef_d,
|
| 730 |
-
lora_name,
|
| 731 |
-
blend_b,
|
| 732 |
-
blend_c,
|
| 733 |
-
blend_d,
|
| 734 |
-
use_advanced,
|
| 735 |
-
steps,
|
| 736 |
-
cfg,
|
| 737 |
-
seed,
|
| 738 |
-
width,
|
| 739 |
-
height,
|
| 740 |
-
batch,
|
| 741 |
-
filth_level,
|
| 742 |
-
username,
|
| 743 |
-
):
|
| 744 |
-
if not prompt or not str(prompt).strip():
|
| 745 |
-
return None, "❌ Prompt missing", "", []
|
| 746 |
-
now = time.time()
|
| 747 |
user_key = username or "anonymous"
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
))]
|
| 764 |
-
n_paths = 1
|
| 765 |
-
else:
|
| 766 |
-
run_jobs = [(name, STACKS[name]) for name in stacks]
|
| 767 |
-
n_paths = len(run_jobs)
|
| 768 |
-
|
| 769 |
-
batch, batch_warn = clamp_batch_for_stacks(n_paths, batch)
|
| 770 |
-
seeds = [
|
| 771 |
-
random.randint(0, 2**31 - 1) if int(seed) < 0 else int(seed) + i
|
| 772 |
-
for i in range(batch)
|
| 773 |
-
]
|
| 774 |
-
enhanced = enhance_prompt(prompt, styles, filth_level)
|
| 775 |
-
neg = BASE_NEGATIVES
|
| 776 |
-
use_steps = int(max(10, min(40, int(steps))))
|
| 777 |
-
remix_steps = max(10, int(use_steps * 0.75))
|
| 778 |
LAST_GEN_TIME[user_key] = now
|
| 779 |
-
gallery = []
|
| 780 |
-
jobs = []
|
| 781 |
-
info_bits = []
|
| 782 |
-
if batch_warn:
|
| 783 |
-
info_bits.append(batch_warn)
|
| 784 |
-
gr.Warning(batch_warn)
|
| 785 |
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 786 |
|
| 787 |
try:
|
| 788 |
-
|
| 789 |
-
results = run_chef_pipeline(
|
| 790 |
-
stack_label=stack_label,
|
| 791 |
-
cfg=stack_cfg,
|
| 792 |
-
enhanced=enhanced,
|
| 793 |
-
neg=neg,
|
| 794 |
-
seeds=seeds,
|
| 795 |
-
use_steps=use_steps,
|
| 796 |
-
remix_steps=remix_steps,
|
| 797 |
-
cfg_scale=cfg,
|
| 798 |
-
width=width,
|
| 799 |
-
height=height,
|
| 800 |
-
device=device,
|
| 801 |
-
)
|
| 802 |
-
for out, s, chain, label in results:
|
| 803 |
-
gallery.append((out, label))
|
| 804 |
-
jobs.append((out, s, chain, prompt, username, width, height, use_steps, cfg))
|
| 805 |
-
info_bits.append(chain)
|
| 806 |
-
unload_all()
|
| 807 |
-
|
| 808 |
-
info = (
|
| 809 |
-
f"{' || '.join(info_bits)} | seeds {seeds} | {width}x{height} | "
|
| 810 |
-
f"steps A={use_steps} remix={remix_steps} | filth={filth_level} | "
|
| 811 |
-
f"paths={n_paths} batch={batch} | enhanced prompt ready"
|
| 812 |
-
)
|
| 813 |
-
return gallery, info, enhanced, jobs
|
| 814 |
except Exception as e:
|
| 815 |
-
logger.exception("
|
| 816 |
msg = str(e)
|
| 817 |
if "gated" in msg.lower() or "401" in msg or "403" in msg:
|
| 818 |
-
msg += "
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
gr.update(),
|
| 835 |
-
)
|
| 836 |
-
cfg = STACKS[stacks[0]]
|
| 837 |
return (
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
gr.update(value=
|
| 845 |
-
gr.update(value=cfg["blend_d"]),
|
| 846 |
)
|
| 847 |
|
| 848 |
|
| 849 |
-
def
|
| 850 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
|
| 853 |
-
|
| 854 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
|
| 856 |
|
| 857 |
js_visitor = """
|
|
@@ -865,177 +581,65 @@ function() {
|
|
| 865 |
}
|
| 866 |
"""
|
| 867 |
|
| 868 |
-
css = """
|
| 869 |
-
.gradio-container, html, body { background:#0b0b19 !important; color:#f3f4f6 !important; }
|
| 870 |
-
.block { background:#111827 !important; border:1px solid #1f2937 !important; border-radius:8px !important; }
|
| 871 |
-
"""
|
| 872 |
-
|
| 873 |
-
THEME = gr.themes.Default(primary_hue="purple", secondary_hue="indigo", neutral_hue="slate")
|
| 874 |
-
|
| 875 |
|
| 876 |
def create_ui():
|
| 877 |
-
|
| 878 |
-
with gr.Blocks(title="JustinD/1 · 21+ Fiction Lab") as demo:
|
| 879 |
-
user_id = gr.Textbox(visible=False)
|
| 880 |
gr.Markdown(
|
| 881 |
-
"
|
|
|
|
|
|
|
|
|
|
| 882 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
with gr.Row():
|
| 884 |
-
with gr.Column(scale=
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
)
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
)
|
|
|
|
|
|
|
| 901 |
with gr.Row():
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
"When override is **off**, each selected stack uses its own chefs. "
|
| 919 |
-
"When **on**, one custom path uses the Advanced slots below (C/D can be skip)."
|
| 920 |
-
)
|
| 921 |
-
chef_a = gr.Dropdown(
|
| 922 |
-
label="Chef A (txt2img base)",
|
| 923 |
-
choices=SDXL_CHOICES,
|
| 924 |
-
value=default_cfg["chef_a"],
|
| 925 |
-
)
|
| 926 |
-
chef_b = gr.Dropdown(
|
| 927 |
-
label="Chef B (img2img remix)",
|
| 928 |
-
choices=SDXL_CHOICES,
|
| 929 |
-
value=default_cfg["chef_b"],
|
| 930 |
-
)
|
| 931 |
-
chef_c = gr.Dropdown(
|
| 932 |
-
label="Chef C (optional img2img)",
|
| 933 |
-
choices=CHEF_OPTIONAL_CHOICES,
|
| 934 |
-
value=SKIP_CHEF,
|
| 935 |
-
)
|
| 936 |
-
chef_d = gr.Dropdown(
|
| 937 |
-
label="Chef D (optional img2img)",
|
| 938 |
-
choices=CHEF_OPTIONAL_CHOICES,
|
| 939 |
-
value=SKIP_CHEF,
|
| 940 |
-
)
|
| 941 |
-
lora_dd = gr.Dropdown(
|
| 942 |
-
label="LoRA on last chef that runs (None = blend only)",
|
| 943 |
-
choices=LORA_CHOICES,
|
| 944 |
-
value=default_cfg["lora"],
|
| 945 |
-
)
|
| 946 |
-
blend_b_sl = gr.Slider(
|
| 947 |
-
label="Blend B strength (higher = more Chef B)",
|
| 948 |
-
minimum=0.30,
|
| 949 |
-
maximum=0.70,
|
| 950 |
-
value=default_cfg["blend_b"],
|
| 951 |
-
step=0.05,
|
| 952 |
-
)
|
| 953 |
-
blend_c_sl = gr.Slider(
|
| 954 |
-
label="Blend C strength (if Chef C set)",
|
| 955 |
-
minimum=0.30,
|
| 956 |
-
maximum=0.70,
|
| 957 |
-
value=default_cfg["blend_c"],
|
| 958 |
-
step=0.05,
|
| 959 |
-
)
|
| 960 |
-
blend_d_sl = gr.Slider(
|
| 961 |
-
label="Blend D strength (if Chef D set)",
|
| 962 |
-
minimum=0.30,
|
| 963 |
-
maximum=0.70,
|
| 964 |
-
value=default_cfg["blend_d"],
|
| 965 |
-
step=0.05,
|
| 966 |
-
)
|
| 967 |
-
with gr.Row():
|
| 968 |
-
seed = gr.Number(label="Seed (-1 = random / lucky)", value=-1, precision=0)
|
| 969 |
-
batch = gr.Slider(
|
| 970 |
-
1,
|
| 971 |
-
3,
|
| 972 |
-
value=1,
|
| 973 |
-
step=1,
|
| 974 |
-
label="Batch (1 safe · stacks×batch capped at 6 · 3+ stacks force batch=1)",
|
| 975 |
-
)
|
| 976 |
-
with gr.Row():
|
| 977 |
-
width = gr.Slider(512, 1536, value=1024, step=64, label="Width")
|
| 978 |
-
height = gr.Slider(512, 1536, value=1024, step=64, label="Height")
|
| 979 |
-
with gr.Row():
|
| 980 |
-
steps = gr.Slider(10, 40, value=28, step=1, label="Steps")
|
| 981 |
-
cfg = gr.Slider(1.0, 12.0, value=6.0, step=0.5, label="CFG")
|
| 982 |
-
with gr.Column(scale=6):
|
| 983 |
-
gallery = gr.Gallery(
|
| 984 |
-
label="Premium outputs",
|
| 985 |
-
columns=1,
|
| 986 |
-
rows=1,
|
| 987 |
-
height=640,
|
| 988 |
-
object_fit="contain",
|
| 989 |
-
preview=True,
|
| 990 |
-
)
|
| 991 |
-
info = gr.Textbox(label="Run info", lines=3)
|
| 992 |
-
meta = gr.Textbox(label="Enhanced prompt (auto)", lines=4)
|
| 993 |
-
|
| 994 |
-
demo.load(fn=None, inputs=None, outputs=user_id, js=js_visitor)
|
| 995 |
-
|
| 996 |
-
stacks.change(
|
| 997 |
-
fn=on_stacks_change,
|
| 998 |
-
inputs=[stacks],
|
| 999 |
-
outputs=[chef_a, chef_b, chef_c, chef_d, lora_dd, blend_b_sl, blend_c_sl, blend_d_sl],
|
| 1000 |
)
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
if jobs:
|
| 1007 |
-
for job in jobs:
|
| 1008 |
-
threading.Thread(target=async_log_upload, args=job, daemon=True).start()
|
| 1009 |
-
if images is None:
|
| 1010 |
-
return None, run_info, enhanced or ""
|
| 1011 |
-
return images, run_info, enhanced
|
| 1012 |
-
|
| 1013 |
-
go.click(
|
| 1014 |
-
fn=on_go,
|
| 1015 |
-
inputs=[
|
| 1016 |
-
prompt,
|
| 1017 |
-
styles,
|
| 1018 |
-
stacks,
|
| 1019 |
-
chef_a,
|
| 1020 |
-
chef_b,
|
| 1021 |
-
chef_c,
|
| 1022 |
-
chef_d,
|
| 1023 |
-
lora_dd,
|
| 1024 |
-
blend_b_sl,
|
| 1025 |
-
blend_c_sl,
|
| 1026 |
-
blend_d_sl,
|
| 1027 |
-
use_advanced,
|
| 1028 |
-
steps,
|
| 1029 |
-
cfg,
|
| 1030 |
-
seed,
|
| 1031 |
-
width,
|
| 1032 |
-
height,
|
| 1033 |
-
batch,
|
| 1034 |
-
filth,
|
| 1035 |
-
user_id,
|
| 1036 |
-
],
|
| 1037 |
-
outputs=[gallery, info, meta],
|
| 1038 |
)
|
|
|
|
| 1039 |
return demo
|
| 1040 |
|
| 1041 |
|
|
@@ -1043,4 +647,4 @@ if __name__ == "__main__":
|
|
| 1043 |
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 1044 |
app = create_ui()
|
| 1045 |
app.queue(max_size=12)
|
| 1046 |
-
app.launch(ssr_mode=False
|
|
|
|
| 2 |
try:
|
| 3 |
import spaces
|
| 4 |
SPACES_AVAILABLE = True
|
| 5 |
+
print("ZeroGPU mode enabled")
|
| 6 |
except ImportError:
|
| 7 |
SPACES_AVAILABLE = False
|
| 8 |
+
print("Running without Spaces (no ZeroGPU)")
|
| 9 |
+
|
| 10 |
+
"""
|
| 11 |
+
Roulette Lab for JustinD/1.
|
| 12 |
+
|
| 13 |
+
One button rolls a random recipe (chef, style, scene, outfit, pose, light,
|
| 14 |
+
camera, woman). Lock any field and the next roll only spins the rest.
|
| 15 |
+
Optional "Tease then reveal" renders the same seed twice.
|
| 16 |
+
Fiction only. Adult women 21+ (late 20s) only.
|
| 17 |
+
"""
|
| 18 |
|
| 19 |
import sys
|
| 20 |
import os
|
| 21 |
import re
|
| 22 |
+
import io
|
| 23 |
import gc
|
|
|
|
| 24 |
import time
|
| 25 |
+
import random
|
| 26 |
+
import logging
|
| 27 |
import threading
|
|
|
|
| 28 |
|
| 29 |
import torch
|
|
|
|
| 30 |
import gradio as gr
|
| 31 |
from huggingface_hub import hf_hub_download, HfApi, CommitOperationAdd
|
|
|
|
| 32 |
|
| 33 |
logging.basicConfig(
|
| 34 |
level=logging.INFO,
|
| 35 |
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 36 |
handlers=[logging.StreamHandler(sys.stdout)],
|
| 37 |
)
|
| 38 |
+
logger = logging.getLogger("RouletteLab")
|
| 39 |
+
logger.info("Roulette Lab boot (21+ adult women only, fiction only)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Settings
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
BACKUP_DATASET_ID = "JustinD/generation-images"
|
|
|
|
|
|
|
| 45 |
LOG_VISITOR_UPLOADS = os.environ.get("LOG_VISITOR_UPLOADS", "0").strip().lower() in {"1", "true", "yes"}
|
| 46 |
+
COOLDOWN_SECONDS = 12
|
| 47 |
+
LAST_GEN_TIME = {}
|
| 48 |
+
HISTORY_SIZE = 10
|
| 49 |
+
GALLERY_SIZE = 12
|
| 50 |
+
|
| 51 |
+
DEFAULT_STEPS = 28
|
| 52 |
+
DEFAULT_GUIDANCE = 6.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
SIZE_PRESETS = {
|
| 55 |
+
"Portrait 832x1216": (832, 1216),
|
| 56 |
+
"Landscape 1344x768": (1344, 768),
|
| 57 |
+
"Square 1024x1024": (1024, 1024),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
+
|
| 60 |
+
# ---------------------------------------------------------------------------
|
| 61 |
+
# Chefs: diffusers SDXL repos, tagged by what they draw best
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
CHEFS = {
|
| 64 |
+
# photoreal
|
| 65 |
+
"Lustify GGWP v7": {"repo": "John6666/lustify-sdxl-nsfw-checkpoint-ggwp-v7-sdxl", "kind": "photo"},
|
| 66 |
+
"Lustify Endgame": {"repo": "John6666/lustify-sdxl-nsfwsfw-endgame-sdxl", "kind": "photo"},
|
| 67 |
+
"URPM XL v6": {"repo": "John6666/uber-realistic-porn-merge-xl-urpmxl-v6final-sdxl", "kind": "photo"},
|
| 68 |
+
"PornWorks Sexy Beauty v04": {"repo": "John6666/pornworks-sexy-beauty-v04-sdxl", "kind": "photo"},
|
| 69 |
+
"OhMySister Asian Woman": {"repo": "John6666/oms-oh-my-sister-beautiful-asian-woman-v10-sdxl", "kind": "photo"},
|
| 70 |
+
# anime / illustration
|
| 71 |
+
"WAI NSFW Illustrious v150": {"repo": "John6666/wai-nsfw-illustrious-sdxl-v150-sdxl", "kind": "anime"},
|
| 72 |
+
"PornMaster Anime Illustrious": {"repo": "John6666/pornmaster-anime-illustrious-v2-sdxl", "kind": "anime"},
|
| 73 |
+
"Perfection Asian Illustrious": {"repo": "John6666/perfection-asian-ilxl-illustrious-xl-nsfw-sfw-checkpoint-v10-sdxl", "kind": "anime"},
|
| 74 |
+
"Janku v5 Illustrious": {"repo": "John6666/janku-v5-nsfw-trained-noobai-rou-wei-illustrious-xl-v50-sdxl", "kind": "anime"},
|
| 75 |
+
"Unearthly NSFW Illustrious": {"repo": "John6666/unearthly-nsfw-illustrious-sdxl-20250610b-sdxl", "kind": "anime"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
+
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
# Decks
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
# Each style says which chef kinds suit it, so an unlocked chef is drawn from a matching pool.
|
| 82 |
+
STYLES = {
|
| 83 |
+
"Photoreal": {
|
| 84 |
+
"kinds": ["photo"],
|
| 85 |
+
"prefix": "photorealistic photo, RAW photo, 85mm lens, natural skin texture, sharp focus",
|
| 86 |
+
"neg": "anime, cartoon, illustration, drawing, painting, 3d render",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
},
|
| 88 |
+
"90s anime cel": {
|
| 89 |
+
"kinds": ["anime"],
|
| 90 |
+
"prefix": "retro 90s anime cel shading, vintage anime key visual, airbrushed highlights, film grain, glossy reflective surfaces",
|
| 91 |
+
"neg": "photo, realistic, 3d render",
|
|
|
|
|
|
|
|
|
|
| 92 |
},
|
| 93 |
+
"Painted PVC figure": {
|
| 94 |
+
"kinds": ["anime"],
|
| 95 |
+
"prefix": "anime figurine, painted PVC 1/7 scale statue, smooth matte skin finish, round display base, product photography, soft studio light",
|
| 96 |
+
"neg": "flat illustration, sketch, real skin pores",
|
|
|
|
|
|
|
|
|
|
| 97 |
},
|
| 98 |
+
"Pop art engraving": {
|
| 99 |
+
"kinds": ["anime", "photo"],
|
| 100 |
+
"prefix": "pop art, Victorian engraving linework, crosshatching, halftone dots, neon pink outline, flat magenta cyan and orange colors",
|
| 101 |
+
"neg": "photo, soft gradient, 3d render",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
},
|
| 103 |
+
"Flat vector pin up": {
|
| 104 |
+
"kinds": ["anime"],
|
| 105 |
+
"prefix": "flat vector illustration, modern pin up, clean linework, flat color blocks with one soft shade, muted dark background",
|
| 106 |
+
"neg": "photo, realistic, 3d render, heavy texture",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
},
|
| 108 |
+
"Oil painting impasto": {
|
| 109 |
+
"kinds": ["anime", "photo"],
|
| 110 |
+
"prefix": "oil painting, thick impasto brushstrokes, visible canvas texture, rich chiaroscuro, classical figure painting",
|
| 111 |
+
"neg": "photo, flat vector, cel shading",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
},
|
| 113 |
}
|
| 114 |
|
| 115 |
+
SCENES = [
|
| 116 |
+
"coffee bar counter with espresso machine",
|
| 117 |
+
"office desk with laptop and city view",
|
| 118 |
+
"kitchen counter with morning sunlight",
|
| 119 |
+
"hotel room window with night city skyline",
|
| 120 |
+
"rooftop pool at dusk",
|
| 121 |
+
"library leather couch with dark wood shelves",
|
| 122 |
+
"neon lit rainy alley",
|
| 123 |
+
"steamy outdoor onsen with stone and pine",
|
| 124 |
+
"artist studio with canvases and paint jars",
|
| 125 |
+
"recording studio with mixing desk",
|
| 126 |
+
"velvet daybed in a dim lounge",
|
| 127 |
+
"bedroom with white sheets and linen curtains",
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
OUTFITS = [
|
| 131 |
+
"black lace lingerie set",
|
| 132 |
+
"unbuttoned oversized white shirt, nothing underneath",
|
| 133 |
+
"open silk robe",
|
| 134 |
+
"black latex corset and sheer stockings",
|
| 135 |
+
"sheer slip dress",
|
| 136 |
+
"white towel slipping",
|
| 137 |
+
"cropped knit sweater and panties",
|
| 138 |
+
"string bikini",
|
| 139 |
+
"black blazer with nothing underneath",
|
| 140 |
+
"topless, denim shorts",
|
| 141 |
+
"fully nude",
|
| 142 |
+
"fully nude, gold body chain",
|
| 143 |
+
]
|
| 144 |
+
|
| 145 |
+
POSES = [
|
| 146 |
+
"caught mid task, glancing at the camera",
|
| 147 |
+
"leaning forward on the counter",
|
| 148 |
+
"kneeling on the bed",
|
| 149 |
+
"sitting on the edge of a desk, legs crossed",
|
| 150 |
+
"stretching arms overhead",
|
| 151 |
+
"lying on her stomach, looking back over her shoulder",
|
| 152 |
+
"standing contrapposto, hand on hip",
|
| 153 |
+
"reclining, propped on one elbow",
|
| 154 |
+
]
|
| 155 |
|
| 156 |
+
LIGHTS = [
|
| 157 |
+
"golden hour window light",
|
| 158 |
+
"pink and blue neon glow",
|
| 159 |
+
"soft overcast window light",
|
| 160 |
+
"warm candlelight",
|
| 161 |
+
"studio softbox with rim light",
|
| 162 |
+
"moonlight through blinds",
|
| 163 |
+
"direct on-camera flash",
|
| 164 |
+
"warm lamp light on her collarbone",
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
CAMERAS = [
|
| 168 |
+
"full body wide shot",
|
| 169 |
+
"three quarter shot",
|
| 170 |
+
"low angle full body",
|
| 171 |
+
"eye level 50mm",
|
| 172 |
+
"high angle looking down",
|
| 173 |
+
"mirror reflection shot",
|
| 174 |
+
"cowboy shot",
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
# Face randomizer (body lock never changes)
|
| 178 |
+
ETHNICITIES = ["Japanese", "Korean", "Chinese", "Taiwanese", "Vietnamese", "Thai", "Filipino", "Indonesian", "Malaysian"]
|
| 179 |
+
HAIRS = [
|
| 180 |
+
"long straight black hair", "messy bun", "chin length bob", "high ponytail",
|
| 181 |
+
"long wavy dark brown hair", "twin braids", "wet slicked back hair", "curtain bangs, shoulder length hair",
|
| 182 |
+
]
|
| 183 |
+
FACES = [
|
| 184 |
+
"soft almond eyes, full lips", "sharp cheekbones, confident smirk", "round face, playful smile",
|
| 185 |
+
"monolid eyes, glossy lips", "heavy lashes, sultry stare", "freckles, bright eyes",
|
| 186 |
+
"beauty mark under one eye", "strong brows, calm gaze",
|
| 187 |
+
]
|
| 188 |
+
DEFAULT_FACE = "adult Japanese woman late 20s, long straight black hair, soft almond eyes, full lips"
|
| 189 |
+
|
| 190 |
+
# Tease then reveal: frame 1 states
|
| 191 |
+
TEASE_STATES = [
|
| 192 |
+
"shirt pulled halfway up, underboob showing",
|
| 193 |
+
"bra strap slipping off one shoulder",
|
| 194 |
+
"silk robe falling open",
|
| 195 |
+
"towel slipping down",
|
| 196 |
+
]
|
| 197 |
+
|
| 198 |
+
FIELDS = ["chef", "style", "scene", "outfit", "pose", "light", "camera", "woman"]
|
| 199 |
+
|
| 200 |
+
# ---------------------------------------------------------------------------
|
| 201 |
+
# Hard locks
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
ADULT_TAGS = "masterpiece, best quality, 1girl, solo, adult woman 21+, late 20s, fictional character"
|
| 204 |
+
BODY_LOCK = (
|
| 205 |
+
"tall, slim body, small frame, tiny waist, "
|
| 206 |
+
"large breasts, full breasts, heavy breasts, cleavage, slender thighs"
|
| 207 |
)
|
| 208 |
|
| 209 |
BASE_NEGATIVES = (
|
| 210 |
"child, loli, underage, teen, young teen, barely legal, "
|
| 211 |
"18, eighteen, 19, nineteen, "
|
| 212 |
+
"man, men, male, boy, guy, guys, male focus, masculine, "
|
| 213 |
"old, elderly, aged, grandma, wrinkly, "
|
| 214 |
"fat, chubby, obese, overweight, belly fat, thick thighs, thick body, "
|
| 215 |
"beard, mustache, penis, dick, balls, "
|
| 216 |
+
"bad hands, extra fingers, deformed, "
|
| 217 |
+
"blurry, lowres, watermark, text, censored, ugly, celebrity, real person"
|
|
|
|
| 218 |
)
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
FORBIDDEN_KEYWORDS = []
|
| 221 |
if os.path.exists("restricted_words.txt"):
|
| 222 |
try:
|
| 223 |
with open("restricted_words.txt", "r", encoding="utf-8") as f:
|
| 224 |
FORBIDDEN_KEYWORDS = [
|
| 225 |
+
line.strip().lower() for line in f
|
|
|
|
| 226 |
if line.strip() and not line.strip().startswith("#")
|
| 227 |
]
|
| 228 |
except Exception as e:
|
|
|
|
| 235 |
r"1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19)"
|
| 236 |
r"\s*[-\s]?\s*(year|yr|y\/o|yo)\b"
|
| 237 |
)
|
| 238 |
+
MINOR_WORDS = re.compile(r"\b(child|kid|loli|teen|teenage|underage|minor|schoolgirl|young girl|little girl)\b")
|
| 239 |
|
| 240 |
|
| 241 |
def check_prompt_safety(prompt: str):
|
| 242 |
+
clean = str(prompt).lower().replace("-", " ").replace("_", " ")
|
| 243 |
for word in FORBIDDEN_KEYWORDS:
|
| 244 |
if word in clean:
|
| 245 |
return False, word
|
| 246 |
+
match = AGE_PATTERN.search(clean) or MINOR_WORDS.search(clean)
|
| 247 |
if match:
|
| 248 |
return False, match.group(0)
|
| 249 |
return True, None
|
| 250 |
|
| 251 |
|
| 252 |
+
# ---------------------------------------------------------------------------
|
| 253 |
+
# Pure recipe logic (no GPU)
|
| 254 |
+
# ---------------------------------------------------------------------------
|
| 255 |
+
def random_woman(rng=random):
|
| 256 |
+
return (
|
| 257 |
+
f"adult {rng.choice(ETHNICITIES)} woman late 20s, "
|
| 258 |
+
f"{rng.choice(HAIRS)}, {rng.choice(FACES)}"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def chef_pool_for_style(style_name):
|
| 263 |
+
kinds = STYLES.get(style_name, STYLES["Photoreal"])["kinds"]
|
| 264 |
+
return [name for name, spec in CHEFS.items() if spec["kind"] in kinds]
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def roll_recipe(prev=None, locks=None, new_woman=True, rng=random):
|
| 268 |
+
"""Slot machine roll. Locked fields keep prev values; the rest spin.
|
| 269 |
+
|
| 270 |
+
The chef is drawn from the pool that suits the (possibly new) style,
|
| 271 |
+
unless the chef field is locked.
|
| 272 |
+
"""
|
| 273 |
+
prev = dict(prev or {})
|
| 274 |
+
locks = locks or {}
|
| 275 |
+
|
| 276 |
+
def keep(field):
|
| 277 |
+
return bool(locks.get(field)) and prev.get(field)
|
| 278 |
+
|
| 279 |
+
recipe = {}
|
| 280 |
+
recipe["style"] = prev["style"] if keep("style") else rng.choice(list(STYLES))
|
| 281 |
+
if keep("chef"):
|
| 282 |
+
recipe["chef"] = prev["chef"]
|
| 283 |
+
else:
|
| 284 |
+
recipe["chef"] = rng.choice(chef_pool_for_style(recipe["style"]))
|
| 285 |
+
recipe["scene"] = prev["scene"] if keep("scene") else rng.choice(SCENES)
|
| 286 |
+
recipe["outfit"] = prev["outfit"] if keep("outfit") else rng.choice(OUTFITS)
|
| 287 |
+
recipe["pose"] = prev["pose"] if keep("pose") else rng.choice(POSES)
|
| 288 |
+
recipe["light"] = prev["light"] if keep("light") else rng.choice(LIGHTS)
|
| 289 |
+
recipe["camera"] = prev["camera"] if keep("camera") else rng.choice(CAMERAS)
|
| 290 |
+
if keep("woman"):
|
| 291 |
+
recipe["woman"] = prev["woman"]
|
| 292 |
+
elif new_woman:
|
| 293 |
+
recipe["woman"] = random_woman(rng)
|
| 294 |
+
else:
|
| 295 |
+
recipe["woman"] = prev.get("woman") or DEFAULT_FACE
|
| 296 |
+
recipe["tease"] = rng.choice(TEASE_STATES)
|
| 297 |
+
return recipe
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _join(*parts):
|
| 301 |
+
return ", ".join(p.strip(" ,") for p in parts if p and str(p).strip(" ,"))
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def build_frames(recipe, tease_reveal=False, extra=""):
|
| 305 |
+
"""Return list of (label, positive, negative). Adult tags + body lock always present."""
|
| 306 |
+
style = STYLES.get(recipe.get("style"), STYLES["Photoreal"])
|
| 307 |
+
negative = _join(BASE_NEGATIVES, style["neg"])
|
| 308 |
+
common_head = _join(style["prefix"], ADULT_TAGS, recipe.get("woman") or DEFAULT_FACE, BODY_LOCK)
|
| 309 |
+
tail = _join(recipe.get("scene"), recipe.get("light"), "confident eye contact", extra)
|
| 310 |
|
| 311 |
+
if not tease_reveal:
|
| 312 |
+
pos = _join(common_head, recipe.get("outfit"), recipe.get("pose"), recipe.get("camera"), tail)
|
| 313 |
+
return [("Roll", pos, negative)]
|
| 314 |
|
| 315 |
+
# Same seed, two frames, both full body so the reveal reads as a pair
|
| 316 |
+
frame1 = _join(
|
| 317 |
+
common_head, recipe.get("tease"), "partially covered, teasing",
|
| 318 |
+
recipe.get("pose"), "full body shot, head to toe", tail,
|
| 319 |
+
)
|
| 320 |
+
frame2 = _join(
|
| 321 |
+
common_head, "fully nude", "nothing covering her",
|
| 322 |
+
recipe.get("pose"), "full body shot, head to toe", tail,
|
| 323 |
+
)
|
| 324 |
+
return [("Tease", frame1, negative), ("Reveal", frame2, negative)]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def recipe_card(recipe, seed=None):
|
| 328 |
+
if not recipe:
|
| 329 |
+
return "_No roll yet. Hit **Spin**._"
|
| 330 |
+
lines = ["### Recipe"]
|
| 331 |
+
for field in FIELDS:
|
| 332 |
+
lines.append(f"- **{field.title()}**: {recipe.get(field, '')}")
|
| 333 |
+
if seed is not None:
|
| 334 |
+
lines.append(f"- **Seed**: {seed}")
|
| 335 |
+
return "\n".join(lines)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def history_label(i, entry):
|
| 339 |
+
r = entry["recipe"]
|
| 340 |
+
return f"{i + 1}. {r['style']} | {r['scene']} | {r['outfit']} | seed {entry['seed']}"
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def push_history(history, recipe, seed):
|
| 344 |
+
history = list(history or [])
|
| 345 |
+
history.insert(0, {"recipe": dict(recipe), "seed": seed})
|
| 346 |
+
return history[:HISTORY_SIZE]
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
# ---------------------------------------------------------------------------
|
| 350 |
+
# Visitor logging (off by default)
|
| 351 |
+
# ---------------------------------------------------------------------------
|
| 352 |
def should_log_upload(username) -> bool:
|
| 353 |
+
return bool(LOG_VISITOR_UPLOADS)
|
|
|
|
|
|
|
| 354 |
|
| 355 |
|
| 356 |
def async_log_upload(pil_img, seed, model_name, prompt, username, width, height, steps, cfg_scale):
|
| 357 |
if not should_log_upload(username):
|
| 358 |
+
logger.info("visitor log upload skipped (LOG_VISITOR_UPLOADS not enabled)")
|
| 359 |
return
|
| 360 |
hf_token = os.environ.get("HF_TOKEN")
|
| 361 |
if not username or not str(username).strip():
|
|
|
|
| 391 |
CommitOperationAdd(path_in_repo=f"data/{filename_base}.webp", path_or_fileobj=raw),
|
| 392 |
]
|
| 393 |
api.create_commit(
|
| 394 |
+
repo_id=BACKUP_DATASET_ID, repo_type="dataset", operations=ops,
|
|
|
|
|
|
|
| 395 |
commit_message=f"gen for {username} @ {timestamp}",
|
| 396 |
)
|
| 397 |
+
logger.info(f"logged {username} attempt {attempt}")
|
| 398 |
return
|
| 399 |
except Exception as e:
|
| 400 |
logger.error(f"log fail {attempt}: {e}")
|
| 401 |
time.sleep(1.5 * attempt)
|
| 402 |
|
| 403 |
|
| 404 |
+
# ---------------------------------------------------------------------------
|
| 405 |
+
# Pipeline (one chef cached at a time)
|
| 406 |
+
# ---------------------------------------------------------------------------
|
| 407 |
_pipe = None
|
| 408 |
+
_pipe_chef = None
|
| 409 |
|
| 410 |
|
| 411 |
def _hf_token():
|
| 412 |
return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
|
| 413 |
|
| 414 |
|
| 415 |
+
def unload_pipe():
|
| 416 |
+
global _pipe, _pipe_chef
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
_pipe = None
|
| 418 |
+
_pipe_chef = None
|
| 419 |
gc.collect()
|
| 420 |
if torch.cuda.is_available():
|
| 421 |
torch.cuda.empty_cache()
|
| 422 |
|
| 423 |
|
| 424 |
+
def get_pipe(chef_name):
|
| 425 |
+
global _pipe, _pipe_chef
|
| 426 |
+
if _pipe is not None and _pipe_chef == chef_name:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
return _pipe
|
| 428 |
+
from diffusers import StableDiffusionXLPipeline, EulerAncestralDiscreteScheduler
|
| 429 |
|
| 430 |
+
unload_pipe()
|
| 431 |
+
spec = CHEFS[chef_name]
|
|
|
|
| 432 |
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
| 433 |
+
logger.info(f"Loading chef {chef_name} ({spec['repo']})")
|
| 434 |
+
pipe = StableDiffusionXLPipeline.from_pretrained(
|
| 435 |
+
spec["repo"], torch_dtype=dtype, use_safetensors=True, token=_hf_token(),
|
| 436 |
+
)
|
| 437 |
+
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
if torch.cuda.is_available():
|
| 439 |
pipe.to("cuda")
|
| 440 |
+
pipe.vae.enable_slicing()
|
| 441 |
+
pipe.vae.enable_tiling()
|
| 442 |
+
_pipe, _pipe_chef = pipe, chef_name
|
|
|
|
|
|
|
|
|
|
| 443 |
return pipe
|
| 444 |
|
| 445 |
|
| 446 |
+
def _gpu(fn):
|
| 447 |
+
return spaces.GPU(duration=240)(fn) if SPACES_AVAILABLE else fn
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
@_gpu
|
| 451 |
+
def render_frames(chef_name, frames, seed, steps, guidance_scale, width, height):
|
| 452 |
+
"""frames: list of (label, positive, negative). Same seed for every frame."""
|
| 453 |
+
pipe = get_pipe(chef_name)
|
| 454 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 455 |
+
images = []
|
| 456 |
+
for label, positive, negative in frames:
|
| 457 |
+
gen = torch.Generator(device=device).manual_seed(int(seed))
|
| 458 |
+
out = pipe(
|
| 459 |
+
prompt=positive,
|
| 460 |
+
negative_prompt=negative,
|
| 461 |
+
num_inference_steps=int(steps),
|
| 462 |
+
guidance_scale=float(guidance_scale),
|
| 463 |
+
width=int(width),
|
| 464 |
+
height=int(height),
|
| 465 |
+
generator=gen,
|
| 466 |
+
).images[0]
|
| 467 |
+
images.append((label, out))
|
| 468 |
+
return images
|
| 469 |
|
| 470 |
|
| 471 |
+
# ---------------------------------------------------------------------------
|
| 472 |
+
# UI handlers
|
| 473 |
+
# ---------------------------------------------------------------------------
|
| 474 |
+
def _locks_from_flags(flags):
|
| 475 |
+
return {field: bool(flag) for field, flag in zip(FIELDS, flags)}
|
|
|
|
|
|
|
| 476 |
|
| 477 |
|
| 478 |
+
def _resolve_seed(seed_value):
|
| 479 |
try:
|
| 480 |
+
s = int(float(seed_value))
|
| 481 |
except (TypeError, ValueError):
|
| 482 |
+
s = -1
|
| 483 |
+
return random.randint(0, 2**31 - 1) if s < 0 else s
|
| 484 |
|
| 485 |
|
| 486 |
+
def _cook(recipe, seed, tease_reveal, extra, size_name, steps, guidance_scale, username, gallery_items, history):
|
| 487 |
+
"""Shared render path for Spin and Re-roll. Returns UI outputs."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
user_key = username or "anonymous"
|
| 489 |
+
now = time.time()
|
| 490 |
+
last = LAST_GEN_TIME.get(user_key)
|
| 491 |
+
if last and now - last < COOLDOWN_SECONDS:
|
| 492 |
+
left = int(COOLDOWN_SECONDS - (now - last))
|
| 493 |
+
raise gr.Error(f"Cooldown {left}s")
|
| 494 |
+
|
| 495 |
+
frames = build_frames(recipe, tease_reveal=bool(tease_reveal), extra=extra or "")
|
| 496 |
+
for _, positive, _ in frames:
|
| 497 |
+
ok, bad = check_prompt_safety(positive)
|
| 498 |
+
if not ok:
|
| 499 |
+
raise gr.Error("Restricted content in prompt")
|
| 500 |
+
|
| 501 |
+
width, height = SIZE_PRESETS.get(size_name, SIZE_PRESETS["Portrait 832x1216"])
|
| 502 |
+
use_steps = int(max(10, min(40, int(steps or DEFAULT_STEPS))))
|
| 503 |
+
use_guidance = float(guidance_scale if guidance_scale is not None else DEFAULT_GUIDANCE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
LAST_GEN_TIME[user_key] = now
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
|
| 506 |
try:
|
| 507 |
+
results = render_frames(recipe["chef"], frames, seed, use_steps, use_guidance, width, height)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
except Exception as e:
|
| 509 |
+
logger.exception("render failed")
|
| 510 |
msg = str(e)
|
| 511 |
if "gated" in msg.lower() or "401" in msg or "403" in msg:
|
| 512 |
+
msg += " (accept the model license and make sure the HF_TOKEN secret can read it)"
|
| 513 |
+
raise gr.Error(msg)
|
| 514 |
+
|
| 515 |
+
new_items = []
|
| 516 |
+
for (label, img), (_, positive, _) in zip(results, frames):
|
| 517 |
+
new_items.append((img, f"{label} | {recipe['chef']} | seed {seed}"))
|
| 518 |
+
threading.Thread(
|
| 519 |
+
target=async_log_upload,
|
| 520 |
+
args=(img, seed, recipe["chef"], positive, username, width, height, use_steps, use_guidance),
|
| 521 |
+
daemon=True,
|
| 522 |
+
).start()
|
| 523 |
+
|
| 524 |
+
gallery_items = (new_items + list(gallery_items or []))[:GALLERY_SIZE]
|
| 525 |
+
history = push_history(history, recipe, seed)
|
| 526 |
+
prompt_text = "\n\n".join(f"[{label}]\n{pos}" for label, pos, _ in frames)
|
| 527 |
+
choices = [history_label(i, h) for i, h in enumerate(history)]
|
|
|
|
|
|
|
|
|
|
| 528 |
return (
|
| 529 |
+
gallery_items,
|
| 530 |
+
gallery_items,
|
| 531 |
+
recipe_card(recipe, seed),
|
| 532 |
+
prompt_text,
|
| 533 |
+
recipe,
|
| 534 |
+
history,
|
| 535 |
+
gr.update(choices=choices, value=choices[0] if choices else None),
|
|
|
|
| 536 |
)
|
| 537 |
|
| 538 |
|
| 539 |
+
def on_spin(recipe, new_woman, tease_reveal, extra, size_name, steps, guidance_scale, seed_value,
|
| 540 |
+
username, gallery_items, history, *lock_flags):
|
| 541 |
+
rolled = roll_recipe(recipe, _locks_from_flags(lock_flags), new_woman=bool(new_woman))
|
| 542 |
+
return _cook(rolled, _resolve_seed(seed_value), tease_reveal, extra, size_name, steps,
|
| 543 |
+
guidance_scale, username, gallery_items, history)
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def on_reroll_history(choice, tease_reveal, extra, size_name, steps, guidance_scale,
|
| 547 |
+
username, gallery_items, history):
|
| 548 |
+
history = list(history or [])
|
| 549 |
+
labels = [history_label(i, h) for i, h in enumerate(history)]
|
| 550 |
+
if not choice or choice not in labels:
|
| 551 |
+
raise gr.Error("Pick a recipe from history first")
|
| 552 |
+
picked = history[labels.index(choice)]["recipe"]
|
| 553 |
+
return _cook(dict(picked), _resolve_seed(-1), tease_reveal, extra, size_name, steps,
|
| 554 |
+
guidance_scale, username, gallery_items, history)
|
| 555 |
+
|
| 556 |
|
| 557 |
+
def on_load_history(choice, history):
|
| 558 |
+
history = list(history or [])
|
| 559 |
+
labels = [history_label(i, h) for i, h in enumerate(history)]
|
| 560 |
+
if not choice or choice not in labels:
|
| 561 |
+
return gr.update(), gr.update()
|
| 562 |
+
entry = history[labels.index(choice)]
|
| 563 |
+
return dict(entry["recipe"]), recipe_card(entry["recipe"], entry["seed"])
|
| 564 |
|
| 565 |
+
|
| 566 |
+
def on_preview(recipe, new_woman, tease_reveal, extra, *lock_flags):
|
| 567 |
+
"""Spin the card without rendering (free, no GPU)."""
|
| 568 |
+
rolled = roll_recipe(recipe, _locks_from_flags(lock_flags), new_woman=bool(new_woman))
|
| 569 |
+
frames = build_frames(rolled, tease_reveal=bool(tease_reveal), extra=extra or "")
|
| 570 |
+
return rolled, recipe_card(rolled), "\n\n".join(f"[{l}]\n{p}" for l, p, _ in frames)
|
| 571 |
|
| 572 |
|
| 573 |
js_visitor = """
|
|
|
|
| 581 |
}
|
| 582 |
"""
|
| 583 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
|
| 585 |
def create_ui():
|
| 586 |
+
with gr.Blocks(title="Roulette Lab") as demo:
|
|
|
|
|
|
|
| 587 |
gr.Markdown(
|
| 588 |
+
"# Roulette Lab\n"
|
| 589 |
+
"Spin a random recipe. Lock what you like, spin again, and only the unlocked slots change. "
|
| 590 |
+
"Fictional adult women 21+ only. Visitor images are not saved unless the Space secret "
|
| 591 |
+
"`LOG_VISITOR_UPLOADS=1` is set."
|
| 592 |
)
|
| 593 |
+
recipe_state = gr.State({})
|
| 594 |
+
history_state = gr.State([])
|
| 595 |
+
gallery_state = gr.State([])
|
| 596 |
+
user_id = gr.Textbox(visible=False)
|
| 597 |
+
|
| 598 |
with gr.Row():
|
| 599 |
+
with gr.Column(scale=1):
|
| 600 |
+
spin_btn = gr.Button("Spin", variant="primary", size="lg")
|
| 601 |
+
preview_btn = gr.Button("Spin card only (no render)")
|
| 602 |
+
card = gr.Markdown(recipe_card({}))
|
| 603 |
+
gr.Markdown("**Lock slots** (locked slots survive the next spin)")
|
| 604 |
+
lock_boxes = [gr.Checkbox(label=f"Lock {f}", value=False) for f in FIELDS]
|
| 605 |
+
new_woman = gr.Checkbox(label="New woman each roll", value=True)
|
| 606 |
+
tease_reveal = gr.Checkbox(label="Tease then reveal (same seed, 2 frames)", value=False)
|
| 607 |
+
extra = gr.Textbox(label="Extra tags (optional)", placeholder="e.g. glossy lips, rain on skin")
|
| 608 |
+
with gr.Accordion("Settings", open=False):
|
| 609 |
+
size_name = gr.Radio(list(SIZE_PRESETS), value="Portrait 832x1216", label="Size")
|
| 610 |
+
steps = gr.Slider(10, 40, value=DEFAULT_STEPS, step=1, label="Steps")
|
| 611 |
+
guidance_scale = gr.Slider(1.0, 12.0, value=DEFAULT_GUIDANCE, step=0.5, label="CFG")
|
| 612 |
+
seed_value = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
|
| 613 |
+
with gr.Column(scale=2):
|
| 614 |
+
gallery = gr.Gallery(label="Output", columns=2, height=720)
|
| 615 |
+
prompt_box = gr.Textbox(label="Prompt sent", lines=6, interactive=False)
|
| 616 |
+
gr.Markdown("**History** (last 10 recipes)")
|
| 617 |
+
history_dd = gr.Dropdown(choices=[], label="Pick a recipe", interactive=True)
|
| 618 |
with gr.Row():
|
| 619 |
+
reroll_btn = gr.Button("Re-roll this recipe, new seed")
|
| 620 |
+
load_btn = gr.Button("Load into card (then lock and spin)")
|
| 621 |
+
|
| 622 |
+
outputs = [gallery, gallery_state, card, prompt_box, recipe_state, history_state, history_dd]
|
| 623 |
+
|
| 624 |
+
spin_btn.click(
|
| 625 |
+
on_spin,
|
| 626 |
+
inputs=[recipe_state, new_woman, tease_reveal, extra, size_name, steps, guidance_scale,
|
| 627 |
+
seed_value, user_id, gallery_state, history_state] + lock_boxes,
|
| 628 |
+
outputs=outputs,
|
| 629 |
+
)
|
| 630 |
+
reroll_btn.click(
|
| 631 |
+
on_reroll_history,
|
| 632 |
+
inputs=[history_dd, tease_reveal, extra, size_name, steps, guidance_scale,
|
| 633 |
+
user_id, gallery_state, history_state],
|
| 634 |
+
outputs=outputs,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
)
|
| 636 |
+
load_btn.click(on_load_history, inputs=[history_dd, history_state], outputs=[recipe_state, card])
|
| 637 |
+
preview_btn.click(
|
| 638 |
+
on_preview,
|
| 639 |
+
inputs=[recipe_state, new_woman, tease_reveal, extra] + lock_boxes,
|
| 640 |
+
outputs=[recipe_state, card, prompt_box],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
)
|
| 642 |
+
demo.load(fn=None, inputs=None, outputs=[user_id], js=js_visitor)
|
| 643 |
return demo
|
| 644 |
|
| 645 |
|
|
|
|
| 647 |
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 648 |
app = create_ui()
|
| 649 |
app.queue(max_size=12)
|
| 650 |
+
app.launch(ssr_mode=False)
|