Professional Noob commited on
Commit
4947c41
·
verified ·
1 Parent(s): 63f2ef7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -408
app.py CHANGED
@@ -1,20 +1,16 @@
1
  import os
2
  import re
3
  import gc
4
- import sys
5
- import time
6
- import random
7
- import threading
8
  import traceback
9
- from typing import Iterable, Optional
10
-
11
  import gradio as gr
12
  import numpy as np
13
  import spaces
14
  import torch
 
15
  from PIL import Image
 
16
 
17
- from huggingface_hub import HfApi, hf_hub_download
18
  from safetensors.torch import load_file as safetensors_load_file
19
 
20
  from gradio.themes import Soft
@@ -104,7 +100,6 @@ orange_red_theme = OrangeRedTheme()
104
 
105
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
106
 
107
- print("===== Application Startup =====")
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)
@@ -116,221 +111,86 @@ if torch.cuda.is_available():
116
  print("Using device:", device)
117
 
118
  # ============================================================
119
- # Pipeline imports (keep your existing transformer class)
120
- # ============================================================
121
-
122
- from diffusers import FlowMatchEulerDiscreteScheduler # noqa: F401
123
- from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
124
- from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
125
- from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
126
-
127
- dtype = torch.bfloat16
128
-
129
- # ============================================================
130
- # AIO versioning + "boot version" persistence
131
  # ============================================================
132
 
133
  AIO_REPO_ID = "Pr0f3ssi0n4ln00b/Phr00t-Qwen-Rapid-AIO"
134
  DEFAULT_AIO_VERSION = "v19"
135
- _AIO_VER_RE = re.compile(r"^(v\d+)$")
136
 
137
- # Preferred boot version sources:
138
- # 1) Space Variable: DEFAULT_AIO_VERSION
139
- # 2) Local preference file in HF cache dir (best-effort; not guaranteed across cold rebuilds)
140
- _PREF_PATH = os.path.join(os.path.expanduser("~"), ".cache", "aio_default_version.txt")
141
 
142
 
143
- def _read_pref_file() -> Optional[str]:
144
- try:
145
- if os.path.isfile(_PREF_PATH):
146
- with open(_PREF_PATH, "r", encoding="utf-8") as f:
147
- v = f.read().strip()
148
- return v or None
149
- except Exception:
150
  return None
 
 
 
 
 
 
 
 
151
  return None
152
 
153
 
154
- def _write_pref_file(v: str) -> None:
155
- os.makedirs(os.path.dirname(_PREF_PATH), exist_ok=True)
156
- with open(_PREF_PATH, "w", encoding="utf-8") as f:
157
- f.write(v)
158
-
159
-
160
- def discover_aio_versions(repo_id: str) -> list[str]:
161
- """
162
- Finds versions by scanning repo file paths with the naming convention:
163
- vNN/transformer/...
164
- """
165
- try:
166
- api = HfApi()
167
- files = api.list_repo_files(repo_id=repo_id, repo_type="model")
168
- versions = set()
169
- for f in files:
170
- if "/transformer/" not in f:
171
- continue
172
- head = f.split("/transformer/", 1)[0]
173
- if _AIO_VER_RE.fullmatch(head):
174
- versions.add(head)
175
-
176
- if not versions:
177
- return [DEFAULT_AIO_VERSION]
178
-
179
- return sorted(versions, key=lambda s: int(s[1:]))
180
- except Exception as e:
181
- print(f"⚠️ AIO version discovery failed: {e}")
182
- return [DEFAULT_AIO_VERSION]
183
-
184
-
185
- AVAILABLE_AIO_VERSIONS = discover_aio_versions(AIO_REPO_ID)
186
-
187
- # pick boot version (env > file > fallback)
188
- _env_boot = (os.environ.get("DEFAULT_AIO_VERSION") or "").strip()
189
- _file_boot = (_read_pref_file() or "").strip()
190
- BOOT_AIO_VERSION = _env_boot or _file_boot or DEFAULT_AIO_VERSION
191
-
192
- if BOOT_AIO_VERSION not in AVAILABLE_AIO_VERSIONS and AVAILABLE_AIO_VERSIONS:
193
- BOOT_AIO_VERSION = AVAILABLE_AIO_VERSIONS[0]
194
-
195
- DEFAULT_AIO_VERSION = BOOT_AIO_VERSION # use boot version as the UI + pipeline default
196
-
197
- # Cache control (prevents double-download when dropdown+run are both triggered)
198
- _CACHED_AIO_VERSIONS: set[str] = set()
199
- _CACHE_LOCKS: dict[str, threading.Lock] = {}
200
- _CACHE_LOCKS_GUARD = threading.Lock()
201
-
202
- # GPU switch lock (prevents concurrent swaps)
203
- _AIO_SWITCH_LOCK = threading.Lock()
204
-
205
-
206
- def _hard_cuda_cleanup():
207
- gc.collect()
208
- if torch.cuda.is_available():
209
- try:
210
- torch.cuda.synchronize()
211
- except Exception:
212
- pass
213
- torch.cuda.empty_cache()
214
- try:
215
- torch.cuda.ipc_collect()
216
- except Exception:
217
- pass
218
-
219
-
220
- def _get_cache_lock(version: str) -> threading.Lock:
221
- with _CACHE_LOCKS_GUARD:
222
- if version not in _CACHE_LOCKS:
223
- _CACHE_LOCKS[version] = threading.Lock()
224
- return _CACHE_LOCKS[version]
225
-
226
-
227
- def ensure_aio_cached(version: str) -> None:
228
- """
229
- CPU-only: download all files under vXX/transformer/ into HF cache.
230
- Idempotent + locked per version to avoid duplicate concurrent downloads.
231
- """
232
- version = version or DEFAULT_AIO_VERSION
233
- if version in _CACHED_AIO_VERSIONS:
234
- return
235
-
236
- lock = _get_cache_lock(version)
237
- with lock:
238
- if version in _CACHED_AIO_VERSIONS:
239
- return
240
-
241
- sub = f"{version}/transformer"
242
- api = HfApi()
243
- files = api.list_repo_files(repo_id=AIO_REPO_ID, repo_type="model")
244
- needed = [f for f in files if f.startswith(sub + "/")]
245
- if not needed:
246
- raise gr.Error(f"No files found under {sub}/ in {AIO_REPO_ID}")
247
-
248
- for f in needed:
249
- hf_hub_download(repo_id=AIO_REPO_ID, filename=f, repo_type="model")
250
-
251
- _CACHED_AIO_VERSIONS.add(version)
252
-
253
 
254
- def ensure_aio_cached_ui(version: str):
255
- """
256
- Gradio handler (CPU): cache selected version.
257
- """
258
- try:
259
- version = version or DEFAULT_AIO_VERSION
260
- if version in _CACHED_AIO_VERSIONS:
261
- return gr.update(value=f"✅ Cached {version} (ready)")
262
-
263
- print(f"⬇️ Caching AIO version on CPU: {version}")
264
- ensure_aio_cached(version)
265
- return gr.update(value=f"✅ Cached {version} (ready)")
266
- except Exception as e:
267
- print("❌ Cache step failed:\n", traceback.format_exc())
268
- raise gr.Error(f"Cache failed for {version}: {e}")
269
-
270
-
271
- def refresh_aio_versions_ui(current_value: str):
272
- global AVAILABLE_AIO_VERSIONS
273
- AVAILABLE_AIO_VERSIONS = discover_aio_versions(AIO_REPO_ID)
274
-
275
- new_value = current_value if current_value in AVAILABLE_AIO_VERSIONS else (AVAILABLE_AIO_VERSIONS[0] if AVAILABLE_AIO_VERSIONS else DEFAULT_AIO_VERSION)
276
- status = f"Found {len(AVAILABLE_AIO_VERSIONS)} version(s): {', '.join(AVAILABLE_AIO_VERSIONS)}"
277
- return gr.update(choices=AVAILABLE_AIO_VERSIONS, value=new_value), gr.update(value=status)
278
-
279
-
280
- def _apply_fa3_if_possible():
281
- try:
282
- pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
283
- print("Flash Attention 3 Processor set successfully.")
284
- except Exception as e:
285
- print(f"Warning: Could not set FA3 processor: {e}")
286
-
287
-
288
- def set_default_and_restart_ui(version: str):
289
- """
290
- Best-effort: store desired boot version and force a restart so it loads at startup
291
- (avoids transformer swapping during inference when possible).
292
- """
293
- version = version or DEFAULT_AIO_VERSION
294
- if version not in AVAILABLE_AIO_VERSIONS:
295
- raise gr.Error(f"Unknown version: {version}")
296
-
297
- try:
298
- _write_pref_file(version)
299
- except Exception as e:
300
- print(f"⚠️ Could not write preference file: {e}")
301
-
302
- # Trigger restart a moment after returning UI update
303
- def _restart_soon():
304
- time.sleep(1.0)
305
- # Let the supervisor restart the process
306
- os._exit(0)
307
-
308
- threading.Thread(target=_restart_soon, daemon=True).start()
309
- return gr.update(value=f"✅ Saved startup version: **{version}**. Restarting Space now…")
310
 
 
 
 
311
 
312
  # ============================================================
313
- # Build pipeline once (boot version at startup)
314
  # ============================================================
315
 
316
- print(f"📦 Boot AIO version: {DEFAULT_AIO_VERSION} (env={_env_boot or '—'}, file={_file_boot or '—'})")
317
- print(f"📦 Loading AIO transformer: {AIO_REPO_ID} / {DEFAULT_AIO_VERSION}/transformer (startup)")
 
 
318
 
319
- pipe = QwenImageEditPlusPipeline.from_pretrained(
320
- "Qwen/Qwen-Image-Edit-2511",
321
- transformer=QwenImageTransformer2DModel.from_pretrained(
322
- AIO_REPO_ID,
323
- subfolder=f"{DEFAULT_AIO_VERSION}/transformer",
324
- torch_dtype=dtype,
325
- device_map="cuda", # keep your existing setup
326
- ),
327
- torch_dtype=dtype,
328
- ).to(device)
329
 
330
- _apply_fa3_if_possible()
331
 
332
- # mark default as cached (it’s loaded anyway)
333
- _CACHED_AIO_VERSIONS.add(DEFAULT_AIO_VERSION)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
  MAX_SEED = np.iinfo(np.int32).max
336
 
@@ -396,7 +256,7 @@ ADAPTER_SPECS = {
396
  "weights": "bfs_head_v5_2511_original.safetensors",
397
  "adapter_name": "BFS-Best-Faceswap",
398
  "strength": 1.0,
399
- "needs_alpha_fix": True,
400
  },
401
  "Multiple-Angles": {
402
  "type": "single",
@@ -473,6 +333,7 @@ LORA_PRESET_PROMPTS = {
473
  "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",
474
  }
475
 
 
476
  LOADED_ADAPTERS = set()
477
 
478
  # ============================================================
@@ -514,9 +375,14 @@ def image2_label_for_lora(lora_adapter: str) -> str:
514
 
515
 
516
  def _to_pil_rgb(x) -> Optional[Image.Image]:
 
 
 
 
517
  if x is None:
518
  return None
519
 
 
520
  if isinstance(x, tuple) and len(x) >= 1:
521
  x = x[0]
522
  if x is None:
@@ -528,6 +394,7 @@ def _to_pil_rgb(x) -> Optional[Image.Image]:
528
  if isinstance(x, np.ndarray):
529
  return Image.fromarray(x).convert("RGB")
530
 
 
531
  try:
532
  return Image.fromarray(np.array(x)).convert("RGB")
533
  except Exception:
@@ -539,8 +406,16 @@ def build_labeled_images(
539
  img2: Optional[Image.Image],
540
  extra_imgs: Optional[list[Image.Image]],
541
  ) -> dict[str, Image.Image]:
 
 
 
 
 
 
 
542
  labeled: dict[str, Image.Image] = {}
543
  idx = 1
 
544
  labeled[f"image_{idx}"] = img1
545
  idx += 1
546
 
@@ -564,7 +439,17 @@ def build_labeled_images(
564
 
565
 
566
  def _inject_missing_alpha_keys(state_dict: dict) -> dict:
 
 
 
 
 
 
 
 
 
567
  bases = {}
 
568
  for k, v in state_dict.items():
569
  if not isinstance(v, torch.Tensor):
570
  continue
@@ -590,6 +475,10 @@ def _inject_missing_alpha_keys(state_dict: dict) -> dict:
590
 
591
 
592
  def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name: str, needs_alpha_fix: bool = False):
 
 
 
 
593
  try:
594
  pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=adapter_name)
595
  return
@@ -601,6 +490,7 @@ def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name:
601
  local_path = hf_hub_download(repo_id=repo, filename=weight_name)
602
  sd = safetensors_load_file(local_path)
603
  sd = _inject_missing_alpha_keys(sd)
 
604
  pipe.load_lora_weights(sd, adapter_name=adapter_name)
605
  return
606
 
@@ -632,13 +522,18 @@ def _ensure_loaded_and_get_active_adapters(selected_lora: str):
632
 
633
  if adapter_name not in LOADED_ADAPTERS:
634
  print(f"--- Downloading and Loading Adapter Part: {selected_lora} / {adapter_name} ---")
635
- _load_lora_weights_with_fallback(
636
- repo=repo,
637
- weight_name=weights,
638
- adapter_name=adapter_name,
639
- needs_alpha_fix=needs_alpha_fix,
640
- )
641
- LOADED_ADAPTERS.add(adapter_name)
 
 
 
 
 
642
 
643
  adapter_names.append(adapter_name)
644
  adapter_weights.append(strength)
@@ -652,13 +547,18 @@ def _ensure_loaded_and_get_active_adapters(selected_lora: str):
652
 
653
  if adapter_name not in LOADED_ADAPTERS:
654
  print(f"--- Downloading and Loading Adapter: {selected_lora} ---")
655
- _load_lora_weights_with_fallback(
656
- repo=repo,
657
- weight_name=weights,
658
- adapter_name=adapter_name,
659
- needs_alpha_fix=needs_alpha_fix,
660
- )
661
- LOADED_ADAPTERS.add(adapter_name)
 
 
 
 
 
662
 
663
  adapter_names = [adapter_name]
664
  adapter_weights = [strength]
@@ -666,89 +566,13 @@ def _ensure_loaded_and_get_active_adapters(selected_lora: str):
666
  return adapter_names, adapter_weights
667
 
668
 
669
- def _unload_all_loras():
670
- global LOADED_ADAPTERS
671
- try:
672
- pipe.set_adapters([], adapter_weights=[])
673
- except Exception:
674
- pass
675
- try:
676
- pipe.unload_lora_weights()
677
- except Exception:
678
- pass
679
- LOADED_ADAPTERS.clear()
680
-
681
-
682
- # ============================================================
683
- # AIO switch (GPU, local cache only)
684
- # ============================================================
685
-
686
-
687
- def _switch_aio_version_local_only(target_version: str, current_loaded: str) -> str:
688
- """
689
- Must be called while already inside a GPU task.
690
- Uses local_files_only=True (assumes ensure_aio_cached ran on CPU first).
691
- Returns the new loaded version (or unchanged).
692
- """
693
- target_version = target_version or DEFAULT_AIO_VERSION
694
- if target_version == current_loaded:
695
- return current_loaded
696
-
697
- with _AIO_SWITCH_LOCK:
698
- if target_version == current_loaded:
699
- return current_loaded
700
-
701
- print(f"🔁 Switching AIO transformer to: {AIO_REPO_ID} / {target_version}/transformer (local-only)")
702
-
703
- _unload_all_loras()
704
-
705
- old_t = getattr(pipe, "transformer", None)
706
-
707
- # Drop module registry refs so old transformer can be freed
708
- try:
709
- if hasattr(pipe, "_modules") and "transformer" in pipe._modules:
710
- pipe._modules.pop("transformer", None)
711
- except Exception:
712
- pass
713
-
714
- try:
715
- pipe.transformer = None
716
- except Exception:
717
- pass
718
-
719
- if old_t is not None:
720
- try:
721
- old_t.to("cpu")
722
- except Exception:
723
- pass
724
- del old_t
725
-
726
- _hard_cuda_cleanup()
727
-
728
- new_t = QwenImageTransformer2DModel.from_pretrained(
729
- AIO_REPO_ID,
730
- subfolder=f"{target_version}/transformer",
731
- torch_dtype=dtype,
732
- local_files_only=True,
733
- ).to(device)
734
-
735
- try:
736
- pipe.add_module("transformer", new_t)
737
- except Exception:
738
- pipe.transformer = new_t
739
-
740
- _apply_fa3_if_possible()
741
- _hard_cuda_cleanup()
742
-
743
- return target_version
744
-
745
-
746
  # ============================================================
747
  # UI handlers
748
  # ============================================================
749
 
750
 
751
  def on_lora_change_ui(selected_lora, current_prompt):
 
752
  if selected_lora != NONE_LORA:
753
  preset = LORA_PRESET_PROMPTS.get(selected_lora, "")
754
  if preset and (current_prompt is None or str(current_prompt).strip() == ""):
@@ -758,6 +582,7 @@ def on_lora_change_ui(selected_lora, current_prompt):
758
  else:
759
  prompt_update = gr.update(value=current_prompt)
760
 
 
761
  if lora_requires_two_images(selected_lora):
762
  img2_update = gr.update(visible=True, label=image2_label_for_lora(selected_lora))
763
  else:
@@ -773,11 +598,9 @@ def on_lora_change_ui(selected_lora, current_prompt):
773
 
774
  @spaces.GPU
775
  def infer(
776
- aio_version,
777
- loaded_version_state,
778
  input_image_1,
779
  input_image_2,
780
- input_images_extra,
781
  prompt,
782
  lora_adapter,
783
  seed,
@@ -786,56 +609,61 @@ def infer(
786
  steps,
787
  progress=gr.Progress(track_tqdm=True),
788
  ):
789
- try:
790
- _hard_cuda_cleanup()
 
791
 
792
- if input_image_1 is None:
793
- raise gr.Error("Please upload Image 1.")
794
 
795
- # Switch (local cache only)
796
- new_loaded = _switch_aio_version_local_only(aio_version, loaded_version_state)
 
 
 
 
 
 
 
 
797
 
798
- # LoRA handling
799
- if lora_adapter == NONE_LORA:
800
- try:
801
- pipe.set_adapters([], adapter_weights=[])
802
- except Exception:
803
- if LOADED_ADAPTERS:
804
- pipe.set_adapters(list(LOADED_ADAPTERS), adapter_weights=[0.0] * len(LOADED_ADAPTERS))
805
- else:
806
- adapter_names, adapter_weights = _ensure_loaded_and_get_active_adapters(lora_adapter)
807
- pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
808
 
809
- if randomize_seed:
810
- seed = random.randint(0, MAX_SEED)
 
 
 
811
 
812
- generator = torch.Generator(device=device).manual_seed(seed)
813
- negative_prompt = (
814
- "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
815
- "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
816
- )
817
 
818
- img1 = input_image_1.convert("RGB")
819
- img2 = input_image_2.convert("RGB") if input_image_2 is not None else None
 
 
 
 
 
820
 
821
- extra_imgs: list[Image.Image] = []
822
- if input_images_extra:
823
- for item in input_images_extra:
824
- pil = _to_pil_rgb(item)
825
- if pil is not None:
826
- extra_imgs.append(pil)
827
 
828
- if lora_requires_two_images(lora_adapter) and img2 is None:
829
- raise gr.Error("This LoRA needs two images. Please upload Image 2 as well.")
830
 
831
- labeled = build_labeled_images(img1, img2, extra_imgs)
832
- pipe_images = list(labeled.values())
833
- if len(pipe_images) == 1:
834
- pipe_images = pipe_images[0]
835
 
836
- target_long_edge = get_target_long_edge_for_lora(lora_adapter)
837
- width, height = compute_dimensions(img1, target_long_edge)
 
838
 
 
839
  result = pipe(
840
  image=pipe_images,
841
  prompt=prompt,
@@ -846,37 +674,23 @@ def infer(
846
  generator=generator,
847
  true_cfg_scale=guidance_scale,
848
  ).images[0]
849
-
850
- status = f"✅ Loaded: **{new_loaded}** | Selected: **{aio_version}**"
851
- return result, seed, new_loaded, gr.update(value=status)
852
- except Exception:
853
- print("❌ Infer failed:\n", traceback.format_exc())
854
- raise
855
  finally:
856
- _hard_cuda_cleanup()
 
 
857
 
858
 
859
  @spaces.GPU
860
- def infer_example(input_image, prompt, lora_adapter, loaded_version_state):
861
  if input_image is None:
862
- return None, 0, loaded_version_state
863
  input_pil = input_image.convert("RGB")
864
  guidance_scale = 1.0
865
  steps = 4
866
- result, seed, new_loaded, _ = infer(
867
- loaded_version_state,
868
- loaded_version_state,
869
- input_pil,
870
- None,
871
- None,
872
- prompt,
873
- lora_adapter,
874
- 0,
875
- True,
876
- guidance_scale,
877
- steps,
878
- )
879
- return result, seed, new_loaded
880
 
881
 
882
  # ============================================================
@@ -891,9 +705,12 @@ css = """
891
  #main-title h1 {font-size: 2.1em !important;}
892
  """
893
 
894
- with gr.Blocks() as demo:
895
- loaded_version_state = gr.State(DEFAULT_AIO_VERSION)
 
 
896
 
 
897
  with gr.Column(elem_id="col-container"):
898
  gr.Markdown("# **Qwen-Image-Edit-2511-LoRAs-Fast**", elem_id="main-title")
899
  gr.Markdown(
@@ -901,20 +718,7 @@ with gr.Blocks() as demo:
901
  "[LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters for the "
902
  "[Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) model."
903
  )
904
-
905
- with gr.Row():
906
- aio_version = gr.Dropdown(
907
- label="Phr00t Rapid AIO Version",
908
- choices=AVAILABLE_AIO_VERSIONS,
909
- value=DEFAULT_AIO_VERSION,
910
- interactive=True,
911
- )
912
- refresh_versions = gr.Button("Refresh", variant="secondary")
913
- set_default_restart = gr.Button("Set as startup version & restart", variant="secondary")
914
-
915
- aio_status = gr.Markdown(
916
- f"✅ Loaded: **{DEFAULT_AIO_VERSION}** | Found {len(AVAILABLE_AIO_VERSIONS)} version(s): {', '.join(AVAILABLE_AIO_VERSIONS)}"
917
- )
918
 
919
  with gr.Row(equal_height=True):
920
  with gr.Column():
@@ -955,32 +759,13 @@ with gr.Blocks() as demo:
955
  guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
956
  steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
957
 
 
958
  lora_adapter.change(
959
  fn=on_lora_change_ui,
960
  inputs=[lora_adapter, prompt],
961
  outputs=[prompt, input_image_2],
962
  )
963
 
964
- # Dropdown change: CPU cache (idempotent + locked)
965
- aio_version.change(
966
- fn=ensure_aio_cached_ui,
967
- inputs=[aio_version],
968
- outputs=[aio_status],
969
- )
970
-
971
- refresh_versions.click(
972
- fn=refresh_aio_versions_ui,
973
- inputs=[aio_version],
974
- outputs=[aio_version, aio_status],
975
- )
976
-
977
- # Save boot version + restart
978
- set_default_restart.click(
979
- fn=set_default_and_restart_ui,
980
- inputs=[aio_version],
981
- outputs=[aio_status],
982
- )
983
-
984
  gr.Examples(
985
  examples=[
986
  ["examples/1.jpg", "Transform into anime.", "Photo-to-Anime"],
@@ -1009,25 +794,16 @@ with gr.Blocks() as demo:
1009
  ["examples/4.jpg", "Switch the camera to a wide-angle lens.", "Multiple-Angles"],
1010
  ["examples/11.jpg", "Upscale this picture to 4K resolution.", "Upscale2K"],
1011
  ],
1012
- inputs=[input_image_1, prompt, lora_adapter, loaded_version_state],
1013
- outputs=[output_image, seed, loaded_version_state],
1014
  fn=infer_example,
1015
  cache_examples=False,
1016
  label="Examples",
1017
  )
1018
 
1019
- # Run:
1020
- # 1) CPU cache selected version (idempotent/locked)
1021
- # 2) GPU infer (switch local-only if needed)
1022
  run_button.click(
1023
- fn=ensure_aio_cached_ui,
1024
- inputs=[aio_version],
1025
- outputs=[aio_status],
1026
- ).then(
1027
  fn=infer,
1028
  inputs=[
1029
- aio_version,
1030
- loaded_version_state,
1031
  input_image_1,
1032
  input_image_2,
1033
  input_images_extra,
@@ -1038,7 +814,7 @@ with gr.Blocks() as demo:
1038
  guidance_scale,
1039
  steps,
1040
  ],
1041
- outputs=[output_image, seed, loaded_version_state, aio_status],
1042
  )
1043
 
1044
  if __name__ == "__main__":
 
1
  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
11
+ from typing import Iterable, Optional
12
 
13
+ from huggingface_hub import hf_hub_download
14
  from safetensors.torch import load_file as safetensors_load_file
15
 
16
  from gradio.themes import Soft
 
100
 
101
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
102
 
 
103
  print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
104
  print("torch.__version__ =", torch.__version__)
105
  print("torch.version.cuda =", torch.version.cuda)
 
111
  print("Using device:", device)
112
 
113
  # ============================================================
114
+ # AIO version (Space variable)
 
 
 
 
 
 
 
 
 
 
 
115
  # ============================================================
116
 
117
  AIO_REPO_ID = "Pr0f3ssi0n4ln00b/Phr00t-Qwen-Rapid-AIO"
118
  DEFAULT_AIO_VERSION = "v19"
 
119
 
120
+ _VER_RE = re.compile(r"^v\d+$")
121
+ _DIGITS_RE = re.compile(r"^\d+$")
 
 
122
 
123
 
124
+ def _normalize_version(raw: str) -> Optional[str]:
125
+ if raw is None:
 
 
 
 
 
126
  return None
127
+ s = str(raw).strip()
128
+ if not s:
129
+ return None
130
+ if _VER_RE.fullmatch(s):
131
+ return s
132
+ # forgiving: allow "21" -> "v21"
133
+ if _DIGITS_RE.fullmatch(s):
134
+ return f"v{s}"
135
  return None
136
 
137
 
138
+ _AIO_ENV_RAW = os.environ.get("AIO_VERSION", "")
139
+ _AIO_ENV_NORM = _normalize_version(_AIO_ENV_RAW)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ AIO_VERSION = _AIO_ENV_NORM or DEFAULT_AIO_VERSION
142
+ AIO_VERSION_SOURCE = "env" if _AIO_ENV_NORM else "default(v19)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ print(f"AIO_VERSION (env raw) = {_AIO_ENV_RAW!r}")
145
+ print(f"AIO_VERSION (normalized) = {_AIO_ENV_NORM!r}")
146
+ print(f"Using AIO_VERSION = {AIO_VERSION} ({AIO_VERSION_SOURCE})")
147
 
148
  # ============================================================
149
+ # Pipeline
150
  # ============================================================
151
 
152
+ from diffusers import FlowMatchEulerDiscreteScheduler # noqa: F401
153
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
154
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
155
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
156
 
157
+ dtype = torch.bfloat16
 
 
 
 
 
 
 
 
 
158
 
 
159
 
160
+ def _load_pipe_with_version(version: str) -> QwenImageEditPlusPipeline:
161
+ sub = f"{version}/transformer"
162
+ print(f"📦 Loading AIO transformer: {AIO_REPO_ID} / {sub}")
163
+ p = QwenImageEditPlusPipeline.from_pretrained(
164
+ "Qwen/Qwen-Image-Edit-2511",
165
+ transformer=QwenImageTransformer2DModel.from_pretrained(
166
+ AIO_REPO_ID,
167
+ subfolder=sub,
168
+ torch_dtype=dtype,
169
+ device_map="cuda",
170
+ ),
171
+ torch_dtype=dtype,
172
+ ).to(device)
173
+ return p
174
+
175
+
176
+ # Forgiving load: try env/default version, fallback to v19 if it fails
177
+ try:
178
+ pipe = _load_pipe_with_version(AIO_VERSION)
179
+ except Exception as e:
180
+ print("❌ Failed to load requested AIO_VERSION. Falling back to v19.")
181
+ print("---- exception ----")
182
+ print(traceback.format_exc())
183
+ print("-------------------")
184
+ AIO_VERSION = DEFAULT_AIO_VERSION
185
+ AIO_VERSION_SOURCE = "fallback_to_v19"
186
+ pipe = _load_pipe_with_version(AIO_VERSION)
187
+
188
+ # Apply FA3 Optimization
189
+ try:
190
+ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
191
+ print("Flash Attention 3 Processor set successfully.")
192
+ except Exception as e:
193
+ print(f"Warning: Could not set FA3 processor: {e}")
194
 
195
  MAX_SEED = np.iinfo(np.int32).max
196
 
 
256
  "weights": "bfs_head_v5_2511_original.safetensors",
257
  "adapter_name": "BFS-Best-Faceswap",
258
  "strength": 1.0,
259
+ "needs_alpha_fix": True, # <-- fixes KeyError 'img_in.alpha'
260
  },
261
  "Multiple-Angles": {
262
  "type": "single",
 
333
  "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",
334
  }
335
 
336
+ # Track what is currently loaded in memory (adapter_name values)
337
  LOADED_ADAPTERS = set()
338
 
339
  # ============================================================
 
375
 
376
 
377
  def _to_pil_rgb(x) -> Optional[Image.Image]:
378
+ """
379
+ Accepts PIL / numpy / (image, caption) tuples from gr.Gallery and returns PIL RGB.
380
+ Gradio Gallery commonly yields tuples like (image, caption).
381
+ """
382
  if x is None:
383
  return None
384
 
385
+ # Gallery often returns (image, caption)
386
  if isinstance(x, tuple) and len(x) >= 1:
387
  x = x[0]
388
  if x is None:
 
394
  if isinstance(x, np.ndarray):
395
  return Image.fromarray(x).convert("RGB")
396
 
397
+ # Best-effort fallback
398
  try:
399
  return Image.fromarray(np.array(x)).convert("RGB")
400
  except Exception:
 
406
  img2: Optional[Image.Image],
407
  extra_imgs: Optional[list[Image.Image]],
408
  ) -> dict[str, Image.Image]:
409
+ """
410
+ Creates labels image_1, image_2, image_3... based on what is actually uploaded:
411
+ - img1 is always image_1
412
+ - img2 becomes image_2 only if present
413
+ - extras start immediately after the last present base box
414
+ The pipeline receives images in this exact order.
415
+ """
416
  labeled: dict[str, Image.Image] = {}
417
  idx = 1
418
+
419
  labeled[f"image_{idx}"] = img1
420
  idx += 1
421
 
 
439
 
440
 
441
  def _inject_missing_alpha_keys(state_dict: dict) -> dict:
442
+ """
443
+ Diffusers' Qwen LoRA converter expects '<module>.alpha' keys.
444
+ BFS safetensors omits them. We inject alpha = rank (neutral scaling).
445
+
446
+ IMPORTANT: diffusers may strip 'diffusion_model.' before lookup, so we
447
+ inject BOTH:
448
+ - diffusion_model.xxx.alpha
449
+ - xxx.alpha
450
+ """
451
  bases = {}
452
+
453
  for k, v in state_dict.items():
454
  if not isinstance(v, torch.Tensor):
455
  continue
 
475
 
476
 
477
  def _load_lora_weights_with_fallback(repo: str, weight_name: str, adapter_name: str, needs_alpha_fix: bool = False):
478
+ """
479
+ Normal path: pipe.load_lora_weights(repo, weight_name=..., adapter_name=...)
480
+ BFS fallback: download safetensors, inject missing alpha keys, then load from dict.
481
+ """
482
  try:
483
  pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=adapter_name)
484
  return
 
490
  local_path = hf_hub_download(repo_id=repo, filename=weight_name)
491
  sd = safetensors_load_file(local_path)
492
  sd = _inject_missing_alpha_keys(sd)
493
+
494
  pipe.load_lora_weights(sd, adapter_name=adapter_name)
495
  return
496
 
 
522
 
523
  if adapter_name not in LOADED_ADAPTERS:
524
  print(f"--- Downloading and Loading Adapter Part: {selected_lora} / {adapter_name} ---")
525
+ try:
526
+ _load_lora_weights_with_fallback(
527
+ repo=repo,
528
+ weight_name=weights,
529
+ adapter_name=adapter_name,
530
+ needs_alpha_fix=needs_alpha_fix,
531
+ )
532
+ LOADED_ADAPTERS.add(adapter_name)
533
+ except Exception as e:
534
+ raise gr.Error(f"Failed to load adapter part {selected_lora}/{adapter_name}: {e}")
535
+ else:
536
+ print(f"--- Adapter part already loaded: {selected_lora} / {adapter_name} ---")
537
 
538
  adapter_names.append(adapter_name)
539
  adapter_weights.append(strength)
 
547
 
548
  if adapter_name not in LOADED_ADAPTERS:
549
  print(f"--- Downloading and Loading Adapter: {selected_lora} ---")
550
+ try:
551
+ _load_lora_weights_with_fallback(
552
+ repo=repo,
553
+ weight_name=weights,
554
+ adapter_name=adapter_name,
555
+ needs_alpha_fix=needs_alpha_fix,
556
+ )
557
+ LOADED_ADAPTERS.add(adapter_name)
558
+ except Exception as e:
559
+ raise gr.Error(f"Failed to load adapter {selected_lora}: {e}")
560
+ else:
561
+ print(f"--- Adapter {selected_lora} is already loaded. ---")
562
 
563
  adapter_names = [adapter_name]
564
  adapter_weights = [strength]
 
566
  return adapter_names, adapter_weights
567
 
568
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  # ============================================================
570
  # UI handlers
571
  # ============================================================
572
 
573
 
574
  def on_lora_change_ui(selected_lora, current_prompt):
575
+ # Preset prompt (fill only if empty)
576
  if selected_lora != NONE_LORA:
577
  preset = LORA_PRESET_PROMPTS.get(selected_lora, "")
578
  if preset and (current_prompt is None or str(current_prompt).strip() == ""):
 
582
  else:
583
  prompt_update = gr.update(value=current_prompt)
584
 
585
+ # Image2 visibility/label
586
  if lora_requires_two_images(selected_lora):
587
  img2_update = gr.update(visible=True, label=image2_label_for_lora(selected_lora))
588
  else:
 
598
 
599
  @spaces.GPU
600
  def infer(
 
 
601
  input_image_1,
602
  input_image_2,
603
+ input_images_extra, # gallery multi-image box
604
  prompt,
605
  lora_adapter,
606
  seed,
 
609
  steps,
610
  progress=gr.Progress(track_tqdm=True),
611
  ):
612
+ gc.collect()
613
+ if torch.cuda.is_available():
614
+ torch.cuda.empty_cache()
615
 
616
+ if input_image_1 is None:
617
+ raise gr.Error("Please upload Image 1.")
618
 
619
+ # Handle "None"
620
+ if lora_adapter == NONE_LORA:
621
+ try:
622
+ pipe.set_adapters([], adapter_weights=[])
623
+ except Exception:
624
+ if LOADED_ADAPTERS:
625
+ pipe.set_adapters(list(LOADED_ADAPTERS), adapter_weights=[0.0] * len(LOADED_ADAPTERS))
626
+ else:
627
+ adapter_names, adapter_weights = _ensure_loaded_and_get_active_adapters(lora_adapter)
628
+ pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
629
 
630
+ if randomize_seed:
631
+ seed = random.randint(0, MAX_SEED)
 
 
 
 
 
 
 
 
632
 
633
+ generator = torch.Generator(device=device).manual_seed(seed)
634
+ negative_prompt = (
635
+ "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
636
+ "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
637
+ )
638
 
639
+ img1 = input_image_1.convert("RGB")
640
+ img2 = input_image_2.convert("RGB") if input_image_2 is not None else None
 
 
 
641
 
642
+ # Normalize extra images (Gallery) to PIL RGB (handles tuples from Gallery)
643
+ extra_imgs: list[Image.Image] = []
644
+ if input_images_extra:
645
+ for item in input_images_extra:
646
+ pil = _to_pil_rgb(item)
647
+ if pil is not None:
648
+ extra_imgs.append(pil)
649
 
650
+ # Enforce existing 2-image LoRA behavior (image_1 + image_2 required)
651
+ if lora_requires_two_images(lora_adapter) and img2 is None:
652
+ raise gr.Error("This LoRA needs two images. Please upload Image 2 as well.")
 
 
 
653
 
654
+ # Label images as image_1, image_2, image_3...
655
+ labeled = build_labeled_images(img1, img2, extra_imgs)
656
 
657
+ # Pass to pipeline in labeled order. Keep single-image call when only one is present.
658
+ pipe_images = list(labeled.values())
659
+ if len(pipe_images) == 1:
660
+ pipe_images = pipe_images[0]
661
 
662
+ # Resolution derived from Image 1 (base/body/target)
663
+ target_long_edge = get_target_long_edge_for_lora(lora_adapter)
664
+ width, height = compute_dimensions(img1, target_long_edge)
665
 
666
+ try:
667
  result = pipe(
668
  image=pipe_images,
669
  prompt=prompt,
 
674
  generator=generator,
675
  true_cfg_scale=guidance_scale,
676
  ).images[0]
677
+ return result, seed
 
 
 
 
 
678
  finally:
679
+ gc.collect()
680
+ if torch.cuda.is_available():
681
+ torch.cuda.empty_cache()
682
 
683
 
684
  @spaces.GPU
685
+ def infer_example(input_image, prompt, lora_adapter):
686
  if input_image is None:
687
+ return None, 0
688
  input_pil = input_image.convert("RGB")
689
  guidance_scale = 1.0
690
  steps = 4
691
+ # Examples don't supply Image 2 or extra images; and example list doesn't include AnyPose/BFS.
692
+ result, seed = infer(input_pil, None, None, prompt, lora_adapter, 0, True, guidance_scale, steps)
693
+ return result, seed
 
 
 
 
 
 
 
 
 
 
 
694
 
695
 
696
  # ============================================================
 
705
  #main-title h1 {font-size: 2.1em !important;}
706
  """
707
 
708
+ aio_status_line = (
709
+ f"**AIO transformer version:** `{AIO_VERSION}` "
710
+ f"({AIO_VERSION_SOURCE}; env `AIO_VERSION`={_AIO_ENV_RAW!r})"
711
+ )
712
 
713
+ with gr.Blocks() as demo:
714
  with gr.Column(elem_id="col-container"):
715
  gr.Markdown("# **Qwen-Image-Edit-2511-LoRAs-Fast**", elem_id="main-title")
716
  gr.Markdown(
 
718
  "[LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters for the "
719
  "[Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) model."
720
  )
721
+ gr.Markdown(aio_status_line)
 
 
 
 
 
 
 
 
 
 
 
 
 
722
 
723
  with gr.Row(equal_height=True):
724
  with gr.Column():
 
759
  guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
760
  steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
761
 
762
+ # On LoRA selection: preset prompt + toggle Image 2
763
  lora_adapter.change(
764
  fn=on_lora_change_ui,
765
  inputs=[lora_adapter, prompt],
766
  outputs=[prompt, input_image_2],
767
  )
768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  gr.Examples(
770
  examples=[
771
  ["examples/1.jpg", "Transform into anime.", "Photo-to-Anime"],
 
794
  ["examples/4.jpg", "Switch the camera to a wide-angle lens.", "Multiple-Angles"],
795
  ["examples/11.jpg", "Upscale this picture to 4K resolution.", "Upscale2K"],
796
  ],
797
+ inputs=[input_image_1, prompt, lora_adapter],
798
+ outputs=[output_image, seed],
799
  fn=infer_example,
800
  cache_examples=False,
801
  label="Examples",
802
  )
803
 
 
 
 
804
  run_button.click(
 
 
 
 
805
  fn=infer,
806
  inputs=[
 
 
807
  input_image_1,
808
  input_image_2,
809
  input_images_extra,
 
814
  guidance_scale,
815
  steps,
816
  ],
817
+ outputs=[output_image, seed],
818
  )
819
 
820
  if __name__ == "__main__":