M3st3rJ4k3l commited on
Commit
cf4feb8
·
verified ·
1 Parent(s): dd93079

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +53 -375
  2. lora_registry.py +10 -13
app.py CHANGED
@@ -6,19 +6,33 @@ import random
6
  import uuid
7
  import zipfile
8
  import threading
9
- from typing import Iterable
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 logging_utils import log_inference
18
-
19
  from image_utils import (
20
  fix_orientation,
21
- compute_base_dimensions,
22
  compute_canvas_dimensions,
23
  fit_to_canvas,
24
  on_base_image_change,
@@ -35,7 +49,6 @@ from image_utils import (
35
  push_pil_to_base,
36
  push_pil_to_reference,
37
  )
38
-
39
  from control_tools import (
40
  generate_depthmap,
41
  detect_pose,
@@ -51,244 +64,23 @@ from control_tools import (
51
  OPENPOSE_KEYPOINT_NAMES,
52
  )
53
 
54
- # requirements.txt: spandrel, pillow-heif
55
-
56
- MODEL_VARIANT = os.environ.get("MODEL_VARIANT", "9B")
57
  if MODEL_VARIANT == "9B-KV":
58
  from diffusers import Flux2KleinKVPipeline as _PipeClass
59
- _MODEL_REPO = "black-forest-labs/FLUX.2-klein-9b-kv"
60
  else:
61
  from diffusers import Flux2KleinPipeline as _PipeClass
62
- _MODEL_REPO = "black-forest-labs/FLUX.2-klein-9B"
63
- MODEL_VARIANT = "9B"
64
 
65
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
66
 
67
- # ── Theme ────────────────────────────────────────────────────────────────────
68
- from gradio.themes import Soft
69
- from gradio.themes.utils import colors, fonts, sizes
70
-
71
- colors.orange_red = colors.Color(
72
- name="orange_red", c50="#FFF0E5", c100="#FFE0CC", c200="#FFC299", c300="#FFA366",
73
- c400="#FF8533", c500="#FF4500", c600="#E63E00", c700="#CC3700", c800="#B33000",
74
- c900="#992900", c950="#802200",
75
- )
76
-
77
- class OrangeRedTheme(Soft):
78
- def __init__(self, *, primary_hue=colors.gray, secondary_hue=colors.orange_red,
79
- neutral_hue=colors.slate, text_size=sizes.text_lg,
80
- font=(fonts.GoogleFont("Outfit"), "Arial", "sans-serif"),
81
- font_mono=(fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace")):
82
- super().__init__(primary_hue=primary_hue, secondary_hue=secondary_hue,
83
- neutral_hue=neutral_hue, text_size=text_size,
84
- font=font, font_mono=font_mono)
85
- super().set(
86
- background_fill_primary="*primary_50",
87
- background_fill_primary_dark="*primary_900",
88
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
89
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
90
- button_primary_text_color="white",
91
- button_primary_text_color_hover="white",
92
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
93
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
94
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
95
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
96
- slider_color="*secondary_500", slider_color_dark="*secondary_600",
97
- block_title_text_weight="600", block_border_width="3px",
98
- block_shadow="*shadow_drop_lg", button_primary_shadow="*shadow_drop_lg",
99
- button_large_padding="11px", color_accent_soft="*primary_100",
100
- block_label_background_fill="*primary_200",
101
- )
102
-
103
- orange_red_theme = OrangeRedTheme()
104
- MAX_SEED = np.iinfo(np.int32).max
105
-
106
- # ── Upscaler models, FACE_SWAP_PROMPT, LORA_STYLES — UNCHANGED from previous step
107
- UPSCALE_MODELS = {
108
- "None": {"scale": None, "file": None, "url": None},
109
- "2× — RealESRGAN (balanced)": {"scale": 2, "file": "RealESRGAN_x2plus.pth",
110
- "url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"},
111
- "4× — RealESRGAN (balanced)": {"scale": 4, "file": "RealESRGAN_x4plus.pth",
112
- "url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"},
113
- "4× — UltraSharp (crisp)": {"scale": 4, "file": "4x-UltraSharpV2.pth",
114
- "url": "https://huggingface.co/Kim2091/UltraSharpV2/resolve/main/4x-UltraSharpV2.pth"},
115
- "4× — Remacri (natural)": {"scale": 4, "file": "4x_foolhardy_Remacri.pth",
116
- "url": "https://huggingface.co/FacehugmanIII/4x_foolhardy_Remacri/resolve/main/4x_foolhardy_Remacri.pth"},
117
- "4× — Nomos2 HQ DAT2 (Photography)": {"scale": 4, "file": "4xNomos2_hq_dat2.pth",
118
- "url": "https://github.com/Phhofm/models/releases/download/4xNomos2_hq_dat2/4xNomos2_hq_dat2.pth"},
119
- }
120
-
121
- FACE_SWAP_PROMPT = """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.
122
- FROM PICTURE 1 (strictly preserve):
123
- - Scene: lighting conditions, shadows, highlights, color temperature, environment, background
124
- - Head positioning: exact rotation angle, tilt, direction the head is facing
125
- - Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion
126
- FROM PICTURE 2 (strictly preserve identity):
127
- - Facial structure: face shape, bone structure, jawline, chin
128
- - All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows
129
- - Hair: color, style, texture, hairline
130
- - Skin: texture, tone, complexion
131
- The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
132
-
133
- MAX_LORA_SLOTS = 6
134
-
135
- LORA_STYLES = [
136
- {
137
- "image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/image.webp",
138
- "title": "None",
139
- "adapter_name": None,
140
- "repo": None,
141
- "weights": None,
142
- "default_prompt": None,
143
- "default_weight": 1.0,
144
- },
145
- {
146
- "title": "Klein-Delight-Style",
147
- "adapter_name": "klein-delight",
148
- "repo": "linoyts/Flux2-Klein-Delight-LoRA",
149
- "weights": "pytorch_lora_weights.safetensors",
150
- "default_prompt": "Relight the image to remove all existing lighting conditions and replace them with neutral, uniform illumination. Apply soft, evenly distributed lighting with no directional shadows, no harsh highlights, and no dramatic contrast. Maintain the original identity of all subjects exactly—preserve facial structure, skin tone, proportions, expressions, hair, clothing, and textures. Do not alter pose, camera angle, background geometry, or image composition. Lighting should appear balanced, and studio-neutral, similar to diffuse overcast or a soft lightbox setup. Ensure consistent exposure across the entire image with realistic depth and subtle shading only where necessary for form.",
151
- "default_weight": 1.0,
152
- },
153
- {
154
- "title": "Klein-Consistency",
155
- "adapter_name": "klein-consistency",
156
- "repo": "dx8152/Flux2-Klein-9B-Consistency",
157
- "weights": "Klein-consistency.safetensors",
158
- "default_prompt": None,
159
- "default_weight": 0.3,
160
- },
161
- {
162
- "title": "Best-Face-Swap",
163
- "adapter_name": "face-swap",
164
- "repo": "Alissonerdx/BFS-Best-Face-Swap",
165
- "weights": "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors",
166
- "default_prompt": FACE_SWAP_PROMPT,
167
- "default_weight": 1.0,
168
- },
169
- {
170
- "title": "NSFW v2",
171
- "adapter_name": "nsfw-v2",
172
- "repo": "diroverflo/FLux_Klein_9B_NSFW",
173
- "weights": "Flux Klein - NSFW v2.safetensors",
174
- "default_prompt": None,
175
- "default_weight": 1.0,
176
- },
177
- {
178
- "title": "Ultimate Upscaler Klein-9b",
179
- "adapter_name": "Ultimate Upscaler",
180
- "repo": "loras",
181
- "weights": "Flux2-Klein-Image-RestoreV1.safetensors",
182
- "default_prompt": "restore the image quality, remove any compression artefacts, remove any haze and soft edges, enrich the original with new intricate detail in all textures and surfaces creating a professional photorealistic photograph with natural lighting and skin texture.",
183
- "default_weight": 1.0,
184
- },
185
- {
186
- "title": "High Resolution",
187
- "adapter_name": "High Resolution",
188
- "repo": "loras",
189
- "weights": "HighResolution9B.safetensors",
190
- "default_prompt": "High Resolution",
191
- "default_weight": 1.0,
192
- },
193
- {
194
- "title": "InstaPic",
195
- "adapter_name": "InstaPic V3",
196
- "repo": "loras",
197
- "weights": "InstaPic V3.safetensors",
198
- "default_prompt": "instapic",
199
- "default_weight": 1.0,
200
- },
201
- {
202
- "title": "Realistic Nudes",
203
- "adapter_name": "Realistic Nudes",
204
- "repo": "loras",
205
- "weights": "realistic_nudes_klein_v3.safetensors",
206
- "default_prompt": None,
207
- "default_weight": 1.0,
208
- },
209
- {
210
- "title": "Perky Pointy Puffy Breasts",
211
- "adapter_name": "Perky Pointy Puffy Breasts",
212
- "repo": "loras",
213
- "weights": "PerkyPointyPuffy_v1.1_small_pointy_breasts_large_puffy_nipples.safetensors",
214
- "default_prompt": "Small pointy breasts with large puffy nipples",
215
- "default_weight": 1.0,
216
- },
217
- {
218
- "title": "Flat Chested",
219
- "adapter_name": "Flat Chested",
220
- "repo": "loras",
221
- "weights": "Flux2-Klein-9b-FlatChested-v1.safetensors",
222
- "default_prompt": "flat chested",
223
- "default_weight": 1.5,
224
- },
225
- {
226
- "title": "Controllight",
227
- "adapter_name": "Controllight",
228
- "repo": "ControlLight/ControlLight",
229
- "weights": "controllight.safetensors",
230
- "default_prompt": None,
231
- "default_weight": 1.0,
232
- },
233
- {
234
- "title": "RefControl - Depth",
235
- "adapter_name": "RefConDep",
236
- "repo": "thedeoxen/refcontrol-FLUX.2-klein-9B-reference-depth-lora",
237
- "weights": "flux2_klein_9b_refcontrol_depth.safetensors",
238
- "default_prompt": "refcontrol",
239
- "default_weight": 1.0,
240
- },
241
- {
242
- "title": "RefControl - Pose",
243
- "adapter_name": "RefConPos",
244
- "repo": "thedeoxen/refcontrol-FLUX.2-klein-9B-reference-pose-lora",
245
- "weights": "refcontrol_v2_poses.safetensors",
246
- "default_prompt": "apply pose from image 1 with reference from image 2",
247
- "default_weight": 1.0,
248
- },
249
- ]
250
-
251
- LOADED_ADAPTERS = set()
252
-
253
-
254
- def get_all_styles(dynamic_loras):
255
- return list(LORA_STYLES) + list((dynamic_loras or {}).values())
256
-
257
- def get_selectable_styles(dynamic_loras):
258
- return [s for s in get_all_styles(dynamic_loras) if s["adapter_name"] is not None]
259
-
260
- def get_style_by_title(title, dynamic_loras):
261
- for s in get_all_styles(dynamic_loras):
262
- if s["title"] == title:
263
- return s
264
- return None
265
-
266
-
267
- print(f"Loading FLUX.2 Klein {MODEL_VARIANT} from {_MODEL_REPO}...")
268
- pipe = _PipeClass.from_pretrained(_MODEL_REPO, torch_dtype=torch.bfloat16).to(device)
269
  print(f"Model loaded successfully: FLUX.2 Klein {MODEL_VARIANT}")
270
 
271
 
272
  # ── UI helper callbacks ──────────────────────────────────────────────────────
273
 
274
- def update_weight_sliders(selected_titles, dynamic_loras):
275
- selected = [get_style_by_title(t, dynamic_loras) for t in (selected_titles or [])
276
- if get_style_by_title(t, dynamic_loras)]
277
- updates = []
278
- for i in range(MAX_LORA_SLOTS):
279
- if i < len(selected):
280
- s = selected[i]
281
- updates.append(gr.update(visible=True,
282
- label=f"{s['title']} — weight",
283
- value=s.get("default_weight", 1.0)))
284
- else:
285
- updates.append(gr.update(visible=False, value=1.0))
286
- prompts = [s.get("default_prompt") for s in selected if s.get("default_prompt")]
287
- if prompts:
288
- return updates + [gr.update(value="\n\n".join(prompts), visible=True)]
289
- return updates + [gr.update(value="", visible=False)]
290
-
291
-
292
  def on_canvas_mode_change(mode):
293
  """Custom W/H sliders only relevant when mode == Custom."""
294
  is_custom = (mode == "Custom")
@@ -317,65 +109,16 @@ def on_gallery_select(evt: gr.SelectData, gallery_value):
317
  return item[0] if isinstance(item, (list, tuple)) else item
318
 
319
 
320
- # ── Upscaler (tiled) — unchanged ─────────────────────────────────────────────
321
-
322
- def _upscale_tiled(model_fn, img_t, tile=512, overlap=32):
323
- _, c, h, w = img_t.shape
324
- with torch.no_grad():
325
- probe = model_fn(img_t[:, :, :min(4, h), :min(4, w)])
326
- scale = probe.shape[-1] // min(4, w)
327
- del probe; torch.cuda.empty_cache()
328
- out_h, out_w = h * scale, w * scale
329
- canvas = torch.zeros(1, c, out_h, out_w, dtype=torch.float32)
330
- weights = torch.zeros(1, 1, out_h, out_w, dtype=torch.float32)
331
- step = max(tile - overlap, 1)
332
- ys = sorted(set(list(range(0, max(h - tile, 0), step)) + [max(h - tile, 0)]))
333
- xs = sorted(set(list(range(0, max(w - tile, 0), step)) + [max(w - tile, 0)]))
334
- for y0 in ys:
335
- for x0 in xs:
336
- y1 = min(y0 + tile, h); x1 = min(x0 + tile, w)
337
- with torch.no_grad():
338
- out = model_fn(img_t[:, :, y0:y1, x0:x1]).cpu().float()
339
- oy0, ox0 = y0 * scale, x0 * scale
340
- oy1, ox1 = y1 * scale, x1 * scale
341
- canvas[:, :, oy0:oy1, ox0:ox1] += out
342
- weights[:, :, oy0:oy1, ox0:ox1] += 1.0
343
- return (canvas / weights.clamp(min=1)).clamp(0, 1)
344
-
345
-
346
- def apply_realesrgan(image, model_key):
347
- cfg = UPSCALE_MODELS[model_key]
348
- try:
349
- from spandrel import ImageModelDescriptor, ModelLoader
350
- except ImportError:
351
- raise gr.Error("spandrel is not installed. Add 'spandrel' to requirements.txt.")
352
- import urllib.request
353
- cache_dir = "/tmp/realesrgan_weights"
354
- os.makedirs(cache_dir, exist_ok=True)
355
- cache_path = os.path.join(cache_dir, cfg["file"])
356
- if not os.path.exists(cache_path):
357
- print(f"Downloading {cfg['file']}…")
358
- urllib.request.urlretrieve(cfg["url"], cache_path)
359
- model = ModelLoader().load_from_file(cache_path)
360
- if not isinstance(model, ImageModelDescriptor):
361
- raise gr.Error(f"Loaded model is not a single-image descriptor: {type(model)}")
362
- sr_model = model.model.to(device).eval()
363
- img_np = np.array(image).astype(np.float32) / 255.0
364
- img_t = torch.from_numpy(img_np).permute(2, 0, 1).unsqueeze(0).to(device)
365
- out_t = _upscale_tiled(sr_model, img_t, tile=512, overlap=32)
366
- del sr_model, img_t
367
- gc.collect(); torch.cuda.empty_cache()
368
- out_np = out_t.squeeze(0).permute(1, 2, 0).numpy()
369
- return Image.fromarray((out_np * 255).astype(np.uint8))
370
-
371
-
372
  # ── Logging ──────────────────────────────────────────────────────────────────
373
 
374
  def _spawn_log(pil_images, result_image, prompt, seed, steps, guidance_scale,
375
  width, height, duration, success, error="",
376
  lora_titles=None, lora_weights=None, upscale_factor="None",
377
  lora_prompt_text=""):
378
- if os.environ.get("ENABLE_LOGGING", "No").strip().lower() != "yes":
 
 
 
379
  return
380
  threading.Thread(
381
  target=log_inference,
@@ -449,7 +192,7 @@ def _infer_gpu(
449
  if upscale_factor and upscale_factor != "None":
450
  gc.collect(); torch.cuda.synchronize(); torch.cuda.empty_cache()
451
  try:
452
- image = apply_realesrgan(image, upscale_factor)
453
  except Exception as e:
454
  gr.Warning(f"Upscaling failed, returning {width}×{height} result: {e}")
455
 
@@ -502,8 +245,6 @@ def infer(
502
  selected_titles = selected_titles or []
503
  batch_count = max(1, int(batch_count))
504
 
505
- # Pre-plan per-iteration seed and weight overrides — keeps the loop simple
506
- # and lets us put the LoRA-sweep values into PNG metadata cleanly.
507
  base_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
508
  seeds, weight_overrides = [], []
509
  for i in range(batch_count):
@@ -582,7 +323,12 @@ def bulk_infer(
582
  into the bulk output gallery as soon as they complete. Outputs and a CSV
583
  manifest are written to /tmp/bulk_<sid>/ with stable filenames, so a
584
  ZeroGPU quota wall mid-run still leaves earlier outputs grabbable from
585
- the gallery and from disk."""
 
 
 
 
 
586
  if not input_files:
587
  raise gr.Error("Upload at least one image first.")
588
 
@@ -619,8 +365,6 @@ def bulk_infer(
619
  *slider_values, progress=progress,
620
  )
621
 
622
- # Stable, predictable filename inside the session work_dir so the
623
- # user can also find outputs on disk if the UI drops.
624
  stem = os.path.splitext(fname)[0]
625
  out_path = os.path.join(work_dir, f"{i:03d}_{stem}.png")
626
  meta = _meta_for(prompt, used_seed, steps, guidance_scale, w, h, upscale_factor,
@@ -642,16 +386,19 @@ def bulk_infer(
642
  upscale_factor=upscale_factor, lora_prompt_text=lora_prompt_text or "")
643
 
644
  status = f"✅ {succeeded}/{total} done ({failed} failed)"
645
- yield results, status, gr.update() # zip not ready yet
646
  except Exception as e:
647
  failed += 1
648
  duration = time.perf_counter() - t0
649
  with open(manifest_path, "a", newline="") as f:
650
  csv.writer(f).writerow([i, fname, "", "", "", "", False, str(e)[:300], f"{duration:.2f}"])
 
 
 
 
651
  yield results, f"⚠️ Image {i+1} failed: {e} | {succeeded} ok, {failed} failed", gr.update()
652
  continue
653
 
654
- # Final pass: bundle a zip. ZIP_STORED because PNGs are already compressed.
655
  zip_path = os.path.join(work_dir, "outputs.zip")
656
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as zf:
657
  for p in results:
@@ -661,44 +408,7 @@ def bulk_infer(
661
  yield results, f"🎉 Done — {succeeded}/{total} succeeded, {failed} failed", zip_path
662
 
663
 
664
- # ── Dynamic LoRA loader (unchanged from previous step) ───────────────────────
665
-
666
- def add_custom_lora(repo_id, weight_name, adapter_name, dynamic_loras_state):
667
- dynamic_loras = dict(dynamic_loras_state or {})
668
- if not repo_id or not repo_id.strip():
669
- return "Please enter a valid HuggingFace repo ID.", gr.update(), dynamic_loras
670
- repo_id = repo_id.strip()
671
- requested_name = adapter_name.strip() if adapter_name and adapter_name.strip() else None
672
- try:
673
- from huggingface_hub import model_info
674
- info = model_info(repo_id)
675
- actual_weight = weight_name.strip() if weight_name and weight_name.strip() else None
676
- if not actual_weight:
677
- for name in ["pytorch_lora_weights.safetensors", "lora.safetensors", "adapter_model.safetensors"]:
678
- if any(f.filename == name for f in info.siblings):
679
- actual_weight = name
680
- break
681
- if not actual_weight:
682
- available = [f.filename for f in info.siblings if f.filename.endswith(('.safetensors', '.bin'))]
683
- return f"No weight found. Available: {', '.join(available) or 'None'}", gr.update(), dynamic_loras
684
- base_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in requested_name) if requested_name else "custom"
685
- static_names = {s["adapter_name"] for s in LORA_STYLES if s["adapter_name"]}
686
- final = f"{base_name}_{uuid.uuid4().hex[:6]}"
687
- while final in static_names or final in LOADED_ADAPTERS:
688
- final = f"{base_name}_{uuid.uuid4().hex[:6]}"
689
- dynamic_loras[final] = {
690
- "image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/image.webp",
691
- "title": f"Custom: {base_name}", "adapter_name": final,
692
- "repo": repo_id, "weights": actual_weight,
693
- "default_prompt": None, "default_weight": 1.0,
694
- }
695
- new_choices = [s["title"] for s in get_selectable_styles(dynamic_loras)]
696
- return f"✅ Added: {base_name} from {repo_id}", gr.update(choices=new_choices), dynamic_loras
697
- except Exception as e:
698
- return f"❌ Failed: {str(e)}", gr.update(), dynamic_loras
699
-
700
-
701
- # ── Custom prompt manager (unchanged) ────────────────────────────────────────
702
 
703
  def add_custom_prompt(name, text, prompts_state, counter_state):
704
  prompts = dict(prompts_state); counter = int(counter_state)
@@ -716,6 +426,7 @@ def add_custom_prompt(name, text, prompts_state, counter_state):
716
  gr.update(choices=choices), gr.update(choices=choices),
717
  gr.update(value="", interactive=True))
718
 
 
719
  def delete_custom_prompt(name, currently_selected, prompts_state):
720
  prompts = dict(prompts_state)
721
  msg = f"🗑️ Deleted: '{name}'" if name and name in prompts else "Nothing to delete."
@@ -727,6 +438,7 @@ def delete_custom_prompt(name, currently_selected, prompts_state):
727
  gr.update(choices=choices, value=new_sel),
728
  gr.update(choices=choices, value=None))
729
 
 
730
  def update_custom_prompt_display(selected_names, prompts_state):
731
  if not selected_names:
732
  return gr.update(value="", visible=False)
@@ -750,16 +462,15 @@ with gr.Blocks() as demo:
750
  custom_prompts_state = gr.State({})
751
  custom_prompt_counter_state = gr.State(0)
752
  dynamic_loras_state = gr.State({})
753
- selected_output_state = gr.State(None) # currently-selected gallery item path
754
 
755
  with gr.Column(elem_id="col-container"):
756
- _logging_on = os.environ.get("ENABLE_LOGGING", "No").strip().lower() == "yes"
757
- _logging_badge = "🟢 On" if _logging_on else "🔴 Off"
758
 
759
  gr.Markdown("# **FLUX.2-Klein-LoRA-Studio**", elem_id="main-title")
760
  gr.Markdown(
761
  f"Apply one or more [LoRA](https://huggingface.co/models?other=base_model:adapter:black-forest-labs/FLUX.2-klein-9B) "
762
- f"adapters using [FLUX.2-Klein-{MODEL_VARIANT}]({_MODEL_REPO}). "
763
  f"**Model:** `{MODEL_VARIANT}` · **Logging:** {_logging_badge}"
764
  )
765
 
@@ -835,11 +546,7 @@ with gr.Blocks() as demo:
835
  sweep_max = gr.Slider(label="Sweep max weight", minimum=0.0, maximum=2.0,
836
  step=0.05, value=1.4, visible=False)
837
 
838
- # ── Right column ─────────────────────────────────────────
839
  with gr.Column(scale=1):
840
- # Gallery so batch runs stream in as they finish; type=
841
- # filepath so PNG metadata round-trips to Send→* buttons
842
- # and to the user's downloads.
843
  output_gallery = gr.Gallery(
844
  label="Output", type="filepath", columns=2, rows=2,
845
  height=420, allow_preview=True, preview=True,
@@ -857,7 +564,6 @@ with gr.Blocks() as demo:
857
  "Civitai / ComfyUI readable).*"
858
  )
859
 
860
- # LoRA selector + weight sliders
861
  gr.Markdown("### 🎨 Select LoRA(s)")
862
  lora_selector = gr.CheckboxGroup(
863
  choices=[s["title"] for s in get_selectable_styles({})],
@@ -967,10 +673,8 @@ with gr.Blocks() as demo:
967
  "Reference image."
968
  )
969
 
970
- # Per-tab state — kept here, not in module globals, so each
971
- # session edits its own pose without crosstalk.
972
- pose_source_state = gr.State(None) # PIL of last source image
973
- pose_keypoints_state = gr.State([]) # list[list[dict]]
974
 
975
  with gr.Row():
976
  with gr.Column(scale=1):
@@ -1000,8 +704,6 @@ with gr.Blocks() as demo:
1000
 
1001
  with gr.Row():
1002
  with gr.Column(scale=1):
1003
- # interactive=False so users can't accidentally upload a new image
1004
- # into this slot. .select still fires for click coordinates.
1005
  pose_overlay = gr.Image(
1006
  label="Editor — click to place active joint",
1007
  type="pil", interactive=False, height=420, format="png",
@@ -1031,21 +733,21 @@ with gr.Blocks() as demo:
1031
  send_pose_ref_btn = gr.Button("→ Send pose to Reference",
1032
  variant="primary")
1033
  send_pose_base_btn = gr.Button("→ Send pose to Base")
1034
-
1035
  # ── Event wiring ─────────────────────────────────────────────────────────
1036
 
1037
- # HEIC preview fix on the main tab
1038
  base_image.upload(fn=reencode_upload, inputs=[base_image], outputs=[base_image])
1039
-
1040
  base_image.change(fn=on_base_image_change, inputs=[base_image], outputs=[size_info])
1041
  reference_images.change(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
1042
 
 
1043
  lora_selector.change(
1044
  fn=update_weight_sliders,
1045
  inputs=[lora_selector, dynamic_loras_state],
1046
  outputs=weight_sliders + [lora_prompt_display],
1047
  )
1048
 
 
1049
  add_lora_btn.click(
1050
  fn=add_custom_lora,
1051
  inputs=[lora_repo_id, lora_weight_name, lora_adapter_name, dynamic_loras_state],
@@ -1069,7 +771,6 @@ with gr.Blocks() as demo:
1069
  outputs=[custom_prompt_display],
1070
  )
1071
 
1072
- # Canvas / fit / batch UI toggles
1073
  canvas_mode.change(fn=on_canvas_mode_change, inputs=[canvas_mode],
1074
  outputs=[custom_width, custom_height])
1075
  canvas_fit_mode.change(fn=on_fit_mode_change, inputs=[canvas_fit_mode],
@@ -1077,12 +778,9 @@ with gr.Blocks() as demo:
1077
  batch_vary.change(fn=on_batch_vary_change, inputs=[batch_vary],
1078
  outputs=[sweep_min, sweep_max])
1079
 
1080
- # Track which gallery item the user clicked, so Send→Base/Ref can use it
1081
  output_gallery.select(fn=on_gallery_select, inputs=[output_gallery],
1082
  outputs=[selected_output_state])
1083
 
1084
- # The Generate-tab generator. .click() returns an event we keep so the
1085
- # bulk Stop button can cancel mid-stream too if desired.
1086
  run_event = run_button.click(
1087
  fn=infer,
1088
  inputs=[base_image, reference_images, prompt, lora_prompt_display, custom_prompt_display,
@@ -1094,8 +792,6 @@ with gr.Blocks() as demo:
1094
  )
1095
 
1096
  # ── Editor tab wiring ────────────────────────────────────────────────────
1097
- # NOTE: do NOT add editor.upload(outputs=[editor]) — remounts the editor.
1098
-
1099
  heic_uploader.upload(fn=load_heic_to_editor, inputs=[heic_uploader], outputs=[editor])
1100
 
1101
  send_to_base_btn.click(fn=send_editor_to_base, inputs=[editor], outputs=[base_image]) \
@@ -1107,7 +803,6 @@ with gr.Blocks() as demo:
1107
  .then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info]) \
1108
  .then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
1109
 
1110
- # Send-output buttons use the selected gallery item (or latest fallback)
1111
  send_out_to_base_btn.click(
1112
  fn=send_output_to_base,
1113
  inputs=[selected_output_state, output_gallery],
@@ -1121,8 +816,6 @@ with gr.Blocks() as demo:
1121
  ).then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
1122
 
1123
  # ── Bulk tab wiring ──────────────────────────────────────────────────────
1124
- # Reuses Generate-tab settings as inputs verbatim — single source of truth,
1125
- # no two-way state sync to keep in step.
1126
  bulk_event = bulk_run_btn.click(
1127
  fn=bulk_infer,
1128
  inputs=[bulk_files,
@@ -1133,26 +826,19 @@ with gr.Blocks() as demo:
1133
  outputs=[bulk_gallery, bulk_status, bulk_zip],
1134
  )
1135
 
1136
- # Stop cancels both running generators; the in-flight GPU call still
1137
- # finishes (ZeroGPU can't be killed mid-step), but no further iterations
1138
- # start. Any already-saved outputs remain in the gallery and on disk.
1139
  bulk_stop_btn.click(fn=lambda: gr.Info("Stop requested — finishing current image."),
1140
  cancels=[bulk_event, run_event])
1141
 
1142
  # ── Depth / Pose tab wiring ──────────────────────────────────────────────
1143
 
1144
- # Cache the last successfully-loaded source so re-renders after edits
1145
- # don't need the user to keep the upload widget populated.
1146
  ctrl_source.change(
1147
  fn=lambda img: img, inputs=[ctrl_source], outputs=[pose_source_state],
1148
  )
1149
 
1150
- # Depth generation
1151
  detect_depth_btn.click(
1152
  fn=generate_depthmap, inputs=[ctrl_source], outputs=[depth_output],
1153
  )
1154
 
1155
- # Pose detection → fills state, dropdowns, both preview images.
1156
  def _on_detect_pose(source):
1157
  if source is None:
1158
  raise gr.Error("Upload a source image first.")
@@ -1175,13 +861,12 @@ with gr.Blocks() as demo:
1175
  outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
1176
  pose_overlay, pose_clean],
1177
  )
1178
- reset_pose_btn.click( # same handler — re-runs detection
1179
  fn=_on_detect_pose, inputs=[ctrl_source],
1180
  outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
1181
  pose_overlay, pose_clean],
1182
  )
1183
 
1184
- # Insert a default standing-figure template centred in the source canvas.
1185
  def _on_insert_blank(source):
1186
  if source is None:
1187
  raise gr.Error("Upload a source image first.")
@@ -1201,9 +886,6 @@ with gr.Blocks() as demo:
1201
  pose_overlay, pose_clean],
1202
  )
1203
 
1204
- # ── Click-to-edit: the heart of the pose editor ──
1205
- # gr.SelectData on a gr.Image gives .index = (x, y) in image pixels, even
1206
- # when interactive=False, which is exactly what we need.
1207
  def _on_overlay_click(evt: gr.SelectData, poses, source, person_label, joint_name):
1208
  if not poses or source is None or evt is None or evt.index is None:
1209
  return gr.update(), gr.update(), gr.update()
@@ -1227,8 +909,6 @@ with gr.Blocks() as demo:
1227
  outputs=[pose_keypoints_state, pose_overlay, pose_clean],
1228
  )
1229
 
1230
- # Changing the active joint or person just re-renders the overlay so the
1231
- # highlight ring follows — keypoints are not mutated.
1232
  def _on_active_change(poses, source, person_label, joint_name):
1233
  if not poses or source is None:
1234
  return gr.update()
@@ -1249,7 +929,6 @@ with gr.Blocks() as demo:
1249
  outputs=[pose_overlay],
1250
  )
1251
 
1252
- # Hide / clear
1253
  def _on_hide_active(poses, source, person_label, joint_name):
1254
  person_idx = parse_person_idx(person_label)
1255
  joint_idx = joint_name_to_index(joint_name)
@@ -1283,8 +962,6 @@ with gr.Blocks() as demo:
1283
  outputs=[pose_keypoints_state, pose_overlay, pose_clean],
1284
  )
1285
 
1286
- # Send → main tab. We reuse the existing base_image / reference_images
1287
- # components so users land back on the Generate tab fully wired up.
1288
  send_depth_ref_btn.click(
1289
  fn=push_pil_to_reference, inputs=[depth_output, reference_images],
1290
  outputs=[reference_images],
@@ -1307,7 +984,8 @@ with gr.Blocks() as demo:
1307
  ).then(fn=on_base_image_change, inputs=[base_image], outputs=[size_info]
1308
  ).then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
1309
 
 
1310
  if __name__ == "__main__":
1311
- # Gradio 6.0: theme and css go here, not on Blocks()
1312
  demo.queue().launch(css=css, theme=orange_red_theme,
1313
  mcp_server=True, ssr_mode=False, show_error=True)
 
6
  import uuid
7
  import zipfile
8
  import threading
 
9
 
10
  import gradio as gr
 
11
  import spaces
12
  import torch
13
  from PIL import Image
14
 
15
+ # ── Local modules — single source of truth for each concern ─────────────────
16
+ from config import (
17
+ MODEL_VARIANT,
18
+ MODEL_REPO,
19
+ MAX_SEED,
20
+ MAX_LORA_SLOTS,
21
+ ENABLE_LOGGING,
22
+ )
23
+ from ui_theme import orange_red_theme
24
+ from upscale import UPSCALE_MODELS, apply_realesrgan
25
+ from lora_registry import (
26
+ LORA_STYLES,
27
+ LOADED_ADAPTERS,
28
+ get_selectable_styles,
29
+ get_style_by_title,
30
+ update_weight_sliders,
31
+ add_custom_lora,
32
+ )
33
  from logging_utils import log_inference
 
34
  from image_utils import (
35
  fix_orientation,
 
36
  compute_canvas_dimensions,
37
  fit_to_canvas,
38
  on_base_image_change,
 
49
  push_pil_to_base,
50
  push_pil_to_reference,
51
  )
 
52
  from control_tools import (
53
  generate_depthmap,
54
  detect_pose,
 
64
  OPENPOSE_KEYPOINT_NAMES,
65
  )
66
 
67
+ # ── Model load ──────────────────────────────────────────────────────────────
68
+ # Pipeline class depends on MODEL_VARIANT and is the only thing here that
69
+ # can't live in config.py (config must stay torch/diffusers-free).
70
  if MODEL_VARIANT == "9B-KV":
71
  from diffusers import Flux2KleinKVPipeline as _PipeClass
 
72
  else:
73
  from diffusers import Flux2KleinPipeline as _PipeClass
 
 
74
 
75
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
76
 
77
+ print(f"Loading FLUX.2 Klein {MODEL_VARIANT} from {MODEL_REPO}...")
78
+ pipe = _PipeClass.from_pretrained(MODEL_REPO, torch_dtype=torch.bfloat16).to(device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  print(f"Model loaded successfully: FLUX.2 Klein {MODEL_VARIANT}")
80
 
81
 
82
  # ── UI helper callbacks ──────────────────────────────────────────────────────
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  def on_canvas_mode_change(mode):
85
  """Custom W/H sliders only relevant when mode == Custom."""
86
  is_custom = (mode == "Custom")
 
109
  return item[0] if isinstance(item, (list, tuple)) else item
110
 
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  # ── Logging ──────────────────────────────────────────────────────────────────
113
 
114
  def _spawn_log(pil_images, result_image, prompt, seed, steps, guidance_scale,
115
  width, height, duration, success, error="",
116
  lora_titles=None, lora_weights=None, upscale_factor="None",
117
  lora_prompt_text=""):
118
+ """Fire-and-forget logger. ENABLE_LOGGING is read once at import time in
119
+ config.py — there's no per-call env lookup. Used by single, batch and
120
+ bulk inference paths."""
121
+ if not ENABLE_LOGGING:
122
  return
123
  threading.Thread(
124
  target=log_inference,
 
192
  if upscale_factor and upscale_factor != "None":
193
  gc.collect(); torch.cuda.synchronize(); torch.cuda.empty_cache()
194
  try:
195
+ image = apply_realesrgan(image, upscale_factor, device)
196
  except Exception as e:
197
  gr.Warning(f"Upscaling failed, returning {width}×{height} result: {e}")
198
 
 
245
  selected_titles = selected_titles or []
246
  batch_count = max(1, int(batch_count))
247
 
 
 
248
  base_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
249
  seeds, weight_overrides = [], []
250
  for i in range(batch_count):
 
323
  into the bulk output gallery as soon as they complete. Outputs and a CSV
324
  manifest are written to /tmp/bulk_<sid>/ with stable filenames, so a
325
  ZeroGPU quota wall mid-run still leaves earlier outputs grabbable from
326
+ the gallery and from disk.
327
+
328
+ Logging: each image is logged individually via _spawn_log on success or
329
+ failure — same code path as single/batch, so bulk shows up in the dataset
330
+ with the same schema, just with extra bulk_index / bulk_total fields
331
+ embedded in the PNG metadata."""
332
  if not input_files:
333
  raise gr.Error("Upload at least one image first.")
334
 
 
365
  *slider_values, progress=progress,
366
  )
367
 
 
 
368
  stem = os.path.splitext(fname)[0]
369
  out_path = os.path.join(work_dir, f"{i:03d}_{stem}.png")
370
  meta = _meta_for(prompt, used_seed, steps, guidance_scale, w, h, upscale_factor,
 
386
  upscale_factor=upscale_factor, lora_prompt_text=lora_prompt_text or "")
387
 
388
  status = f"✅ {succeeded}/{total} done ({failed} failed)"
389
+ yield results, status, gr.update()
390
  except Exception as e:
391
  failed += 1
392
  duration = time.perf_counter() - t0
393
  with open(manifest_path, "a", newline="") as f:
394
  csv.writer(f).writerow([i, fname, "", "", "", "", False, str(e)[:300], f"{duration:.2f}"])
395
+ _spawn_log([], None, prompt, 0, steps, guidance_scale,
396
+ 0, 0, duration, False, str(e),
397
+ lora_titles=log_titles, lora_weights=log_weights,
398
+ upscale_factor=upscale_factor, lora_prompt_text=lora_prompt_text or "")
399
  yield results, f"⚠️ Image {i+1} failed: {e} | {succeeded} ok, {failed} failed", gr.update()
400
  continue
401
 
 
402
  zip_path = os.path.join(work_dir, "outputs.zip")
403
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as zf:
404
  for p in results:
 
408
  yield results, f"🎉 Done — {succeeded}/{total} succeeded, {failed} failed", zip_path
409
 
410
 
411
+ # ── Custom prompt manager (session-local) ────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
  def add_custom_prompt(name, text, prompts_state, counter_state):
414
  prompts = dict(prompts_state); counter = int(counter_state)
 
426
  gr.update(choices=choices), gr.update(choices=choices),
427
  gr.update(value="", interactive=True))
428
 
429
+
430
  def delete_custom_prompt(name, currently_selected, prompts_state):
431
  prompts = dict(prompts_state)
432
  msg = f"🗑️ Deleted: '{name}'" if name and name in prompts else "Nothing to delete."
 
438
  gr.update(choices=choices, value=new_sel),
439
  gr.update(choices=choices, value=None))
440
 
441
+
442
  def update_custom_prompt_display(selected_names, prompts_state):
443
  if not selected_names:
444
  return gr.update(value="", visible=False)
 
462
  custom_prompts_state = gr.State({})
463
  custom_prompt_counter_state = gr.State(0)
464
  dynamic_loras_state = gr.State({})
465
+ selected_output_state = gr.State(None)
466
 
467
  with gr.Column(elem_id="col-container"):
468
+ _logging_badge = "🟢 On" if ENABLE_LOGGING else "🔴 Off"
 
469
 
470
  gr.Markdown("# **FLUX.2-Klein-LoRA-Studio**", elem_id="main-title")
471
  gr.Markdown(
472
  f"Apply one or more [LoRA](https://huggingface.co/models?other=base_model:adapter:black-forest-labs/FLUX.2-klein-9B) "
473
+ f"adapters using [FLUX.2-Klein-{MODEL_VARIANT}]({MODEL_REPO}). "
474
  f"**Model:** `{MODEL_VARIANT}` · **Logging:** {_logging_badge}"
475
  )
476
 
 
546
  sweep_max = gr.Slider(label="Sweep max weight", minimum=0.0, maximum=2.0,
547
  step=0.05, value=1.4, visible=False)
548
 
 
549
  with gr.Column(scale=1):
 
 
 
550
  output_gallery = gr.Gallery(
551
  label="Output", type="filepath", columns=2, rows=2,
552
  height=420, allow_preview=True, preview=True,
 
564
  "Civitai / ComfyUI readable).*"
565
  )
566
 
 
567
  gr.Markdown("### 🎨 Select LoRA(s)")
568
  lora_selector = gr.CheckboxGroup(
569
  choices=[s["title"] for s in get_selectable_styles({})],
 
673
  "Reference image."
674
  )
675
 
676
+ pose_source_state = gr.State(None)
677
+ pose_keypoints_state = gr.State([])
 
 
678
 
679
  with gr.Row():
680
  with gr.Column(scale=1):
 
704
 
705
  with gr.Row():
706
  with gr.Column(scale=1):
 
 
707
  pose_overlay = gr.Image(
708
  label="Editor — click to place active joint",
709
  type="pil", interactive=False, height=420, format="png",
 
733
  send_pose_ref_btn = gr.Button("→ Send pose to Reference",
734
  variant="primary")
735
  send_pose_base_btn = gr.Button("→ Send pose to Base")
736
+
737
  # ── Event wiring ─────────────────────────────────────────────────────────
738
 
 
739
  base_image.upload(fn=reencode_upload, inputs=[base_image], outputs=[base_image])
 
740
  base_image.change(fn=on_base_image_change, inputs=[base_image], outputs=[size_info])
741
  reference_images.change(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
742
 
743
+ # update_weight_sliders is the one imported from lora_registry now.
744
  lora_selector.change(
745
  fn=update_weight_sliders,
746
  inputs=[lora_selector, dynamic_loras_state],
747
  outputs=weight_sliders + [lora_prompt_display],
748
  )
749
 
750
+ # add_custom_lora is also imported from lora_registry.
751
  add_lora_btn.click(
752
  fn=add_custom_lora,
753
  inputs=[lora_repo_id, lora_weight_name, lora_adapter_name, dynamic_loras_state],
 
771
  outputs=[custom_prompt_display],
772
  )
773
 
 
774
  canvas_mode.change(fn=on_canvas_mode_change, inputs=[canvas_mode],
775
  outputs=[custom_width, custom_height])
776
  canvas_fit_mode.change(fn=on_fit_mode_change, inputs=[canvas_fit_mode],
 
778
  batch_vary.change(fn=on_batch_vary_change, inputs=[batch_vary],
779
  outputs=[sweep_min, sweep_max])
780
 
 
781
  output_gallery.select(fn=on_gallery_select, inputs=[output_gallery],
782
  outputs=[selected_output_state])
783
 
 
 
784
  run_event = run_button.click(
785
  fn=infer,
786
  inputs=[base_image, reference_images, prompt, lora_prompt_display, custom_prompt_display,
 
792
  )
793
 
794
  # ── Editor tab wiring ────────────────────────────────────────────────────
 
 
795
  heic_uploader.upload(fn=load_heic_to_editor, inputs=[heic_uploader], outputs=[editor])
796
 
797
  send_to_base_btn.click(fn=send_editor_to_base, inputs=[editor], outputs=[base_image]) \
 
803
  .then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info]) \
804
  .then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
805
 
 
806
  send_out_to_base_btn.click(
807
  fn=send_output_to_base,
808
  inputs=[selected_output_state, output_gallery],
 
816
  ).then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
817
 
818
  # ── Bulk tab wiring ──────────────────────────────────────────────────────
 
 
819
  bulk_event = bulk_run_btn.click(
820
  fn=bulk_infer,
821
  inputs=[bulk_files,
 
826
  outputs=[bulk_gallery, bulk_status, bulk_zip],
827
  )
828
 
 
 
 
829
  bulk_stop_btn.click(fn=lambda: gr.Info("Stop requested — finishing current image."),
830
  cancels=[bulk_event, run_event])
831
 
832
  # ── Depth / Pose tab wiring ──────────────────────────────────────────────
833
 
 
 
834
  ctrl_source.change(
835
  fn=lambda img: img, inputs=[ctrl_source], outputs=[pose_source_state],
836
  )
837
 
 
838
  detect_depth_btn.click(
839
  fn=generate_depthmap, inputs=[ctrl_source], outputs=[depth_output],
840
  )
841
 
 
842
  def _on_detect_pose(source):
843
  if source is None:
844
  raise gr.Error("Upload a source image first.")
 
861
  outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
862
  pose_overlay, pose_clean],
863
  )
864
+ reset_pose_btn.click(
865
  fn=_on_detect_pose, inputs=[ctrl_source],
866
  outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
867
  pose_overlay, pose_clean],
868
  )
869
 
 
870
  def _on_insert_blank(source):
871
  if source is None:
872
  raise gr.Error("Upload a source image first.")
 
886
  pose_overlay, pose_clean],
887
  )
888
 
 
 
 
889
  def _on_overlay_click(evt: gr.SelectData, poses, source, person_label, joint_name):
890
  if not poses or source is None or evt is None or evt.index is None:
891
  return gr.update(), gr.update(), gr.update()
 
909
  outputs=[pose_keypoints_state, pose_overlay, pose_clean],
910
  )
911
 
 
 
912
  def _on_active_change(poses, source, person_label, joint_name):
913
  if not poses or source is None:
914
  return gr.update()
 
929
  outputs=[pose_overlay],
930
  )
931
 
 
932
  def _on_hide_active(poses, source, person_label, joint_name):
933
  person_idx = parse_person_idx(person_label)
934
  joint_idx = joint_name_to_index(joint_name)
 
962
  outputs=[pose_keypoints_state, pose_overlay, pose_clean],
963
  )
964
 
 
 
965
  send_depth_ref_btn.click(
966
  fn=push_pil_to_reference, inputs=[depth_output, reference_images],
967
  outputs=[reference_images],
 
984
  ).then(fn=on_base_image_change, inputs=[base_image], outputs=[size_info]
985
  ).then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
986
 
987
+
988
  if __name__ == "__main__":
989
+ # Gradio 6.0: theme and css go on launch(), not Blocks()
990
  demo.queue().launch(css=css, theme=orange_red_theme,
991
  mcp_server=True, ssr_mode=False, show_error=True)
lora_registry.py CHANGED
@@ -33,8 +33,7 @@ FROM PICTURE 2 (strictly preserve identity):
33
  - Skin: texture, tone, complexion
34
  The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
35
 
36
- # MAX number of simultaneous LoRA weight sliders to pre-render in the UI
37
- MAX_LORA_SLOTS = 6
38
 
39
  LORA_STYLES = [
40
  {
@@ -85,7 +84,7 @@ LORA_STYLES = [
85
  "weights": "Flux2-Klein-Image-RestoreV1.safetensors",
86
  "default_prompt": "restore the image quality, remove any compression artefacts, remove any haze and soft edges, enrich the original with new intricate detail in all textures and surfaces creating a professional photorealistic photograph with natural lighting and skin texture.",
87
  "default_weight": 1.0,
88
- },
89
  {
90
  "title": "High Resolution",
91
  "adapter_name": "High Resolution",
@@ -93,7 +92,7 @@ LORA_STYLES = [
93
  "weights": "HighResolution9B.safetensors",
94
  "default_prompt": "High Resolution",
95
  "default_weight": 1.0,
96
- },
97
  {
98
  "title": "InstaPic",
99
  "adapter_name": "InstaPic V3",
@@ -101,7 +100,7 @@ LORA_STYLES = [
101
  "weights": "InstaPic V3.safetensors",
102
  "default_prompt": "instapic",
103
  "default_weight": 1.0,
104
- },
105
  {
106
  "title": "Realistic Nudes",
107
  "adapter_name": "Realistic Nudes",
@@ -109,7 +108,7 @@ LORA_STYLES = [
109
  "weights": "realistic_nudes_klein_v3.safetensors",
110
  "default_prompt": None,
111
  "default_weight": 1.0,
112
- },
113
  {
114
  "title": "Perky Pointy Puffy Breasts",
115
  "adapter_name": "Perky Pointy Puffy Breasts",
@@ -117,7 +116,7 @@ LORA_STYLES = [
117
  "weights": "PerkyPointyPuffy_v1.1_small_pointy_breasts_large_puffy_nipples.safetensors",
118
  "default_prompt": "Small pointy breasts with large puffy nipples",
119
  "default_weight": 1.0,
120
- },
121
  {
122
  "title": "Flat Chested",
123
  "adapter_name": "Flat Chested",
@@ -125,7 +124,7 @@ LORA_STYLES = [
125
  "weights": "Flux2-Klein-9b-FlatChested-v1.safetensors",
126
  "default_prompt": "flat chested",
127
  "default_weight": 1.5,
128
- },
129
  {
130
  "title": "Controllight",
131
  "adapter_name": "Controllight",
@@ -133,7 +132,7 @@ LORA_STYLES = [
133
  "weights": "controllight.safetensors",
134
  "default_prompt": None,
135
  "default_weight": 1.0,
136
- },
137
  {
138
  "title": "RefControl - Depth",
139
  "adapter_name": "RefConDep",
@@ -141,7 +140,7 @@ LORA_STYLES = [
141
  "weights": "flux2_klein_9b_refcontrol_depth.safetensors",
142
  "default_prompt": "refcontrol",
143
  "default_weight": 1.0,
144
- },
145
  {
146
  "title": "RefControl - Pose",
147
  "adapter_name": "RefConPos",
@@ -149,11 +148,10 @@ LORA_STYLES = [
149
  "weights": "refcontrol_v2_poses.safetensors",
150
  "default_prompt": "apply pose from image 1 with reference from image 2",
151
  "default_weight": 1.0,
152
- },
153
  ]
154
 
155
 
156
-
157
  # LOADED_ADAPTERS is the only piece of LoRA state that's legitimately global:
158
  # it just tracks which adapter names have been loaded onto the shared `pipe`
159
  # at least once, so we don't re-download/re-attach weights on every call.
@@ -218,7 +216,6 @@ def update_weight_sliders(selected_titles, dynamic_loras):
218
  else:
219
  slider_updates.append(gr.update(visible=False, value=1.0))
220
 
221
- # Collect default prompts from ALL selected LoRAs (not just one)
222
  lora_prompts = [s.get("default_prompt") for s in selected_styles if s.get("default_prompt")]
223
  if lora_prompts:
224
  combined = "\n\n".join(lora_prompts)
 
33
  - Skin: texture, tone, complexion
34
  The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
35
 
36
+ # NOTE: MAX_LORA_SLOTS is imported from config.py do NOT re-declare it here.
 
37
 
38
  LORA_STYLES = [
39
  {
 
84
  "weights": "Flux2-Klein-Image-RestoreV1.safetensors",
85
  "default_prompt": "restore the image quality, remove any compression artefacts, remove any haze and soft edges, enrich the original with new intricate detail in all textures and surfaces creating a professional photorealistic photograph with natural lighting and skin texture.",
86
  "default_weight": 1.0,
87
+ },
88
  {
89
  "title": "High Resolution",
90
  "adapter_name": "High Resolution",
 
92
  "weights": "HighResolution9B.safetensors",
93
  "default_prompt": "High Resolution",
94
  "default_weight": 1.0,
95
+ },
96
  {
97
  "title": "InstaPic",
98
  "adapter_name": "InstaPic V3",
 
100
  "weights": "InstaPic V3.safetensors",
101
  "default_prompt": "instapic",
102
  "default_weight": 1.0,
103
+ },
104
  {
105
  "title": "Realistic Nudes",
106
  "adapter_name": "Realistic Nudes",
 
108
  "weights": "realistic_nudes_klein_v3.safetensors",
109
  "default_prompt": None,
110
  "default_weight": 1.0,
111
+ },
112
  {
113
  "title": "Perky Pointy Puffy Breasts",
114
  "adapter_name": "Perky Pointy Puffy Breasts",
 
116
  "weights": "PerkyPointyPuffy_v1.1_small_pointy_breasts_large_puffy_nipples.safetensors",
117
  "default_prompt": "Small pointy breasts with large puffy nipples",
118
  "default_weight": 1.0,
119
+ },
120
  {
121
  "title": "Flat Chested",
122
  "adapter_name": "Flat Chested",
 
124
  "weights": "Flux2-Klein-9b-FlatChested-v1.safetensors",
125
  "default_prompt": "flat chested",
126
  "default_weight": 1.5,
127
+ },
128
  {
129
  "title": "Controllight",
130
  "adapter_name": "Controllight",
 
132
  "weights": "controllight.safetensors",
133
  "default_prompt": None,
134
  "default_weight": 1.0,
135
+ },
136
  {
137
  "title": "RefControl - Depth",
138
  "adapter_name": "RefConDep",
 
140
  "weights": "flux2_klein_9b_refcontrol_depth.safetensors",
141
  "default_prompt": "refcontrol",
142
  "default_weight": 1.0,
143
+ },
144
  {
145
  "title": "RefControl - Pose",
146
  "adapter_name": "RefConPos",
 
148
  "weights": "refcontrol_v2_poses.safetensors",
149
  "default_prompt": "apply pose from image 1 with reference from image 2",
150
  "default_weight": 1.0,
151
+ },
152
  ]
153
 
154
 
 
155
  # LOADED_ADAPTERS is the only piece of LoRA state that's legitimately global:
156
  # it just tracks which adapter names have been loaded onto the shared `pipe`
157
  # at least once, so we don't re-download/re-attach weights on every call.
 
216
  else:
217
  slider_updates.append(gr.update(visible=False, value=1.0))
218
 
 
219
  lora_prompts = [s.get("default_prompt") for s in selected_styles if s.get("default_prompt")]
220
  if lora_prompts:
221
  combined = "\n\n".join(lora_prompts)