Professional Noob commited on
Commit
62bf95e
·
verified ·
1 Parent(s): 9d7f77a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +529 -604
app.py CHANGED
@@ -2,23 +2,23 @@ import os
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
- AutoProcessor,
15
- RTDetrForObjectDetection,
16
- VitPoseForPoseEstimation,
17
  AutoImageProcessor,
18
  AutoModelForDepthEstimation,
19
  )
20
 
21
  from huggingface_hub import hf_hub_download
 
22
  from safetensors.torch import load_file as safetensors_load_file
23
 
24
  from gradio.themes import Soft
@@ -137,7 +137,6 @@ def _normalize_version(raw: str) -> Optional[str]:
137
  return None
138
  if _VER_RE.fullmatch(s):
139
  return s
140
- # forgiving: allow "21" -> "v21"
141
  if _DIGITS_RE.fullmatch(s):
142
  return f"v{s}"
143
  return None
@@ -181,19 +180,15 @@ def _load_pipe_with_version(version: str) -> QwenImageEditPlusPipeline:
181
  return p
182
 
183
 
184
- # Forgiving load: try env/default version, fallback to v19 if it fails
185
  try:
186
  pipe = _load_pipe_with_version(AIO_VERSION)
187
- except Exception as e:
188
  print("❌ Failed to load requested AIO_VERSION. Falling back to v19.")
189
- print("---- exception ----")
190
  print(traceback.format_exc())
191
- print("-------------------")
192
  AIO_VERSION = DEFAULT_AIO_VERSION
193
  AIO_VERSION_SOURCE = "fallback_to_v19"
194
  pipe = _load_pipe_with_version(AIO_VERSION)
195
 
196
- # Apply FA3 Optimization
197
  try:
198
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
199
  print("Flash Attention 3 Processor set successfully.")
@@ -203,56 +198,17 @@ except Exception as e:
203
  MAX_SEED = np.iinfo(np.int32).max
204
 
205
  # ============================================================
206
- # Derived conditioning (Transformers): Pose + Depth
207
  # ============================================================
208
- # Pose estimation uses ViTPose (top-down). Official docs show RT-DETR -> ViTPose flow:
209
- # https://huggingface.co/docs/transformers/model_doc/vitpose
210
- # Depth uses Depth Anything V2 Small (Transformers-compatible):
211
- # https://huggingface.co/depth-anything/Depth-Anything-V2-Small-hf
212
 
213
- POSE_MODEL_ID = "usyd-community/vitpose-base-simple"
214
- POSE_DETECTOR_ID = "PekingU/rtdetr_r50vd_coco_o365"
215
  DEPTH_MODEL_ID = "depth-anything/Depth-Anything-V2-Small-hf"
216
-
217
- # Lazy caches keyed by device string ("cpu" / "cuda")
218
- _POSE_CACHE = {}
219
  _DEPTH_CACHE = {}
220
 
221
- # COCO-17 skeleton connections (approx "OpenPose-like" stick figure)
222
- COCO17_EDGES = [
223
- (0, 1), (0, 2), (1, 3), (2, 4), # head
224
- (5, 6), # shoulders
225
- (5, 7), (7, 9), # left arm
226
- (6, 8), (8, 10), # right arm
227
- (5, 11), (6, 12), (11, 12), # torso
228
- (11, 13), (13, 15), # left leg
229
- (12, 14), (14, 16), # right leg
230
- ]
231
 
232
  def _derived_device(use_gpu: bool) -> torch.device:
233
  return torch.device("cuda" if (use_gpu and torch.cuda.is_available()) else "cpu")
234
 
235
 
236
- def _load_pose_models(dev: torch.device):
237
- key = str(dev)
238
- if key in _POSE_CACHE:
239
- return _POSE_CACHE[key]
240
-
241
- # Detector (optional but used for multi-person boxes)
242
- det_proc = AutoProcessor.from_pretrained(POSE_DETECTOR_ID)
243
- det_model = RTDetrForObjectDetection.from_pretrained(POSE_DETECTOR_ID).to(dev)
244
-
245
- # Pose model
246
- pose_proc = AutoProcessor.from_pretrained(POSE_MODEL_ID)
247
- pose_model = VitPoseForPoseEstimation.from_pretrained(POSE_MODEL_ID).to(dev)
248
-
249
- det_model.eval()
250
- pose_model.eval()
251
-
252
- _POSE_CACHE[key] = (det_proc, det_model, pose_proc, pose_model)
253
- return _POSE_CACHE[key]
254
-
255
-
256
  def _load_depth_models(dev: torch.device):
257
  key = str(dev)
258
  if key in _DEPTH_CACHE:
@@ -266,116 +222,7 @@ def _load_depth_models(dev: torch.device):
266
  return _DEPTH_CACHE[key]
267
 
268
 
269
- def _draw_skeleton_on_blank(
270
- size: tuple[int, int],
271
- persons_keypoints: list[np.ndarray],
272
- persons_scores: list[np.ndarray],
273
- kp_thresh: float = 0.20,
274
- point_r: int = 3,
275
- line_w: int = 3,
276
- ) -> Image.Image:
277
- w, h = size
278
- canvas = Image.new("RGB", (w, h), (0, 0, 0))
279
- draw = ImageDraw.Draw(canvas)
280
-
281
- for kps, sc in zip(persons_keypoints, persons_scores):
282
- # Draw edges
283
- for a, b in COCO17_EDGES:
284
- if a >= len(sc) or b >= len(sc):
285
- continue
286
- if sc[a] < kp_thresh or sc[b] < kp_thresh:
287
- continue
288
- xa, ya = float(kps[a, 0]), float(kps[a, 1])
289
- xb, yb = float(kps[b, 0]), float(kps[b, 1])
290
- draw.line([(xa, ya), (xb, yb)], fill=(255, 255, 255), width=line_w)
291
-
292
- # Draw keypoints
293
- for i in range(min(len(sc), len(kps))):
294
- if sc[i] < kp_thresh:
295
- continue
296
- x, y = float(kps[i, 0]), float(kps[i, 1])
297
- draw.ellipse(
298
- [(x - point_r, y - point_r), (x + point_r, y + point_r)],
299
- fill=(255, 255, 255),
300
- outline=None,
301
- )
302
-
303
- return canvas
304
-
305
-
306
- def make_pose_map(
307
- img: Image.Image,
308
- *,
309
- use_gpu: bool,
310
- mode: str,
311
- det_thresh: float = 0.30,
312
- max_people: int = 4,
313
- ) -> Image.Image:
314
- """Return an OpenPose-like skeleton map (RGB) using Transformers models.
315
-
316
- mode:
317
- - "fast": full-frame box (no detector). Good when Image 1 is already a single subject.
318
- - "detect": RT-DETR person boxes -> ViTPose. Better for multi-person scenes.
319
- """
320
- img = img.convert("RGB")
321
- dev = _derived_device(use_gpu)
322
- det_proc, det_model, pose_proc, pose_model = _load_pose_models(dev)
323
-
324
- w, h = img.size
325
-
326
- if mode == "fast":
327
- # Single box covering whole image, COCO format [x, y, w, h]
328
- boxes = np.array([[0.0, 0.0, float(w), float(h)]], dtype=np.float32)
329
- else:
330
- # Detect people
331
- inputs = det_proc(images=img, return_tensors="pt").to(dev)
332
- with torch.no_grad():
333
- outputs = det_model(**inputs)
334
-
335
- results = det_proc.post_process_object_detection(
336
- outputs,
337
- target_sizes=torch.tensor([(h, w)], device=dev),
338
- threshold=det_thresh,
339
- )[0]
340
-
341
- # COCO label 0 is "person" for COCO-trained detectors
342
- person_boxes = results["boxes"][results["labels"] == 0].detach().cpu().numpy()
343
-
344
- if person_boxes.size == 0:
345
- # Fallback to full-frame
346
- boxes = np.array([[0.0, 0.0, float(w), float(h)]], dtype=np.float32)
347
- else:
348
- # Convert VOC x1,y1,x2,y2 to COCO x,y,w,h
349
- person_boxes[:, 2] = person_boxes[:, 2] - person_boxes[:, 0]
350
- person_boxes[:, 3] = person_boxes[:, 3] - person_boxes[:, 1]
351
- boxes = person_boxes.astype(np.float32)
352
-
353
- if boxes.shape[0] > max_people:
354
- boxes = boxes[:max_people]
355
-
356
- pose_inputs = pose_proc(img, boxes=[boxes], return_tensors="pt").to(dev)
357
- with torch.no_grad():
358
- pose_outputs = pose_model(**pose_inputs)
359
-
360
- pose_results = pose_proc.post_process_pose_estimation(pose_outputs, boxes=[boxes])[0]
361
-
362
- persons_kps = []
363
- persons_sc = []
364
- for pr in pose_results:
365
- kps = pr["keypoints"].detach().cpu().numpy()
366
- sc = pr["scores"].detach().cpu().numpy()
367
- persons_kps.append(kps)
368
- persons_sc.append(sc)
369
-
370
- if not persons_kps:
371
- # No pose found; return black canvas
372
- return Image.new("RGB", img.size, (0, 0, 0))
373
-
374
- return _draw_skeleton_on_blank(img.size, persons_kps, persons_sc)
375
-
376
-
377
  def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image:
378
- """Return a grayscale (RGB) depth map using Depth Anything V2 Small."""
379
  img = img.convert("RGB")
380
  dev = _derived_device(use_gpu)
381
  proc, model = _load_depth_models(dev)
@@ -386,10 +233,7 @@ def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image:
386
  with torch.no_grad():
387
  out = model(**inputs)
388
 
389
- # predicted_depth: (B, H, W)
390
  pred = out.predicted_depth
391
-
392
- # Upsample to original image size
393
  pred = torch.nn.functional.interpolate(
394
  pred.unsqueeze(1),
395
  size=(img.height, img.width),
@@ -403,8 +247,19 @@ def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image:
403
  arr = arr / denom
404
 
405
  depth8 = (arr * 255.0).clip(0, 255).astype(np.uint8)
406
- depth_img = Image.fromarray(depth8, mode="L").convert("RGB")
407
- return depth_img
 
 
 
 
 
 
 
 
 
 
 
408
 
409
 
410
  def _append_to_gallery(existing, new_img: Image.Image):
@@ -417,6 +272,7 @@ def _append_to_gallery(existing, new_img: Image.Image):
417
  items.append(new_img)
418
  return items
419
 
 
420
  # ============================================================
421
  # LoRA adapters + presets
422
  # ============================================================
@@ -486,7 +342,7 @@ ADAPTER_SPECS = {
486
  "weights": "bfs_head_v5_2511_original.safetensors",
487
  "adapter_name": "BFS-Best-Faceswap",
488
  "strength": 1.0,
489
- "needs_alpha_fix": True, # <-- fixes KeyError 'img_in.alpha'
490
  },
491
  "BFS-Best-FaceSwap-merge": {
492
  "type": "single",
@@ -496,7 +352,7 @@ ADAPTER_SPECS = {
496
  "weights": "bfs_head_v5_2511_merged_version_rank_32_fp32.safetensors",
497
  "adapter_name": "BFS-Best-Faceswap-merge",
498
  "strength": 1.1,
499
- "needs_alpha_fix": True, # <-- fixes KeyError 'img_in.alpha'
500
  },
501
  "F2P": {
502
  "type": "single",
@@ -575,480 +431,507 @@ LORA_PRESET_PROMPTS = {
575
  "Any2Real_2601": "change the picture 1 to realistic photograph",
576
  "Semirealistic-photo-detailer": "transform the image to semi-realistic image",
577
  "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.",
578
- "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.",
579
  "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.",
580
  "Upscale2K": "Upscale this picture to 4K resolution.",
581
  "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",
582
  "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",
583
  }
584
 
585
- # Track what is currently loaded in memory (adapter_name values)
586
  LOADED_ADAPTERS = set()
587
 
588
  # ============================================================
589
  # Helpers: resolution
590
  # ============================================================
591
 
592
- # We prefer *area-based* sizing (≈ megapixels) over long-edge sizing.
593
- # This aligns better with Qwen-Image-Edit's internal assumptions and reduces FOV drift.
594
 
595
  def _round_to_multiple(x: int, m: int) -> int:
596
  return max(m, (int(x) // m) * m)
597
 
 
598
  def compute_canvas_dimensions_from_area(
599
  image: Image.Image,
600
  target_area: int,
601
- multiple_of: int,
602
- ) -> tuple[int, int]:
603
- """Compute (width, height) that matches image aspect ratio and approximates target_area.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
- The result is floored to be divisible by multiple_of (typically vae_scale_factor*2).
606
- """
607
- w, h = image.size
608
- aspect = w / h if h else 1.0
 
 
 
 
609
 
610
- # Use the pipeline's own area->(w,h) helper for consistency.
611
- from qwenimage.pipeline_qwenimage_edit_plus import calculate_dimensions
612
 
613
- width, height = calculate_dimensions(int(target_area), float(aspect))
614
- width = _round_to_multiple(int(width), int(multiple_of))
615
- height = _round_to_multiple(int(height), int(multiple_of))
616
- return width, height
617
 
618
- def get_target_area_for_lora(
619
- image: Image.Image,
620
- lora_adapter: str,
621
- user_target_megapixels: float,
622
- ) -> int:
623
- """Return target pixel area for the canvas.
624
-
625
- Priority:
626
- 1) Adapter spec: target_area (pixels) or target_megapixels
627
- 2) Adapter spec: target_long_edge (legacy) -> converted to area using image aspect
628
- 3) User slider target megapixels
629
- """
630
- spec = ADAPTER_SPECS.get(lora_adapter, {})
631
 
632
- if "target_area" in spec:
633
- try:
634
- return int(spec["target_area"])
635
- except Exception:
636
- pass
637
 
638
- if "target_megapixels" in spec:
639
- try:
640
- mp = float(spec["target_megapixels"])
641
- return int(mp * 1024 * 1024)
642
- except Exception:
643
- pass
644
 
645
- # Legacy support (e.g. Upscale2K)
646
- if "target_long_edge" in spec:
647
- try:
648
- long_edge = int(spec["target_long_edge"])
649
- w, h = image.size
650
- if w >= h:
651
- new_w = long_edge
652
- new_h = int(round(long_edge * (h / w)))
653
- else:
654
- new_h = long_edge
655
- new_w = int(round(long_edge * (w / h)))
656
- return int(new_w * new_h)
657
- except Exception:
658
- pass
659
 
660
- # User default
661
- return int(float(user_target_megapixels) * 1024 * 1024)
 
 
 
 
 
 
 
662
 
663
- # ============================================================
664
- # Helpers: multi-input routing + gallery normalization
665
- # ============================================================
666
 
 
 
 
 
 
 
 
667
 
668
- def lora_requires_two_images(lora_adapter: str) -> bool:
669
- return bool(ADAPTER_SPECS.get(lora_adapter, {}).get("requires_two_images", False))
670
 
 
 
 
 
 
 
 
 
 
671
 
672
- def image2_label_for_lora(lora_adapter: str) -> str:
673
- return str(ADAPTER_SPECS.get(lora_adapter, {}).get("image2_label", "Upload Reference (Image 2)"))
 
 
 
 
 
 
674
 
675
 
676
- def _to_pil_rgb(x) -> Optional[Image.Image]:
677
- """
678
- Accepts PIL / numpy / (image, caption) tuples from gr.Gallery and returns PIL RGB.
679
- Gradio Gallery commonly yields tuples like (image, caption).
680
- """
681
- if x is None:
682
- return None
683
 
684
- # Gallery often returns (image, caption)
685
- if isinstance(x, tuple) and len(x) >= 1:
686
- x = x[0]
687
- if x is None:
688
- return None
689
 
690
- if isinstance(x, Image.Image):
691
- return x.convert("RGB")
 
692
 
693
- if isinstance(x, np.ndarray):
694
- return Image.fromarray(x).convert("RGB")
695
 
696
- # Best-effort fallback
697
- try:
698
- return Image.fromarray(np.array(x)).convert("RGB")
699
- except Exception:
700
- return None
 
 
701
 
702
 
703
- def build_labeled_images(
704
- img1: Image.Image,
705
- img2: Optional[Image.Image],
706
- extra_imgs: Optional[list[Image.Image]],
707
- ) -> dict[str, Image.Image]:
708
- """
709
- Creates labels image_1, image_2, image_3... based on what is actually uploaded:
710
- - img1 is always image_1
711
- - img2 becomes image_2 only if present
712
- - extras start immediately after the last present base box
713
- The pipeline receives images in this exact order.
714
- """
715
- labeled: dict[str, Image.Image] = {}
716
- idx = 1
717
 
718
- labeled[f"image_{idx}"] = img1
719
- idx += 1
720
 
721
- if img2 is not None:
722
- labeled[f"image_{idx}"] = img2
723
- idx += 1
724
 
725
- if extra_imgs:
726
- for im in extra_imgs:
727
- if im is None:
728
- continue
729
- labeled[f"image_{idx}"] = im
730
- idx += 1
731
 
732
- return labeled
 
733
 
734
 
735
  # ============================================================
736
- # Helpers: BFS alpha key fix
737
  # ============================================================
738
 
739
 
740
- def _inject_missing_alpha_keys(state_dict: dict) -> dict:
741
- """
742
- Diffusers' Qwen LoRA converter expects '<module>.alpha' keys.
743
- BFS safetensors omits them. We inject alpha = rank (neutral scaling).
744
 
745
- IMPORTANT: diffusers may strip 'diffusion_model.' before lookup, so we
746
- inject BOTH:
747
- - diffusion_model.xxx.alpha
748
- - xxx.alpha
749
- """
750
- bases = {}
751
 
752
- for k, v in state_dict.items():
753
- if not isinstance(v, torch.Tensor):
754
- continue
755
- if k.endswith(".lora_down.weight") and v.ndim >= 1:
756
- base = k[: -len(".lora_down.weight")]
757
- rank = int(v.shape[0])
758
- bases[base] = rank
759
 
760
- for base, rank in bases.items():
761
- alpha_tensor = torch.tensor(float(rank), dtype=torch.float32)
762
 
763
- full_alpha = f"{base}.alpha"
764
- if full_alpha not in state_dict:
765
- state_dict[full_alpha] = alpha_tensor
 
766
 
767
- if base.startswith("diffusion_model."):
768
- stripped_base = base[len("diffusion_model.") :]
769
- stripped_alpha = f"{stripped_base}.alpha"
770
- if stripped_alpha not in state_dict:
771
- state_dict[stripped_alpha] = alpha_tensor
772
 
773
- return state_dict
 
 
 
774
 
 
 
775
 
776
- def _filter_to_diffusers_lora_keys(state_dict: dict) -> tuple[dict, dict]:
777
- """Return (filtered_state_dict, stats).
778
 
779
- Some ComfyUI/Qwen safetensors (especially "merged" variants) include non-LoRA
780
- delta/patch keys like `*.diff` and `*.diff_b` alongside real LoRA tensors.
781
- Diffusers' internal Qwen LoRA converter is strict: any leftover keys cause an
782
- error (`state_dict should be empty...`).
783
 
784
- This helper keeps only the keys Diffusers can consume as a LoRA:
785
- - `*.lora_up.weight`
786
- - `*.lora_down.weight`
787
- - (rare) `*.lora_mid.weight`
788
- - alpha keys: `*.alpha` (or `*.lora_alpha` which we normalize to `*.alpha`)
789
 
790
- It also drops known patch keys (`*.diff`, `*.diff_b`) and everything else.
791
- """
792
 
793
- keep_suffixes = (
794
- ".lora_up.weight",
795
- ".lora_down.weight",
796
- ".lora_mid.weight",
797
- ".alpha",
798
- ".lora_alpha",
799
- )
800
 
801
- dropped_patch = 0
802
- dropped_other = 0
803
- kept = 0
804
- normalized_alpha = 0
805
-
806
- out: dict[str, torch.Tensor] = {}
807
- for k, v in state_dict.items():
808
- if not isinstance(v, torch.Tensor):
809
- # Ignore non-tensor entries if any.
810
- dropped_other += 1
811
- continue
812
-
813
- # Drop ComfyUI "delta" keys that Diffusers' LoRA loader will never consume.
814
- if k.endswith(".diff") or k.endswith(".diff_b"):
815
- dropped_patch += 1
816
- continue
817
-
818
- if not k.endswith(keep_suffixes):
819
- dropped_other += 1
820
- continue
821
-
822
- if k.endswith(".lora_alpha"):
823
- # Normalize common alt name to what Diffusers expects.
824
- base = k[: -len(".lora_alpha")]
825
- k2 = f"{base}.alpha"
826
- out[k2] = v.float() if v.dtype != torch.float32 else v
827
- normalized_alpha += 1
828
- kept += 1
829
- continue
830
-
831
- out[k] = v
832
- kept += 1
833
-
834
- stats = {
835
- "kept": kept,
836
- "dropped_patch": dropped_patch,
837
- "dropped_other": dropped_other,
838
- "normalized_alpha": normalized_alpha,
839
- }
840
- return out, stats
841
-
842
-
843
- def _duplicate_stripped_prefix_keys(state_dict: dict, prefix: str = "diffusion_model.") -> dict:
844
- """Ensure both prefixed and unprefixed variants exist for LoRA-related keys.
845
-
846
- Diffusers' Qwen LoRA conversion may strip `diffusion_model.` when looking up
847
- modules. Some exports only include prefixed keys. To be maximally compatible,
848
- we duplicate LoRA keys (and alpha) in stripped form when missing.
849
- """
850
 
851
- out = dict(state_dict)
852
- for k, v in list(state_dict.items()):
853
- if not k.startswith(prefix):
854
- continue
855
- stripped = k[len(prefix) :]
856
- if stripped not in out:
857
- out[stripped] = v
858
- return out
859
 
860
 
861
- def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name: str, needs_alpha_fix: bool = False):
 
 
 
 
 
 
 
 
 
 
 
862
  """
863
- Normal path: pipe.load_lora_weights(repo, weight_name=..., adapter_name=...)
864
- BFS fallback: download safetensors, inject missing alpha keys, then load from dict.
865
  """
866
- try:
867
- pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=adapter_name)
868
- return
869
- except (KeyError, ValueError) as e:
870
- # KeyError: missing required alpha keys (common in BFS)
871
- # ValueError: Diffusers Qwen converter found leftover keys (e.g. .diff/.diff_b)
872
- if not needs_alpha_fix:
873
- raise
874
-
875
- print(
876
- "⚠️ LoRA load failed (will try safe dict fallback). "
877
- f"Adapter={adapter_name!r} file={weight_name!r} error={type(e).__name__}: {e}"
878
- )
879
 
880
- local_path = hf_hub_download(repo_id=repo, filename=weight_name)
881
- sd = safetensors_load_file(local_path)
 
 
 
882
 
883
- # 1) Inject required `<module>.alpha` keys (neutral scaling alpha=rank).
884
- sd = _inject_missing_alpha_keys(sd)
885
 
886
- # 2) Keep only LoRA + alpha keys; drop ComfyUI patch/delta keys.
887
- sd, stats = _filter_to_diffusers_lora_keys(sd)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
 
889
- # 3) Duplicate stripped keys (remove `diffusion_model.`) for compatibility.
890
- sd = _duplicate_stripped_prefix_keys(sd)
891
 
892
- print(
893
- "🧹 LoRA dict cleanup stats: "
894
- f"kept={stats['kept']} dropped_patch={stats['dropped_patch']} "
895
- f"dropped_other={stats['dropped_other']} normalized_alpha={stats['normalized_alpha']}"
896
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
 
898
- pipe.load_lora_weights(sd, adapter_name=adapter_name)
899
- return
900
 
 
 
901
 
902
- # ============================================================
903
- # LoRA loader: single/package + strengths
904
- # ============================================================
905
 
 
 
 
 
906
 
907
- def _ensure_loaded_and_get_active_adapters(selected_lora: str):
908
- spec = ADAPTER_SPECS.get(selected_lora)
909
- if not spec:
910
- raise gr.Error(f"Configuration not found for: {selected_lora}")
 
 
 
 
911
 
912
- adapter_names = []
913
- adapter_weights = []
914
-
915
- if spec.get("type") == "package":
916
- parts = spec.get("parts", [])
917
- if not parts:
918
- raise gr.Error(f"Package spec has no parts: {selected_lora}")
919
-
920
- for part in parts:
921
- repo = part["repo"]
922
- weights = part["weights"]
923
- adapter_name = part["adapter_name"]
924
- strength = float(part.get("strength", 1.0))
925
- needs_alpha_fix = bool(part.get("needs_alpha_fix", False))
926
-
927
- if adapter_name not in LOADED_ADAPTERS:
928
- print(f"--- Downloading and Loading Adapter Part: {selected_lora} / {adapter_name} ---")
929
- try:
930
- _load_lora_weights_with_fallback(
931
- repo=repo,
932
- weight_name=weights,
933
- adapter_name=adapter_name,
934
- needs_alpha_fix=needs_alpha_fix,
935
- )
936
- LOADED_ADAPTERS.add(adapter_name)
937
- except Exception as e:
938
- raise gr.Error(f"Failed to load adapter part {selected_lora}/{adapter_name}: {e}")
939
- else:
940
- print(f"--- Adapter part already loaded: {selected_lora} / {adapter_name} ---")
941
 
942
- adapter_names.append(adapter_name)
943
- adapter_weights.append(strength)
 
 
944
 
945
- else:
946
- repo = spec["repo"]
947
- weights = spec["weights"]
948
- adapter_name = spec["adapter_name"]
949
- strength = float(spec.get("strength", 1.0))
950
- needs_alpha_fix = bool(spec.get("needs_alpha_fix", False))
951
-
952
- if adapter_name not in LOADED_ADAPTERS:
953
- print(f"--- Downloading and Loading Adapter: {selected_lora} ---")
954
- try:
955
- _load_lora_weights_with_fallback(
956
- repo=repo,
957
- weight_name=weights,
958
- adapter_name=adapter_name,
959
- needs_alpha_fix=needs_alpha_fix,
960
- )
961
- LOADED_ADAPTERS.add(adapter_name)
962
- except Exception as e:
963
- raise gr.Error(f"Failed to load adapter {selected_lora}: {e}")
964
- else:
965
- print(f"--- Adapter {selected_lora} is already loaded. ---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
 
967
- adapter_names = [adapter_name]
968
- adapter_weights = [strength]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
 
970
- return adapter_names, adapter_weights
 
971
 
 
 
972
 
973
- # ============================================================
974
- # UI handlers
975
- # ============================================================
 
976
 
 
 
 
 
 
 
 
977
 
978
- def on_lora_change_ui(selected_lora, current_prompt, current_extras_condition_only):
979
- # Preset prompt (fill only if empty)
980
- if selected_lora != NONE_LORA:
981
- preset = LORA_PRESET_PROMPTS.get(selected_lora, "")
982
- if preset and (current_prompt is None or str(current_prompt).strip() == ""):
983
- prompt_update = gr.update(value=preset)
984
- else:
985
- prompt_update = gr.update(value=current_prompt)
986
- else:
987
- prompt_update = gr.update(value=current_prompt)
988
 
989
- # Image2 visibility/label
990
- if lora_requires_two_images(selected_lora):
991
- img2_update = gr.update(visible=True, label=image2_label_for_lora(selected_lora))
992
- else:
993
- img2_update = gr.update(visible=False, value=None, label="Upload Reference (Image 2)")
994
 
995
- # Extra references routing default:
996
- # For BFS/AnyPose-like adapters, it's usually safer to keep extra refs as conditioning-only.
997
- if selected_lora in ("BFS-Best-FaceSwap", "BFS-Best-FaceSwap-merge", "AnyPose"):
998
- extras_update = gr.update(value=True)
999
- else:
1000
- extras_update = gr.update(value=current_extras_condition_only)
1001
 
1002
- return prompt_update, img2_update, extras_update
1003
- # ============================================================
1004
- # UI helpers: output routing + derived conditioning
1005
- # ============================================================
1006
 
1007
- def set_output_as_image1(last):
1008
- if last is None:
1009
- raise gr.Error("No output available yet.")
1010
- return gr.update(value=last)
1011
 
 
 
 
 
 
 
 
 
 
 
1012
 
1013
- def set_output_as_image2(last):
1014
- if last is None:
1015
- raise gr.Error("No output available yet.")
1016
- return gr.update(value=last)
 
 
 
 
 
 
 
1017
 
1018
 
1019
- def set_output_as_extra(last, existing_extra):
1020
- if last is None:
1021
- raise gr.Error("No output available yet.")
1022
- return _append_to_gallery(existing_extra, last)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
 
1024
 
1025
- @spaces.GPU
1026
- def add_derived_ref(img1, existing_extra, derived_type, derived_use_gpu, derived_max_people):
1027
- if img1 is None:
1028
- raise gr.Error("Please upload Image 1 first.")
 
1029
 
1030
- if derived_type == "None":
1031
- return gr.update(value=existing_extra), gr.update(visible=False, value=None)
 
 
 
1032
 
1033
- base = img1.convert("RGB")
1034
 
1035
- if derived_type == "Pose (ViTPose, fast)":
1036
- derived = make_pose_map(base, use_gpu=bool(derived_use_gpu), mode="fast")
1037
- elif derived_type == "Pose (ViTPose + RT-DETR detect)":
1038
- derived = make_pose_map(
1039
- base,
1040
- use_gpu=bool(derived_use_gpu),
1041
- mode="detect",
1042
- max_people=int(derived_max_people),
1043
- )
1044
- elif derived_type == "Depth (Depth Anything V2 Small)":
1045
- derived = make_depth_map(base, use_gpu=bool(derived_use_gpu))
1046
- else:
1047
- raise gr.Error(f"Unknown derived type: {derived_type}")
1048
 
1049
- new_gallery = _append_to_gallery(existing_extra, derived)
1050
- return gr.update(value=new_gallery), gr.update(visible=True, value=derived)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
 
 
 
 
 
 
 
 
 
 
 
 
1052
 
1053
 
1054
  # ============================================================
@@ -1060,7 +943,7 @@ def add_derived_ref(img1, existing_extra, derived_type, derived_use_gpu, derived
1060
  def infer(
1061
  input_image_1,
1062
  input_image_2,
1063
- input_images_extra, # gallery multi-image box
1064
  prompt,
1065
  lora_adapter,
1066
  seed,
@@ -1079,7 +962,6 @@ def infer(
1079
  if input_image_1 is None:
1080
  raise gr.Error("Please upload Image 1.")
1081
 
1082
- # Handle "None"
1083
  if lora_adapter == NONE_LORA:
1084
  try:
1085
  pipe.set_adapters([], adapter_weights=[])
@@ -1102,7 +984,6 @@ def infer(
1102
  img1 = input_image_1.convert("RGB")
1103
  img2 = input_image_2.convert("RGB") if input_image_2 is not None else None
1104
 
1105
- # Normalize extra images (Gallery) to PIL RGB (handles tuples from Gallery)
1106
  extra_imgs: list[Image.Image] = []
1107
  if input_images_extra:
1108
  for item in input_images_extra:
@@ -1110,20 +991,14 @@ def infer(
1110
  if pil is not None:
1111
  extra_imgs.append(pil)
1112
 
1113
- # Enforce existing 2-image LoRA behavior (image_1 + image_2 required)
1114
  if lora_requires_two_images(lora_adapter) and img2 is None:
1115
  raise gr.Error("This LoRA needs two images. Please upload Image 2 as well.")
1116
 
1117
- # Label images as image_1, image_2, image_3...
1118
  labeled = build_labeled_images(img1, img2, extra_imgs)
1119
-
1120
- # Pass to pipeline in labeled order. Keep single-image call when only one is present.
1121
  pipe_images = list(labeled.values())
1122
  if len(pipe_images) == 1:
1123
  pipe_images = pipe_images[0]
1124
 
1125
- # Resolution derived from Image 1 (base/body/target)
1126
- # Use target *area* (≈ megapixels) rather than long-edge sizing to reduce FOV drift.
1127
  target_area = get_target_area_for_lora(img1, lora_adapter, float(target_megapixels))
1128
  width, height = compute_canvas_dimensions_from_area(
1129
  img1,
@@ -1131,19 +1006,12 @@ def infer(
1131
  multiple_of=int(pipe.vae_scale_factor * 2),
1132
  )
1133
 
1134
- # Decide which images participate in the VAE latent stream.
1135
- # If enabled, extra references beyond (Img_1, Img_2) become conditioning-only.
1136
  vae_image_indices = None
1137
  if extras_condition_only:
1138
  if isinstance(pipe_images, list) and len(pipe_images) > 2:
1139
  vae_image_indices = [0, 1] if len(pipe_images) >= 2 else [0]
1140
 
1141
  try:
1142
- print(
1143
- "[DEBUG][infer] submitting request | "
1144
- f"lora_adapter={lora_adapter!r} seed={seed} prompt={prompt!r}"
1145
- )
1146
-
1147
  result = pipe(
1148
  image=pipe_images,
1149
  prompt=prompt,
@@ -1170,8 +1038,20 @@ def infer_example(input_image, prompt, lora_adapter):
1170
  input_pil = input_image.convert("RGB")
1171
  guidance_scale = 1.0
1172
  steps = 4
1173
- # Examples don't supply Image 2 or extra images; and example list doesn't include AnyPose/BFS.
1174
- result, seed, last = infer(input_pil, None, None, prompt, lora_adapter, 0, True, guidance_scale, steps, 1.0, True, True)
 
 
 
 
 
 
 
 
 
 
 
 
1175
  return result, seed, last
1176
 
1177
 
@@ -1180,11 +1060,8 @@ def infer_example(input_image, prompt, lora_adapter):
1180
  # ============================================================
1181
 
1182
  css = """
1183
- #col-container {
1184
- margin: 0 auto;
1185
- max-width: 960px;
1186
- }
1187
- #main-title h1 {font-size: 2.1em !important;}
1188
  """
1189
 
1190
  aio_status_line = (
@@ -1198,7 +1075,7 @@ with gr.Blocks() as demo:
1198
  gr.Markdown(
1199
  "Perform diverse image edits using specialized "
1200
  "[LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters for the "
1201
- "[Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) model. Uses a Diffusers compatible extraction of the transformers from Phr00t's Rapid AIO merge. If a different AIO version is desired, copy the space and set the space variable to change version.'"
1202
  )
1203
  gr.Markdown(aio_status_line)
1204
 
@@ -1222,11 +1099,45 @@ with gr.Blocks() as demo:
1222
  placeholder="e.g., transform into photo..",
1223
  )
1224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1225
  run_button = gr.Button("Edit Image", variant="primary")
1226
 
1227
  with gr.Column():
1228
  output_image = gr.Image(label="Output Image", interactive=False, format="png", height=353)
1229
-
1230
  last_output = gr.State(value=None)
1231
 
1232
  with gr.Row():
@@ -1251,25 +1162,13 @@ with gr.Blocks() as demo:
1251
  )
1252
 
1253
  with gr.Accordion("Advanced Settings", open=False, visible=True):
1254
- with gr.Accordion("Derived Conditioning (Pose / Depth)", open=False):
1255
  derived_type = gr.Dropdown(
1256
  label="Derived Type (from Image 1)",
1257
- choices=[
1258
- "None",
1259
- "Pose (ViTPose, fast)",
1260
- "Pose (ViTPose + RT-DETR detect)",
1261
- "Depth (Depth Anything V2 Small)",
1262
- ],
1263
  value="None",
1264
  )
1265
  derived_use_gpu = gr.Checkbox(label="Use GPU for derived model", value=False)
1266
- derived_max_people = gr.Slider(
1267
- label="Max people (pose detect mode)",
1268
- minimum=1,
1269
- maximum=10,
1270
- step=1,
1271
- value=4,
1272
- )
1273
  add_derived_btn = gr.Button("➕ Add derived ref to Extras (conditioning-only recommended)")
1274
 
1275
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
@@ -1292,38 +1191,64 @@ with gr.Blocks() as demo:
1292
  value=True,
1293
  )
1294
 
1295
- # On LoRA selection: preset prompt + toggle Image 2
1296
  lora_adapter.change(
1297
  fn=on_lora_change_ui,
1298
  inputs=[lora_adapter, prompt, extras_condition_only],
1299
  outputs=[prompt, input_image_2, extras_condition_only],
1300
  )
1301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1302
  gr.Examples(
1303
  examples=[
1304
  ["examples/5.jpg", "Remove shadows and relight the image using soft lighting.", "Light-Restoration"],
1305
  ["examples/4.jpg", "Use a subtle golden-hour filter with smooth light diffusion.", "Relight"],
1306
  ["examples/2.jpeg", "Rotate the camera 45 degrees to the left.", "Multiple-Angles"],
1307
- [
1308
- "examples/12.jpg",
1309
- "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.",
1310
- "Flat-Log",
1311
- ],
1312
- ["examples/7.jpg", "Light source from the Right Rear", "Multi-Angle-Lighting"],
1313
- ["examples/10.jpeg", "Upscale the image.", "Upscale-Image"],
1314
- ["examples/7.jpg", "Light source from the Below", "Multi-Angle-Lighting"],
1315
- ["examples/2.jpeg", "Switch the camera to a top-down right corner view.", "Multiple-Angles"],
1316
- [
1317
- "examples/9.jpg",
1318
- "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.",
1319
- "Next-Scene",
1320
- ],
1321
- ["examples/8.jpg", "Make the subjects skin details more prominent and natural.", "Edit-Skin"],
1322
- ["examples/6.jpg", "Switch the camera to a bottom-up view.", "Multiple-Angles"],
1323
- ["examples/6.jpg", "Rotate the camera 180 degrees upside down.", "Multiple-Angles"],
1324
- ["examples/4.jpg", "Rotate the camera 45 degrees to the right.", "Multiple-Angles"],
1325
- ["examples/4.jpg", "Switch the camera to a top-down view.", "Multiple-Angles"],
1326
- ["examples/4.jpg", "Switch the camera to a wide-angle lens.", "Multiple-Angles"],
1327
  ["examples/11.jpg", "Upscale this picture to 4K resolution.", "Upscale2K"],
1328
  ],
1329
  inputs=[input_image_1, prompt, lora_adapter],
@@ -1352,18 +1277,18 @@ with gr.Blocks() as demo:
1352
  outputs=[output_image, seed, last_output],
1353
  )
1354
 
1355
- # Output routing buttons
1356
  btn_out_to_img1.click(fn=set_output_as_image1, inputs=[last_output], outputs=[input_image_1])
1357
  btn_out_to_img2.click(fn=set_output_as_image2, inputs=[last_output], outputs=[input_image_2])
1358
  btn_out_to_extra.click(fn=set_output_as_extra, inputs=[last_output, input_images_extra], outputs=[input_images_extra])
1359
-
1360
- # Derived conditioning: append pose/depth map as extra ref (UI shows preview)
1361
  add_derived_btn.click(
1362
  fn=add_derived_ref,
1363
- inputs=[input_image_1, input_images_extra, derived_type, derived_use_gpu, derived_max_people],
1364
  outputs=[input_images_extra, derived_preview],
1365
  )
1366
-
1367
  if __name__ == "__main__":
1368
  demo.queue(max_size=30).launch(
1369
  css=css,
 
2
  import re
3
  import gc
4
  import traceback
5
+ import base64
6
+ import io
7
  import gradio as gr
8
  import numpy as np
9
  import spaces
10
  import torch
11
  import random
12
+ from PIL import Image
13
+ from typing import Iterable, Optional, Tuple
14
 
15
  from transformers import (
 
 
 
16
  AutoImageProcessor,
17
  AutoModelForDepthEstimation,
18
  )
19
 
20
  from huggingface_hub import hf_hub_download
21
+ from huggingface_hub import InferenceClient
22
  from safetensors.torch import load_file as safetensors_load_file
23
 
24
  from gradio.themes import Soft
 
137
  return None
138
  if _VER_RE.fullmatch(s):
139
  return s
 
140
  if _DIGITS_RE.fullmatch(s):
141
  return f"v{s}"
142
  return None
 
180
  return p
181
 
182
 
 
183
  try:
184
  pipe = _load_pipe_with_version(AIO_VERSION)
185
+ except Exception:
186
  print("❌ Failed to load requested AIO_VERSION. Falling back to v19.")
 
187
  print(traceback.format_exc())
 
188
  AIO_VERSION = DEFAULT_AIO_VERSION
189
  AIO_VERSION_SOURCE = "fallback_to_v19"
190
  pipe = _load_pipe_with_version(AIO_VERSION)
191
 
 
192
  try:
193
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
194
  print("Flash Attention 3 Processor set successfully.")
 
198
  MAX_SEED = np.iinfo(np.int32).max
199
 
200
  # ============================================================
201
+ # Derived conditioning (Depth Anything) ONLY ViTPose removed
202
  # ============================================================
 
 
 
 
203
 
 
 
204
  DEPTH_MODEL_ID = "depth-anything/Depth-Anything-V2-Small-hf"
 
 
 
205
  _DEPTH_CACHE = {}
206
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  def _derived_device(use_gpu: bool) -> torch.device:
209
  return torch.device("cuda" if (use_gpu and torch.cuda.is_available()) else "cpu")
210
 
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  def _load_depth_models(dev: torch.device):
213
  key = str(dev)
214
  if key in _DEPTH_CACHE:
 
222
  return _DEPTH_CACHE[key]
223
 
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image:
 
226
  img = img.convert("RGB")
227
  dev = _derived_device(use_gpu)
228
  proc, model = _load_depth_models(dev)
 
233
  with torch.no_grad():
234
  out = model(**inputs)
235
 
 
236
  pred = out.predicted_depth
 
 
237
  pred = torch.nn.functional.interpolate(
238
  pred.unsqueeze(1),
239
  size=(img.height, img.width),
 
247
  arr = arr / denom
248
 
249
  depth8 = (arr * 255.0).clip(0, 255).astype(np.uint8)
250
+ return Image.fromarray(depth8, mode="L").convert("RGB")
251
+
252
+
253
+ def _to_pil_rgb(item):
254
+ if item is None:
255
+ return None
256
+ if isinstance(item, (tuple, list)) and len(item) >= 1:
257
+ item = item[0]
258
+ if isinstance(item, Image.Image):
259
+ return item.convert("RGB")
260
+ if isinstance(item, np.ndarray):
261
+ return Image.fromarray(item).convert("RGB")
262
+ return None
263
 
264
 
265
  def _append_to_gallery(existing, new_img: Image.Image):
 
272
  items.append(new_img)
273
  return items
274
 
275
+
276
  # ============================================================
277
  # LoRA adapters + presets
278
  # ============================================================
 
342
  "weights": "bfs_head_v5_2511_original.safetensors",
343
  "adapter_name": "BFS-Best-Faceswap",
344
  "strength": 1.0,
345
+ "needs_alpha_fix": True,
346
  },
347
  "BFS-Best-FaceSwap-merge": {
348
  "type": "single",
 
352
  "weights": "bfs_head_v5_2511_merged_version_rank_32_fp32.safetensors",
353
  "adapter_name": "BFS-Best-Faceswap-merge",
354
  "strength": 1.1,
355
+ "needs_alpha_fix": True,
356
  },
357
  "F2P": {
358
  "type": "single",
 
431
  "Any2Real_2601": "change the picture 1 to realistic photograph",
432
  "Semirealistic-photo-detailer": "transform the image to semi-realistic image",
433
  "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.",
434
+ "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 background, an 85mm macro photographic look, and raw photo grading without retouching to maintain realism and original details.",
435
  "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.",
436
  "Upscale2K": "Upscale this picture to 4K resolution.",
437
  "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",
438
  "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",
439
  }
440
 
 
441
  LOADED_ADAPTERS = set()
442
 
443
  # ============================================================
444
  # Helpers: resolution
445
  # ============================================================
446
 
 
 
447
 
448
  def _round_to_multiple(x: int, m: int) -> int:
449
  return max(m, (int(x) // m) * m)
450
 
451
+
452
  def compute_canvas_dimensions_from_area(
453
  image: Image.Image,
454
  target_area: int,
455
+ multiple_of: int = 64,
456
+ ) -> Tuple[int, int]:
457
+ w0, h0 = image.size
458
+ if w0 <= 0 or h0 <= 0:
459
+ return 512, 512
460
+ aspect = w0 / h0
461
+ w = int((target_area * aspect) ** 0.5)
462
+ h = int(w / aspect) if aspect != 0 else int((target_area) ** 0.5)
463
+ w = _round_to_multiple(w, multiple_of)
464
+ h = _round_to_multiple(h, multiple_of)
465
+ w = max(multiple_of, w)
466
+ h = max(multiple_of, h)
467
+ return w, h
468
+
469
+
470
+ def get_target_area_for_lora(image: Image.Image, lora_adapter: str, target_megapixels: float) -> int:
471
+ spec = ADAPTER_SPECS.get(lora_adapter, {})
472
+ long_edge = spec.get("target_long_edge", None)
473
 
474
+ if long_edge:
475
+ w0, h0 = image.size
476
+ if w0 <= 0 or h0 <= 0:
477
+ return int(1.0 * 1024 * 1024)
478
+ scale = float(long_edge) / float(max(w0, h0))
479
+ w = int(w0 * scale)
480
+ h = int(h0 * scale)
481
+ return max(64 * 64, w * h)
482
 
483
+ mp = float(target_megapixels)
484
+ return max(64 * 64, int(mp * 1_000_000))
485
 
 
 
 
 
486
 
487
+ # ============================================================
488
+ # Helpers: LoRA loading + alpha fix
489
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
490
 
 
 
 
 
 
491
 
492
+ def _download_from_hf(repo_id: str, filename: str) -> str:
493
+ return hf_hub_download(repo_id=repo_id, filename=filename)
 
 
 
 
494
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
+ def _maybe_apply_alpha_fix(state_dict: dict) -> dict:
497
+ if "img_in.alpha" not in state_dict:
498
+ for k in list(state_dict.keys()):
499
+ if k.endswith("img_in.weight") or k.endswith("img_in.bias"):
500
+ t = state_dict[k]
501
+ if hasattr(t, "new_zeros"):
502
+ state_dict["img_in.alpha"] = t.new_zeros(())
503
+ break
504
+ return state_dict
505
 
 
 
 
506
 
507
+ def _load_single_lora(spec: dict):
508
+ local_path = _download_from_hf(spec["repo"], spec["weights"])
509
+ sd = safetensors_load_file(local_path)
510
+ if spec.get("needs_alpha_fix", False):
511
+ sd = _maybe_apply_alpha_fix(sd)
512
+ pipe.load_lora_weights(sd, adapter_name=spec["adapter_name"])
513
+ LOADED_ADAPTERS.add(spec["adapter_name"])
514
 
 
 
515
 
516
+ def _ensure_loaded_and_get_active_adapters(lora_adapter: str):
517
+ spec = ADAPTER_SPECS.get(lora_adapter, None)
518
+ if spec is None:
519
+ return [], []
520
+
521
+ if spec["type"] == "single":
522
+ if spec["adapter_name"] not in LOADED_ADAPTERS:
523
+ _load_single_lora(spec)
524
+ return [spec["adapter_name"]], [spec.get("strength", 1.0)]
525
 
526
+ adapter_names = []
527
+ weights = []
528
+ for part in spec["parts"]:
529
+ if part["adapter_name"] not in LOADED_ADAPTERS:
530
+ _load_single_lora(part)
531
+ adapter_names.append(part["adapter_name"])
532
+ weights.append(part.get("strength", 1.0))
533
+ return adapter_names, weights
534
 
535
 
536
+ def lora_requires_two_images(lora_adapter: str) -> bool:
537
+ spec = ADAPTER_SPECS.get(lora_adapter, {})
538
+ return bool(spec.get("requires_two_images", False))
 
 
 
 
539
 
 
 
 
 
 
540
 
541
+ def get_image2_label_for_lora(lora_adapter: str) -> str:
542
+ spec = ADAPTER_SPECS.get(lora_adapter, {})
543
+ return spec.get("image2_label", "Upload Reference (Image 2)")
544
 
 
 
545
 
546
+ def build_labeled_images(img1: Image.Image, img2: Optional[Image.Image], extras: list[Image.Image]):
547
+ labeled = {"image_1": img1}
548
+ if img2 is not None:
549
+ labeled["image_2"] = img2
550
+ for ex in extras:
551
+ labeled[f"image_{len(labeled) + 1}"] = ex
552
+ return labeled
553
 
554
 
555
+ # ============================================================
556
+ # UI: lora change handler
557
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
 
558
 
 
 
559
 
560
+ def on_lora_change_ui(lora_adapter, current_prompt, current_extras_condition_only):
561
+ preset = LORA_PRESET_PROMPTS.get(lora_adapter, None)
562
+ prompt_update = gr.update(value=preset) if preset else gr.update(value=current_prompt)
563
 
564
+ needs_two = lora_requires_two_images(lora_adapter)
565
+ img2_update = gr.update(visible=needs_two, label=get_image2_label_for_lora(lora_adapter))
 
 
 
 
566
 
567
+ extras_update = gr.update(value=True) if needs_two else gr.update(value=current_extras_condition_only)
568
+ return prompt_update, img2_update, extras_update
569
 
570
 
571
  # ============================================================
572
+ # Output routing + derived conditioning
573
  # ============================================================
574
 
575
 
576
+ def set_output_as_image1(last):
577
+ if last is None:
578
+ raise gr.Error("No output available yet.")
579
+ return gr.update(value=last)
580
 
 
 
 
 
 
 
581
 
582
+ def set_output_as_image2(last):
583
+ if last is None:
584
+ raise gr.Error("No output available yet.")
585
+ return gr.update(value=last)
 
 
 
586
 
 
 
587
 
588
+ def set_output_as_extra(last, existing_extra):
589
+ if last is None:
590
+ raise gr.Error("No output available yet.")
591
+ return _append_to_gallery(existing_extra, last)
592
 
 
 
 
 
 
593
 
594
+ @spaces.GPU
595
+ def add_derived_ref(img1, existing_extra, derived_type, derived_use_gpu):
596
+ if img1 is None:
597
+ raise gr.Error("Please upload Image 1 first.")
598
 
599
+ if derived_type == "None":
600
+ return gr.update(value=existing_extra), gr.update(visible=False, value=None)
601
 
602
+ base = img1.convert("RGB")
 
603
 
604
+ if derived_type == "Depth (Depth Anything V2 Small)":
605
+ derived = make_depth_map(base, use_gpu=bool(derived_use_gpu))
606
+ else:
607
+ raise gr.Error(f"Unknown derived type: {derived_type}")
608
 
609
+ new_gallery = _append_to_gallery(existing_extra, derived)
610
+ return gr.update(value=new_gallery), gr.update(visible=True, value=derived)
 
 
 
611
 
 
 
612
 
613
+ # ============================================================
614
+ # Prompt Helper (outsourced VLM calls, UI stays clean)
615
+ # ============================================================
 
 
 
 
616
 
617
+ # Configuration via env vars (no UI clutter)
618
+ HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() or os.environ.get("HUGGINGFACEHUB_API_TOKEN", "").strip()
619
+ HF_PROVIDER = os.environ.get("HF_PROVIDER", "nebius").strip()
620
+ HF_VLM_MODEL = os.environ.get("HF_VLM_MODEL", "Qwen/Qwen2.5-VL-7B-Instruct").strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
 
622
+ _client_cache = {}
 
 
 
 
 
 
 
623
 
624
 
625
+ def _get_client() -> InferenceClient:
626
+ key = (HF_PROVIDER, bool(HF_TOKEN))
627
+ if key in _client_cache:
628
+ return _client_cache[key]
629
+ if not HF_TOKEN:
630
+ raise gr.Error("Captioning is not configured (missing HF_TOKEN).")
631
+ client = InferenceClient(provider=HF_PROVIDER, api_key=HF_TOKEN)
632
+ _client_cache[key] = client
633
+ return client
634
+
635
+
636
+ def _encode_image_data_url(img: Image.Image, max_side: int = 1536, fmt: str = "PNG") -> str:
637
  """
638
+ Converts PIL to data URL (base64). Downscales to keep payload reasonable.
 
639
  """
640
+ img = img.convert("RGB")
641
+ w, h = img.size
642
+ scale = min(1.0, float(max_side) / float(max(w, h))) if max(w, h) > 0 else 1.0
643
+ if scale < 1.0:
644
+ img = img.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS)
 
 
 
 
 
 
 
 
645
 
646
+ buf = io.BytesIO()
647
+ img.save(buf, format=fmt)
648
+ b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
649
+ mime = "image/png" if fmt.upper() == "PNG" else "image/jpeg"
650
+ return f"data:{mime};base64,{b64}"
651
 
 
 
652
 
653
+ def _chat_with_image(
654
+ system_prompt: str,
655
+ user_text: str,
656
+ image: Image.Image,
657
+ *,
658
+ max_tokens: int,
659
+ temperature: float,
660
+ ) -> str:
661
+ client = _get_client()
662
+ data_url = _encode_image_data_url(image)
663
+
664
+ messages = [
665
+ {"role": "system", "content": system_prompt},
666
+ {
667
+ "role": "user",
668
+ "content": [
669
+ {"type": "text", "text": user_text},
670
+ {"type": "image_url", "image_url": {"url": data_url}},
671
+ ],
672
+ },
673
+ ]
674
+
675
+ # Hugging Face chat.completions interface
676
+ resp = client.chat.completions.create(
677
+ model=HF_VLM_MODEL,
678
+ messages=messages,
679
+ max_tokens=int(max_tokens),
680
+ temperature=float(temperature),
681
+ )
682
+ return (resp.choices[0].message.content or "").strip()
683
 
 
 
684
 
685
+ def _chat_text_only(
686
+ system_prompt: str,
687
+ user_text: str,
688
+ *,
689
+ max_tokens: int,
690
+ temperature: float,
691
+ ) -> str:
692
+ client = _get_client()
693
+ messages = [
694
+ {"role": "system", "content": system_prompt},
695
+ {"role": "user", "content": [{"type": "text", "text": user_text}]},
696
+ ]
697
+ resp = client.chat.completions.create(
698
+ model=HF_VLM_MODEL,
699
+ messages=messages,
700
+ max_tokens=int(max_tokens),
701
+ temperature=float(temperature),
702
+ )
703
+ return (resp.choices[0].message.content or "").strip()
704
 
 
 
705
 
706
+ def _has_header(text: str, header: str) -> bool:
707
+ return header in (text or "")
708
 
 
 
 
709
 
710
+ def _enforce_once_retry_image(system_prompt: str, user_text: str, image: Image.Image, header: str, max_tokens: int, temperature: float) -> str:
711
+ out = _chat_with_image(system_prompt, user_text, image, max_tokens=max_tokens, temperature=temperature)
712
+ if _has_header(out, header):
713
+ return out
714
 
715
+ # one strict retry
716
+ retry_user = (
717
+ user_text
718
+ + "\n\nIMPORTANT: You did not follow the required output format. "
719
+ + f"Return EXACTLY the block starting with {header} and fill each line. No extra text."
720
+ )
721
+ out2 = _chat_with_image(system_prompt, retry_user, image, max_tokens=max_tokens, temperature=temperature)
722
+ return out2
723
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
+ def _enforce_once_retry_text(system_prompt: str, user_text: str, header: str, max_tokens: int, temperature: float) -> str:
726
+ out = _chat_text_only(system_prompt, user_text, max_tokens=max_tokens, temperature=temperature)
727
+ if _has_header(out, header):
728
+ return out
729
 
730
+ retry_user = (
731
+ user_text
732
+ + "\n\nIMPORTANT: You did not follow the required output format. "
733
+ + f"Return EXACTLY the sections starting with {header}. No extra text."
734
+ )
735
+ return _chat_text_only(system_prompt, retry_user, max_tokens=max_tokens, temperature=temperature)
736
+
737
+
738
+ # --------- BASE (Pic1) extraction prompt (no identity) ----------
739
+ BFS_BASE_SYSTEM = """You are extracting non-identity facial and contextual signals from Picture 1 (BASE) for a head/face swap.
740
+
741
+ CRITICAL: DO NOT describe identity/likeness traits. That means:
742
+ - No age, ethnicity/race/nationality guesses, attractiveness judgments, “looks like X”
743
+ - No skin tone, facial structure descriptions, “round face”, “strong jaw”, etc.
744
+ - No hair color/style as identity markers (only mention hair if it occludes the face, e.g. “hair covering left eye”)
745
+
746
+ Focus ONLY on:
747
+ - Head pose (yaw/pitch/roll, tilt, chin/jaw position)
748
+ - Gaze and eyelids (direction, openness)
749
+ - Micro-expressions / muscle cues (brow knit/raise, squint, lip tension, mouth corners, cheek tension, jaw set)
750
+ - Mouth details (open/closed, teeth, tongue if visible)
751
+ - Mood inference (max 2 labels) with visible evidence cues
752
+ - Occlusions and interactions (hands, objects, glasses, shadows) relevant to face recreation
753
+ - Visibility notes (unclear/occluded/shadowed)
754
+
755
+ Output format (return exactly this block, nothing else):
756
+
757
+ [BASE_SIGNALS_PIC1]
758
+ Head pose:
759
+ Gaze & eyelids:
760
+ Expression (muscle cues):
761
+ Mouth details:
762
+ Mood (max 2 labels):
763
+ Evidence for mood (visible cues only):
764
+ Occlusions & interactions:
765
+ Visibility notes (unclear/occluded/shadowed areas):
766
+ """
767
 
768
+ BFS_BASE_USER = """Analyze the single provided image as Picture 1 (BASE).
769
+ Fill every line with either an observation or the word "unclear". Keep it concise."""
770
+
771
+ # --------- DONOR (Pic2) extraction prompt (identity only) ----------
772
+ BFS_DONOR_SYSTEM = """You are extracting inherent identity/likeness traits from Picture 2 (DONOR) for a head/face swap.
773
+
774
+ CRITICAL: DO NOT describe expression, mood, gaze direction, head pose/rotation, body pose, or actions.
775
+
776
+ Focus ONLY on visible physical traits:
777
+ - Face shape & proportions (jawline, cheekbones, chin shape)
778
+ - Skin tone/undertone + texture (freckles/moles only if visible)
779
+ - Eyes (color, shape), brows (shape/thickness)
780
+ - Nose structure (bridge, tip, nostrils)
781
+ - Lips/mouth shape (fullness, cupid’s bow)
782
+ - Chin/jaw details
783
+ - Hair (color, style, hairline)
784
+ - Distinctive traits (scars/moles/freckles if visible)
785
+ - Visibility notes (unclear/occluded/shadowed)
786
+
787
+ Output format (return exactly this block, nothing else):
788
+
789
+ [DONOR_TRAITS_PIC2]
790
+ Face shape & proportions:
791
+ Skin tone & texture:
792
+ Eyes & brows:
793
+ Nose structure:
794
+ Lips & mouth shape:
795
+ Chin/jaw details:
796
+ Hair (color, style, hairline):
797
+ Distinctive traits (scars/moles/freckles if visible):
798
+ Visibility notes (unclear/occluded/shadowed areas):
799
+ """
800
 
801
+ BFS_DONOR_USER = """Analyze the single provided image as Picture 2 (DONOR).
802
+ Fill every line with either an observation or the word "unclear". Keep it concise."""
803
 
804
+ # --------- Text-only prompt builder ----------
805
+ BFS_BUILDER_SYSTEM = """You are a prompt editor for BFS-BestFaceSwap.
806
 
807
+ Input you may receive:
808
+ - A core prompt (already includes head_swap instructions)
809
+ - BASE_SIGNALS_PIC1 text (pose/expression/mood/occlusions; non-identity)
810
+ - Optional DONOR_TRAITS_PIC2 text (identity-only traits)
811
 
812
+ Your job:
813
+ - Produce a compact addendum that improves expressiveness transfer and reduces ambiguity.
814
+ - Do NOT add any identity traits from the base signals.
815
+ - Do NOT add any pose/expression/mood from donor traits.
816
+ - Prefer concrete, visible cues over vague adjectives.
817
+ - Keep it short (ideally 6–14 lines total).
818
+ - If donor traits are missing or mostly "unclear", omit donor section entirely.
819
 
820
+ Output EXACTLY two sections (donor section may be omitted if not provided/usable):
821
+ [ADDENDUM_BASE]
822
+ (bullets or short lines; use the best cues from BASE_SIGNALS)
 
 
 
 
 
 
 
823
 
824
+ [ADDENDUM_DONOR]
825
+ (optional; only if donor traits contain useful visible info; no pose/expression)
826
+ """
 
 
827
 
 
 
 
 
 
 
828
 
829
+ def scrub_placeholder(text: str, enabled: bool) -> str:
830
+ # Placeholder for future strict scrubber pass (no-op).
831
+ return text
 
832
 
 
 
 
 
833
 
834
+ @spaces.GPU
835
+ def caption_base_pic1(
836
+ img1,
837
+ max_new_tokens: int,
838
+ temperature: float,
839
+ strict_scrubber: bool,
840
+ show_debug: bool,
841
+ ):
842
+ if img1 is None:
843
+ raise gr.Error("Please upload Image 1 (base) first.")
844
 
845
+ raw = _enforce_once_retry_image(
846
+ BFS_BASE_SYSTEM,
847
+ BFS_BASE_USER,
848
+ img1,
849
+ header="[BASE_SIGNALS_PIC1]",
850
+ max_tokens=int(max_new_tokens),
851
+ temperature=float(temperature),
852
+ )
853
+ out = scrub_placeholder(raw, enabled=bool(strict_scrubber))
854
+ debug = raw if bool(show_debug) else ""
855
+ return out, debug
856
 
857
 
858
+ @spaces.GPU
859
+ def caption_donor_pic2(
860
+ img2,
861
+ max_new_tokens: int,
862
+ temperature: float,
863
+ strict_scrubber: bool,
864
+ show_debug: bool,
865
+ ):
866
+ if img2 is None:
867
+ raise gr.Error("Please upload Image 2 (donor) first.")
868
+
869
+ raw = _enforce_once_retry_image(
870
+ BFS_DONOR_SYSTEM,
871
+ BFS_DONOR_USER,
872
+ img2,
873
+ header="[DONOR_TRAITS_PIC2]",
874
+ max_tokens=int(max_new_tokens),
875
+ temperature=float(temperature),
876
+ )
877
+ out = scrub_placeholder(raw, enabled=bool(strict_scrubber))
878
+ debug = raw if bool(show_debug) else ""
879
+ return out, debug
880
 
881
 
882
+ def _compose_final_prompt(core_prompt: str, addendum_text: str, mode: str) -> str:
883
+ core = (core_prompt or "").strip()
884
+ addendum = (addendum_text or "").strip()
885
+ if not addendum:
886
+ return core
887
 
888
+ if (mode or "").lower().startswith("inject"):
889
+ injected = core
890
+ if "{BFS_ADDENDUM}" in injected:
891
+ injected = injected.replace("{BFS_ADDENDUM}", addendum + "\n")
892
+ return injected.strip()
893
 
894
+ return (core + "\n\n" + addendum).strip()
895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
 
897
+ @spaces.GPU
898
+ def build_bfs_addendum_and_final_prompt(
899
+ core_prompt: str,
900
+ base_caption: str,
901
+ donor_caption: str,
902
+ integration_mode: str,
903
+ max_new_tokens: int,
904
+ temperature: float,
905
+ show_debug: bool,
906
+ ):
907
+ base = (base_caption or "").strip()
908
+ donor = (donor_caption or "").strip()
909
+ core = (core_prompt or "").strip()
910
+
911
+ if not base:
912
+ raise gr.Error("Generate BASE signals (Pic1) first (or paste them) before building an addendum.")
913
+
914
+ user_text = (
915
+ "CORE PROMPT:\n"
916
+ f"{core}\n\n"
917
+ "BASE_SIGNALS_PIC1:\n"
918
+ f"{base}\n\n"
919
+ "DONOR_TRAITS_PIC2:\n"
920
+ f"{donor if donor else '(none)'}\n\n"
921
+ "Produce the addendum now."
922
+ )
923
 
924
+ raw = _enforce_once_retry_text(
925
+ BFS_BUILDER_SYSTEM,
926
+ user_text,
927
+ header="[ADDENDUM_BASE]",
928
+ max_tokens=int(max_new_tokens),
929
+ temperature=float(temperature),
930
+ )
931
+
932
+ final_prompt = _compose_final_prompt(core, raw, integration_mode)
933
+ debug = raw if bool(show_debug) else ""
934
+ return raw, final_prompt, debug
935
 
936
 
937
  # ============================================================
 
943
  def infer(
944
  input_image_1,
945
  input_image_2,
946
+ input_images_extra,
947
  prompt,
948
  lora_adapter,
949
  seed,
 
962
  if input_image_1 is None:
963
  raise gr.Error("Please upload Image 1.")
964
 
 
965
  if lora_adapter == NONE_LORA:
966
  try:
967
  pipe.set_adapters([], adapter_weights=[])
 
984
  img1 = input_image_1.convert("RGB")
985
  img2 = input_image_2.convert("RGB") if input_image_2 is not None else None
986
 
 
987
  extra_imgs: list[Image.Image] = []
988
  if input_images_extra:
989
  for item in input_images_extra:
 
991
  if pil is not None:
992
  extra_imgs.append(pil)
993
 
 
994
  if lora_requires_two_images(lora_adapter) and img2 is None:
995
  raise gr.Error("This LoRA needs two images. Please upload Image 2 as well.")
996
 
 
997
  labeled = build_labeled_images(img1, img2, extra_imgs)
 
 
998
  pipe_images = list(labeled.values())
999
  if len(pipe_images) == 1:
1000
  pipe_images = pipe_images[0]
1001
 
 
 
1002
  target_area = get_target_area_for_lora(img1, lora_adapter, float(target_megapixels))
1003
  width, height = compute_canvas_dimensions_from_area(
1004
  img1,
 
1006
  multiple_of=int(pipe.vae_scale_factor * 2),
1007
  )
1008
 
 
 
1009
  vae_image_indices = None
1010
  if extras_condition_only:
1011
  if isinstance(pipe_images, list) and len(pipe_images) > 2:
1012
  vae_image_indices = [0, 1] if len(pipe_images) >= 2 else [0]
1013
 
1014
  try:
 
 
 
 
 
1015
  result = pipe(
1016
  image=pipe_images,
1017
  prompt=prompt,
 
1038
  input_pil = input_image.convert("RGB")
1039
  guidance_scale = 1.0
1040
  steps = 4
1041
+ result, seed, last = infer(
1042
+ input_pil,
1043
+ None,
1044
+ None,
1045
+ prompt,
1046
+ lora_adapter,
1047
+ 0,
1048
+ True,
1049
+ guidance_scale,
1050
+ steps,
1051
+ 1.0,
1052
+ True,
1053
+ True,
1054
+ )
1055
  return result, seed, last
1056
 
1057
 
 
1060
  # ============================================================
1061
 
1062
  css = """
1063
+ #col-container { margin: 0 auto; max-width: 960px; }
1064
+ #main-title h1 { font-size: 2.1em !important; }
 
 
 
1065
  """
1066
 
1067
  aio_status_line = (
 
1075
  gr.Markdown(
1076
  "Perform diverse image edits using specialized "
1077
  "[LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters for the "
1078
+ "[Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) model."
1079
  )
1080
  gr.Markdown(aio_status_line)
1081
 
 
1099
  placeholder="e.g., transform into photo..",
1100
  )
1101
 
1102
+ with gr.Accordion("BFS Prompt Helper", open=False):
1103
+ with gr.Row():
1104
+ helper_max_tokens = gr.Slider(label="Max new tokens", minimum=64, maximum=1024, step=16, value=384)
1105
+ helper_temperature = gr.Slider(label="Temperature (0 = deterministic)", minimum=0.0, maximum=1.2, step=0.05, value=0.2)
1106
+
1107
+ with gr.Row():
1108
+ strict_scrubber = gr.Checkbox(label="Strict scrubber (placeholder, no-op)", value=False)
1109
+ show_debug = gr.Checkbox(label="Show debug outputs", value=False)
1110
+
1111
+ with gr.Row():
1112
+ btn_cap_base = gr.Button("Generate BASE signals (Pic1)", variant="secondary")
1113
+ btn_cap_donor = gr.Button("Generate DONOR traits (Pic2) (optional)", variant="secondary")
1114
+
1115
+ with gr.Row():
1116
+ caption_pic1 = gr.Textbox(label="BASE signals (from Image 1)", lines=12, value="")
1117
+ caption_pic2 = gr.Textbox(label="DONOR traits (from Image 2) (optional)", lines=12, value="")
1118
+
1119
+ with gr.Row():
1120
+ debug_base = gr.Textbox(label="Debug: raw BASE output", lines=8, visible=False)
1121
+ debug_donor = gr.Textbox(label="Debug: raw DONOR output", lines=8, visible=False)
1122
+
1123
+ integration_mode = gr.Radio(
1124
+ label="How to apply addendum to the core prompt",
1125
+ choices=["Concatenate", "Inject (placeholder {BFS_ADDENDUM})"],
1126
+ value="Concatenate",
1127
+ )
1128
+
1129
+ with gr.Row():
1130
+ btn_build_addendum = gr.Button("Build addendum + final prompt", variant="primary")
1131
+ btn_apply_final = gr.Button("Apply final prompt → Edit Prompt", variant="secondary")
1132
+
1133
+ bfs_addendum = gr.Textbox(label="Built addendum (editable)", lines=10, value="")
1134
+ bfs_final_prompt = gr.Textbox(label="Final prompt preview (editable)", lines=10, value="")
1135
+ debug_builder = gr.Textbox(label="Debug: raw builder output", lines=8, visible=False)
1136
+
1137
  run_button = gr.Button("Edit Image", variant="primary")
1138
 
1139
  with gr.Column():
1140
  output_image = gr.Image(label="Output Image", interactive=False, format="png", height=353)
 
1141
  last_output = gr.State(value=None)
1142
 
1143
  with gr.Row():
 
1162
  )
1163
 
1164
  with gr.Accordion("Advanced Settings", open=False, visible=True):
1165
+ with gr.Accordion("Derived Conditioning (Depth)", open=False):
1166
  derived_type = gr.Dropdown(
1167
  label="Derived Type (from Image 1)",
1168
+ choices=["None", "Depth (Depth Anything V2 Small)"],
 
 
 
 
 
1169
  value="None",
1170
  )
1171
  derived_use_gpu = gr.Checkbox(label="Use GPU for derived model", value=False)
 
 
 
 
 
 
 
1172
  add_derived_btn = gr.Button("➕ Add derived ref to Extras (conditioning-only recommended)")
1173
 
1174
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
 
1191
  value=True,
1192
  )
1193
 
1194
+ # LoRA selection: preset prompt + toggle Image 2
1195
  lora_adapter.change(
1196
  fn=on_lora_change_ui,
1197
  inputs=[lora_adapter, prompt, extras_condition_only],
1198
  outputs=[prompt, input_image_2, extras_condition_only],
1199
  )
1200
 
1201
+ # Debug visibility toggles
1202
+ show_debug.change(
1203
+ fn=lambda x: (
1204
+ gr.update(visible=bool(x)),
1205
+ gr.update(visible=bool(x)),
1206
+ gr.update(visible=bool(x)),
1207
+ ),
1208
+ inputs=[show_debug],
1209
+ outputs=[debug_base, debug_donor, debug_builder],
1210
+ )
1211
+
1212
+ # Caption buttons (single-image)
1213
+ btn_cap_base.click(
1214
+ fn=caption_base_pic1,
1215
+ inputs=[input_image_1, helper_max_tokens, helper_temperature, strict_scrubber, show_debug],
1216
+ outputs=[caption_pic1, debug_base],
1217
+ )
1218
+
1219
+ btn_cap_donor.click(
1220
+ fn=caption_donor_pic2,
1221
+ inputs=[input_image_2, helper_max_tokens, helper_temperature, strict_scrubber, show_debug],
1222
+ outputs=[caption_pic2, debug_donor],
1223
+ )
1224
+
1225
+ # Builder (text-only)
1226
+ btn_build_addendum.click(
1227
+ fn=build_bfs_addendum_and_final_prompt,
1228
+ inputs=[
1229
+ prompt,
1230
+ caption_pic1,
1231
+ caption_pic2,
1232
+ integration_mode,
1233
+ helper_max_tokens,
1234
+ helper_temperature,
1235
+ show_debug,
1236
+ ],
1237
+ outputs=[bfs_addendum, bfs_final_prompt, debug_builder],
1238
+ )
1239
+
1240
+ # Apply final prompt to the Edit Prompt box
1241
+ btn_apply_final.click(
1242
+ fn=lambda x: gr.update(value=x),
1243
+ inputs=[bfs_final_prompt],
1244
+ outputs=[prompt],
1245
+ )
1246
+
1247
  gr.Examples(
1248
  examples=[
1249
  ["examples/5.jpg", "Remove shadows and relight the image using soft lighting.", "Light-Restoration"],
1250
  ["examples/4.jpg", "Use a subtle golden-hour filter with smooth light diffusion.", "Relight"],
1251
  ["examples/2.jpeg", "Rotate the camera 45 degrees to the left.", "Multiple-Angles"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  ["examples/11.jpg", "Upscale this picture to 4K resolution.", "Upscale2K"],
1253
  ],
1254
  inputs=[input_image_1, prompt, lora_adapter],
 
1277
  outputs=[output_image, seed, last_output],
1278
  )
1279
 
1280
+ # Output routing
1281
  btn_out_to_img1.click(fn=set_output_as_image1, inputs=[last_output], outputs=[input_image_1])
1282
  btn_out_to_img2.click(fn=set_output_as_image2, inputs=[last_output], outputs=[input_image_2])
1283
  btn_out_to_extra.click(fn=set_output_as_extra, inputs=[last_output, input_images_extra], outputs=[input_images_extra])
1284
+
1285
+ # Derived conditioning: append depth map
1286
  add_derived_btn.click(
1287
  fn=add_derived_ref,
1288
+ inputs=[input_image_1, input_images_extra, derived_type, derived_use_gpu],
1289
  outputs=[input_images_extra, derived_preview],
1290
  )
1291
+
1292
  if __name__ == "__main__":
1293
  demo.queue(max_size=30).launch(
1294
  css=css,