Professional Noob commited on
Commit
9763378
·
verified ·
1 Parent(s): c16eecf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +670 -353
app.py CHANGED
@@ -2,16 +2,19 @@ import os
2
  import re
3
  import gc
4
  import traceback
5
- import random
6
- from typing import Iterable, Optional
7
-
8
  import gradio as gr
9
  import numpy as np
10
  import spaces
11
  import torch
12
- from PIL import Image
 
 
 
 
 
 
 
13
 
14
- from transformers import AutoImageProcessor, AutoModelForDepthEstimation
15
  from huggingface_hub import hf_hub_download
16
  from safetensors.torch import load_file as safetensors_load_file
17
 
@@ -37,6 +40,7 @@ colors.orange_red = colors.Color(
37
  c950="#802200",
38
  )
39
 
 
40
  class OrangeRedTheme(Soft):
41
  def __init__(
42
  self,
@@ -92,6 +96,7 @@ class OrangeRedTheme(Soft):
92
  block_label_background_fill="*primary_200",
93
  )
94
 
 
95
  orange_red_theme = OrangeRedTheme()
96
 
97
  # ============================================================
@@ -99,6 +104,7 @@ orange_red_theme = OrangeRedTheme()
99
  # ============================================================
100
 
101
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
102
  print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
103
  print("torch.__version__ =", torch.__version__)
104
  print("torch.version.cuda =", torch.version.cuda)
@@ -109,18 +115,17 @@ if torch.cuda.is_available():
109
  print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))
110
  print("Using device:", device)
111
 
112
- dtype = torch.bfloat16
113
- MAX_SEED = np.iinfo(np.int32).max
114
-
115
  # ============================================================
116
  # AIO version (Space variable)
117
  # ============================================================
118
 
119
  AIO_REPO_ID = "Pr0f3ssi0n4ln00b/Phr00t-Qwen-Rapid-AIO"
120
  DEFAULT_AIO_VERSION = "v19"
 
121
  _VER_RE = re.compile(r"^v\d+$")
122
  _DIGITS_RE = re.compile(r"^\d+$")
123
 
 
124
  def _normalize_version(raw: str) -> Optional[str]:
125
  if raw is None:
126
  return None
@@ -129,14 +134,18 @@ def _normalize_version(raw: str) -> Optional[str]:
129
  return None
130
  if _VER_RE.fullmatch(s):
131
  return s
 
132
  if _DIGITS_RE.fullmatch(s):
133
  return f"v{s}"
134
  return None
135
 
 
136
  _AIO_ENV_RAW = os.environ.get("AIO_VERSION", "")
137
  _AIO_ENV_NORM = _normalize_version(_AIO_ENV_RAW)
 
138
  AIO_VERSION = _AIO_ENV_NORM or DEFAULT_AIO_VERSION
139
  AIO_VERSION_SOURCE = "env" if _AIO_ENV_NORM else "default(v19)"
 
140
  print(f"AIO_VERSION (env raw) = {_AIO_ENV_RAW!r}")
141
  print(f"AIO_VERSION (normalized) = {_AIO_ENV_NORM!r}")
142
  print(f"Using AIO_VERSION = {AIO_VERSION} ({AIO_VERSION_SOURCE})")
@@ -150,9 +159,12 @@ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
150
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
151
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
152
 
 
 
 
153
  def _load_pipe_with_version(version: str) -> QwenImageEditPlusPipeline:
154
  sub = f"{version}/transformer"
155
- print(f"Loading AIO transformer: {AIO_REPO_ID} / {sub}")
156
  p = QwenImageEditPlusPipeline.from_pretrained(
157
  "Qwen/Qwen-Image-Edit-2511",
158
  transformer=QwenImageTransformer2DModel.from_pretrained(
@@ -165,9 +177,11 @@ def _load_pipe_with_version(version: str) -> QwenImageEditPlusPipeline:
165
  ).to(device)
166
  return p
167
 
 
 
168
  try:
169
  pipe = _load_pipe_with_version(AIO_VERSION)
170
- except Exception:
171
  print("❌ Failed to load requested AIO_VERSION. Falling back to v19.")
172
  print("---- exception ----")
173
  print(traceback.format_exc())
@@ -176,12 +190,15 @@ except Exception:
176
  AIO_VERSION_SOURCE = "fallback_to_v19"
177
  pipe = _load_pipe_with_version(AIO_VERSION)
178
 
 
179
  try:
180
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
181
  print("Flash Attention 3 Processor set successfully.")
182
  except Exception as e:
183
  print(f"Warning: Could not set FA3 processor: {e}")
184
 
 
 
185
  # ============================================================
186
  # VAE tiling toggle (UI-controlled; OFF by default)
187
  # ============================================================
@@ -189,7 +206,9 @@ except Exception as e:
189
  def _apply_vae_tiling(enabled: bool):
190
  """
191
  Toggle VAE tiling on the global pipeline.
 
192
  This does NOT require a Space restart; it applies to the next pipe(...) call.
 
193
  """
194
  try:
195
  if enabled:
@@ -204,20 +223,25 @@ def _apply_vae_tiling(enabled: bool):
204
  else:
205
  if hasattr(pipe, "disable_vae_tiling"):
206
  pipe.disable_vae_tiling()
207
- print("VAE tiling DISABLED (per UI).")
208
  elif hasattr(pipe, "vae") and hasattr(pipe.vae, "disable_tiling"):
209
  pipe.vae.disable_tiling()
210
- print("VAE tiling DISABLED via pipe.vae.disable_tiling() (per UI).")
211
  else:
 
212
  print("⚠️ No disable_vae_tiling()/vae.disable_tiling() found; leaving current state unchanged.")
213
  except Exception as e:
214
  print(f"⚠️ VAE tiling toggle failed: {e}")
215
 
216
  # ============================================================
217
- # Derived conditioning (Depth only) — ViTPose REMOVED
218
  # ============================================================
 
 
219
 
220
  DEPTH_MODEL_ID = "depth-anything/Depth-Anything-V2-Small-hf"
 
 
221
  _DEPTH_CACHE = {}
222
 
223
  def _derived_device(use_gpu: bool) -> torch.device:
@@ -233,32 +257,27 @@ def _load_depth_models(dev: torch.device):
233
  _DEPTH_CACHE[key] = (proc, model)
234
  return _DEPTH_CACHE[key]
235
 
 
236
  def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image:
237
- img = img.convert("RGB")
238
  dev = _derived_device(use_gpu)
239
  proc, model = _load_depth_models(dev)
240
 
241
- inputs = proc(images=img, return_tensors="pt")
242
- inputs = {k: v.to(dev) for k, v in inputs.items()}
243
-
244
- with torch.no_grad():
245
- out = model(**inputs)
246
 
247
- pred = out.predicted_depth # (B,H,W)
248
- pred = torch.nn.functional.interpolate(
249
- pred.unsqueeze(1),
250
- size=(img.height, img.width),
251
  mode="bicubic",
252
  align_corners=False,
253
  ).squeeze(1)[0]
254
 
255
- arr = pred.detach().float().cpu().numpy()
256
- arr = arr - float(arr.min())
257
- denom = float(arr.max()) + 1e-8
258
- arr = arr / denom
259
-
260
- depth8 = (arr * 255.0).clip(0, 255).astype(np.uint8)
261
- return Image.fromarray(depth8, mode="L").convert("RGB")
262
 
263
  # ============================================================
264
  # LoRA adapters + presets
@@ -284,7 +303,7 @@ ADAPTER_SPECS = {
284
  "AnyPose": {
285
  "type": "package",
286
  "requires_two_images": True,
287
- "image2_label": "Picture 2 (Pose Reference)",
288
  "parts": [
289
  {
290
  "repo": "lilylilith/AnyPose",
@@ -324,22 +343,22 @@ ADAPTER_SPECS = {
324
  "BFS-Best-FaceSwap": {
325
  "type": "single",
326
  "requires_two_images": True,
327
- "image2_label": "Picture 2 (Head/Face Donor)",
328
  "repo": "Alissonerdx/BFS-Best-Face-Swap",
329
  "weights": "bfs_head_v5_2511_original.safetensors",
330
  "adapter_name": "BFS-Best-Faceswap",
331
  "strength": 1.0,
332
- "needs_alpha_fix": True,
333
  },
334
  "BFS-Best-FaceSwap-merge": {
335
  "type": "single",
336
  "requires_two_images": True,
337
- "image2_label": "Picture 2 (Head/Face Donor)",
338
  "repo": "Alissonerdx/BFS-Best-Face-Swap",
339
  "weights": "bfs_head_v5_2511_merged_version_rank_32_fp32.safetensors",
340
  "adapter_name": "BFS-Best-Faceswap-merge",
341
  "strength": 1.1,
342
- "needs_alpha_fix": True,
343
  },
344
  "F2P": {
345
  "type": "single",
@@ -417,64 +436,75 @@ ADAPTER_SPECS = {
417
  LORA_PRESET_PROMPTS = {
418
  "Any2Real_2601": "change the picture 1 to realistic photograph",
419
  "Semirealistic-photo-detailer": "transform the image to semi-realistic image",
420
- "AnyPose": (
421
- "Make the person in image 1 do the exact same pose of the person in image 2. "
422
- "Changing the style and background of the image of the person in image 1 is undesirable, so don't do it. "
423
- "The new pose should be pixel accurate to the pose we are trying to copy. "
424
- "Change the field of view and angle to match exactly image 2."
425
- ),
426
- "Hyperrealistic-Portrait": (
427
- "Transform the image into an ultra-realistic photorealistic portrait with strict identity preservation, "
428
- "facing straight to the camera. Enhance pore-level skin textures, realistic moisture effects, and natural wet hair clumping. "
429
- "Use shallow depth of field with a clean background."
430
- ),
431
- "Ultrarealistic-Portrait": (
432
- "Transform the image into an ultra-realistic glamour portrait while strictly preserving the subject’s identity. "
433
- "Enhance cinematic directional lighting and keep realism without over-smoothing."
434
- ),
435
  "Upscale2K": "Upscale this picture to 4K resolution.",
436
- "BFS-Best-FaceSwap": (
437
- "head_swap: start with Picture 1 as the base image. replace the head with Picture 2, preserving identity of Picture 2. "
438
- "copy eye direction and micro-expressions from Picture 1. high quality, sharp details, 4k"
439
- ),
440
- "BFS-Best-FaceSwap-merge": (
441
- "head_swap: start with Picture 1 as the base image. replace the head with Picture 2, preserving identity of Picture 2. "
442
- "copy eye direction and micro-expressions from Picture 1. high quality, sharp details, 4k"
443
- ),
444
  }
445
 
 
446
  LOADED_ADAPTERS = set()
447
 
448
  # ============================================================
449
  # Helpers: resolution
450
  # ============================================================
451
 
 
 
 
452
  def _round_to_multiple(x: int, m: int) -> int:
453
- m = max(1, int(m))
454
  return max(m, (int(x) // m) * m)
455
 
456
- def compute_canvas_dimensions_from_area(image: Image.Image, target_area: int, multiple_of: int) -> tuple[int, int]:
 
 
 
 
 
 
 
 
457
  w, h = image.size
458
  aspect = w / h if h else 1.0
 
 
459
  from qwenimage.pipeline_qwenimage_edit_plus import calculate_dimensions
460
- width, height = calculate_dimensions(int(target_area), float(aspect), multiple=int(multiple_of))
 
461
  width = _round_to_multiple(int(width), int(multiple_of))
462
  height = _round_to_multiple(int(height), int(multiple_of))
463
  return width, height
464
 
465
- def get_target_area_for_lora(image: Image.Image, lora_adapter: str, user_target_megapixels: float) -> int:
 
 
 
 
 
 
 
 
 
 
 
466
  spec = ADAPTER_SPECS.get(lora_adapter, {})
 
467
  if "target_area" in spec:
468
  try:
469
  return int(spec["target_area"])
470
  except Exception:
471
  pass
 
472
  if "target_megapixels" in spec:
473
  try:
474
  mp = float(spec["target_megapixels"])
475
  return int(mp * 1024 * 1024)
476
  except Exception:
477
  pass
 
 
478
  if "target_long_edge" in spec:
479
  try:
480
  long_edge = int(spec["target_long_edge"])
@@ -488,50 +518,109 @@ def get_target_area_for_lora(image: Image.Image, lora_adapter: str, user_target_
488
  return int(new_w * new_h)
489
  except Exception:
490
  pass
491
- return int(float(user_target_megapixels) * 1024 * 1024)
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
  # ============================================================
494
- # Helpers: gallery normalization
495
  # ============================================================
496
 
 
 
 
 
 
 
 
 
 
497
  def _to_pil_rgb(x) -> Optional[Image.Image]:
 
 
 
 
498
  if x is None:
499
  return None
 
 
500
  if isinstance(x, tuple) and len(x) >= 1:
501
  x = x[0]
502
- if x is None:
503
- return None
 
504
  if isinstance(x, Image.Image):
505
  return x.convert("RGB")
 
506
  if isinstance(x, np.ndarray):
507
  return Image.fromarray(x).convert("RGB")
 
 
508
  try:
509
  return Image.fromarray(np.array(x)).convert("RGB")
510
  except Exception:
511
  return None
512
 
513
- def _append_to_gallery(existing, new_img: Image.Image):
514
- items = []
515
- if existing:
516
- for it in existing:
517
- pil = _to_pil_rgb(it)
518
- if pil is not None:
519
- items.append(pil)
520
- items.append(new_img)
521
- return items
522
 
523
- def lora_requires_two_images(lora_adapter: str) -> bool:
524
- return bool(ADAPTER_SPECS.get(lora_adapter, {}).get("requires_two_images", False))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
 
526
- def image2_label_for_lora(lora_adapter: str) -> str:
527
- return str(ADAPTER_SPECS.get(lora_adapter, {}).get("image2_label", "Picture 2"))
528
 
529
  # ============================================================
530
- # Helpers: BFS alpha key fix / strict filtering for merged safetensors
531
  # ============================================================
532
 
 
533
  def _inject_missing_alpha_keys(state_dict: dict) -> dict:
 
 
 
 
 
 
 
 
 
534
  bases = {}
 
535
  for k, v in state_dict.items():
536
  if not isinstance(v, torch.Tensor):
537
  continue
@@ -542,17 +631,37 @@ def _inject_missing_alpha_keys(state_dict: dict) -> dict:
542
 
543
  for base, rank in bases.items():
544
  alpha_tensor = torch.tensor(float(rank), dtype=torch.float32)
 
545
  full_alpha = f"{base}.alpha"
546
  if full_alpha not in state_dict:
547
  state_dict[full_alpha] = alpha_tensor
 
548
  if base.startswith("diffusion_model."):
549
  stripped_base = base[len("diffusion_model.") :]
550
  stripped_alpha = f"{stripped_base}.alpha"
551
  if stripped_alpha not in state_dict:
552
  state_dict[stripped_alpha] = alpha_tensor
 
553
  return state_dict
554
 
 
555
  def _filter_to_diffusers_lora_keys(state_dict: dict) -> tuple[dict, dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  keep_suffixes = (
557
  ".lora_up.weight",
558
  ".lora_down.weight",
@@ -560,29 +669,37 @@ def _filter_to_diffusers_lora_keys(state_dict: dict) -> tuple[dict, dict]:
560
  ".alpha",
561
  ".lora_alpha",
562
  )
 
563
  dropped_patch = 0
564
  dropped_other = 0
565
  kept = 0
566
  normalized_alpha = 0
567
 
568
- out = {}
569
  for k, v in state_dict.items():
570
  if not isinstance(v, torch.Tensor):
 
571
  dropped_other += 1
572
  continue
 
 
573
  if k.endswith(".diff") or k.endswith(".diff_b"):
574
  dropped_patch += 1
575
  continue
 
576
  if not k.endswith(keep_suffixes):
577
  dropped_other += 1
578
  continue
 
579
  if k.endswith(".lora_alpha"):
 
580
  base = k[: -len(".lora_alpha")]
581
  k2 = f"{base}.alpha"
582
  out[k2] = v.float() if v.dtype != torch.float32 else v
583
  normalized_alpha += 1
584
  kept += 1
585
  continue
 
586
  out[k] = v
587
  kept += 1
588
 
@@ -594,7 +711,15 @@ def _filter_to_diffusers_lora_keys(state_dict: dict) -> tuple[dict, dict]:
594
  }
595
  return out, stats
596
 
 
597
  def _duplicate_stripped_prefix_keys(state_dict: dict, prefix: str = "diffusion_model.") -> dict:
 
 
 
 
 
 
 
598
  out = dict(state_dict)
599
  for k, v in list(state_dict.items()):
600
  if not k.startswith(prefix):
@@ -604,11 +729,18 @@ def _duplicate_stripped_prefix_keys(state_dict: dict, prefix: str = "diffusion_m
604
  out[stripped] = v
605
  return out
606
 
 
607
  def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name: str, needs_alpha_fix: bool = False):
 
 
 
 
608
  try:
609
  pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=adapter_name)
610
  return
611
  except (KeyError, ValueError) as e:
 
 
612
  if not needs_alpha_fix:
613
  raise
614
 
@@ -616,15 +748,34 @@ def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name:
616
  "⚠️ LoRA load failed (will try safe dict fallback). "
617
  f"Adapter={adapter_name!r} file={weight_name!r} error={type(e).__name__}: {e}"
618
  )
 
619
  local_path = hf_hub_download(repo_id=repo, filename=weight_name)
620
  sd = safetensors_load_file(local_path)
 
 
621
  sd = _inject_missing_alpha_keys(sd)
 
 
622
  sd, stats = _filter_to_diffusers_lora_keys(sd)
 
 
623
  sd = _duplicate_stripped_prefix_keys(sd)
624
- print("LoRA dict stats:", stats)
 
 
 
 
 
 
625
  pipe.load_lora_weights(sd, adapter_name=adapter_name)
626
  return
627
 
 
 
 
 
 
 
628
  def _ensure_loaded_and_get_active_adapters(selected_lora: str):
629
  spec = ADAPTER_SPECS.get(selected_lora)
630
  if not spec:
@@ -637,356 +788,522 @@ def _ensure_loaded_and_get_active_adapters(selected_lora: str):
637
  parts = spec.get("parts", [])
638
  if not parts:
639
  raise gr.Error(f"Package spec has no parts: {selected_lora}")
 
640
  for part in parts:
641
  repo = part["repo"]
642
  weights = part["weights"]
643
- name = part["adapter_name"]
644
  strength = float(part.get("strength", 1.0))
645
  needs_alpha_fix = bool(part.get("needs_alpha_fix", False))
646
 
647
- if name not in LOADED_ADAPTERS:
648
- _load_lora_weights_with_fallback(repo, weights, name, needs_alpha_fix=needs_alpha_fix)
649
- LOADED_ADAPTERS.add(name)
 
 
 
 
 
 
 
 
 
 
 
650
 
651
- adapter_names.append(name)
652
  adapter_weights.append(strength)
653
 
654
  else:
655
  repo = spec["repo"]
656
  weights = spec["weights"]
657
- name = spec["adapter_name"]
658
  strength = float(spec.get("strength", 1.0))
659
  needs_alpha_fix = bool(spec.get("needs_alpha_fix", False))
660
 
661
- if name not in LOADED_ADAPTERS:
662
- _load_lora_weights_with_fallback(repo, weights, name, needs_alpha_fix=needs_alpha_fix)
663
- LOADED_ADAPTERS.add(name)
 
 
 
 
 
 
 
 
 
 
 
664
 
665
- adapter_names.append(name)
666
- adapter_weights.append(strength)
667
 
668
  return adapter_names, adapter_weights
669
 
 
670
  # ============================================================
671
- # UI helpers
672
  # ============================================================
673
 
674
- def _fmt_img_info(img: Optional[Image.Image]) -> str:
675
- if img is None:
676
- return "—"
677
- w, h = img.size
678
- mp = (w * h) / (1024 * 1024)
679
- ar = (w / h) if h else 0
680
- return f"**{w}×{h}** • **{mp:.2f} MP** • **AR {ar:.3f}**"
681
-
682
- def _bfs_tooltip(selected_lora: str) -> gr.Update:
683
- if selected_lora in ("BFS-Best-FaceSwap", "BFS-Best-FaceSwap-merge"):
684
- return gr.update(
685
- visible=True,
686
- value="ℹ️ **BFS FaceSwap:** Picture 1 = **Base** (scene), Picture 2 = **Donor** (head/face).",
687
- )
688
- if selected_lora == "AnyPose":
689
- return gr.update(
690
- visible=True,
691
- value="ℹ️ **AnyPose:** Picture 1 = **Subject**, Picture 2 = **Pose reference**.",
692
- )
693
- return gr.update(visible=False, value="")
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  # ============================================================
696
- # Inference
697
  # ============================================================
698
 
699
- def _seed_everything(seed: int):
700
- random.seed(seed)
701
- np.random.seed(seed)
702
- torch.manual_seed(seed)
703
- if torch.cuda.is_available():
704
- torch.cuda.manual_seed_all(seed)
705
 
706
- @spaces.GPU
707
- def infer(
708
- img1: Image.Image,
709
- img2: Optional[Image.Image],
710
- extra_gallery,
711
- prompt: str,
712
- lora_adapter: str,
713
- seed: int,
714
- randomize_seed: bool,
715
- guidance_scale: float,
716
- steps: int,
717
- target_megapixels: float,
718
- use_input_area: bool,
719
- keep_2x_output: bool,
720
- vae_tiling: bool,
721
- extras_condition_only: bool,
722
- resolution_multiple: int,
723
- vae_ref_megapixels: float,
724
- use_depth: bool,
725
- derived_on_gpu: bool,
726
- ):
727
- if img1 is None:
728
- raise gr.Error("Picture 1 is required.")
729
 
730
- img1 = img1.convert("RGB")
731
- img2 = img2.convert("RGB") if img2 is not None else None
 
 
732
 
733
- # Seed
734
- if randomize_seed:
735
- seed = random.randint(0, MAX_SEED)
736
- seed = int(seed) % MAX_SEED
737
- _seed_everything(seed)
738
 
739
- # VAE tiling toggle
740
- _apply_vae_tiling(bool(vae_tiling))
 
 
741
 
742
- # Load / activate LoRA
743
- if lora_adapter != NONE_LORA:
744
- adapter_names, adapter_weights = _ensure_loaded_and_get_active_adapters(lora_adapter)
745
- pipe.set_adapters(adapter_names, adapter_weights)
746
- else:
747
- try:
748
- pipe.set_adapters([])
749
- except Exception:
750
- pass
751
-
752
- # Images list: Picture1, Picture2 (optional), extras..., derived (optional)
753
- images = [img1]
754
- base_count = 1
755
 
756
- if lora_requires_two_images(lora_adapter):
757
- if img2 is None:
758
- raise gr.Error(f"{lora_adapter} requires Picture 2.")
759
- images.append(img2)
760
- base_count = 2
761
- else:
762
- img2 = None # ignore if not needed
763
-
764
- extras = []
765
- if extra_gallery:
766
- for it in extra_gallery:
767
- p = _to_pil_rgb(it)
768
- if p is not None:
769
- extras.append(p)
770
- images.extend(extras)
771
-
772
- derived_preview = None
773
- derived_index = None
774
- if use_depth:
775
- derived_preview = make_depth_map(img1, use_gpu=bool(derived_on_gpu))
776
- images.append(derived_preview)
777
- derived_index = len(images) - 1
778
-
779
- # Canvas sizing
780
- res_mult = int(resolution_multiple)
781
- if use_input_area or float(target_megapixels) <= 0.0:
782
- target_area = int(img1.width * img1.height)
783
- else:
784
- target_area = int(get_target_area_for_lora(img1, lora_adapter, float(target_megapixels)))
785
 
786
- base_w, base_h = compute_canvas_dimensions_from_area(img1, target_area, res_mult)
 
787
 
788
- # Generate at 2x, then downsample unless keep_2x_output
789
- gen_w, gen_h = int(base_w * 2), int(base_h * 2)
790
 
791
- # Extra refs routing (VAE vs conditioning-only)
792
- if extras_condition_only:
793
- vae_indices = list(range(base_count))
794
  else:
795
- vae_indices = list(range(len(images)))
796
-
797
- # Derived depth should ALWAYS be conditioning-only
798
- if derived_index is not None and derived_index in vae_indices:
799
- vae_indices = [i for i in vae_indices if i != derived_index]
800
-
801
- # VAE ref size override for extras only
802
- vae_ref_area = None
803
- if float(vae_ref_megapixels) > 0.0:
804
- vae_ref_area = int(float(vae_ref_megapixels) * 1024 * 1024)
805
-
806
- # Run
807
- out = pipe(
808
- image=images,
809
- prompt=prompt,
810
- true_cfg_scale=float(guidance_scale),
811
- num_inference_steps=int(steps),
812
- width=int(gen_w),
813
- height=int(gen_h),
814
- pad_to_canvas=True,
815
- vae_image_indices=vae_indices,
816
- resolution_multiple=int(res_mult),
817
- vae_ref_area=vae_ref_area,
818
- vae_ref_start_index=int(base_count),
819
- generator=torch.Generator(device=device).manual_seed(seed),
820
- )
821
 
822
- result = out.images[0] if hasattr(out, "images") else out[0][0]
823
- if isinstance(result, np.ndarray):
824
- result = Image.fromarray(result)
825
 
826
- result = result.convert("RGB")
827
 
828
- if not keep_2x_output:
829
- result = result.resize((base_w, base_h), Image.Resampling.LANCZOS)
 
830
 
831
- # Cleanup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  gc.collect()
833
  if torch.cuda.is_available():
834
  torch.cuda.empty_cache()
835
 
836
- return result, seed, derived_preview
 
837
 
838
- # ============================================================
839
- # UI
840
- # ============================================================
 
 
 
 
 
 
 
841
 
842
- def _on_lora_change(selected_lora: str):
843
- # Prompt preset
844
- preset = LORA_PRESET_PROMPTS.get(selected_lora, "")
845
- prompt_update = gr.update(value=preset) if preset else gr.update()
846
 
847
- # Picture 2 visibility/label
848
- if lora_requires_two_images(selected_lora):
849
- img2_update = gr.update(visible=True, label=image2_label_for_lora(selected_lora))
850
- else:
851
- img2_update = gr.update(visible=True, label="Picture 2") # keep visible, but optional
852
- tooltip_update = _bfs_tooltip(selected_lora)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
 
854
- return prompt_update, img2_update, tooltip_update
 
 
 
 
 
855
 
856
- def _out_to_pic1(out_img):
857
- return gr.update(value=out_img)
 
858
 
859
- def _out_to_pic2(out_img):
860
- return gr.update(value=out_img)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
 
862
- def _out_to_extras(existing, out_img):
863
- if out_img is None:
864
- return gr.update()
865
- return gr.update(value=_append_to_gallery(existing, out_img))
866
 
867
- with gr.Blocks(theme=orange_red_theme) as demo:
868
- gr.Markdown(
869
- f"""
870
- # Qwen Image Edit — Rapid AIO LoRAs (Merged)
871
- This experimental space for **QIE-2511** uses an extracted Rapid AIO transformer with LoRA support and extra routing features.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
 
873
- **Enabled features**
874
- - Optional conditioning-only routing for extra reference latents
875
- - Uncapped canvas sizing (MP-based) + **2× generation with optional downsample**
876
- - Optional **VAE tiling** (for high resolutions)
877
- - Optional **Depth mapping** for conditioning
878
- - Optional output routing back to inputs
879
 
880
- **Active AIO version:** `{AIO_VERSION}` *(source: {AIO_VERSION_SOURCE})*
 
 
 
 
 
 
 
 
 
881
  """
882
- )
883
 
884
- with gr.Row():
885
- with gr.Column(scale=1):
886
- img1 = gr.Image(label="Picture 1", type="pil")
887
- img1_info = gr.Markdown("—")
888
- img2 = gr.Image(label="Picture 2", type="pil")
889
- img2_info = gr.Markdown("—")
890
-
891
- bfs_tip = gr.Markdown(visible=False)
892
-
893
- extra_gallery = gr.Gallery(
894
- label="Extra references (optional)",
895
- columns=4,
896
- height=180,
897
- )
898
-
899
- with gr.Row():
900
- use_depth = gr.Checkbox(label="Use Depth conditioning (adds a derived reference)", value=False)
901
- derived_on_gpu = gr.Checkbox(label="Run depth on GPU (if available)", value=True)
902
-
903
- derived_preview = gr.Image(label="Derived conditioning preview", interactive=False, format="png")
904
-
905
- with gr.Column(scale=1):
906
- lora_adapter = gr.Dropdown(
907
- label="LoRA",
908
- choices=[NONE_LORA] + sorted(list(ADAPTER_SPECS.keys())),
909
- value=NONE_LORA,
910
- )
911
-
912
- prompt = gr.Textbox(label="Prompt", lines=4, placeholder="Describe the edit…")
913
-
914
- with gr.Row():
915
- steps = gr.Slider(1, 80, value=40, step=1, label="Steps")
916
- guidance = gr.Slider(1.0, 10.0, value=4.0, step=0.1, label="CFG (true_cfg_scale)")
917
-
918
- with gr.Row():
919
- resolution_multiple = gr.Dropdown(
920
- label="Resolution step (LCD lattice)",
921
- choices=[32, 56, 112],
922
- value=32,
923
- )
924
- vae_ref_megapixels = gr.Slider(
925
- 0.0, 4.0, value=0.0, step=0.1,
926
- label="VAE ref MP override (extras only, 0 = off)"
927
  )
928
 
929
- with gr.Row():
930
- target_megapixels = gr.Slider(
931
- 0.0, 12.0, value=1.0, step=0.1,
932
- label="Canvas megapixels (0 = same as Picture 1)"
933
  )
934
- use_input_area = gr.Checkbox(label="Use Picture 1 pixel area", value=False)
935
 
936
- with gr.Row():
937
- keep_2x_output = gr.Checkbox(label="Keep 2× output (otherwise downsample)", value=False)
938
- extras_condition_only = gr.Checkbox(label="Route extras as conditioning-only (no VAE)", value=True)
939
 
940
- with gr.Row():
941
- vae_tiling = gr.Checkbox(label="VAE tiling", value=False)
942
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
943
 
944
- seed = gr.Number(label="Seed", value=0, precision=0)
945
 
946
- run_btn = gr.Button("Run", variant="primary")
947
- out_img = gr.Image(label="Output", type="pil")
 
 
948
 
949
- with gr.Row():
950
- to_pic1 = gr.Button("Output Picture 1")
951
- to_pic2 = gr.Button("Output → Picture 2")
952
- to_extras = gr.Button("Output → Extras (append)")
 
 
 
953
 
954
- # Live info updates
955
- img1.change(lambda x: _fmt_img_info(x), inputs=[img1], outputs=[img1_info])
956
- img2.change(lambda x: _fmt_img_info(x), inputs=[img2], outputs=[img2_info])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
 
958
- # LoRA change
959
- lora_adapter.change(_on_lora_change, inputs=[lora_adapter], outputs=[prompt, img2, bfs_tip])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
960
 
961
- # Run
962
- run_btn.click(
963
- infer,
964
  inputs=[
965
- img1,
966
- img2,
967
- extra_gallery,
968
  prompt,
969
  lora_adapter,
970
  seed,
971
  randomize_seed,
972
- guidance,
973
  steps,
974
  target_megapixels,
975
- use_input_area,
976
- keep_2x_output,
977
- vae_tiling,
978
  extras_condition_only,
 
 
979
  resolution_multiple,
980
  vae_ref_megapixels,
981
- use_depth,
982
- derived_on_gpu,
983
  ],
984
- outputs=[out_img, seed, derived_preview],
985
  )
986
 
987
  # Output routing buttons
988
- to_pic1.click(_out_to_pic1, inputs=[out_img], outputs=[img1])
989
- to_pic2.click(_out_to_pic2, inputs=[out_img], outputs=[img2])
990
- to_extras.click(_out_to_extras, inputs=[extra_gallery, out_img], outputs=[extra_gallery])
 
 
 
 
 
 
 
991
 
992
- demo.queue(max_size=32).launch()
 
 
 
 
 
 
 
 
2
  import re
3
  import gc
4
  import traceback
 
 
 
5
  import gradio as gr
6
  import numpy as np
7
  import spaces
8
  import torch
9
+ import random
10
+ from PIL import Image, ImageDraw
11
+ from typing import Iterable, Optional
12
+
13
+ from transformers import (
14
+ AutoImageProcessor,
15
+ AutoModelForDepthEstimation,
16
+ )
17
 
 
18
  from huggingface_hub import hf_hub_download
19
  from safetensors.torch import load_file as safetensors_load_file
20
 
 
40
  c950="#802200",
41
  )
42
 
43
+
44
  class OrangeRedTheme(Soft):
45
  def __init__(
46
  self,
 
96
  block_label_background_fill="*primary_200",
97
  )
98
 
99
+
100
  orange_red_theme = OrangeRedTheme()
101
 
102
  # ============================================================
 
104
  # ============================================================
105
 
106
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
107
+
108
  print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
109
  print("torch.__version__ =", torch.__version__)
110
  print("torch.version.cuda =", torch.version.cuda)
 
115
  print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))
116
  print("Using device:", device)
117
 
 
 
 
118
  # ============================================================
119
  # AIO version (Space variable)
120
  # ============================================================
121
 
122
  AIO_REPO_ID = "Pr0f3ssi0n4ln00b/Phr00t-Qwen-Rapid-AIO"
123
  DEFAULT_AIO_VERSION = "v19"
124
+
125
  _VER_RE = re.compile(r"^v\d+$")
126
  _DIGITS_RE = re.compile(r"^\d+$")
127
 
128
+
129
  def _normalize_version(raw: str) -> Optional[str]:
130
  if raw is None:
131
  return None
 
134
  return None
135
  if _VER_RE.fullmatch(s):
136
  return s
137
+ # forgiving: allow "21" -> "v21"
138
  if _DIGITS_RE.fullmatch(s):
139
  return f"v{s}"
140
  return None
141
 
142
+
143
  _AIO_ENV_RAW = os.environ.get("AIO_VERSION", "")
144
  _AIO_ENV_NORM = _normalize_version(_AIO_ENV_RAW)
145
+
146
  AIO_VERSION = _AIO_ENV_NORM or DEFAULT_AIO_VERSION
147
  AIO_VERSION_SOURCE = "env" if _AIO_ENV_NORM else "default(v19)"
148
+
149
  print(f"AIO_VERSION (env raw) = {_AIO_ENV_RAW!r}")
150
  print(f"AIO_VERSION (normalized) = {_AIO_ENV_NORM!r}")
151
  print(f"Using AIO_VERSION = {AIO_VERSION} ({AIO_VERSION_SOURCE})")
 
159
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
160
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
161
 
162
+ dtype = torch.bfloat16
163
+
164
+
165
  def _load_pipe_with_version(version: str) -> QwenImageEditPlusPipeline:
166
  sub = f"{version}/transformer"
167
+ print(f"📦 Loading AIO transformer: {AIO_REPO_ID} / {sub}")
168
  p = QwenImageEditPlusPipeline.from_pretrained(
169
  "Qwen/Qwen-Image-Edit-2511",
170
  transformer=QwenImageTransformer2DModel.from_pretrained(
 
177
  ).to(device)
178
  return p
179
 
180
+
181
+ # Forgiving load: try env/default version, fallback to v19 if it fails
182
  try:
183
  pipe = _load_pipe_with_version(AIO_VERSION)
184
+ except Exception as e:
185
  print("❌ Failed to load requested AIO_VERSION. Falling back to v19.")
186
  print("---- exception ----")
187
  print(traceback.format_exc())
 
190
  AIO_VERSION_SOURCE = "fallback_to_v19"
191
  pipe = _load_pipe_with_version(AIO_VERSION)
192
 
193
+ # Apply FA3 Optimization
194
  try:
195
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
196
  print("Flash Attention 3 Processor set successfully.")
197
  except Exception as e:
198
  print(f"Warning: Could not set FA3 processor: {e}")
199
 
200
+ MAX_SEED = np.iinfo(np.int32).max
201
+
202
  # ============================================================
203
  # VAE tiling toggle (UI-controlled; OFF by default)
204
  # ============================================================
 
206
  def _apply_vae_tiling(enabled: bool):
207
  """
208
  Toggle VAE tiling on the global pipeline.
209
+
210
  This does NOT require a Space restart; it applies to the next pipe(...) call.
211
+ Note: this is global process state, so concurrent users could flip it between runs.
212
  """
213
  try:
214
  if enabled:
 
223
  else:
224
  if hasattr(pipe, "disable_vae_tiling"):
225
  pipe.disable_vae_tiling()
226
+ print("🛑 VAE tiling DISABLED (per UI).")
227
  elif hasattr(pipe, "vae") and hasattr(pipe.vae, "disable_tiling"):
228
  pipe.vae.disable_tiling()
229
+ print("🛑 VAE tiling DISABLED via pipe.vae.disable_tiling() (per UI).")
230
  else:
231
+ # If no disable method exists, we leave current state unchanged.
232
  print("⚠️ No disable_vae_tiling()/vae.disable_tiling() found; leaving current state unchanged.")
233
  except Exception as e:
234
  print(f"⚠️ VAE tiling toggle failed: {e}")
235
 
236
  # ============================================================
237
+ # Derived conditioning (Transformers): Depth
238
  # ============================================================
239
+ # Depth uses Depth Anything V2 Small (Transformers-compatible):
240
+ # https://huggingface.co/depth-anything/Depth-Anything-V2-Small-hf
241
 
242
  DEPTH_MODEL_ID = "depth-anything/Depth-Anything-V2-Small-hf"
243
+
244
+ # Lazy cache keyed by device string ("cpu" / "cuda")
245
  _DEPTH_CACHE = {}
246
 
247
  def _derived_device(use_gpu: bool) -> torch.device:
 
257
  _DEPTH_CACHE[key] = (proc, model)
258
  return _DEPTH_CACHE[key]
259
 
260
+ @torch.inference_mode()
261
  def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image:
 
262
  dev = _derived_device(use_gpu)
263
  proc, model = _load_depth_models(dev)
264
 
265
+ w, h = img.size
266
+ inputs = proc(images=img.convert("RGB"), return_tensors="pt").to(dev)
267
+ outputs = model(**inputs)
268
+ predicted = outputs.predicted_depth # [B, H, W]
 
269
 
270
+ depth = torch.nn.functional.interpolate(
271
+ predicted.unsqueeze(1),
272
+ size=(h, w),
 
273
  mode="bicubic",
274
  align_corners=False,
275
  ).squeeze(1)[0]
276
 
277
+ depth = depth - depth.min()
278
+ depth = depth / (depth.max() + 1e-8)
279
+ depth = (depth * 255.0).clamp(0, 255).to(torch.uint8).cpu().numpy()
280
+ return Image.fromarray(depth).convert("RGB")
 
 
 
281
 
282
  # ============================================================
283
  # LoRA adapters + presets
 
303
  "AnyPose": {
304
  "type": "package",
305
  "requires_two_images": True,
306
+ "image2_label": "Upload Pose Reference (Image 2)",
307
  "parts": [
308
  {
309
  "repo": "lilylilith/AnyPose",
 
343
  "BFS-Best-FaceSwap": {
344
  "type": "single",
345
  "requires_two_images": True,
346
+ "image2_label": "Upload Head/Face Donor (Image 2)",
347
  "repo": "Alissonerdx/BFS-Best-Face-Swap",
348
  "weights": "bfs_head_v5_2511_original.safetensors",
349
  "adapter_name": "BFS-Best-Faceswap",
350
  "strength": 1.0,
351
+ "needs_alpha_fix": True, # <-- fixes KeyError 'img_in.alpha'
352
  },
353
  "BFS-Best-FaceSwap-merge": {
354
  "type": "single",
355
  "requires_two_images": True,
356
+ "image2_label": "Upload Head/Face Donor (Image 2)",
357
  "repo": "Alissonerdx/BFS-Best-Face-Swap",
358
  "weights": "bfs_head_v5_2511_merged_version_rank_32_fp32.safetensors",
359
  "adapter_name": "BFS-Best-Faceswap-merge",
360
  "strength": 1.1,
361
+ "needs_alpha_fix": True, # <-- fixes KeyError 'img_in.alpha'
362
  },
363
  "F2P": {
364
  "type": "single",
 
436
  LORA_PRESET_PROMPTS = {
437
  "Any2Real_2601": "change the picture 1 to realistic photograph",
438
  "Semirealistic-photo-detailer": "transform the image to semi-realistic image",
439
+ "AnyPose": "Make the person in image 1 do the exact same pose of the person in image 2. Changing the style and background of the image of the person in image 1 is undesirable, so don't do it. The new pose should be pixel accurate to the pose we are trying to copy. The position of the arms and head and legs should be the same as the pose we are trying to copy. Change the field of view and angle to match exactly image 2. Head tilt and eye gaze pose should match the person in image 2.",
440
+ "Hyperrealistic-Portrait": "Transform the image into an ultra-realistic photorealistic portrait with strict identity preservation, facing straight to the camera. Enhance pore-level skin textures, realistic moisture effects, and natural wet hair clumping against the skin. Apply cool-toned soft-box lighting with subtle highlights and shadows, maintain realistic green-hazel eye catchlights without synthetic gloss, and preserve soft natural lip texture. Use shallow depth of field with a clean bokeh background, an 85mm macro photographic look, and raw photo grading without retouching to maintain realism and original details.",
441
+ "Ultrarealistic-Portrait": "Transform the image into an ultra-realistic glamour portrait while strictly preserving the subject’s identity. Apply a close-up composition with a slight head tilt and a hand near the face, enhance cinematic directional lighting with dramatic fashion-style highlights, and refine makeup details including glowing skin, glossy lips, luminous highlighter, and defined eyes. Increase skin realism with detailed epidermal textures such as micropores, microhairs, subtle oil sheen, natural highlights, soft wrinkles, and subsurface scattering. Maintain a luxury fashion-magazine look in a 9:16 aspect ratio, preserving realism, facial structure, and original details without over-smoothing or retouching.",
 
 
 
 
 
 
 
 
 
 
 
 
442
  "Upscale2K": "Upscale this picture to 4K resolution.",
443
+ "BFS-Best-FaceSwap": "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, and nose structure of Picture 2. copy the eye direction, head rotation, and micro-expressions from Picture 1. high quality, sharp details, 4k",
444
+ "BFS-Best-FaceSwap-merge": "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, and nose structure of Picture 2. copy the eye direction, head rotation, and micro-expressions from Picture 1. high quality, sharp details, 4k",
 
 
 
 
 
 
445
  }
446
 
447
+ # Track what is currently loaded in memory (adapter_name values)
448
  LOADED_ADAPTERS = set()
449
 
450
  # ============================================================
451
  # Helpers: resolution
452
  # ============================================================
453
 
454
+ # We prefer *area-based* sizing (≈ megapixels) over long-edge sizing.
455
+ # This aligns better with Qwen-Image-Edit's internal assumptions and reduces FOV drift.
456
+
457
  def _round_to_multiple(x: int, m: int) -> int:
 
458
  return max(m, (int(x) // m) * m)
459
 
460
+ def compute_canvas_dimensions_from_area(
461
+ image: Image.Image,
462
+ target_area: int,
463
+ multiple_of: int,
464
+ ) -> tuple[int, int]:
465
+ """Compute (width, height) that matches image aspect ratio and approximates target_area.
466
+
467
+ The result is floored to be divisible by multiple_of (typically vae_scale_factor*2).
468
+ """
469
  w, h = image.size
470
  aspect = w / h if h else 1.0
471
+
472
+ # Use the pipeline's own area->(w,h) helper for consistency.
473
  from qwenimage.pipeline_qwenimage_edit_plus import calculate_dimensions
474
+
475
+ width, height = calculate_dimensions(int(target_area), float(aspect))
476
  width = _round_to_multiple(int(width), int(multiple_of))
477
  height = _round_to_multiple(int(height), int(multiple_of))
478
  return width, height
479
 
480
+ def get_target_area_for_lora(
481
+ image: Image.Image,
482
+ lora_adapter: str,
483
+ user_target_megapixels: float,
484
+ ) -> int:
485
+ """Return target pixel area for the canvas.
486
+
487
+ Priority:
488
+ 1) Adapter spec: target_area (pixels) or target_megapixels
489
+ 2) Adapter spec: target_long_edge (legacy) -> converted to area using image aspect
490
+ 3) User slider target megapixels
491
+ """
492
  spec = ADAPTER_SPECS.get(lora_adapter, {})
493
+
494
  if "target_area" in spec:
495
  try:
496
  return int(spec["target_area"])
497
  except Exception:
498
  pass
499
+
500
  if "target_megapixels" in spec:
501
  try:
502
  mp = float(spec["target_megapixels"])
503
  return int(mp * 1024 * 1024)
504
  except Exception:
505
  pass
506
+
507
+ # Legacy support (e.g. Upscale2K)
508
  if "target_long_edge" in spec:
509
  try:
510
  long_edge = int(spec["target_long_edge"])
 
518
  return int(new_w * new_h)
519
  except Exception:
520
  pass
521
+
522
+ # User default
523
+ try:
524
+ mp = float(user_target_megapixels)
525
+ except Exception:
526
+ mp = 1.0
527
+
528
+ # Treat 0 MP as "match input area"
529
+ if mp <= 0:
530
+ w, h = image.size
531
+ return int(w * h)
532
+
533
+ return int(mp * 1024 * 1024)
534
 
535
  # ============================================================
536
+ # Helpers: multi-input routing + gallery normalization
537
  # ============================================================
538
 
539
+
540
+ def lora_requires_two_images(lora_adapter: str) -> bool:
541
+ return bool(ADAPTER_SPECS.get(lora_adapter, {}).get("requires_two_images", False))
542
+
543
+
544
+ def image2_label_for_lora(lora_adapter: str) -> str:
545
+ return str(ADAPTER_SPECS.get(lora_adapter, {}).get("image2_label", "Upload Reference (Image 2)"))
546
+
547
+
548
  def _to_pil_rgb(x) -> Optional[Image.Image]:
549
+ """
550
+ Accepts PIL / numpy / (image, caption) tuples from gr.Gallery and returns PIL RGB.
551
+ Gradio Gallery commonly yields tuples like (image, caption).
552
+ """
553
  if x is None:
554
  return None
555
+
556
+ # Gallery often returns (image, caption)
557
  if isinstance(x, tuple) and len(x) >= 1:
558
  x = x[0]
559
+ if x is None:
560
+ return None
561
+
562
  if isinstance(x, Image.Image):
563
  return x.convert("RGB")
564
+
565
  if isinstance(x, np.ndarray):
566
  return Image.fromarray(x).convert("RGB")
567
+
568
+ # Best-effort fallback
569
  try:
570
  return Image.fromarray(np.array(x)).convert("RGB")
571
  except Exception:
572
  return None
573
 
 
 
 
 
 
 
 
 
 
574
 
575
+ def build_labeled_images(
576
+ img1: Image.Image,
577
+ img2: Optional[Image.Image],
578
+ extra_imgs: Optional[list[Image.Image]],
579
+ ) -> dict[str, Image.Image]:
580
+ """
581
+ Creates labels image_1, image_2, image_3... based on what is actually uploaded:
582
+ - img1 is always image_1
583
+ - img2 becomes image_2 only if present
584
+ - extras start immediately after the last present base box
585
+ The pipeline receives images in this exact order.
586
+ """
587
+ labeled: dict[str, Image.Image] = {}
588
+ idx = 1
589
+
590
+ labeled[f"image_{idx}"] = img1
591
+ idx += 1
592
+
593
+ if img2 is not None:
594
+ labeled[f"image_{idx}"] = img2
595
+ idx += 1
596
+
597
+ if extra_imgs:
598
+ for im in extra_imgs:
599
+ if im is None:
600
+ continue
601
+ labeled[f"image_{idx}"] = im
602
+ idx += 1
603
+
604
+ return labeled
605
 
 
 
606
 
607
  # ============================================================
608
+ # Helpers: BFS alpha key fix
609
  # ============================================================
610
 
611
+
612
  def _inject_missing_alpha_keys(state_dict: dict) -> dict:
613
+ """
614
+ Diffusers' Qwen LoRA converter expects '<module>.alpha' keys.
615
+ BFS safetensors omits them. We inject alpha = rank (neutral scaling).
616
+
617
+ IMPORTANT: diffusers may strip 'diffusion_model.' before lookup, so we
618
+ inject BOTH:
619
+ - diffusion_model.xxx.alpha
620
+ - xxx.alpha
621
+ """
622
  bases = {}
623
+
624
  for k, v in state_dict.items():
625
  if not isinstance(v, torch.Tensor):
626
  continue
 
631
 
632
  for base, rank in bases.items():
633
  alpha_tensor = torch.tensor(float(rank), dtype=torch.float32)
634
+
635
  full_alpha = f"{base}.alpha"
636
  if full_alpha not in state_dict:
637
  state_dict[full_alpha] = alpha_tensor
638
+
639
  if base.startswith("diffusion_model."):
640
  stripped_base = base[len("diffusion_model.") :]
641
  stripped_alpha = f"{stripped_base}.alpha"
642
  if stripped_alpha not in state_dict:
643
  state_dict[stripped_alpha] = alpha_tensor
644
+
645
  return state_dict
646
 
647
+
648
  def _filter_to_diffusers_lora_keys(state_dict: dict) -> tuple[dict, dict]:
649
+ """Return (filtered_state_dict, stats).
650
+
651
+ Some ComfyUI/Qwen safetensors (especially "merged" variants) include non-LoRA
652
+ delta/patch keys like `*.diff` and `*.diff_b` alongside real LoRA tensors.
653
+ Diffusers' internal Qwen LoRA converter is strict: any leftover keys cause an
654
+ error (`state_dict should be empty...`).
655
+
656
+ This helper keeps only the keys Diffusers can consume as a LoRA:
657
+ - `*.lora_up.weight`
658
+ - `*.lora_down.weight`
659
+ - (rare) `*.lora_mid.weight`
660
+ - alpha keys: `*.alpha` (or `*.lora_alpha` which we normalize to `*.alpha`)
661
+
662
+ It also drops known patch keys (`*.diff`, `*.diff_b`) and everything else.
663
+ """
664
+
665
  keep_suffixes = (
666
  ".lora_up.weight",
667
  ".lora_down.weight",
 
669
  ".alpha",
670
  ".lora_alpha",
671
  )
672
+
673
  dropped_patch = 0
674
  dropped_other = 0
675
  kept = 0
676
  normalized_alpha = 0
677
 
678
+ out: dict[str, torch.Tensor] = {}
679
  for k, v in state_dict.items():
680
  if not isinstance(v, torch.Tensor):
681
+ # Ignore non-tensor entries if any.
682
  dropped_other += 1
683
  continue
684
+
685
+ # Drop ComfyUI "delta" keys that Diffusers' LoRA loader will never consume.
686
  if k.endswith(".diff") or k.endswith(".diff_b"):
687
  dropped_patch += 1
688
  continue
689
+
690
  if not k.endswith(keep_suffixes):
691
  dropped_other += 1
692
  continue
693
+
694
  if k.endswith(".lora_alpha"):
695
+ # Normalize common alt name to what Diffusers expects.
696
  base = k[: -len(".lora_alpha")]
697
  k2 = f"{base}.alpha"
698
  out[k2] = v.float() if v.dtype != torch.float32 else v
699
  normalized_alpha += 1
700
  kept += 1
701
  continue
702
+
703
  out[k] = v
704
  kept += 1
705
 
 
711
  }
712
  return out, stats
713
 
714
+
715
  def _duplicate_stripped_prefix_keys(state_dict: dict, prefix: str = "diffusion_model.") -> dict:
716
+ """Ensure both prefixed and unprefixed variants exist for LoRA-related keys.
717
+
718
+ Diffusers' Qwen LoRA conversion may strip `diffusion_model.` when looking up
719
+ modules. Some exports only include prefixed keys. To be maximally compatible,
720
+ we duplicate LoRA keys (and alpha) in stripped form when missing.
721
+ """
722
+
723
  out = dict(state_dict)
724
  for k, v in list(state_dict.items()):
725
  if not k.startswith(prefix):
 
729
  out[stripped] = v
730
  return out
731
 
732
+
733
  def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name: str, needs_alpha_fix: bool = False):
734
+ """
735
+ Normal path: pipe.load_lora_weights(repo, weight_name=..., adapter_name=...)
736
+ BFS fallback: download safetensors, inject missing alpha keys, then load from dict.
737
+ """
738
  try:
739
  pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=adapter_name)
740
  return
741
  except (KeyError, ValueError) as e:
742
+ # KeyError: missing required alpha keys (common in BFS)
743
+ # ValueError: Diffusers Qwen converter found leftover keys (e.g. .diff/.diff_b)
744
  if not needs_alpha_fix:
745
  raise
746
 
 
748
  "⚠️ LoRA load failed (will try safe dict fallback). "
749
  f"Adapter={adapter_name!r} file={weight_name!r} error={type(e).__name__}: {e}"
750
  )
751
+
752
  local_path = hf_hub_download(repo_id=repo, filename=weight_name)
753
  sd = safetensors_load_file(local_path)
754
+
755
+ # 1) Inject required `<module>.alpha` keys (neutral scaling alpha=rank).
756
  sd = _inject_missing_alpha_keys(sd)
757
+
758
+ # 2) Keep only LoRA + alpha keys; drop ComfyUI patch/delta keys.
759
  sd, stats = _filter_to_diffusers_lora_keys(sd)
760
+
761
+ # 3) Duplicate stripped keys (remove `diffusion_model.`) for compatibility.
762
  sd = _duplicate_stripped_prefix_keys(sd)
763
+
764
+ print(
765
+ "🧹 LoRA dict cleanup stats: "
766
+ f"kept={stats['kept']} dropped_patch={stats['dropped_patch']} "
767
+ f"dropped_other={stats['dropped_other']} normalized_alpha={stats['normalized_alpha']}"
768
+ )
769
+
770
  pipe.load_lora_weights(sd, adapter_name=adapter_name)
771
  return
772
 
773
+
774
+ # ============================================================
775
+ # LoRA loader: single/package + strengths
776
+ # ============================================================
777
+
778
+
779
  def _ensure_loaded_and_get_active_adapters(selected_lora: str):
780
  spec = ADAPTER_SPECS.get(selected_lora)
781
  if not spec:
 
788
  parts = spec.get("parts", [])
789
  if not parts:
790
  raise gr.Error(f"Package spec has no parts: {selected_lora}")
791
+
792
  for part in parts:
793
  repo = part["repo"]
794
  weights = part["weights"]
795
+ adapter_name = part["adapter_name"]
796
  strength = float(part.get("strength", 1.0))
797
  needs_alpha_fix = bool(part.get("needs_alpha_fix", False))
798
 
799
+ if adapter_name not in LOADED_ADAPTERS:
800
+ print(f"--- Downloading and Loading Adapter Part: {selected_lora} / {adapter_name} ---")
801
+ try:
802
+ _load_lora_weights_with_fallback(
803
+ repo=repo,
804
+ weight_name=weights,
805
+ adapter_name=adapter_name,
806
+ needs_alpha_fix=needs_alpha_fix,
807
+ )
808
+ LOADED_ADAPTERS.add(adapter_name)
809
+ except Exception as e:
810
+ raise gr.Error(f"Failed to load adapter part {selected_lora}/{adapter_name}: {e}")
811
+ else:
812
+ print(f"--- Adapter part already loaded: {selected_lora} / {adapter_name} ---")
813
 
814
+ adapter_names.append(adapter_name)
815
  adapter_weights.append(strength)
816
 
817
  else:
818
  repo = spec["repo"]
819
  weights = spec["weights"]
820
+ adapter_name = spec["adapter_name"]
821
  strength = float(spec.get("strength", 1.0))
822
  needs_alpha_fix = bool(spec.get("needs_alpha_fix", False))
823
 
824
+ if adapter_name not in LOADED_ADAPTERS:
825
+ print(f"--- Downloading and Loading Adapter: {selected_lora} ---")
826
+ try:
827
+ _load_lora_weights_with_fallback(
828
+ repo=repo,
829
+ weight_name=weights,
830
+ adapter_name=adapter_name,
831
+ needs_alpha_fix=needs_alpha_fix,
832
+ )
833
+ LOADED_ADAPTERS.add(adapter_name)
834
+ except Exception as e:
835
+ raise gr.Error(f"Failed to load adapter {selected_lora}: {e}")
836
+ else:
837
+ print(f"--- Adapter {selected_lora} is already loaded. ---")
838
 
839
+ adapter_names = [adapter_name]
840
+ adapter_weights = [strength]
841
 
842
  return adapter_names, adapter_weights
843
 
844
+
845
  # ============================================================
846
+ # UI handlers
847
  # ============================================================
848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
 
850
+ def on_lora_change_ui(selected_lora, current_prompt, current_extras_condition_only):
851
+ # Preset prompt (fill only if empty)
852
+ if selected_lora != NONE_LORA:
853
+ preset = LORA_PRESET_PROMPTS.get(selected_lora, "")
854
+ if preset and (current_prompt is None or str(current_prompt).strip() == ""):
855
+ prompt_update = gr.update(value=preset)
856
+ else:
857
+ prompt_update = gr.update(value=current_prompt)
858
+ else:
859
+ prompt_update = gr.update(value=current_prompt)
860
+
861
+ # Image2 visibility/label
862
+ if lora_requires_two_images(selected_lora):
863
+ img2_update = gr.update(visible=True, label=image2_label_for_lora(selected_lora))
864
+ else:
865
+ img2_update = gr.update(visible=False, value=None, label="Upload Reference (Image 2)")
866
+
867
+ # Extra references routing default:
868
+ # For BFS/AnyPose-like adapters, it's usually safer to keep extra refs as conditioning-only.
869
+ if selected_lora in ("BFS-Best-FaceSwap", "BFS-Best-FaceSwap-merge", "AnyPose"):
870
+ extras_update = gr.update(value=True)
871
+ else:
872
+ extras_update = gr.update(value=current_extras_condition_only)
873
+
874
+ return prompt_update, img2_update, extras_update
875
  # ============================================================
876
+ # UI helpers: output routing + derived conditioning
877
  # ============================================================
878
 
879
+ def set_output_as_image1(last):
880
+ if last is None:
881
+ raise gr.Error("No output available yet.")
882
+ return gr.update(value=last)
 
 
883
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
+ def set_output_as_image2(last):
886
+ if last is None:
887
+ raise gr.Error("No output available yet.")
888
+ return gr.update(value=last)
889
 
 
 
 
 
 
890
 
891
+ def set_output_as_extra(last, existing_extra):
892
+ if last is None:
893
+ raise gr.Error("No output available yet.")
894
+ return _append_to_gallery(existing_extra, last)
895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
 
897
+ @spaces.GPU
898
+ def add_derived_ref(img1, existing_extra, derived_type, derived_use_gpu):
899
+ if img1 is None:
900
+ raise gr.Error("Please upload Image 1 first.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
901
 
902
+ if derived_type == "None":
903
+ return gr.update(value=existing_extra), gr.update(visible=False, value=None)
904
 
905
+ base = img1.convert("RGB")
 
906
 
907
+ if derived_type == "Depth (Depth Anything V2 Small)":
908
+ derived = make_depth_map(base, use_gpu=bool(derived_use_gpu))
 
909
  else:
910
+ raise gr.Error(f"Unknown derived type: {derived_type}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
911
 
912
+ new_gallery = _append_to_gallery(existing_extra, derived)
913
+ return gr.update(value=new_gallery), gr.update(visible=True, value=derived)
 
914
 
 
915
 
916
+ # ============================================================
917
+ # Inference
918
+ # ============================================================
919
 
920
+
921
+ @spaces.GPU
922
+ def infer(
923
+ input_image_1,
924
+ input_image_2,
925
+ input_images_extra, # gallery multi-image box
926
+ prompt,
927
+ lora_adapter,
928
+ seed,
929
+ randomize_seed,
930
+ guidance_scale,
931
+ steps,
932
+ target_megapixels,
933
+ extras_condition_only,
934
+ pad_to_canvas,
935
+ vae_tiling, # VAE tiling toggle
936
+ resolution_multiple,
937
+ vae_ref_megapixels,
938
+ decoder_vae,
939
+ keep_decoder_2x,
940
+ progress=gr.Progress(track_tqdm=True),
941
+ ):
942
  gc.collect()
943
  if torch.cuda.is_available():
944
  torch.cuda.empty_cache()
945
 
946
+ if input_image_1 is None:
947
+ raise gr.Error("Please upload Image 1.")
948
 
949
+ # Handle "None"
950
+ if lora_adapter == NONE_LORA:
951
+ try:
952
+ pipe.set_adapters([], adapter_weights=[])
953
+ except Exception:
954
+ if LOADED_ADAPTERS:
955
+ pipe.set_adapters(list(LOADED_ADAPTERS), adapter_weights=[0.0] * len(LOADED_ADAPTERS))
956
+ else:
957
+ adapter_names, adapter_weights = _ensure_loaded_and_get_active_adapters(lora_adapter)
958
+ pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
959
 
960
+ if randomize_seed:
961
+ seed = random.randint(0, MAX_SEED)
 
 
962
 
963
+ generator = torch.Generator(device=device).manual_seed(seed)
964
+ negative_prompt = (
965
+ "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
966
+ "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
967
+ )
968
+
969
+ img1 = input_image_1.convert("RGB")
970
+ img2 = input_image_2.convert("RGB") if input_image_2 is not None else None
971
+
972
+ # Normalize extra images (Gallery) to PIL RGB (handles tuples from Gallery)
973
+ extra_imgs: list[Image.Image] = []
974
+ if input_images_extra:
975
+ for item in input_images_extra:
976
+ pil = _to_pil_rgb(item)
977
+ if pil is not None:
978
+ extra_imgs.append(pil)
979
+
980
+ # Enforce existing 2-image LoRA behavior (image_1 + image_2 required)
981
+ if lora_requires_two_images(lora_adapter) and img2 is None:
982
+ raise gr.Error("This LoRA needs two images. Please upload Image 2 as well.")
983
+
984
+ # Label images as image_1, image_2, image_3...
985
+ labeled = build_labeled_images(img1, img2, extra_imgs)
986
+
987
+ # Pass to pipeline in labeled order. Keep single-image call when only one is present.
988
+ pipe_images = list(labeled.values())
989
+ if len(pipe_images) == 1:
990
+ pipe_images = pipe_images[0]
991
+
992
+ # Resolution derived from Image 1 (base/body/target)
993
+ # Use target *area* (≈ megapixels) rather than long-edge sizing to reduce FOV drift.
994
+ target_area = get_target_area_for_lora(img1, lora_adapter, float(target_megapixels))
995
+ width, height = compute_canvas_dimensions_from_area(
996
+ img1,
997
+ target_area=target_area,
998
+ multiple_of=int(resolution_multiple),
999
+ )
1000
+
1001
+ # Decide which images participate in the VAE latent stream.
1002
+ # If enabled, extra references beyond (Img_1, Img_2) become conditioning-only.
1003
+ vae_image_indices = None
1004
+ if extras_condition_only:
1005
+ if isinstance(pipe_images, list) and len(pipe_images) > 2:
1006
+ vae_image_indices = [0, 1] if len(pipe_images) >= 2 else [0]
1007
 
1008
+ try:
1009
+ print(
1010
+ "[DEBUG][infer] submitting request | "
1011
+ f"lora_adapter={lora_adapter!r} seed={seed} prompt={prompt!r}"
1012
+ )
1013
+ print(f"[DEBUG][infer] canvas={width}x{height} (~{(width*height)/1_048_576:.3f} MP) vae_tiling={bool(vae_tiling)}")
1014
 
1015
+ # ✅ Apply UI toggle per-request (OFF by default)
1016
+ # Lattice multiple passed to pipeline too (anti-drift / valid size grid)
1017
+ res_mult = int(resolution_multiple) if resolution_multiple is not None else int(pipe.vae_scale_factor * 2)
1018
 
1019
+ # Optional: override VAE sizing for *extra* references (beyond Image 1 / Image 2)
1020
+ # Interpreted as megapixels; 0 disables override (uses canvas).
1021
+ try:
1022
+ mp_ref = float(vae_ref_megapixels)
1023
+ except Exception:
1024
+ mp_ref = 0.0
1025
+
1026
+ vae_ref_area = int(mp_ref * 1024 * 1024) if mp_ref and mp_ref > 0 else None
1027
+
1028
+ # Extras start index depends on whether Image 2 exists
1029
+ base_ref_count = 2 if img2 is not None else 1
1030
+
1031
+ _apply_vae_tiling(bool(vae_tiling))
1032
+
1033
+ result = pipe(
1034
+ image=pipe_images,
1035
+ prompt=prompt,
1036
+ negative_prompt=negative_prompt,
1037
+ height=height,
1038
+ width=width,
1039
+ num_inference_steps=steps,
1040
+ generator=generator,
1041
+ true_cfg_scale=guidance_scale,
1042
+ vae_image_indices=vae_image_indices,
1043
+ pad_to_canvas=bool(pad_to_canvas),
1044
+ resolution_multiple=res_mult,
1045
+ vae_ref_area=vae_ref_area,
1046
+ vae_ref_start_index=base_ref_count,
1047
+ decoder_vae=str(decoder_vae).lower(),
1048
+ keep_decoder_2x=bool(keep_decoder_2x),
1049
+ ).images[0]
1050
+ return result, seed, result
1051
+ finally:
1052
+ gc.collect()
1053
+ if torch.cuda.is_available():
1054
+ torch.cuda.empty_cache()
1055
 
 
 
 
 
1056
 
1057
+ @spaces.GPU
1058
+ def infer_example(input_image, prompt, lora_adapter):
1059
+ if input_image is None:
1060
+ return None, 0, None
1061
+ input_pil = input_image.convert("RGB")
1062
+ guidance_scale = 1.0
1063
+ steps = 4
1064
+ # Examples don't supply Image 2 or extra images; and example list doesn't include AnyPose/BFS.
1065
+ # Keep VAE tiling OFF in examples (matches default).
1066
+ result, seed, last = infer(
1067
+ input_pil,
1068
+ None,
1069
+ None,
1070
+ prompt,
1071
+ lora_adapter,
1072
+ 0,
1073
+ True,
1074
+ guidance_scale,
1075
+ steps,
1076
+ 1.0,
1077
+ True,
1078
+ True,
1079
+ False, # vae_tiling
1080
+ )
1081
+ return result, seed, last
1082
 
 
 
 
 
 
 
1083
 
1084
+ # ============================================================
1085
+ # UI
1086
+ # ============================================================
1087
+
1088
+ css = """
1089
+ #col-container {
1090
+ margin: 0 auto;
1091
+ max-width: 960px;
1092
+ }
1093
+ #main-title h1 {font-size: 2.1em !important;}
1094
  """
 
1095
 
1096
+ aio_status_line = (
1097
+ f"**AIO transformer version:** `{AIO_VERSION}` "
1098
+ f"({AIO_VERSION_SOURCE}; env `AIO_VERSION`={_AIO_ENV_RAW!r})"
1099
+ )
1100
+
1101
+ with gr.Blocks() as demo:
1102
+ with gr.Column(elem_id="col-container"):
1103
+ gr.Markdown("# **Qwen-Image-Edit-2511-LoRAs-Fast**", elem_id="main-title")
1104
+ gr.Markdown(
1105
+ f"""This experimental space for QIE-2511 utilizes extracted transformers of Phr00t’s Rapid AIO merge with LoRA support and a couple of extra features:
1106
+
1107
+ - Optional conditioning-only routing for extra reference latents
1108
+ - Uncapped canvas resolution
1109
+ - Optional VAE tiling for high resolutions
1110
+ - Optional depth mapping for conditioning
1111
+ - Optional routing of output to input for further iterations
1112
+ - Optional alternative decoder VAE (Wan2x )
1113
+
1114
+ Current environment is running **{AIO_VERSION}** of the Rapid AIO. Duplicate the space and set the **AIO_VERSION** space variable to use a different version."""
1115
+ )
1116
+ gr.Markdown(aio_status_line)
1117
+
1118
+ with gr.Row(equal_height=True):
1119
+ with gr.Column():
1120
+ input_image_1 = gr.Image(label="Upload Image 1 (Base / Target)", type="pil", height=290)
1121
+ input_image_2 = gr.Image(label="Upload Reference (Image 2)", type="pil", height=290, visible=False)
1122
+
1123
+ input_images_extra = gr.Gallery(
1124
+ label="Upload Additional Images (auto-indexed after Image 1/2)",
1125
+ type="pil",
1126
+ height=290,
1127
+ columns=4,
1128
+ rows=2,
1129
+ interactive=True,
 
 
 
 
 
 
 
 
 
1130
  )
1131
 
1132
+ prompt = gr.Text(
1133
+ label="Edit Prompt",
1134
+ show_label=True,
1135
+ placeholder="e.g., transform into photo..",
1136
  )
 
1137
 
1138
+ run_button = gr.Button("Edit Image", variant="primary")
 
 
1139
 
1140
+ with gr.Column():
1141
+ output_image = gr.Image(label="Output Image", interactive=False, format="png", height=353)
 
1142
 
1143
+ last_output = gr.State(value=None)
1144
 
1145
+ with gr.Row():
1146
+ btn_out_to_img1 = gr.Button("⬅️ Output → Image 1", variant="secondary")
1147
+ btn_out_to_img2 = gr.Button("⬅️ Output → Image 2", variant="secondary")
1148
+ btn_out_to_extra = gr.Button("➕ Output → Extra Ref", variant="secondary")
1149
 
1150
+ derived_preview = gr.Image(
1151
+ label="Derived Conditioning Preview",
1152
+ interactive=False,
1153
+ format="png",
1154
+ height=200,
1155
+ visible=False,
1156
+ )
1157
 
1158
+ with gr.Row():
1159
+ lora_choices = [NONE_LORA] + list(ADAPTER_SPECS.keys())
1160
+ lora_adapter = gr.Dropdown(
1161
+ label="Choose Editing Style",
1162
+ choices=lora_choices,
1163
+ value=NONE_LORA,
1164
+ )
1165
+
1166
+ with gr.Accordion("Advanced Settings", open=False, visible=True):
1167
+ with gr.Accordion("Derived Conditioning (Pose / Depth)", open=False):
1168
+ derived_type = gr.Dropdown(
1169
+ label="Derived Type (from Image 1)",
1170
+ choices=["None", "Depth (Depth Anything V2 Small)"],
1171
+ value="None",
1172
+ )
1173
+ derived_use_gpu = gr.Checkbox(label="Use GPU for derived model", value=False)
1174
+ add_derived_btn = gr.Button("➕ Add derived ref to Extras (conditioning-only recommended)")
1175
+
1176
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
1177
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
1178
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
1179
+ steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
1180
+ target_megapixels = gr.Slider(
1181
+ label="Target Megapixels (canvas, 0 = match input area)",
1182
+ minimum=0.0,
1183
+ maximum=6.0,
1184
+ step=0.1,
1185
+ value=1.0,
1186
+ )
1187
+ resolution_multiple = gr.Dropdown(
1188
+ label="Resolution lattice multiple (anti-drift)",
1189
+ choices=[32, 56, 112],
1190
+ value=56,
1191
+ interactive=True,
1192
+ )
1193
+ vae_ref_megapixels = gr.Slider(
1194
+ label="Extra refs VAE megapixels override (0 = use canvas)",
1195
+ minimum=0.0,
1196
+ maximum=6.0,
1197
+ step=0.1,
1198
+ value=0.0,
1199
+ )
1200
+ decoder_vae = gr.Dropdown(
1201
+ label="Decoder VAE",
1202
+ choices=["qwen", "wan2x"],
1203
+ value="qwen",
1204
+ interactive=True,
1205
+ )
1206
+ keep_decoder_2x = gr.Checkbox(
1207
+ label="Keep 2× output (wan2x only)",
1208
+ value=False,
1209
+ )
1210
+ extras_condition_only = gr.Checkbox(
1211
+ label="Extra references are conditioning-only (exclude from VAE)",
1212
+ value=True,
1213
+ )
1214
+ pad_to_canvas = gr.Checkbox(
1215
+ label="Pad images to canvas aspect (avoid warping)",
1216
+ value=True,
1217
+ )
1218
+
1219
+ # ✅ NEW: VAE tiling toggle (OFF by default)
1220
+ vae_tiling = gr.Checkbox(
1221
+ label="VAE tiling (lower VRAM, slower)",
1222
+ value=False,
1223
+ )
1224
+
1225
+ # On LoRA selection: preset prompt + toggle Image 2
1226
+ lora_adapter.change(
1227
+ fn=on_lora_change_ui,
1228
+ inputs=[lora_adapter, prompt, extras_condition_only],
1229
+ outputs=[prompt, input_image_2, extras_condition_only],
1230
+ )
1231
 
1232
+ gr.Examples(
1233
+ examples=[
1234
+ ["examples/5.jpg", "Remove shadows and relight the image using soft lighting.", "Light-Restoration"],
1235
+ ["examples/4.jpg", "Use a subtle golden-hour filter with smooth light diffusion.", "Relight"],
1236
+ ["examples/2.jpeg", "Rotate the camera 45 degrees to the left.", "Multiple-Angles"],
1237
+ [
1238
+ "examples/12.jpg",
1239
+ "flatcolor Desaturate the image and lower the contrast to create a flat, ungraded look similar to a camera log profile. Preserve details in the highlights and shadows.",
1240
+ "Flat-Log",
1241
+ ],
1242
+ ["examples/7.jpg", "Light source from the Right Rear", "Multi-Angle-Lighting"],
1243
+ ["examples/10.jpeg", "Upscale the image.", "Upscale-Image"],
1244
+ ["examples/7.jpg", "Light source from the Below", "Multi-Angle-Lighting"],
1245
+ ["examples/2.jpeg", "Switch the camera to a top-down right corner view.", "Multiple-Angles"],
1246
+ [
1247
+ "examples/9.jpg",
1248
+ "The camera moves slightly forward as sunlight breaks through the clouds, casting a soft glow around the character's silhouette in the mist. Realistic cinematic style, atmospheric depth.",
1249
+ "Next-Scene",
1250
+ ],
1251
+ ["examples/8.jpg", "Make the subjects skin details more prominent and natural.", "Edit-Skin"],
1252
+ ["examples/6.jpg", "Switch the camera to a bottom-up view.", "Multiple-Angles"],
1253
+ ["examples/6.jpg", "Rotate the camera 180 degrees upside down.", "Multiple-Angles"],
1254
+ ["examples/4.jpg", "Rotate the camera 45 degrees to the right.", "Multiple-Angles"],
1255
+ ["examples/4.jpg", "Switch the camera to a top-down view.", "Multiple-Angles"],
1256
+ ["examples/4.jpg", "Switch the camera to a wide-angle lens.", "Multiple-Angles"],
1257
+ ["examples/11.jpg", "Upscale this picture to 4K resolution.", "Upscale2K"],
1258
+ ],
1259
+ inputs=[input_image_1, prompt, lora_adapter],
1260
+ outputs=[output_image, seed, last_output],
1261
+ fn=infer_example,
1262
+ cache_examples=False,
1263
+ label="Examples",
1264
+ )
1265
 
1266
+ run_button.click(
1267
+ fn=infer,
 
1268
  inputs=[
1269
+ input_image_1,
1270
+ input_image_2,
1271
+ input_images_extra,
1272
  prompt,
1273
  lora_adapter,
1274
  seed,
1275
  randomize_seed,
1276
+ guidance_scale,
1277
  steps,
1278
  target_megapixels,
 
 
 
1279
  extras_condition_only,
1280
+ pad_to_canvas,
1281
+ vae_tiling,
1282
  resolution_multiple,
1283
  vae_ref_megapixels,
1284
+ decoder_vae,
1285
+ keep_decoder_2x,
1286
  ],
1287
+ outputs=[output_image, seed, last_output],
1288
  )
1289
 
1290
  # Output routing buttons
1291
+ btn_out_to_img1.click(fn=set_output_as_image1, inputs=[last_output], outputs=[input_image_1])
1292
+ btn_out_to_img2.click(fn=set_output_as_image2, inputs=[last_output], outputs=[input_image_2])
1293
+ btn_out_to_extra.click(fn=set_output_as_extra, inputs=[last_output, input_images_extra], outputs=[input_images_extra])
1294
+
1295
+ # Derived conditioning: append pose/depth map as extra ref (UI shows preview)
1296
+ add_derived_btn.click(
1297
+ fn=add_derived_ref,
1298
+ inputs=[input_image_1, input_images_extra, derived_type, derived_use_gpu],
1299
+ outputs=[input_images_extra, derived_preview],
1300
+ )
1301
 
1302
+ if __name__ == "__main__":
1303
+ demo.queue(max_size=30).launch(
1304
+ css=css,
1305
+ theme=orange_red_theme,
1306
+ mcp_server=True,
1307
+ ssr_mode=False,
1308
+ show_error=True,
1309
+ )