JustinD commited on
Commit
bda6f07
·
verified ·
1 Parent(s): 79f1be3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +454 -850
app.py CHANGED
@@ -2,352 +2,227 @@
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 optimization")
 
 
 
 
 
 
 
 
 
9
 
10
  import sys
11
  import os
12
  import re
13
- import random
14
  import gc
15
- import logging
16
  import time
17
- import io
 
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("NextGenEngine")
33
- logger.info("🚀 JustinD Fiction Lab boot (21+ · multi-select stacks · optional quad chefs)")
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
- MAX_STACK_BATCH_PRODUCT = 6
45
-
46
- HF_REPO_ID = "AriaVale2/my-safetensors-backup"
47
-
48
- # Single-file chefs (Asian realism + proven NSFW pantry)
49
- MODEL_REPOS = {}
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
- SDXL_CHOICES = list(DIFFUSERS_MODELS.keys()) + list(MODEL_REPOS.keys())
75
- SKIP_CHEF = "(skip)"
76
- CHEF_OPTIONAL_CHOICES = [SKIP_CHEF] + SDXL_CHOICES
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
- LORA_CHOICES = list(LORA_REPOS.keys())
91
-
92
- # Named multi-chef stacks. Schema supports optional Chef C/D (quad).
93
- # Two-chef stacks keep chef_c/d=None. Quad stacks set all four.
94
- # LoRA applies on the last chef that actually runs.
95
- STACK_PHOTO_ILLUSTRIOUS = "Photo + Illustrious"
96
- STACK_HARDCORE_FACE = "Hardcore + face keeper"
97
- STACK_ANIME_REAL = "Anime Desire × Real Porn"
98
- STACK_AI_AI_AI = "AI + AI × AI"
99
- STACK_SOFT_DOMME = "Soft Domme + glossy"
100
- STACK_MIRROR_HOTEL = "Mirror hotel"
101
- STACK_NEON_ALLEY = "Neon alley"
102
- STACK_LUSTIFY_REAL = "Lustify × Real Porn"
103
- STACK_ANIME_ILLUSTRIOUS = "Anime Desire × Illustrious"
104
- STACK_HARDCORE_LUSTIFY = "Hardcore × Lustify"
105
- STACK_FACE_ILLUSTRIOUS = "Face keeper × Illustrious"
106
- STACK_SPICY_WAI = "Spicy Realism × WAI"
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
- STACK_CHOICES = list(STACKS.keys())
226
- DEFAULT_STACK = STACK_AI_AI_AI
227
- DEFAULT_STACKS = [DEFAULT_STACK]
228
-
229
- # Style packs stack via CheckboxGroup. Testing Tits is the heat base; scene packs layer on top.
230
- # Cosmic e-girl booth is FICTIONAL DJ only (no real person names).
231
- STYLE_PACKS = {
232
- "Testing Tits": {
233
- "prefix": (
234
- "nsfw, explicit, erotic nude, explicit, "
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
- "3": {
257
- "prefix": (
258
- "nude"
259
- ),
260
- "suffix": (
261
- "art, messy art"
262
- ),
263
  },
264
- "3": {
265
- "prefix": (
266
- "string, pantys"
267
- ),
268
- "suffix": (
269
- "no bra"
270
- ),
271
  },
272
- "4": {
273
- "prefix": (
274
- "hard domme energy, "
275
- "smile, dominant, man gaze"
276
- ),
277
- "suffix": (
278
- "hard sexy smile"
279
- ),
280
  },
281
- "5": {
282
- "prefix": (
283
- "hand over her breast, small tank top, "
284
- ""
285
- ),
286
- "suffix": (
287
- ""
288
- ),
289
  },
290
- "6": {
291
- "prefix": (
292
- "leather, green, glass, "
293
- "hard glam light"
294
- ),
295
- "suffix": (
296
- "turtleneck pulled up over breasts, hard glam filthy under knit, "
297
- "breasts out under sweater"
298
- ),
299
  },
300
  }
301
 
302
- # Filth dial extras (0 = soft tease, 1 = pack heat only, 2 = max explicit). Body lock untouched.
303
- FILTH_LEVELS = {
304
- 0: {
305
- "prefix": "sexy tease, nude, cleavage, erotic Asian",
306
- "suffix": "China, Chinese, Japanese, Japan, Thai, Thailand",
307
- },
308
- 1: {
309
- "prefix": "Woman, Female",
310
- "suffix": "big breasts, huge breasts,",
311
- },
312
- 2: {
313
- "prefix": (
314
- "filthy explicit, slut"
315
- ),
316
- "suffix": (
317
- "body offering, chest forward"
318
- ),
319
- },
320
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
- # Master lock: every gen inherits adult 21+ East/SE Asian woman body lock
323
- DEFAULT_QUALITY = (
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
- "small breasts, flat chest, bad hands, extra fingers, deformed, "
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
- def build_styles(selected):
377
- if not selected:
378
- return "", ""
379
- if isinstance(selected, str):
380
- selected = [selected]
381
- prefixes, suffixes = [], []
382
- for name in selected:
383
- cfg = STYLE_PACKS.get(name)
384
- if not cfg:
385
- continue
386
- if cfg.get("prefix"):
387
- prefixes.append(cfg["prefix"])
388
- if cfg.get("suffix"):
389
- suffixes.append(cfg["suffix"])
390
- return ", ".join(prefixes), ", ".join(suffixes)
391
-
392
-
393
- def filth_boost(level):
394
- """Scale explicit heat without touching body lock (heavy breasts)."""
395
- try:
396
- lvl = int(round(float(level)))
397
- except (TypeError, ValueError):
398
- lvl = 1
399
- lvl = max(0, min(2, lvl))
400
- cfg = FILTH_LEVELS.get(lvl, FILTH_LEVELS[1])
401
- return cfg.get("prefix", ""), cfg.get("suffix", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
 
 
 
 
403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  def should_log_upload(username) -> bool:
405
- if not LOG_VISITOR_UPLOADS:
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("⏭️ visitor log upload skipped (LOG_VISITOR_UPLOADS not enabled)")
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"☁️ logged {username} attempt {attempt}")
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
- _loaded_key = None
462
 
463
 
464
  def _hf_token():
465
  return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
466
 
467
 
468
- def unload_all():
469
- global _pipe, _loaded_key
470
- if _pipe is not None:
471
- try:
472
- del _pipe
473
- except Exception:
474
- pass
475
  _pipe = None
476
- _loaded_key = None
477
  gc.collect()
478
  if torch.cuda.is_available():
479
  torch.cuda.empty_cache()
480
 
481
 
482
- def _apply_lora(pipe, lora_name: str) -> bool:
483
- """Try load_lora_weights. Returns True if applied, False if skipped/failed (gen continues)."""
484
- if not lora_name or lora_name == LORA_NONE:
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
- from diffusers import StableDiffusionXLPipeline, EulerDiscreteScheduler
510
-
511
- unload_all()
512
  dtype = torch.float16 if torch.cuda.is_available() else torch.float32
513
- logger.info(f"Loading Adult SDXL chef: {model_name}")
514
- if model_name in DIFFUSERS_MODELS:
515
- pipe = StableDiffusionXLPipeline.from_pretrained(
516
- DIFFUSERS_MODELS[model_name],
517
- torch_dtype=dtype,
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
- if hasattr(pipe, "vae"):
536
- pipe.vae.enable_slicing()
537
- pipe.vae.enable_tiling()
538
- _apply_lora(pipe, lora_name)
539
- _pipe = pipe
540
- _loaded_key = key
541
  return pipe
542
 
543
 
544
- def enhance_prompt(prompt, styles, filth_level=1):
545
- pre, suf = build_styles(styles)
546
- f_pre, f_suf = filth_boost(filth_level)
547
- parts = [DEFAULT_QUALITY, pre, f_pre, prompt.strip(), suf, f_suf]
548
- return ", ".join([p for p in parts if p])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
 
551
- def _norm_chef(name):
552
- if name is None:
553
- return None
554
- s = str(name).strip()
555
- if not s or s == SKIP_CHEF or s.lower() in {"none", "null", "skip"}:
556
- return None
557
- return s
558
 
559
 
560
- def _clamp_blend(val, fallback=0.55):
561
  try:
562
- strength = float(val)
563
  except (TypeError, ValueError):
564
- strength = float(fallback)
565
- return max(0.05, min(0.95, strength))
566
 
567
 
568
- def resolve_chef_chain(cfg):
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
- if user_key in LAST_GEN_TIME and now - LAST_GEN_TIME[user_key] < COOLDOWN_SECONDS:
749
- left = int(COOLDOWN_SECONDS - (now - LAST_GEN_TIME[user_key]))
750
- gr.Warning(f"Cooldown {left}s")
751
- return None, f"⏳ Cooldown {left}s", "", []
752
- ok, bad = check_prompt_safety(prompt)
753
- if not ok:
754
- gr.Warning("Restricted content")
755
- return None, "❌ Restricted content", "", []
756
-
757
- use_advanced = bool(use_advanced)
758
- stacks = normalize_selected_stacks(selected_stacks)
759
- if use_advanced:
760
- # Single custom path: Advanced overrides ignore named stacks
761
- run_jobs = [("Advanced override", advanced_cfg_from_ui(
762
- chef_a, chef_b, chef_c, chef_d, lora_name, blend_b, blend_c, blend_d
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
- for stack_label, stack_cfg in run_jobs:
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("generate failed")
816
  msg = str(e)
817
  if "gated" in msg.lower() or "401" in msg or "403" in msg:
818
- msg += " - accept the model license and ensure Space secret HF_TOKEN can read it."
819
- return None, f"❌ {msg}", "", []
820
-
821
-
822
- def on_stacks_change(selected):
823
- """Sync Advanced slots when exactly one named stack is selected."""
824
- stacks = normalize_selected_stacks(selected)
825
- if len(stacks) != 1:
826
- return (
827
- gr.update(),
828
- gr.update(),
829
- gr.update(),
830
- gr.update(),
831
- gr.update(),
832
- gr.update(),
833
- gr.update(),
834
- gr.update(),
835
- )
836
- cfg = STACKS[stacks[0]]
837
  return (
838
- gr.update(value=cfg["chef_a"]),
839
- gr.update(value=cfg["chef_b"]),
840
- gr.update(value=cfg["chef_c"] if cfg.get("chef_c") else SKIP_CHEF),
841
- gr.update(value=cfg["chef_d"] if cfg.get("chef_d") else SKIP_CHEF),
842
- gr.update(value=cfg["lora"]),
843
- gr.update(value=cfg["blend_b"]),
844
- gr.update(value=cfg["blend_c"]),
845
- gr.update(value=cfg["blend_d"]),
846
  )
847
 
848
 
849
- def lucky_seed():
850
- return -1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
 
 
 
 
 
 
 
 
852
 
853
- def random_starter():
854
- return random.choice(RANDOM_PROMPTS)
 
 
 
 
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
- default_cfg = STACKS[DEFAULT_STACK]
878
- with gr.Blocks(title="JustinD/1 · 21+ Fiction Lab") as demo:
879
- user_id = gr.Textbox(visible=False)
880
  gr.Markdown(
881
- """# JustinD/1"""
 
 
 
882
  )
 
 
 
 
 
883
  with gr.Row():
884
- with gr.Column(scale=5):
885
- stacks = gr.CheckboxGroup(
886
- label="Named stacks (multi-select · empty = AI + AI × AI alone)",
887
- choices=STACK_CHOICES,
888
- value=list(DEFAULT_STACKS),
889
- )
890
- styles = gr.CheckboxGroup(
891
- label="Style packs (stackable · Testing Tits on by default)",
892
- choices=list(STYLE_PACKS.keys()),
893
- value=["Testing Tits"],
894
- )
895
- prompt = gr.Textbox(
896
- label="Prompt (scene + vibe)",
897
- value=random.choice(RANDOM_PROMPTS),
898
- lines=6,
899
- placeholder="Asian woman late 20s",
900
- )
 
 
901
  with gr.Row():
902
- lucky_prompt_btn = gr.Button("🎲 Sex Wildcard", variant="secondary")
903
- lucky_seed_btn = gr.Button("🍀 Lucky seed", variant="secondary")
904
- filth = gr.Slider(
905
- label="Filth dial (0 soft · 1 pack heat · 2 max explicit)",
906
- minimum=0,
907
- maximum=2,
908
- value=1,
909
- step=1,
910
- )
911
- go = gr.Button("🔥 GENERATE", variant="primary", size="lg")
912
- with gr.Accordion("Advanced (Chef A/B/C/D · LoRA · blends · size)", open=False):
913
- use_advanced = gr.Checkbox(
914
- label="Use Advanced override (ignore stack chefs)",
915
- value=False,
916
- )
917
- gr.Markdown(
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
- lucky_prompt_btn.click(fn=random_starter, inputs=None, outputs=prompt)
1002
- lucky_seed_btn.click(fn=lucky_seed, inputs=None, outputs=seed)
1003
-
1004
- def on_go(*args):
1005
- images, run_info, enhanced, jobs = generate(*args)
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, theme=THEME, css=css)
 
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)