JustinD commited on
Commit
4d383bb
·
verified ·
1 Parent(s): 9f1096f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +257 -149
app.py CHANGED
@@ -30,7 +30,7 @@ logging.basicConfig(
30
  handlers=[logging.StreamHandler(sys.stdout)],
31
  )
32
  logger = logging.getLogger("NextGenEngine")
33
- logger.info("🚀 NSFW East/SE Asian Lab boot (21+ · Adult SDXL / FLUX / SD3.5)")
34
 
35
  if hasattr(sys.stdout, "reconfigure"):
36
  sys.stdout.reconfigure(encoding="utf-8")
@@ -42,11 +42,6 @@ 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
- ENGINE_FLUX = "FLUX.1 Schnell (latest fast)"
46
- ENGINE_SD35 = "SD 3.5 Medium (gated / optional)"
47
- ENGINE_SDXL = "Adult SDXL (NSFW chefs)"
48
- ENGINE_CHOICES = [ENGINE_SDXL, ENGINE_FLUX, ENGINE_SD35]
49
-
50
  HF_REPO_ID = "AriaVale2/my-safetensors-backup"
51
 
52
  # Single-file chefs (Asian realism + proven NSFW pantry)
@@ -111,10 +106,59 @@ DIFFUSERS_MODELS = {
111
  }
112
 
113
  SDXL_CHOICES = list(DIFFUSERS_MODELS.keys()) + list(MODEL_REPOS.keys())
114
- DEFAULT_SDXL = "WAI NSFW Illustrious v150"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  # Style packs stack via CheckboxGroup. Testing Tits is the heat base; scene packs layer on top.
117
- # No packs named "1" or "2". Cosmic e-girl booth is FICTIONAL DJ only (no real person names).
118
  STYLE_PACKS = {
119
  "Testing Tits": {
120
  "prefix": (
@@ -346,80 +390,57 @@ def async_log_upload(pil_img, seed, model_name, prompt, username, width, height,
346
  time.sleep(1.5 * attempt)
347
 
348
 
349
- _pipes = {}
 
350
 
351
 
352
- def _dtype():
353
- if torch.cuda.is_available():
354
- return torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
355
- return torch.float32
356
 
357
 
358
  def unload_all():
359
- global _pipes
360
- _pipes.clear()
 
 
 
 
 
 
361
  gc.collect()
362
  if torch.cuda.is_available():
363
  torch.cuda.empty_cache()
364
 
365
 
366
- def _hf_token():
367
- return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
368
-
369
-
370
- def get_flux_pipe():
371
- key = "flux-schnell"
372
- if key in _pipes:
373
- return _pipes[key]
374
- from diffusers import FluxPipeline
375
- unload_all()
376
- logger.info("Loading FLUX.1 Schnell…")
377
- token = _hf_token()
378
- pipe = FluxPipeline.from_pretrained(
379
- "black-forest-labs/FLUX.1-schnell",
380
- torch_dtype=_dtype(),
381
- token=token,
382
- )
383
- if torch.cuda.is_available():
384
- pipe.to("cuda")
385
- _pipes[key] = pipe
386
- return pipe
387
-
388
-
389
- def get_sd35_pipe():
390
- key = "sd35-medium"
391
- if key in _pipes:
392
- return _pipes[key]
393
- from diffusers import StableDiffusion3Pipeline
394
- from huggingface_hub import login as hf_login
395
- token = _hf_token()
396
- if not token:
397
- raise RuntimeError(
398
- "SD 3.5 is gated. Add HF_TOKEN secret and accept "
399
- "https://huggingface.co/stabilityai/stable-diffusion-3.5-medium"
400
- )
401
  try:
402
- hf_login(token=token, add_to_git_credential=False)
 
 
 
 
403
  except Exception as e:
404
- logger.warning(f"hf_login warning: {e}")
405
- unload_all()
406
- logger.info("Loading SD 3.5 Medium (authenticated)…")
407
- pipe = StableDiffusion3Pipeline.from_pretrained(
408
- "stabilityai/stable-diffusion-3.5-medium",
409
- torch_dtype=_dtype(),
410
- token=token,
411
- )
412
- if torch.cuda.is_available():
413
- pipe.to("cuda")
414
- _pipes[key] = pipe
415
- return pipe
416
 
 
 
 
 
 
 
417
 
418
- def get_sdxl_pipe(model_name: str):
419
- key = f"sdxl:{model_name}"
420
- if key in _pipes:
421
- return _pipes[key]
422
  from diffusers import StableDiffusionXLPipeline, EulerDiscreteScheduler
 
423
  unload_all()
424
  dtype = torch.float16 if torch.cuda.is_available() else torch.float32
425
  logger.info(f"Loading Adult SDXL chef: {model_name}")
@@ -431,7 +452,7 @@ def get_sdxl_pipe(model_name: str):
431
  )
432
  else:
433
  repo = MODEL_REPOS.get(model_name, HF_REPO_ID)
434
- path = hf_hub_download(repo_id=repo, filename=model_name)
435
  pipe = StableDiffusionXLPipeline.from_single_file(
436
  path,
437
  torch_dtype=dtype,
@@ -447,7 +468,9 @@ def get_sdxl_pipe(model_name: str):
447
  if hasattr(pipe, "vae"):
448
  pipe.vae.enable_slicing()
449
  pipe.vae.enable_tiling()
450
- _pipes[key] = pipe
 
 
451
  return pipe
452
 
453
 
@@ -457,13 +480,30 @@ def enhance_prompt(prompt, styles):
457
  return ", ".join([p for p in parts if p])
458
 
459
 
460
- @spaces.GPU(duration=180) if SPACES_AVAILABLE else (lambda f: f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  def generate(
462
- engine,
463
  prompt,
464
  styles,
465
  negative,
466
- sdxl_model,
 
 
 
 
467
  steps,
468
  cfg,
469
  seed,
@@ -484,6 +524,7 @@ def generate(
484
  if not ok:
485
  gr.Warning("Restricted content")
486
  return None, "❌ Restricted content", "", []
 
487
  batch = int(max(1, min(3, int(batch))))
488
  seeds = [
489
  random.randint(0, 2**31 - 1) if int(seed) < 0 else int(seed) + i
@@ -493,69 +534,80 @@ def generate(
493
  neg = BASE_NEGATIVES
494
  if negative and str(negative).strip():
495
  neg = f"{BASE_NEGATIVES}, {negative.strip()}"
 
 
 
 
 
 
496
  LAST_GEN_TIME[user_key] = now
497
  gallery = []
498
  jobs = []
 
 
499
  try:
500
- device = "cuda" if torch.cuda.is_available() else "cpu"
501
- if engine == ENGINE_FLUX:
502
- pipe = get_flux_pipe()
503
- use_steps = int(max(1, min(8, int(steps) if int(steps) <= 8 else 4)))
504
- flux_prompt = (
505
- enhanced
506
- + ", 1girl, solo, adult East Asian OR Southeast Asian woman 21+ only, "
507
- "late 20s, no men, no males, no old people, "
508
- "tall slim small frame tiny waist, large full heavy breasts"
509
- )
510
- for s in seeds:
511
- gen = torch.Generator(device=device).manual_seed(int(s))
512
- out = pipe(
513
- prompt=flux_prompt,
514
- num_inference_steps=use_steps,
515
- guidance_scale=0.0,
516
- width=int(width),
517
- height=int(height),
518
- generator=gen,
519
- max_sequence_length=256,
520
- ).images[0]
521
- label = f"FLUX · seed {s} · {datetime.now().strftime('%I:%M:%S %p')}"
522
- gallery.append((out, label))
523
- jobs.append((out, s, "FLUX.1-schnell", prompt, username, width, height, use_steps, 0.0))
524
- elif engine == ENGINE_SD35:
525
- pipe = get_sd35_pipe()
526
- use_steps = int(max(15, min(40, int(steps))))
527
- for s in seeds:
528
- gen = torch.Generator(device=device).manual_seed(int(s))
529
- out = pipe(
530
- prompt=enhanced,
531
- negative_prompt=neg,
532
- num_inference_steps=use_steps,
533
- guidance_scale=float(cfg),
534
- width=int(width),
535
- height=int(height),
536
- generator=gen,
537
- ).images[0]
538
- label = f"SD3.5 · seed {s} · {datetime.now().strftime('%I:%M:%S %p')}"
539
- gallery.append((out, label))
540
- jobs.append((out, s, "SD3.5-medium", prompt, username, width, height, use_steps, cfg))
541
- else:
542
- pipe = get_sdxl_pipe(sdxl_model)
543
- use_steps = int(max(10, min(40, int(steps))))
544
- for s in seeds:
545
- gen = torch.Generator(device=device).manual_seed(int(s))
546
- out = pipe(
547
- prompt=enhanced,
548
- negative_prompt=neg,
549
- num_inference_steps=use_steps,
550
- guidance_scale=float(cfg),
551
- width=int(width),
552
- height=int(height),
553
- generator=gen,
554
- ).images[0]
555
- label = f"SDXL · {sdxl_model[:28]} · seed {s}"
556
- gallery.append((out, label))
557
- jobs.append((out, s, sdxl_model, prompt, username, width, height, use_steps, cfg))
558
- info = f"{engine} | seeds {seeds} | {width}x{height} | enhanced prompt ready"
 
 
 
559
  return gallery, info, enhanced, jobs
560
  except Exception as e:
561
  logger.exception("generate failed")
@@ -565,6 +617,16 @@ def generate(
565
  return None, f"❌ {msg}", "", []
566
 
567
 
 
 
 
 
 
 
 
 
 
 
568
  js_visitor = """
569
  function() {
570
  let id = localStorage.getItem('adult_ai_visitor_id');
@@ -585,23 +647,29 @@ THEME = gr.themes.Default(primary_hue="purple", secondary_hue="indigo", neutral_
585
 
586
 
587
  def create_ui():
 
588
  with gr.Blocks() as demo:
589
  user_id = gr.Textbox(visible=False)
590
- gr.Markdown("# ⚡ 21+ Adult East/SE Asian Lab")
591
  gr.Markdown(
592
  """### 21+ only · Fiction Space · ZeroGPU · first model load is slow · queues happen
593
 
594
  **Scene + body lab for adult East / Southeast Asian women (21+, late 20s fiction).**
595
  Body lock: tall slim small frame, tiny waist, large/full/heavy breasts, cleavage. No thick thighs.
596
- Default engine: Adult SDXL. Chef list is **porn / NSFW only** (Illustrious + Pony + photo porn). Default: **WAI NSFW Illustrious v150**.
 
597
  Style packs stack: start with **Testing Tits**, layer scene packs (Cosmic e-girl booth, night club, hotel mirror, rain alley, Soft Domme, Quiet beg, library whiskey couch).
598
- FLUX and SD 3.5 stay optional. Visitor gens are **not** saved unless secret `LOG_VISITOR_UPLOADS=1`."""
599
  )
600
  with gr.Row():
601
  with gr.Column(scale=2):
602
- prompt = gr.Textbox(label="Prompt (scene + body)", value=random.choice(RANDOM_PROMPTS), lines=7)
 
 
 
 
603
  negative = gr.Textbox(
604
- label="Negatives (SD3.5 / SDXL; ignored by FLUX)",
605
  value=BASE_NEGATIVES,
606
  lines=4,
607
  )
@@ -610,29 +678,53 @@ FLUX and SD 3.5 stay optional. Visitor gens are **not** saved unless secret `LOG
610
  choices=list(STYLE_PACKS.keys()),
611
  value=["Testing Tits"],
612
  )
 
 
 
 
 
613
  go = gr.Button("GENERATE", variant="primary", size="lg")
614
  with gr.Column(scale=1):
615
- engine = gr.Radio(
616
- label="Engine (Adult SDXL = default)",
617
- choices=ENGINE_CHOICES,
618
- value=ENGINE_SDXL,
619
- )
620
- sdxl_model = gr.Dropdown(
621
- label="Adult SDXL chef (only used on Adult SDXL engine)",
622
- choices=SDXL_CHOICES,
623
- value=DEFAULT_SDXL,
624
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  seed = gr.Number(label="Seed (-1 random)", value=-1, precision=0)
626
  width = gr.Slider(512, 1536, value=1024, step=64, label="Width")
627
  height = gr.Slider(512, 1536, value=1024, step=64, label="Height")
628
- steps = gr.Slider(1, 40, value=28, step=1, label="Steps (SDXL~28, FLUX~4, SD3.5~28)")
629
- cfg = gr.Slider(0.0, 12.0, value=6.0, step=0.5, label="CFG (SDXL~6, FLUX uses 0)")
630
  batch = gr.Slider(1, 3, value=1, step=1, label="Batch (max 3)")
631
  gallery = gr.Gallery(label="Outputs", columns=3, height="auto")
632
  info = gr.Textbox(label="Run info", visible=True)
633
  meta = gr.Textbox(label="Enhanced prompt", visible=True)
634
  demo.load(fn=None, inputs=None, outputs=user_id, js=js_visitor)
635
 
 
 
 
 
 
 
636
  def on_go(*args):
637
  images, run_info, enhanced, jobs = generate(*args)
638
  if jobs:
@@ -644,7 +736,23 @@ FLUX and SD 3.5 stay optional. Visitor gens are **not** saved unless secret `LOG
644
 
645
  go.click(
646
  fn=on_go,
647
- inputs=[engine, prompt, styles, negative, sdxl_model, steps, cfg, seed, width, height, batch, user_id],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  outputs=[gallery, info, meta],
649
  )
650
  return demo
 
30
  handlers=[logging.StreamHandler(sys.stdout)],
31
  )
32
  logger = logging.getLogger("NextGenEngine")
33
+ logger.info("🚀 NSFW East/SE Asian Lab boot (21+ · Multi-chef stacks)")
34
 
35
  if hasattr(sys.stdout, "reconfigure"):
36
  sys.stdout.reconfigure(encoding="utf-8")
 
42
  COOLDOWN_SECONDS = 12
43
  LOG_VISITOR_UPLOADS = os.environ.get("LOG_VISITOR_UPLOADS", "0").strip().lower() in {"1", "true", "yes"}
44
 
 
 
 
 
 
45
  HF_REPO_ID = "AriaVale2/my-safetensors-backup"
46
 
47
  # Single-file chefs (Asian realism + proven NSFW pantry)
 
106
  }
107
 
108
  SDXL_CHOICES = list(DIFFUSERS_MODELS.keys()) + list(MODEL_REPOS.keys())
109
+
110
+ # LoRA pantry from known SDXL repos (patch_app.py pattern). Fail soft if download/load breaks.
111
+ LORA_NONE = "None"
112
+ LORA_REPOS = {
113
+ LORA_NONE: None,
114
+ "sexy-slider (Soft Domme heat)": ("ntc-ai/SDXL-LoRA-slider.sexy", "sexy.safetensors"),
115
+ "oily-bodystocking": ("Muapi/oily-bodystocking-sdxl-and-pony", "oily-bodystocking-sdxl-and-pony.safetensors"),
116
+ "super-skinny-body": (
117
+ "Muapi/super-skinny-body-for-pony-illustrious-sdxl-and-sd1.5",
118
+ "super-skinny-body-for-pony-illustrious-sdxl-and-sd1.5.safetensors",
119
+ ),
120
+ "glass-body": ("Muapi/transparent-glass-body-lora-1.5-sdxl", "transparent-glass-body-lora-1.5-sdxl.safetensors"),
121
+ }
122
+ LORA_CHOICES = list(LORA_REPOS.keys())
123
+
124
+ # Named multi-chef stacks. Every gen = Chef A txt2img then Chef B img2img remix.
125
+ # Default stack = filthiest (AI + AI x AI) with sexy-slider LoRA.
126
+ STACK_PHOTO_ILLUSTRIOUS = "Photo + Illustrious"
127
+ STACK_HARDCORE_FACE = "Hardcore + face keeper"
128
+ STACK_ANIME_REAL = "Anime Desire × Real Porn"
129
+ STACK_AI_AI_AI = "AI + AI × AI"
130
+
131
+ STACKS = {
132
+ STACK_PHOTO_ILLUSTRIOUS: {
133
+ "chef_a": "PornWorks Real Porn v04",
134
+ "chef_b": "WAI NSFW Illustrious v150",
135
+ "lora": LORA_NONE,
136
+ "blend": 0.50,
137
+ },
138
+ STACK_HARDCORE_FACE: {
139
+ "chef_a": "Hardcore Asian Cosplay Illustrious",
140
+ "chef_b": "OhMySister Beautiful Asian Woman",
141
+ "lora": LORA_NONE,
142
+ "blend": 0.60,
143
+ },
144
+ STACK_ANIME_REAL: {
145
+ "chef_a": "PornWorks Anime Desire NSFW",
146
+ "chef_b": "PornWorks Real Porn v04",
147
+ "lora": LORA_NONE,
148
+ "blend": 0.55,
149
+ },
150
+ STACK_AI_AI_AI: {
151
+ "chef_a": "Lustify NSFW GGWP v7",
152
+ "chef_b": "WAI NSFW Illustrious v150",
153
+ "lora": "sexy-slider (Soft Domme heat)",
154
+ "blend": 0.55,
155
+ },
156
+ }
157
+ STACK_CHOICES = list(STACKS.keys())
158
+ DEFAULT_STACK = STACK_AI_AI_AI
159
 
160
  # Style packs stack via CheckboxGroup. Testing Tits is the heat base; scene packs layer on top.
161
+ # Cosmic e-girl booth is FICTIONAL DJ only (no real person names).
162
  STYLE_PACKS = {
163
  "Testing Tits": {
164
  "prefix": (
 
390
  time.sleep(1.5 * attempt)
391
 
392
 
393
+ _pipe = None
394
+ _loaded_key = None
395
 
396
 
397
+ def _hf_token():
398
+ return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
 
 
399
 
400
 
401
  def unload_all():
402
+ global _pipe, _loaded_key
403
+ if _pipe is not None:
404
+ try:
405
+ del _pipe
406
+ except Exception:
407
+ pass
408
+ _pipe = None
409
+ _loaded_key = None
410
  gc.collect()
411
  if torch.cuda.is_available():
412
  torch.cuda.empty_cache()
413
 
414
 
415
+ def _apply_lora(pipe, lora_name: str) -> bool:
416
+ """Try load_lora_weights. Returns True if applied, False if skipped/failed (gen continues)."""
417
+ if not lora_name or lora_name == LORA_NONE:
418
+ return False
419
+ spec = LORA_REPOS.get(lora_name)
420
+ if not spec:
421
+ logger.warning(f"LoRA '{lora_name}' not in LORA_REPOS; skipping")
422
+ return False
423
+ lora_repo, lora_file = spec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  try:
425
+ logger.info(f"Loading LoRA {lora_name} from {lora_repo}/{lora_file}")
426
+ lora_path = hf_hub_download(repo_id=lora_repo, filename=lora_file, token=_hf_token())
427
+ pipe.load_lora_weights(lora_path)
428
+ logger.info(f"✅ LoRA applied: {lora_name}")
429
+ return True
430
  except Exception as e:
431
+ logger.error(f"⚠️ LoRA load failed for {lora_name}: {e} (continuing A+B blend alone)")
432
+ return False
433
+
 
 
 
 
 
 
 
 
 
434
 
435
+ def load_sdxl_pipe(model_name: str, lora_name: str = LORA_NONE):
436
+ """Load one Adult SDXL chef. Unloads previous. Optional LoRA after load."""
437
+ global _pipe, _loaded_key
438
+ key = f"{model_name}::{lora_name}"
439
+ if _loaded_key == key and _pipe is not None:
440
+ return _pipe
441
 
 
 
 
 
442
  from diffusers import StableDiffusionXLPipeline, EulerDiscreteScheduler
443
+
444
  unload_all()
445
  dtype = torch.float16 if torch.cuda.is_available() else torch.float32
446
  logger.info(f"Loading Adult SDXL chef: {model_name}")
 
452
  )
453
  else:
454
  repo = MODEL_REPOS.get(model_name, HF_REPO_ID)
455
+ path = hf_hub_download(repo_id=repo, filename=model_name, token=_hf_token())
456
  pipe = StableDiffusionXLPipeline.from_single_file(
457
  path,
458
  torch_dtype=dtype,
 
468
  if hasattr(pipe, "vae"):
469
  pipe.vae.enable_slicing()
470
  pipe.vae.enable_tiling()
471
+ _apply_lora(pipe, lora_name)
472
+ _pipe = pipe
473
+ _loaded_key = key
474
  return pipe
475
 
476
 
 
480
  return ", ".join([p for p in parts if p])
481
 
482
 
483
+ def resolve_stack_slots(stack_name, chef_a, chef_b, lora_name, blend):
484
+ """Use Advanced overrides when set; fill gaps from named stack."""
485
+ cfg = STACKS.get(stack_name) or STACKS[DEFAULT_STACK]
486
+ a = chef_a if chef_a and str(chef_a).strip() else cfg["chef_a"]
487
+ b = chef_b if chef_b and str(chef_b).strip() else cfg["chef_b"]
488
+ lora = lora_name if lora_name else cfg["lora"]
489
+ try:
490
+ strength = float(blend)
491
+ except (TypeError, ValueError):
492
+ strength = float(cfg["blend"])
493
+ strength = max(0.05, min(0.95, strength))
494
+ return a, b, lora, strength
495
+
496
+
497
+ @spaces.GPU(duration=240) if SPACES_AVAILABLE else (lambda f: f)
498
  def generate(
 
499
  prompt,
500
  styles,
501
  negative,
502
+ stack_name,
503
+ chef_a,
504
+ chef_b,
505
+ lora_name,
506
+ blend,
507
  steps,
508
  cfg,
509
  seed,
 
524
  if not ok:
525
  gr.Warning("Restricted content")
526
  return None, "❌ Restricted content", "", []
527
+
528
  batch = int(max(1, min(3, int(batch))))
529
  seeds = [
530
  random.randint(0, 2**31 - 1) if int(seed) < 0 else int(seed) + i
 
534
  neg = BASE_NEGATIVES
535
  if negative and str(negative).strip():
536
  neg = f"{BASE_NEGATIVES}, {negative.strip()}"
537
+
538
+ model_a, model_b, lora, strength = resolve_stack_slots(
539
+ stack_name, chef_a, chef_b, lora_name, blend
540
+ )
541
+ use_steps = int(max(10, min(40, int(steps))))
542
+ remix_steps = max(10, int(use_steps * 0.75))
543
  LAST_GEN_TIME[user_key] = now
544
  gallery = []
545
  jobs = []
546
+ device = "cuda" if torch.cuda.is_available() else "cpu"
547
+
548
  try:
549
+ # Pass A: txt2img with Chef A (no LoRA on A unless A==B and LoRA set later)
550
+ pipe_a = load_sdxl_pipe(model_a, LORA_NONE)
551
+ base_images = []
552
+ for s in seeds:
553
+ gen = torch.Generator(device=device).manual_seed(int(s))
554
+ out = pipe_a(
555
+ prompt=enhanced,
556
+ negative_prompt=neg,
557
+ num_inference_steps=use_steps,
558
+ guidance_scale=float(cfg),
559
+ width=int(width),
560
+ height=int(height),
561
+ generator=gen,
562
+ ).images[0]
563
+ base_images.append((out, s))
564
+
565
+ # Unload A, load B (+ optional LoRA), img2img remix
566
+ unload_all()
567
+ from diffusers import StableDiffusionXLImg2ImgPipeline
568
+
569
+ pipe_b = load_sdxl_pipe(model_b, lora)
570
+ img2img = StableDiffusionXLImg2ImgPipeline(
571
+ vae=pipe_b.vae,
572
+ text_encoder=pipe_b.text_encoder,
573
+ text_encoder_2=pipe_b.text_encoder_2,
574
+ tokenizer=pipe_b.tokenizer,
575
+ tokenizer_2=pipe_b.tokenizer_2,
576
+ unet=pipe_b.unet,
577
+ scheduler=pipe_b.scheduler,
578
+ )
579
+ if torch.cuda.is_available():
580
+ img2img = img2img.to("cuda")
581
+
582
+ remixed = []
583
+ for img, s in base_images:
584
+ gen = torch.Generator(device=device).manual_seed(int(s) + 17)
585
+ out = img2img(
586
+ image=img,
587
+ prompt=enhanced,
588
+ negative_prompt=neg,
589
+ strength=float(strength),
590
+ num_inference_steps=remix_steps,
591
+ guidance_scale=float(cfg),
592
+ generator=gen,
593
+ ).images[0]
594
+ remixed.append((out, s))
595
+ del img2img
596
+ gc.collect()
597
+ if torch.cuda.is_available():
598
+ torch.cuda.empty_cache()
599
+
600
+ lora_tag = lora if lora and lora != LORA_NONE else "no-LoRA"
601
+ chain = f"{stack_name} | A:{model_a} → B:{model_b}@{strength:.2f} | {lora_tag}"
602
+ for out, s in remixed:
603
+ label = f"seed {s} · {stack_name} · A→B {strength:.2f}"
604
+ gallery.append((out, label))
605
+ jobs.append((out, s, chain, prompt, username, width, height, use_steps, cfg))
606
+
607
+ info = (
608
+ f"{chain} | seeds {seeds} | {width}x{height} | "
609
+ f"steps A={use_steps} B={remix_steps} | enhanced prompt ready"
610
+ )
611
  return gallery, info, enhanced, jobs
612
  except Exception as e:
613
  logger.exception("generate failed")
 
617
  return None, f"❌ {msg}", "", []
618
 
619
 
620
+ def on_stack_change(stack_name):
621
+ cfg = STACKS.get(stack_name) or STACKS[DEFAULT_STACK]
622
+ return (
623
+ gr.update(value=cfg["chef_a"]),
624
+ gr.update(value=cfg["chef_b"]),
625
+ gr.update(value=cfg["lora"]),
626
+ gr.update(value=cfg["blend"]),
627
+ )
628
+
629
+
630
  js_visitor = """
631
  function() {
632
  let id = localStorage.getItem('adult_ai_visitor_id');
 
647
 
648
 
649
  def create_ui():
650
+ default_cfg = STACKS[DEFAULT_STACK]
651
  with gr.Blocks() as demo:
652
  user_id = gr.Textbox(visible=False)
653
+ gr.Markdown("# ⚡ 21+ Adult East/SE Asian Lab · Fiction Space · Multi-chef stacks")
654
  gr.Markdown(
655
  """### 21+ only · Fiction Space · ZeroGPU · first model load is slow · queues happen
656
 
657
  **Scene + body lab for adult East / Southeast Asian women (21+, late 20s fiction).**
658
  Body lock: tall slim small frame, tiny waist, large/full/heavy breasts, cleavage. No thick thighs.
659
+ Every gen runs **Chef A txt2img → Chef B img2img remix** (no single-chef path).
660
+ Pick a named **stack** chip, or override slots in Advanced.
661
  Style packs stack: start with **Testing Tits**, layer scene packs (Cosmic e-girl booth, night club, hotel mirror, rain alley, Soft Domme, Quiet beg, library whiskey couch).
662
+ Visitor gens are **not** saved unless secret `LOG_VISITOR_UPLOADS=1`."""
663
  )
664
  with gr.Row():
665
  with gr.Column(scale=2):
666
+ prompt = gr.Textbox(
667
+ label="Prompt (scene + body)",
668
+ value=random.choice(RANDOM_PROMPTS),
669
+ lines=7,
670
+ )
671
  negative = gr.Textbox(
672
+ label="Negatives",
673
  value=BASE_NEGATIVES,
674
  lines=4,
675
  )
 
678
  choices=list(STYLE_PACKS.keys()),
679
  value=["Testing Tits"],
680
  )
681
+ stack = gr.Radio(
682
+ label="Stack (named multi-chef chips)",
683
+ choices=STACK_CHOICES,
684
+ value=DEFAULT_STACK,
685
+ )
686
  go = gr.Button("GENERATE", variant="primary", size="lg")
687
  with gr.Column(scale=1):
688
+ with gr.Accordion("Advanced (override stack slots)", open=False):
689
+ chef_a = gr.Dropdown(
690
+ label="Chef A (txt2img base)",
691
+ choices=SDXL_CHOICES,
692
+ value=default_cfg["chef_a"],
693
+ )
694
+ chef_b = gr.Dropdown(
695
+ label="Chef B (img2img remix)",
696
+ choices=SDXL_CHOICES,
697
+ value=default_cfg["chef_b"],
698
+ )
699
+ lora_dd = gr.Dropdown(
700
+ label="LoRA (None = blend only)",
701
+ choices=LORA_CHOICES,
702
+ value=default_cfg["lora"],
703
+ )
704
+ blend_sl = gr.Slider(
705
+ label="Blend A→B strength (higher = more Chef B)",
706
+ minimum=0.30,
707
+ maximum=0.70,
708
+ value=default_cfg["blend"],
709
+ step=0.05,
710
+ )
711
  seed = gr.Number(label="Seed (-1 random)", value=-1, precision=0)
712
  width = gr.Slider(512, 1536, value=1024, step=64, label="Width")
713
  height = gr.Slider(512, 1536, value=1024, step=64, label="Height")
714
+ steps = gr.Slider(10, 40, value=28, step=1, label="Steps")
715
+ cfg = gr.Slider(1.0, 12.0, value=6.0, step=0.5, label="CFG")
716
  batch = gr.Slider(1, 3, value=1, step=1, label="Batch (max 3)")
717
  gallery = gr.Gallery(label="Outputs", columns=3, height="auto")
718
  info = gr.Textbox(label="Run info", visible=True)
719
  meta = gr.Textbox(label="Enhanced prompt", visible=True)
720
  demo.load(fn=None, inputs=None, outputs=user_id, js=js_visitor)
721
 
722
+ stack.change(
723
+ fn=on_stack_change,
724
+ inputs=[stack],
725
+ outputs=[chef_a, chef_b, lora_dd, blend_sl],
726
+ )
727
+
728
  def on_go(*args):
729
  images, run_info, enhanced, jobs = generate(*args)
730
  if jobs:
 
736
 
737
  go.click(
738
  fn=on_go,
739
+ inputs=[
740
+ prompt,
741
+ styles,
742
+ negative,
743
+ stack,
744
+ chef_a,
745
+ chef_b,
746
+ lora_dd,
747
+ blend_sl,
748
+ steps,
749
+ cfg,
750
+ seed,
751
+ width,
752
+ height,
753
+ batch,
754
+ user_id,
755
+ ],
756
  outputs=[gallery, info, meta],
757
  )
758
  return demo