Lisandro commited on
Commit
be27524
·
1 Parent(s): 748097d

Refactor run_lora_multi for multi-LoRA support, MOCK mode, and UI improvements

Browse files
Files changed (3) hide show
  1. app copy 2.py +866 -0
  2. app.py +269 -113
  3. demo.py +173 -0
app copy 2.py ADDED
@@ -0,0 +1,866 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import json
4
+ import logging
5
+ import torch
6
+ from PIL import Image
7
+ import spaces
8
+ from diffusers import DiffusionPipeline, FlowMatchEulerDiscreteScheduler
9
+ from huggingface_hub import hf_hub_download, HfFileSystem, ModelCard, snapshot_download
10
+ import copy
11
+ import random
12
+ import time
13
+ import re
14
+ import math
15
+ import numpy as np
16
+ import traceback
17
+ from prompt_rewrite import rewrite
18
+ import hashlib
19
+
20
+
21
+ def apply_aspect_ratio(ratio):
22
+ sizes = {
23
+ "1:1": (1024, 1024),
24
+ "16:9": (1365, 768),
25
+ "9:16": (768, 1365),
26
+ "3:2": (1254, 836),
27
+ "2:3": (836, 1254),
28
+ "3:1": (1774, 591),
29
+ "2:1": (1448, 724),
30
+ }
31
+ return sizes.get(ratio, (1024, 1024))
32
+
33
+ DEFAULT_ASPECT_RATIO = "16:9"
34
+
35
+ # ✅ NUEVO: importar optimización avanzada tipo Qwen-Image-MultipleAngles
36
+ #from optimization import optimize_pipeline_
37
+
38
+ LORAS_CACHE = {
39
+ "data": [],
40
+ "last_hash": None,
41
+ }
42
+ def load_loras_hot():
43
+ """Load loras.json and detect changes."""
44
+ path = hf_hub_download(
45
+ repo_id="lichorosario/qwen-image-lora-dlc-v3",
46
+ filename="loras.json",
47
+ repo_type="space",
48
+ )
49
+
50
+ with open(path, "r", encoding="utf-8") as f:
51
+ raw = f.read()
52
+
53
+ current_hash = hashlib.sha256(raw.encode("utf-8")).hexdigest()
54
+
55
+ if current_hash != LORAS_CACHE["last_hash"]:
56
+ LORAS_CACHE["data"] = json.loads(raw)
57
+ LORAS_CACHE["last_hash"] = current_hash
58
+ print("🔁 LoRA config updated")
59
+
60
+ return LORAS_CACHE["data"]
61
+
62
+
63
+ # Load LoRAs from JSON file
64
+ def load_loras_from_file():
65
+ """Load LoRA configurations from external JSON file."""
66
+ try:
67
+ with open('loras.json', 'r', encoding='utf-8') as f:
68
+ return json.load(f)
69
+ except FileNotFoundError:
70
+ print("Warning: loras.json file not found. Using empty list.")
71
+ return []
72
+ except json.JSONDecodeError as e:
73
+ print(f"Error parsing loras.json: {e}")
74
+ return []
75
+
76
+
77
+ # Load the LoRAs
78
+ #loras = load_loras_from_file()
79
+ loras = load_loras_hot()
80
+
81
+ saved_loras = []
82
+
83
+ # Initialize the base model
84
+ dtype = torch.bfloat16
85
+ device = "cuda" if torch.cuda.is_available() else "cpu"
86
+ base_model = "Qwen/Qwen-Image-2512"
87
+
88
+
89
+ # Scheduler configuration from the Qwen-Image-Lightning repository
90
+ scheduler_config = {
91
+ "base_image_seq_len": 256,
92
+ "base_shift": math.log(3),
93
+ "invert_sigmas": False,
94
+ "max_image_seq_len": 8192,
95
+ "max_shift": math.log(3),
96
+ "num_train_timesteps": 1000,
97
+ "shift": 1.0,
98
+ "shift_terminal": None,
99
+ "stochastic_sampling": False,
100
+ "time_shift_type": "exponential",
101
+ "use_beta_sigmas": False,
102
+ "use_dynamic_shifting": True,
103
+ "use_exponential_sigmas": False,
104
+ "use_karras_sigmas": False,
105
+ }
106
+
107
+
108
+ scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
109
+
110
+ pipe = DiffusionPipeline.from_pretrained(
111
+ "Qwen/Qwen-Image-2512", scheduler=scheduler, torch_dtype=dtype
112
+ ).to(device)
113
+
114
+
115
+
116
+ """
117
+ # ✅ NUEVO BLOQUE: aplicar AOT optimization (igual que Qwen-Image-MultipleAngles)
118
+ try:
119
+ example_args = (
120
+ "a cute cat in a spacesuit",
121
+ )
122
+ example_kwargs = dict(
123
+ num_inference_steps=4,
124
+ true_cfg_scale=3.5,
125
+ width=1024,
126
+ height=1024,
127
+ num_images_per_prompt=1,
128
+ )
129
+ optimize_pipeline_(pipe, *example_args, **example_kwargs)
130
+ print("✅ Transformer AOT optimization complete.")
131
+ except Exception as e:
132
+ print(f"⚠️ AOT optimization skipped: {e}")
133
+ """
134
+
135
+
136
+ # Lightning LoRA info (no global state)
137
+ LIGHTNING_LORA_REPO = "lightx2v/Qwen-Image-2512-Lightning"
138
+ LIGHTNING_LORA_WEIGHT = "Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors"
139
+ LIGHTNING8_LORA_WEIGHT = "Qwen-Image-2512-Lightning-8steps-V1.0-fp32.safetensors"
140
+ LIGHTNING_FP8_4STEPS_LORA_WEIGHT = "Qwen-Image-fp8-e4m3fn-Lightning-4steps-V1.0-bf16.safetensors"
141
+
142
+ #LIGHTNING_LORA_REPO = "Wuli-art/Qwen-Image-2512-Turbo-LoRA"
143
+ #LIGHTNING_LORA_WEIGHT = "Wuli-Qwen-Image-2512-Turbo-LoRA-4steps-V1.0-bf16.safetensors"
144
+ #LIGHTNING8_LORA_WEIGHT = "Wuli-Qwen-Image-2512-Turbo-LoRA-4steps-V1.0-bf16.safetensors"
145
+
146
+ MAX_SEED = np.iinfo(np.int32).max
147
+
148
+
149
+ ### MODIFICACIÓN 1: AÑADIR FUNCIONES PARA GESTIONAR EL HISTORIAL ###
150
+ def update_history(new_images, history):
151
+ """Añade las nuevas imágenes generadas al principio de la lista del historial."""
152
+ if history is None:
153
+ history = []
154
+ if new_images is not None and len(new_images) > 0:
155
+ updated_history = new_images + history
156
+ return updated_history[:24]
157
+ return history
158
+
159
+
160
+ def clear_history():
161
+ """Devuelve una lista vacía para limpiar la galería de historial."""
162
+ return []
163
+ ### FIN DE LA MODIFICACIÓN 1 ###
164
+
165
+
166
+
167
+
168
+ class calculateDuration:
169
+ def __init__(self, activity_name=""):
170
+ self.activity_name = activity_name
171
+
172
+
173
+ def __enter__(self):
174
+ self.start_time = time.time()
175
+ return self
176
+
177
+ def __exit__(self, exc_type, exc_value, traceback):
178
+ self.end_time = time.time()
179
+ self.elapsed_time = self.end_time - self.start_time
180
+ if self.activity_name:
181
+ print(f"Elapsed time for {self.activity_name}: {self.elapsed_time:.6f} seconds")
182
+ else:
183
+ print(f"Elapsed time: {self.elapsed_time:.6f} seconds")
184
+
185
+
186
+
187
+
188
+ def update_selection(evt: gr.SelectData, width, height):
189
+ selected_lora = loras[evt.index]
190
+ new_placeholder = f"Type a prompt for {selected_lora['title']}"
191
+ lora_repo = selected_lora["repo"]
192
+ updated_text = f"### Selected: [{lora_repo}](https://huggingface.co/{lora_repo}) ✨"
193
+
194
+ examples_list = []
195
+ try:
196
+ model_card = ModelCard.load(lora_repo)
197
+ widget_data = model_card.data.get("widget", [])
198
+ if widget_data and len(widget_data) > 0:
199
+ for example in widget_data[:4]:
200
+ if "output" in example and "url" in example["output"]:
201
+ image_url = f"https://huggingface.co/{lora_repo}/resolve/main/{example['output']['url']}"
202
+ prompt_text = example.get("text", "")
203
+ examples_list.append([prompt_text])
204
+ except Exception as e:
205
+ print(f"Could not load model card for {lora_repo}: {e}")
206
+
207
+ return (
208
+ gr.update(placeholder=new_placeholder),
209
+ updated_text,
210
+ evt.index,
211
+ width,
212
+ height,
213
+ gr.update(interactive=True)
214
+ )
215
+
216
+
217
+ def handle_speed_mode(speed_mode):
218
+ """Update UI based on speed/quality toggle."""
219
+ if speed_mode == "light 4":
220
+ return gr.update(value="Light mode (4 steps) selected"), 4, 1.0
221
+ elif speed_mode == "light 4 fp8":
222
+ return gr.update(value="Light mode (4 steps fp8) selected"), 4, 1.0
223
+ elif speed_mode == "light 8":
224
+ return gr.update(value="Light mode (8 steps) selected"), 8, 1.0
225
+ elif speed_mode == "Wuli-art":
226
+ return gr.update(value="Light mode (4 steps) Wuli-art selected"), 4, 1.0
227
+ else:
228
+ return gr.update(value="Normal quality (45 steps) selected"), 45, 3.5
229
+
230
+
231
+ @spaces.GPU(duration=70)
232
+ def generate_image(
233
+ prompt_mash,
234
+ steps,
235
+ seed,
236
+ cfg_scale,
237
+ width,
238
+ height,
239
+ lora_scale,
240
+ negative_prompt="",
241
+ num_images=1,
242
+ prompt_enhance=False,
243
+ ):
244
+ pipe.to("cuda")
245
+ # if negative_prompt == '':
246
+ # negative_prompt = "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。"
247
+
248
+
249
+ if prompt_enhance:
250
+ with calculateDuration("Enjancing prompt"):
251
+ print(f"Calling pipeline with prompt: '{prompt_mash}'")
252
+ prompt_mash = rewrite(prompt_mash)
253
+
254
+ seeds = [seed + (i * 100) for i in range(num_images)]
255
+ generators = [torch.Generator(device="cuda").manual_seed(s) for s in seeds]
256
+
257
+ images = []
258
+
259
+ with calculateDuration("Generating images (sequential)"):
260
+ for i in range(num_images):
261
+ current_seed = seed + (i * 100)
262
+ generator = torch.Generator(device="cuda").manual_seed(current_seed)
263
+
264
+ result = pipe(
265
+ prompt=prompt_mash,
266
+ negative_prompt=negative_prompt,
267
+ num_inference_steps=steps,
268
+ true_cfg_scale=cfg_scale,
269
+ width=width,
270
+ height=height,
271
+ num_images_per_prompt=1,
272
+ generator=generator,
273
+ )
274
+
275
+ images.append((result.images[0], current_seed))
276
+ return images
277
+
278
+
279
+
280
+ def generate_images_for_prompts(
281
+ prompts,
282
+ negative_prompt,
283
+ steps,
284
+ seed,
285
+ cfg_scale,
286
+ width,
287
+ height,
288
+ quantity, # ✅ FIX: ahora entra como parámetro
289
+ prompt_enhance=False,
290
+ ):
291
+ pipe.to("cuda")
292
+ # if negative_prompt == '':
293
+ # negative_prompt = "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲。"
294
+
295
+ images = []
296
+
297
+ for prompt in prompts:
298
+ current_seed = seed
299
+ if prompt_enhance:
300
+ prompt = rewrite(prompt)
301
+
302
+ # ✅ FIX: quantity ya no es el componente global; es un int real
303
+ for _ in range(int(quantity)):
304
+ generator = torch.Generator(device="cuda").manual_seed(current_seed)
305
+
306
+ result = pipe(
307
+ prompt=prompt,
308
+ negative_prompt=negative_prompt,
309
+ num_inference_steps=steps,
310
+ true_cfg_scale=cfg_scale,
311
+ width=width,
312
+ height=height,
313
+ num_images_per_prompt=1,
314
+ generator=generator,
315
+ )
316
+
317
+ images.append((result.images[0], current_seed))
318
+ current_seed += 100 # separación segura
319
+
320
+ return images
321
+
322
+
323
+
324
+ @spaces.GPU(duration=70)
325
+ def run_lora_multi(
326
+ prompt_1, prompt_2, prompt_3, prompt_4,
327
+ negative_prompt,
328
+ cfg_scale, steps,
329
+ selected_index,
330
+ randomize_seed, seed,
331
+ width, height,
332
+ lora_scale,
333
+ speed_mode,
334
+ quality_multiplier,
335
+ quantity, # se ignora acá (pero ahora lo usamos bien)
336
+ history,
337
+ prompt_enhance=False,
338
+ progress=gr.Progress(track_tqdm=True)
339
+ ):
340
+ if selected_index is None:
341
+ raise gr.Error("You must select a LoRA before proceeding.")
342
+
343
+ prompts = [
344
+ p.strip() for p in [prompt_1, prompt_2, prompt_3, prompt_4]
345
+ if p and p.strip()
346
+ ]
347
+
348
+ if not prompts:
349
+ raise gr.Error("You must fill at least one prompt.")
350
+
351
+ selected_lora = loras[selected_index]
352
+ lora_path = selected_lora["repo"]
353
+ trigger_word = selected_lora["trigger_word"]
354
+
355
+ # aplicar trigger word por prompt
356
+ final_prompts = []
357
+ for p in prompts:
358
+ if trigger_word:
359
+ if selected_lora.get("trigger_position") == "append":
360
+ final_prompts.append(f"{p} {trigger_word}")
361
+ else:
362
+ final_prompts.append(f"{trigger_word} {p}")
363
+ else:
364
+ final_prompts.append(p)
365
+
366
+ # limpiar LoRAs previas
367
+ pipe.unload_lora_weights()
368
+
369
+ # 🔥 CARGA DE LORAs (UNA SOLA VEZ)
370
+ if speed_mode == "light 4":
371
+ pipe.load_lora_weights(
372
+ LIGHTNING_LORA_REPO,
373
+ weight_name=LIGHTNING_LORA_WEIGHT,
374
+ adapter_name="lightning"
375
+ )
376
+ pipe.load_lora_weights(
377
+ lora_path,
378
+ weight_name=selected_lora.get("weights"),
379
+ adapter_name="style"
380
+ )
381
+ pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
382
+
383
+ elif speed_mode == "light 8":
384
+ pipe.load_lora_weights(
385
+ LIGHTNING_LORA_REPO,
386
+ weight_name=LIGHTNING8_LORA_WEIGHT,
387
+ adapter_name="lightning"
388
+ )
389
+ pipe.load_lora_weights(
390
+ lora_path,
391
+ weight_name=selected_lora.get("weights"),
392
+ adapter_name="style"
393
+ )
394
+ pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
395
+
396
+ else:
397
+ pipe.load_lora_weights(
398
+ lora_path,
399
+ weight_name=selected_lora.get("weights"),
400
+ adapter_name="style"
401
+ )
402
+ pipe.set_adapters(["style"], adapter_weights=[lora_scale])
403
+
404
+ if randomize_seed:
405
+ seed = random.randint(0, MAX_SEED)
406
+
407
+ multiplier = float(quality_multiplier.replace("x", ""))
408
+ width = int(width * multiplier)
409
+ height = int(height * multiplier)
410
+
411
+ # ✅ FIX: quantity viene como index 0..3 (por type="index"), convertimos a 1..4
412
+ real_quantity = int(quantity) + 1
413
+
414
+
415
+ if (history is None):
416
+ history = []
417
+ gallery_images = []
418
+
419
+ for prompt in prompts:
420
+ current_seed = seed
421
+ if prompt_enhance:
422
+ prompt = rewrite(prompt)
423
+
424
+ # ✅ FIX: quantity ya no es el componente global; es un int real
425
+ for _ in range(real_quantity):
426
+ generator = torch.Generator(device="cuda").manual_seed(current_seed)
427
+
428
+ result = pipe(
429
+ prompt=prompt,
430
+ negative_prompt=negative_prompt,
431
+ num_inference_steps=steps,
432
+ true_cfg_scale=cfg_scale,
433
+ width=width,
434
+ height=height,
435
+ num_images_per_prompt=1,
436
+ generator=generator,
437
+ )
438
+
439
+ img = result.images[0]
440
+ imgtuple = (img, str(current_seed))
441
+ # images.append(imgtuple)
442
+ gallery_images.append(imgtuple)
443
+
444
+ # history persistente (acumula)
445
+ history = [(img, str(current_seed))] + history
446
+ history = history[:24]
447
+
448
+ yield gallery_images, history, history, seed
449
+
450
+ current_seed += 100 # separación segura
451
+
452
+ #return images
453
+
454
+ #images = generate_images_for_prompts(
455
+ # prompts=final_prompts,
456
+ # negative_prompt=negative_prompt,
457
+ # steps=steps,
458
+ # seed=seed,
459
+ # cfg_scale=cfg_scale,
460
+ # width=width,
461
+ # height=height,
462
+ # quantity=real_quantity, # ✅ FIX: ahora se pasa
463
+ # prompt_enhance=prompt_enhance,
464
+ #)
465
+
466
+ #gallery_images = [(img, str(s)) for img, s in images]
467
+ #return gallery_images, seed
468
+
469
+
470
+
471
+ # ... (El resto de las funciones como get_huggingface_safetensors, check_custom_model, etc., permanecen sin cambios) ...
472
+ def get_huggingface_safetensors(link):
473
+ split_link = link.split("/")
474
+ if len(split_link) != 2:
475
+ raise Exception("Invalid Hugging Face repository link format.")
476
+ print(f"Repository attempted: {split_link}")
477
+ model_card = ModelCard.load(link)
478
+ base_model = model_card.data.get("base_model")
479
+ print(f"Base model: {base_model}")
480
+ acceptable_models = {
481
+ "Qwen/Qwen-Image",
482
+ "Qwen/Qwen-Image-2512",
483
+ }
484
+ models_to_check = base_model if isinstance(base_model, list) else [base_model]
485
+ if not any(model in acceptable_models for model in models_to_check):
486
+ raise Exception("Not a Qwen-Image LoRA!")
487
+ image_path = model_card.data.get("widget", [{}])[0].get("output", {}).get("url", None)
488
+ trigger_word = model_card.data.get("instance_prompt", "")
489
+ image_url = f"https://huggingface.co/{link}/resolve/main/{image_path}" if image_path else None
490
+ fs = HfFileSystem()
491
+ try:
492
+ list_of_files = fs.ls(link, detail=False)
493
+ safetensors_name = None
494
+ for file in list_of_files:
495
+ filename = file.split("/")[-1]
496
+ if filename.endswith(".safetensors"):
497
+ safetensors_name = filename
498
+ break
499
+ if not safetensors_name:
500
+ raise Exception("No valid *.safetensors file found in the repository.")
501
+ except Exception as e:
502
+ print(e)
503
+ raise Exception("You didn't include a valid Hugging Face repository with a *.safetensors LoRA")
504
+ return split_link[1], link, safetensors_name, trigger_word, image_url
505
+
506
+
507
+ def check_custom_model(link):
508
+ print(f"Checking a custom model on: {link}")
509
+ if link.endswith('.safetensors'):
510
+ if 'huggingface.co' in link:
511
+ parts = link.split('/')
512
+ try:
513
+ hf_index = parts.index('huggingface.co')
514
+ username = parts[hf_index + 1]
515
+ repo_name = parts[hf_index + 2]
516
+ repo = f"{username}/{repo_name}"
517
+ safetensors_name = parts[-1]
518
+ try:
519
+ model_card = ModelCard.load(repo)
520
+ trigger_word = model_card.data.get("instance_prompt", "")
521
+ image_path = model_card.data.get("widget", [{}])[0].get("output", {}).get("url", None)
522
+ image_url = f"https://huggingface.co/{repo}/resolve/main/{image_path}" if image_path else None
523
+ except:
524
+ trigger_word = ""
525
+ image_url = None
526
+ return repo_name, repo, safetensors_name, trigger_word, image_url
527
+ except:
528
+ raise Exception("Invalid safetensors URL format")
529
+ if link.startswith("https://"):
530
+ if link.startswith("https://huggingface.co") or link.startswith("https://www.huggingface.co"):
531
+ link_split = link.split("huggingface.co/")
532
+ return get_huggingface_safetensors(link_split[1])
533
+ else:
534
+ return get_huggingface_safetensors(link)
535
+
536
+
537
+ def add_custom_lora(custom_lora):
538
+ global loras
539
+ if custom_lora:
540
+ try:
541
+ title, repo, path, trigger_word, image = check_custom_model(custom_lora)
542
+ print(f"Loaded custom LoRA: {repo}")
543
+ model_card_examples = ""
544
+ try:
545
+ model_card = ModelCard.load(repo)
546
+ widget_data = model_card.data.get("widget", [])
547
+ if widget_data and len(widget_data) > 0:
548
+ examples_html = '<div style="margin-top: 10px;">'
549
+ examples_html += '<h4 style="margin-bottom: 8px; font-size: 0.9em;">Sample Images:</h4>'
550
+ examples_html += '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;">'
551
+ for i, example in enumerate(widget_data[:4]):
552
+ if "output" in example and "url" in example["output"]:
553
+ image_url = f"https://huggingface.co/{repo}/resolve/main/{example['output']['url']}"
554
+ caption = example.get("text", f"Example {i+1}")
555
+ examples_html += f'''
556
+ <div style="text-align: center;">
557
+ <img src="{image_url}" style="width: 100%; height: auto; border-radius: 4px;" />
558
+ <p style="font-size: 0.7em; margin: 2px 0;">{caption[:30]}{'...' if len(caption) > 30 else ''}</p>
559
+ </div>
560
+ '''
561
+ examples_html += '</div></div>'
562
+ model_card_examples = examples_html
563
+ except Exception as e:
564
+ print(f"Could not load model card examples for custom LoRA: {e}")
565
+ card = f'''
566
+ <div class="custom_lora_card">
567
+ <span>Loaded custom LoRA:</span>
568
+ <div class="card_internal">
569
+ <img src="{image}" />
570
+ <div>
571
+ <h3>{title}</h3>
572
+ <small>{"Using: <code><b>"+trigger_word+"</code></b> as the trigger word" if trigger_word else "No trigger word found. If there's a trigger word, include it in your prompt"}<br></small>
573
+ </div>
574
+ </div>
575
+ {model_card_examples}
576
+ </div>
577
+ '''
578
+ existing_item_index = next((index for (index, item) in enumerate(loras) if item['repo'] == repo), None)
579
+ if existing_item_index is None:
580
+ new_item = {"image": image, "title": title, "repo": repo, "weights": path, "trigger_word": trigger_word}
581
+ print(new_item)
582
+ loras.append(new_item)
583
+ existing_item_index = len(loras) - 1
584
+ return gr.update(visible=True, value=card), gr.update(visible=True), gr.Gallery(selected_index=None), f"Custom: {path}", existing_item_index, trigger_word, gr.update(interactive=True)
585
+ except Exception as e:
586
+ full_traceback = traceback.format_exc()
587
+ print(f"Full traceback:\n{full_traceback}")
588
+ gr.Warning(f"Invalid LoRA: either you entered an invalid link, or a non-Qwen-Image LoRA, this was the issue: {e}")
589
+ return gr.update(visible=True, value=f"Invalid LoRA: either you entered an invalid link, a non-Qwen-Image LoRA"), gr.update(visible=True), gr.update(), "", None, "", gr.update(interactive=False)
590
+ else:
591
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, "", gr.update(interactive=False)
592
+
593
+
594
+ def remove_custom_lora():
595
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, "", gr.update(interactive=False)
596
+
597
+
598
+ def reload_loras_gallery():
599
+ global loras
600
+ loras = load_loras_hot()
601
+
602
+ gallery_items = [
603
+ (item["image"], item.get("title") or item.get("name"))
604
+ for item in loras
605
+ if item.get("image")
606
+ ]
607
+
608
+ return gr.update(value=gallery_items)
609
+
610
+
611
+
612
+ def init(speed_mode, aspect_ratio):
613
+ loras_result = reload_loras_gallery()
614
+ speed_mode_result = handle_speed_mode(speed_mode)
615
+ aspect_ratio_result = apply_aspect_ratio(aspect_ratio)
616
+ return (
617
+ *speed_mode_result,
618
+ *aspect_ratio_result,
619
+ loras_result
620
+ )
621
+
622
+
623
+
624
+
625
+ css = '''
626
+ #gen_btn{height: 100%}
627
+ #gen_column{align-self: stretch}
628
+ #title{text-align: center}
629
+ #title h1{font-size: 3em; display:inline-flex; align-items:center}
630
+ #title img{width: 100px; margin-right: 0.5em}
631
+ #gallery .grid-wrap{height: 10vh}
632
+ #lora_list{background: var(--block-background-fill);padding: 0 1em .3em; font-size: 90%}
633
+ .card_internal{display: flex;height: 100px;margin-top: .5em}
634
+ .card_internal img{margin-right: 1em}
635
+ .styler{--form-gap-width: 0px !important}
636
+ #speed_status{padding: .5em; border-radius: 5px; margin: 1em 0}
637
+ '''
638
+
639
+
640
+ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
641
+ title = gr.HTML(
642
+ """<h1 style=\"color:#644fea\">Qwen-Image-2512</h1>
643
+ <h3 style=\"margin-top: -10px\">LoRA🦜 ChoquinLabs Explorer</h3>""",
644
+ elem_id="title",
645
+ )
646
+
647
+ selected_index = gr.State(None)
648
+
649
+ with gr.Row():
650
+ with gr.Column(scale=3):
651
+
652
+ prompt_1 = gr.Textbox(label="Prompt 1", lines=1)
653
+ prompt_2 = gr.Textbox(label="Prompt 2", lines=1)
654
+ prompt_3 = gr.Textbox(label="Prompt 3", lines=1)
655
+ prompt_4 = gr.Textbox(label="Prompt 4", lines=1)
656
+
657
+ negative_prompt = gr.Textbox(label="Negative Prompt", lines=1, placeholder="Optional: what to avoid")
658
+ prompt_enhance = gr.Checkbox(label="Prompt Enhance", value=False)
659
+ with gr.Column(scale=1, elem_id="gen_column"):
660
+ generate_button = gr.Button("Generate", variant="primary", elem_id="gen_btn", interactive=False)
661
+
662
+
663
+
664
+ with gr.Row():
665
+ with gr.Column():
666
+ selected_info = gr.Markdown("")
667
+ examples_component = gr.Examples(examples=[], inputs=[prompt_1], label="Sample Prompts", visible=False)
668
+ gallery = gr.Gallery(
669
+ [(item["image"], item["title"]) for item in loras],
670
+ label="LoRA Gallery",
671
+ allow_preview=False,
672
+ columns=3,
673
+ elem_id="gallery",
674
+ show_share_button=False
675
+ )
676
+ reload_btn = gr.Button("🔄 Reload LoRAs")
677
+
678
+
679
+ with gr.Group():
680
+ custom_lora = gr.Textbox(label="Custom LoRA", info="LoRA Hugging Face path", placeholder="username/qwen-image-custom-lora")
681
+ gr.Markdown("[Check Qwen-Image LoRAs](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image)", elem_id="lora_list")
682
+ custom_lora_info = gr.HTML(visible=False)
683
+ custom_lora_button = gr.Button("Remove custom LoRA", visible=False)
684
+
685
+ with gr.Column():
686
+ result = gr.Gallery(label="Generated Images", show_label=True, elem_id="result_gallery")
687
+ history_state = gr.State([])
688
+ ### MODIFICACIÓN 2: AÑADIR LOS COMPONENTES DE LA UI DEL HISTORIAL ###
689
+ with gr.Group():
690
+ with gr.Row():
691
+ gr.Markdown("### 📜 History")
692
+ clear_history_button = gr.Button("🗑️ Clear History", size="sm")
693
+
694
+
695
+ history_gallery = gr.Gallery(
696
+ label="Generation History",
697
+ show_label=False,
698
+ columns=4,
699
+ object_fit="contain",
700
+ height="auto",
701
+ interactive=False
702
+ )
703
+ ### FIN DE LA MODIFICACIÓN 2 ###
704
+
705
+ with gr.Row():
706
+ with gr.Column():
707
+ speed_mode = gr.Radio(
708
+ label="Generation Mode",
709
+ choices=["light 4", "Wuli-art", "light 4 fp8", "light 8", "normal"],
710
+ value="light 4",
711
+ info="'light' modes use Lightning LoRA for faster generation"
712
+ )
713
+ with gr.Column():
714
+ quantity = gr.Radio(
715
+ label="Quantity",
716
+ choices=["1", "2", "3", "4"],
717
+ value="1",
718
+ type="index"
719
+ )
720
+
721
+ speed_status = gr.Markdown("Quality mode active", elem_id="speed_status")
722
+
723
+ with gr.Row():
724
+ aspect_ratio = gr.Radio(
725
+ label="Aspect Ratio",
726
+ choices=["1:1", "16:9", "9:16", "3:2", "2:3", "3:1", "2:1"],
727
+ value="16:9"
728
+ )
729
+
730
+
731
+ with gr.Row():
732
+ width = gr.Slider(
733
+ label="Width",
734
+ minimum=256,
735
+ maximum=1920,
736
+ step=1,
737
+ value=1920
738
+ )
739
+ height = gr.Slider(
740
+ label="Height",
741
+ minimum=256,
742
+ maximum=1920,
743
+ step=1,
744
+ value=1080
745
+ )
746
+
747
+
748
+ with gr.Row():
749
+ quality_multiplier = gr.Radio(
750
+ label="Quality (Size Multiplier)",
751
+ choices=["0.5x", "0.75x", "1x", "1.5x", "2x"],
752
+ value="1x"
753
+ )
754
+
755
+
756
+ with gr.Row():
757
+ with gr.Accordion("Advanced Settings", open=False):
758
+ with gr.Column():
759
+ with gr.Row():
760
+ cfg_scale = gr.Slider(
761
+ label="Guidance Scale (True CFG)",
762
+ minimum=1.0,
763
+ maximum=5.0,
764
+ step=0.1,
765
+ value=3.5,
766
+ info="Lower for speed mode, higher for quality"
767
+ )
768
+ steps = gr.Slider(
769
+ label="Steps",
770
+ minimum=4,
771
+ maximum=50,
772
+ step=1,
773
+ value=45,
774
+ info="Automatically set by speed mode"
775
+ )
776
+
777
+ with gr.Row():
778
+ randomize_seed = gr.Checkbox(True, label="Randomize seed")
779
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, randomize=True)
780
+ lora_scale = gr.Slider(label="LoRA Scale", minimum=0, maximum=3, step=0.01, value=1.0)
781
+
782
+
783
+ # Event handlers
784
+ gallery.select(
785
+ update_selection,
786
+ inputs=[width, height],
787
+ outputs=[prompt_1, selected_info, selected_index, width, height, generate_button]
788
+ )
789
+
790
+ speed_mode.change(
791
+ handle_speed_mode,
792
+ inputs=[speed_mode],
793
+ outputs=[speed_status, steps, cfg_scale]
794
+ )
795
+
796
+ custom_lora.input(
797
+ add_custom_lora,
798
+ inputs=[custom_lora],
799
+ outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, prompt_1, generate_button]
800
+ )
801
+
802
+ custom_lora_button.click(
803
+ remove_custom_lora,
804
+ outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, custom_lora, generate_button]
805
+ )
806
+
807
+ ### MODIFICACIÓN 3: CONECTAR LOS EVENTOS DEL HISTORIAL ###
808
+ # Evento principal de generación
809
+ generate_event = gr.on(
810
+ triggers=[generate_button.click, prompt_1.submit],
811
+
812
+ fn=run_lora_multi,
813
+ inputs=[
814
+ prompt_1, prompt_2, prompt_3, prompt_4,
815
+ negative_prompt,
816
+ cfg_scale, steps, selected_index,
817
+ randomize_seed, seed,
818
+ width, height, lora_scale,
819
+ speed_mode, quality_multiplier,
820
+ quantity,
821
+ history_state,
822
+ prompt_enhance
823
+ ],
824
+ outputs=[result, history_gallery, history_state, seed]
825
+ )
826
+
827
+
828
+
829
+ # Encadenar la actualización del historial para que se ejecute DESPUÉS de la generación
830
+ # generate_event.then(
831
+ # fn=update_history,
832
+ # inputs=[result, history_gallery],
833
+ # outputs=history_gallery,
834
+ # show_api=False
835
+ # )
836
+
837
+
838
+ # Evento para el botón de limpiar historial
839
+ clear_history_button.click(
840
+ fn=clear_history,
841
+ inputs=None,
842
+ outputs=[history_state, history_gallery],
843
+ show_api=False
844
+ )
845
+ ### FIN DE LA MODIFICACIÓN 3 ###
846
+
847
+ aspect_ratio.change(
848
+ fn=apply_aspect_ratio,
849
+ inputs=[aspect_ratio],
850
+ outputs=[width, height]
851
+ )
852
+
853
+ reload_btn.click(
854
+ fn=reload_loras_gallery,
855
+ outputs=gallery,
856
+ )
857
+
858
+ app.load(
859
+ fn=init,
860
+ inputs=[gr.State("light 4"), gr.State(DEFAULT_ASPECT_RATIO)],
861
+ outputs=[speed_status, steps, cfg_scale, width, height, gallery]
862
+ )
863
+
864
+
865
+ app.queue()
866
+ app.launch()
app.py CHANGED
@@ -1,12 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import gradio as gr
3
  import json
4
  import logging
5
- import torch
6
  from PIL import Image
7
  import spaces
8
- from diffusers import DiffusionPipeline, FlowMatchEulerDiscreteScheduler
9
- from huggingface_hub import hf_hub_download, HfFileSystem, ModelCard, snapshot_download
10
  import copy
11
  import random
12
  import time
@@ -16,18 +35,17 @@ import numpy as np
16
  import traceback
17
  from prompt_rewrite import rewrite
18
  import hashlib
 
19
 
20
- ###################################
21
- base_model = "Qwen/Qwen-Image-2512"
 
 
22
 
23
- # Lightning LoRA info (no global state)
24
- LIGHTNING_LORA_REPO = "lightx2v/Qwen-Image-2512-Lightning"
25
- LIGHTNING_LORA_WEIGHT = "Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors"
26
- LIGHTNING8_LORA_WEIGHT = "Qwen-Image-2512-Lightning-8steps-V1.0-fp32.safetensors"
27
- LIGHTNING_FP8_4STEPS_LORA_WEIGHT = "Qwen-Image-fp8-e4m3fn-Lightning-4steps-V1.0-bf16.safetensors"
28
 
29
- ###################################
30
 
 
 
31
 
32
  def apply_aspect_ratio(ratio):
33
  sizes = {
@@ -43,6 +61,8 @@ def apply_aspect_ratio(ratio):
43
 
44
  DEFAULT_ASPECT_RATIO = "16:9"
45
 
 
 
46
  # ✅ NUEVO: importar optimización avanzada tipo Qwen-Image-MultipleAngles
47
  #from optimization import optimize_pipeline_
48
 
@@ -51,6 +71,9 @@ LORAS_CACHE = {
51
  "last_hash": None,
52
  }
53
  def load_loras_hot():
 
 
 
54
  """Load loras.json and detect changes."""
55
  path = hf_hub_download(
56
  repo_id="lichorosario/qwen-image-lora-dlc-v3",
@@ -89,10 +112,13 @@ def load_loras_from_file():
89
  #loras = load_loras_from_file()
90
  loras = load_loras_hot()
91
 
 
 
 
 
 
 
92
 
93
- # Initialize the base model
94
- dtype = torch.bfloat16
95
- device = "cuda" if torch.cuda.is_available() else "cpu"
96
 
97
 
98
  # Scheduler configuration from the Qwen-Image-Lightning repository
@@ -114,11 +140,12 @@ scheduler_config = {
114
  }
115
 
116
 
117
- scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
 
118
 
119
- pipe = DiffusionPipeline.from_pretrained(
120
- base_model, scheduler=scheduler, torch_dtype=dtype
121
- ).to(device)
122
 
123
 
124
 
@@ -142,7 +169,15 @@ except Exception as e:
142
  """
143
 
144
 
 
 
 
 
 
145
 
 
 
 
146
 
147
  MAX_SEED = np.iinfo(np.int32).max
148
 
@@ -210,8 +245,7 @@ def update_selection(evt: gr.SelectData, width, height):
210
  updated_text,
211
  evt.index,
212
  width,
213
- height,
214
- gr.update(interactive=True)
215
  )
216
 
217
 
@@ -327,19 +361,22 @@ def run_lora_multi(
327
  prompt_1, prompt_2, prompt_3, prompt_4,
328
  negative_prompt,
329
  cfg_scale, steps,
330
- selected_index,
331
  randomize_seed, seed,
332
  width, height,
333
- lora_scale,
334
  speed_mode,
335
  quality_multiplier,
336
- quantity, # se ignora acá (pero ahora lo usamos bien)
337
  history,
338
  prompt_enhance=False,
339
  progress=gr.Progress(track_tqdm=True)
340
  ):
341
- if selected_index is None:
342
- raise gr.Error("You must select a LoRA before proceeding.")
 
 
 
 
343
 
344
  prompts = [
345
  p.strip() for p in [prompt_1, prompt_2, prompt_3, prompt_4]
@@ -349,91 +386,66 @@ def run_lora_multi(
349
  if not prompts:
350
  raise gr.Error("You must fill at least one prompt.")
351
 
352
- selected_lora = loras[selected_index]
353
- lora_path = selected_lora["repo"]
354
- trigger_word = selected_lora["trigger_word"]
355
-
356
- # aplicar trigger word por prompt
357
- final_prompts = []
358
- for p in prompts:
359
- if trigger_word:
360
- if selected_lora.get("trigger_position") == "append":
361
- final_prompts.append(f"{p} {trigger_word}")
362
- else:
363
- final_prompts.append(f"{trigger_word} {p}")
364
- else:
365
- final_prompts.append(p)
366
-
367
  # limpiar LoRAs previas
368
  pipe.unload_lora_weights()
369
 
370
- # 🔥 CARGA DE LORAs (UNA SOLA VEZ)
 
 
 
 
371
  if speed_mode == "light 4":
372
  pipe.load_lora_weights(
373
  LIGHTNING_LORA_REPO,
374
  weight_name=LIGHTNING_LORA_WEIGHT,
375
  adapter_name="lightning"
376
  )
377
- pipe.load_lora_weights(
378
- lora_path,
379
- weight_name=selected_lora.get("weights"),
380
- adapter_name="style"
381
- )
382
- pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
383
-
384
  elif speed_mode == "light 8":
385
  pipe.load_lora_weights(
386
  LIGHTNING_LORA_REPO,
387
  weight_name=LIGHTNING8_LORA_WEIGHT,
388
  adapter_name="lightning"
389
  )
 
 
 
 
 
 
 
 
 
390
  pipe.load_lora_weights(
391
  lora_path,
392
  weight_name=selected_lora.get("weights"),
393
- adapter_name="style"
394
  )
395
- pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
 
396
 
397
- elif speed_mode == "Wuli-art":
398
- with calculateDuration("Loading Lightning LoRA and style LoRA"):
399
- pipe.load_lora_weights(
400
- 'Wuli-Art/Qwen-Image-2512-Turbo-LoRA',
401
- weight_name='Wuli-Qwen-Image-2512-Turbo-LoRA-4steps-V2.0-bf16.safetensors',
402
- adapter_name="lightning"
403
- )
404
- weight_name = selected_lora.get("weights", None)
405
- pipe.load_lora_weights(
406
- lora_path,
407
- weight_name=weight_name,
408
- low_cpu_mem_usage=True,
409
- adapter_name="style"
410
- )
411
- pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
412
-
413
- elif speed_mode == "light 4 fp8":
414
- with calculateDuration("Loading Lightning LoRA and style LoRA"):
415
- pipe.load_lora_weights(
416
- LIGHTNING_LORA_REPO,
417
- weight_name=LIGHTNING_FP8_4STEPS_LORA_WEIGHT,
418
- adapter_name="lightning"
419
- )
420
- weight_name = selected_lora.get("weights", None)
421
- pipe.load_lora_weights(
422
- lora_path,
423
- weight_name=weight_name,
424
- low_cpu_mem_usage=True,
425
- adapter_name="style"
426
- )
427
- pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
428
-
429
 
430
- else:
431
- pipe.load_lora_weights(
432
- lora_path,
433
- weight_name=selected_lora.get("weights"),
434
- adapter_name="style"
435
- )
436
- pipe.set_adapters(["style"], adapter_weights=[lora_scale])
 
 
 
 
437
 
438
  if randomize_seed:
439
  seed = random.randint(0, MAX_SEED)
@@ -615,18 +627,18 @@ def add_custom_lora(custom_lora):
615
  print(new_item)
616
  loras.append(new_item)
617
  existing_item_index = len(loras) - 1
618
- return gr.update(visible=True, value=card), gr.update(visible=True), gr.Gallery(selected_index=None), f"Custom: {path}", existing_item_index, trigger_word, gr.update(interactive=True)
619
  except Exception as e:
620
  full_traceback = traceback.format_exc()
621
  print(f"Full traceback:\n{full_traceback}")
622
  gr.Warning(f"Invalid LoRA: either you entered an invalid link, or a non-Qwen-Image LoRA, this was the issue: {e}")
623
- return gr.update(visible=True, value=f"Invalid LoRA: either you entered an invalid link, a non-Qwen-Image LoRA"), gr.update(visible=True), gr.update(), "", None, "", gr.update(interactive=False)
624
  else:
625
- return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, "", gr.update(interactive=False)
626
 
627
 
628
  def remove_custom_lora():
629
- return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, "", gr.update(interactive=False)
630
 
631
 
632
  def reload_loras_gallery():
@@ -650,9 +662,91 @@ def init(speed_mode, aspect_ratio):
650
  return (
651
  *speed_mode_result,
652
  *aspect_ratio_result,
653
- loras_result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  )
655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  css = '''
657
  #gen_btn{height: 100%}
658
  #gen_column{align-self: stretch}
@@ -688,12 +782,43 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
688
  negative_prompt = gr.Textbox(label="Negative Prompt", lines=1, placeholder="Optional: what to avoid")
689
  prompt_enhance = gr.Checkbox(label="Prompt Enhance", value=False)
690
  with gr.Column(scale=1, elem_id="gen_column"):
691
- generate_button = gr.Button("Generate", variant="primary", elem_id="gen_btn", interactive=False)
692
 
693
 
694
 
 
 
 
 
 
 
 
695
  with gr.Row():
696
  with gr.Column():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  selected_info = gr.Markdown("")
698
  examples_component = gr.Examples(examples=[], inputs=[prompt_1], label="Sample Prompts", visible=False)
699
  gallery = gr.Gallery(
@@ -701,8 +826,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
701
  label="LoRA Gallery",
702
  allow_preview=False,
703
  columns=3,
704
- elem_id="gallery",
705
- show_share_button=False
706
  )
707
  reload_btn = gr.Button("🔄 Reload LoRAs")
708
 
@@ -737,7 +861,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
737
  with gr.Column():
738
  speed_mode = gr.Radio(
739
  label="Generation Mode",
740
- choices=["light 4", "light 8", "Wuli-art", "light 4 fp8", "normal"],
741
  value="light 4",
742
  info="'light' modes use Lightning LoRA for faster generation"
743
  )
@@ -808,15 +932,14 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
808
  with gr.Row():
809
  randomize_seed = gr.Checkbox(True, label="Randomize seed")
810
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, randomize=True)
811
- lora_scale = gr.Slider(label="LoRA Scale", minimum=0, maximum=3, step=0.01, value=1.0)
812
 
813
 
814
  # Event handlers
815
- gallery.select(
816
- update_selection,
817
- inputs=[width, height],
818
- outputs=[prompt_1, selected_info, selected_index, width, height, generate_button]
819
- )
820
 
821
  speed_mode.change(
822
  handle_speed_mode,
@@ -827,12 +950,12 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
827
  custom_lora.input(
828
  add_custom_lora,
829
  inputs=[custom_lora],
830
- outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, prompt_1, generate_button]
831
  )
832
 
833
  custom_lora_button.click(
834
  remove_custom_lora,
835
- outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, custom_lora, generate_button]
836
  )
837
 
838
  ### MODIFICACIÓN 3: CONECTAR LOS EVENTOS DEL HISTORIAL ###
@@ -844,9 +967,9 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
844
  inputs=[
845
  prompt_1, prompt_2, prompt_3, prompt_4,
846
  negative_prompt,
847
- cfg_scale, steps, selected_index,
848
  randomize_seed, seed,
849
- width, height, lora_scale,
850
  speed_mode, quality_multiplier,
851
  quantity,
852
  history_state,
@@ -870,8 +993,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
870
  clear_history_button.click(
871
  fn=clear_history,
872
  inputs=None,
873
- outputs=[history_state, history_gallery],
874
- show_api=False
875
  )
876
  ### FIN DE LA MODIFICACIÓN 3 ###
877
 
@@ -885,12 +1007,46 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css, delete_cache=(60, 60)) as app:
885
  fn=reload_loras_gallery,
886
  outputs=gallery,
887
  )
888
-
889
- app.load(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  fn=init,
891
  inputs=[gr.State("light 4"), gr.State(DEFAULT_ASPECT_RATIO)],
892
- outputs=[speed_status, steps, cfg_scale, width, height, gallery]
893
  )
 
 
894
 
895
 
896
  app.queue()
 
1
+
2
+ MOCK = True
3
+
4
+ if (MOCK):
5
+ import sys
6
+ from unittest.mock import MagicMock
7
+
8
+ # Creamos un módulo falso llamado 'spaces'
9
+ mock_spaces = MagicMock()
10
+
11
+ # Definimos el decorador GPU para que simplemente devuelva la función original sin cambios
12
+ def mock_gpu_decorator(duration=None):
13
+ def decorator(func):
14
+ return func
15
+ return decorator
16
+
17
+ mock_spaces.GPU = mock_gpu_decorator
18
+
19
+ # Lo insertamos en los módulos del sistema para que 'import spaces' funcione
20
+ sys.modules["spaces"] = mock_spaces
21
+
22
+
23
  import os
24
  import gradio as gr
25
  import json
26
  import logging
 
27
  from PIL import Image
28
  import spaces
 
 
29
  import copy
30
  import random
31
  import time
 
35
  import traceback
36
  from prompt_rewrite import rewrite
37
  import hashlib
38
+ from functools import partial
39
 
40
+ if (not MOCK):
41
+ import torchapp.py
42
+ from diffusers import DiffusionPipeline, FlowMatchEulerDiscreteScheduler
43
+ from huggingface_hub import hf_hub_download, HfFileSystem, ModelCard, snapshot_download
44
 
 
 
 
 
 
45
 
 
46
 
47
+ NUM_LORAS = 2
48
+ base_model = "Qwen/Qwen-Image-2512"
49
 
50
  def apply_aspect_ratio(ratio):
51
  sizes = {
 
61
 
62
  DEFAULT_ASPECT_RATIO = "16:9"
63
 
64
+
65
+
66
  # ✅ NUEVO: importar optimización avanzada tipo Qwen-Image-MultipleAngles
67
  #from optimization import optimize_pipeline_
68
 
 
71
  "last_hash": None,
72
  }
73
  def load_loras_hot():
74
+ if MOCK:
75
+ return load_loras_from_file()
76
+
77
  """Load loras.json and detect changes."""
78
  path = hf_hub_download(
79
  repo_id="lichorosario/qwen-image-lora-dlc-v3",
 
112
  #loras = load_loras_from_file()
113
  loras = load_loras_hot()
114
 
115
+ selected_loras = []
116
+
117
+ if not MOCK:
118
+ # Initialize the base model
119
+ dtype = torch.bfloat16
120
+ device = "cuda" if torch.cuda.is_available() else "cpu"
121
 
 
 
 
122
 
123
 
124
  # Scheduler configuration from the Qwen-Image-Lightning repository
 
140
  }
141
 
142
 
143
+ if not MOCK:
144
+ scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
145
 
146
+ pipe = DiffusionPipeline.from_pretrained(
147
+ "Qwen/Qwen-Image-2512", scheduler=scheduler, torch_dtype=dtype
148
+ ).to(device)
149
 
150
 
151
 
 
169
  """
170
 
171
 
172
+ # Lightning LoRA info (no global state)
173
+ LIGHTNING_LORA_REPO = "lightx2v/Qwen-Image-2512-Lightning"
174
+ LIGHTNING_LORA_WEIGHT = "Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors"
175
+ LIGHTNING8_LORA_WEIGHT = "Qwen-Image-2512-Lightning-8steps-V1.0-fp32.safetensors"
176
+ LIGHTNING_FP8_4STEPS_LORA_WEIGHT = "Qwen-Image-fp8-e4m3fn-Lightning-4steps-V1.0-bf16.safetensors"
177
 
178
+ #LIGHTNING_LORA_REPO = "Wuli-art/Qwen-Image-2512-Turbo-LoRA"
179
+ #LIGHTNING_LORA_WEIGHT = "Wuli-Qwen-Image-2512-Turbo-LoRA-4steps-V1.0-bf16.safetensors"
180
+ #LIGHTNING8_LORA_WEIGHT = "Wuli-Qwen-Image-2512-Turbo-LoRA-4steps-V1.0-bf16.safetensors"
181
 
182
  MAX_SEED = np.iinfo(np.int32).max
183
 
 
245
  updated_text,
246
  evt.index,
247
  width,
248
+ height
 
249
  )
250
 
251
 
 
361
  prompt_1, prompt_2, prompt_3, prompt_4,
362
  negative_prompt,
363
  cfg_scale, steps,
364
+ selected_loras_state, # Changed from selected_index
365
  randomize_seed, seed,
366
  width, height,
 
367
  speed_mode,
368
  quality_multiplier,
369
+ quantity,
370
  history,
371
  prompt_enhance=False,
372
  progress=gr.Progress(track_tqdm=True)
373
  ):
374
+ # selected_loras_state is a list of tuples: [(image_index, scale), ...]
375
+ # Filter to get only columns with loaded LoRAs
376
+ loaded_loras = [(idx, image_idx, scale) for idx, (image_idx, scale) in enumerate(selected_loras_state) if image_idx is not None]
377
+
378
+ if not loaded_loras:
379
+ raise gr.Error("You must select at least one LoRA before proceeding.")
380
 
381
  prompts = [
382
  p.strip() for p in [prompt_1, prompt_2, prompt_3, prompt_4]
 
386
  if not prompts:
387
  raise gr.Error("You must fill at least one prompt.")
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  # limpiar LoRAs previas
390
  pipe.unload_lora_weights()
391
 
392
+ # 🔥 CARGA DE MÚLTIPLES LORAs
393
+ adapter_names = []
394
+ adapter_weights = []
395
+
396
+ # Add lightning LoRA if in speed mode
397
  if speed_mode == "light 4":
398
  pipe.load_lora_weights(
399
  LIGHTNING_LORA_REPO,
400
  weight_name=LIGHTNING_LORA_WEIGHT,
401
  adapter_name="lightning"
402
  )
403
+ adapter_names.append("lightning")
404
+ adapter_weights.append(1.0)
 
 
 
 
 
405
  elif speed_mode == "light 8":
406
  pipe.load_lora_weights(
407
  LIGHTNING_LORA_REPO,
408
  weight_name=LIGHTNING8_LORA_WEIGHT,
409
  adapter_name="lightning"
410
  )
411
+ adapter_names.append("lightning")
412
+ adapter_weights.append(1.0)
413
+
414
+ # Load all selected LoRAs from columns
415
+ for col_idx, image_idx, scale in loaded_loras:
416
+ selected_lora = loras[image_idx]
417
+ lora_path = selected_lora["repo"]
418
+ adapter_name = f"lora_{col_idx}"
419
+
420
  pipe.load_lora_weights(
421
  lora_path,
422
  weight_name=selected_lora.get("weights"),
423
+ adapter_name=adapter_name
424
  )
425
+ adapter_names.append(adapter_name)
426
+ adapter_weights.append(scale)
427
 
428
+ # Set all adapters
429
+ pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
430
+
431
+ # Colectar trigger words de todos los LoRAs cargados
432
+ all_trigger_words = []
433
+ for _, image_idx, _ in loaded_loras:
434
+ trigger = loras[image_idx].get("trigger_word", "")
435
+ if trigger:
436
+ all_trigger_words.append(trigger)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
+ combined_trigger = " ".join(all_trigger_words)
439
+
440
+ # Aplicar trigger words a los prompts
441
+ final_prompts = []
442
+ for p in prompts:
443
+ if combined_trigger:
444
+ final_prompts.append(f"{combined_trigger} {p}")
445
+ else:
446
+ final_prompts.append(p)
447
+
448
+ prompts = final_prompts
449
 
450
  if randomize_seed:
451
  seed = random.randint(0, MAX_SEED)
 
627
  print(new_item)
628
  loras.append(new_item)
629
  existing_item_index = len(loras) - 1
630
+ return gr.update(visible=True, value=card), gr.update(visible=True), gr.Gallery(selected_index=None), f"Custom: {path}", existing_item_index, trigger_word
631
  except Exception as e:
632
  full_traceback = traceback.format_exc()
633
  print(f"Full traceback:\n{full_traceback}")
634
  gr.Warning(f"Invalid LoRA: either you entered an invalid link, or a non-Qwen-Image LoRA, this was the issue: {e}")
635
+ return gr.update(visible=True, value=f"Invalid LoRA: either you entered an invalid link, a non-Qwen-Image LoRA"), gr.update(visible=True), gr.update(), "", None, ""
636
  else:
637
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, ""
638
 
639
 
640
  def remove_custom_lora():
641
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None, ""
642
 
643
 
644
  def reload_loras_gallery():
 
662
  return (
663
  *speed_mode_result,
664
  *aspect_ratio_result,
665
+ loras_result,
666
+ "1" # Preselect first radio
667
+ )
668
+
669
+
670
+
671
+ def update_slider_state(val, state, idx):
672
+ # state is list of tuples (image_index, scale)
673
+ new_state = list(state)
674
+ current_image = new_state[idx][0]
675
+ new_state[idx] = (current_image, val)
676
+ return new_state
677
+
678
+ def remove_lora(state, idx):
679
+ new_state = list(state)
680
+ new_state[idx] = (None, 1.0) # Reset to default
681
+
682
+ # Return updates for: global_state, radio, slider, markdown, delete_btn, output_text
683
+ return (
684
+ new_state, # selected_loras
685
+ gr.update(value=None), # radio
686
+ gr.update(visible=False, value=1.0), # slider
687
+ gr.update(value=""), # markdown
688
+ gr.update(visible=False), # delete_btn
689
  )
690
 
691
+ def validate_generate_button(state):
692
+ """Enable generate button if at least one LoRA is loaded."""
693
+ # state is list of tuples (image_index, scale)
694
+ # Check if any column has a LoRA loaded (image_index is not None)
695
+ has_lora = any(item[0] is not None for item in state)
696
+ return gr.update(interactive=has_lora)
697
+
698
+ def get_selection(evt: gr.SelectData, *args):
699
+ # args: radios (N) + scales (N) + mds (N) + selected_loras (1) + gallery (1)
700
+
701
+ num_radios = NUM_LORAS
702
+ radio_vals = args[:num_radios]
703
+ scale_vals = args[num_radios:num_radios*2]
704
+ md_vals = args[num_radios*2:num_radios*3]
705
+ current_state = args[-2]
706
+ gallery_val = args[-1]
707
+
708
+ selected_val = next((val for val in radio_vals if val is not None), None)
709
+
710
+
711
+ # Identify index of selected radio
712
+ selected_index = -1
713
+ for i, val in enumerate(radio_vals):
714
+ if val is not None:
715
+ selected_index = i
716
+ break
717
+
718
+ # Prepare outputs: [output_text] + [md_0_update, ...] + [selected_loras_update] + [scale_0_update, ...] + [del_btn_0_update, ...] + [thumb_0_update, ...]
719
+
720
+ md_updates = [gr.update() for _ in range(num_radios)]
721
+ scale_updates = [gr.update() for _ in range(num_radios)]
722
+ del_btn_updates = [gr.update() for _ in range(num_radios)]
723
+
724
+ new_state = current_state
725
+
726
+ selected_image_info = ""
727
+
728
+ if selected_index != -1:
729
+ # Get LoRA details
730
+ lora = loras[evt.index]
731
+ lora_name = lora["title"]
732
+ lora_image = lora["image"]
733
+
734
+ # Update specific markdown with image and name
735
+ selected_image_info = f"Selected LoRA: {lora_name}"
736
+ markdown_content = f"<img src='{lora_image}' style='height:100px; display:block; margin-bottom:10px;' />\n\n**{lora_name}**"
737
+ md_updates[selected_index] = gr.update(value=markdown_content)
738
+ scale_updates[selected_index] = gr.update(visible=True)
739
+ del_btn_updates[selected_index] = gr.update(visible=True)
740
+
741
+ # Update state for this column: (image_index, scale)
742
+ new_state = list(current_state)
743
+ new_state[selected_index] = (evt.index, scale_vals[selected_index])
744
+
745
+ return md_updates + [new_state] + scale_updates + del_btn_updates
746
+
747
+
748
+
749
+
750
  css = '''
751
  #gen_btn{height: 100%}
752
  #gen_column{align-self: stretch}
 
782
  negative_prompt = gr.Textbox(label="Negative Prompt", lines=1, placeholder="Optional: what to avoid")
783
  prompt_enhance = gr.Checkbox(label="Prompt Enhance", value=False)
784
  with gr.Column(scale=1, elem_id="gen_column"):
785
+ generate_button = gr.Button("Generate", variant="primary", elem_id="gen_btn")
786
 
787
 
788
 
789
+ selected_loras = gr.State([(None, 1.0)] * NUM_LORAS)
790
+ radios = []
791
+ scales = []
792
+ mds = []
793
+ delete_btns = []
794
+
795
+
796
  with gr.Row():
797
  with gr.Column():
798
+
799
+
800
+ with gr.Row():
801
+ for i in range(NUM_LORAS):
802
+ with gr.Column():
803
+ # Each radio has a single choice which is its column number
804
+ r = gr.Radio(
805
+ [str(i + 1)],
806
+ label=f"Lora {i + 1}"
807
+ )
808
+ radios.append(r)
809
+
810
+ md = gr.Markdown("")
811
+ mds.append(md)
812
+
813
+ lora_scale = gr.Slider(label="LoRA Scale", minimum=0, maximum=3, step=0.1, value=1.0, interactive=True, visible=False)
814
+ scales.append(lora_scale)
815
+
816
+ del_btn = gr.Button("🗑️", visible=False)
817
+ delete_btns.append(del_btn)
818
+
819
+
820
+
821
+
822
  selected_info = gr.Markdown("")
823
  examples_component = gr.Examples(examples=[], inputs=[prompt_1], label="Sample Prompts", visible=False)
824
  gallery = gr.Gallery(
 
826
  label="LoRA Gallery",
827
  allow_preview=False,
828
  columns=3,
829
+ elem_id="gallery"
 
830
  )
831
  reload_btn = gr.Button("🔄 Reload LoRAs")
832
 
 
861
  with gr.Column():
862
  speed_mode = gr.Radio(
863
  label="Generation Mode",
864
+ choices=["light 4", "Wuli-art", "light 4 fp8", "light 8", "normal"],
865
  value="light 4",
866
  info="'light' modes use Lightning LoRA for faster generation"
867
  )
 
932
  with gr.Row():
933
  randomize_seed = gr.Checkbox(True, label="Randomize seed")
934
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, randomize=True)
 
935
 
936
 
937
  # Event handlers
938
+ # gallery.select(
939
+ # update_selection,
940
+ # inputs=[width, height],
941
+ # outputs=[prompt_1, selected_info, selected_index, width, height, generate_button]
942
+ # )
943
 
944
  speed_mode.change(
945
  handle_speed_mode,
 
950
  custom_lora.input(
951
  add_custom_lora,
952
  inputs=[custom_lora],
953
+ outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, prompt_1]
954
  )
955
 
956
  custom_lora_button.click(
957
  remove_custom_lora,
958
+ outputs=[custom_lora_info, custom_lora_button, gallery, selected_info, selected_index, custom_lora]
959
  )
960
 
961
  ### MODIFICACIÓN 3: CONECTAR LOS EVENTOS DEL HISTORIAL ###
 
967
  inputs=[
968
  prompt_1, prompt_2, prompt_3, prompt_4,
969
  negative_prompt,
970
+ cfg_scale, steps, selected_loras, # Changed from selected_index
971
  randomize_seed, seed,
972
+ width, height, # Removed lora_scale
973
  speed_mode, quality_multiplier,
974
  quantity,
975
  history_state,
 
993
  clear_history_button.click(
994
  fn=clear_history,
995
  inputs=None,
996
+ outputs=[history_state, history_gallery]
 
997
  )
998
  ### FIN DE LA MODIFICACIÓN 3 ###
999
 
 
1007
  fn=reload_loras_gallery,
1008
  outputs=gallery,
1009
  )
1010
+
1011
+
1012
+
1013
+ for i, r in enumerate(radios):
1014
+ others = radios[:i] + radios[i+1:]
1015
+ # JS: if val is selected (true), return nulls for all others.
1016
+ # Otherwise return current values (no change).
1017
+ js_code = f"(val, ...args) => val ? args.map(_ => null) : args"
1018
+ r.change(fn=None, inputs=[r] + others, outputs=others, js=js_code)
1019
+
1020
+ # JS toggle for gallery class
1021
+ js_gallery_toggle = "(...args) => { const gallery = document.getElementById('gallery'); const anySelected = args.some(v => v !== null && v !== ''); if (gallery) { if (anySelected) gallery.classList.remove('disabled'); else gallery.classList.add('disabled'); } }"
1022
+ r.change(fn=None, inputs=radios, outputs=None, js=js_gallery_toggle)
1023
+
1024
+ # Bind slider changes separately to ensure all inputs/outputs are available
1025
+ for i, scale in enumerate(scales):
1026
+ scale_event = scale.change(fn=partial(update_slider_state, idx=i), inputs=[scale, selected_loras], outputs=[selected_loras])
1027
+ scale_event.then(fn=validate_generate_button, inputs=[selected_loras], outputs=[generate_button])
1028
+
1029
+ # Bind delete buttons
1030
+ for i, del_btn in enumerate(delete_btns):
1031
+ del_event = del_btn.click(
1032
+ fn=partial(remove_lora, idx=i),
1033
+ inputs=[selected_loras],
1034
+ outputs=[selected_loras, radios[i], scales[i], mds[i], del_btn]
1035
+ )
1036
+ del_event.then(fn=validate_generate_button, inputs=[selected_loras], outputs=[generate_button])
1037
+
1038
+ gallery_event = gallery.select(fn=get_selection, inputs=radios + scales + mds + [selected_loras, gallery], outputs=mds + [selected_loras] + scales + delete_btns)
1039
+ gallery_event.then(fn=validate_generate_button, inputs=[selected_loras], outputs=[generate_button])
1040
+
1041
+
1042
+
1043
+ load_event = app.load(
1044
  fn=init,
1045
  inputs=[gr.State("light 4"), gr.State(DEFAULT_ASPECT_RATIO)],
1046
+ outputs=[speed_status, steps, cfg_scale, width, height, gallery, radios[0]]
1047
  )
1048
+ load_event.then(fn=validate_generate_button, inputs=[selected_loras], outputs=[generate_button])
1049
+
1050
 
1051
 
1052
  app.queue()
demo.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from functools import partial
3
+
4
+ NUM_LORAS = 5
5
+
6
+ LORA_LIST = [
7
+ {"image": "https://picsum.photos/seed/1/400/300", "title": "Cinematic Style"},
8
+ {"image": "https://picsum.photos/seed/2/400/300", "title": "Anime Style"},
9
+ {"image": "https://picsum.photos/seed/3/400/300", "title": "Portrait LoRA"},
10
+ {"image": "https://picsum.photos/seed/4/400/300", "title": "Landscape LoRA"},
11
+ {"image": "https://picsum.photos/seed/5/400/300", "title": "Sci-Fi Style"}
12
+ ]
13
+
14
+
15
+ def update_slider_state(val, state, idx):
16
+ # state is list of tuples (image_index, scale)
17
+ new_state = list(state)
18
+ current_image = new_state[idx][0]
19
+ new_state[idx] = (current_image, val)
20
+ return new_state, f"State Updated: {new_state}"
21
+
22
+ def remove_lora(state, idx):
23
+ new_state = list(state)
24
+ new_state[idx] = (None, 1.0) # Reset to default
25
+
26
+ # Return updates for: global_state, radio, slider, markdown, delete_btn, output_text
27
+ return (
28
+ new_state, # selected_loras
29
+ gr.update(value=None), # radio
30
+ gr.update(visible=False, value=1.0), # slider
31
+ gr.update(value=""), # markdown
32
+ gr.update(visible=False), # delete_btn
33
+ f"State Updated: {new_state}" # output
34
+ )
35
+
36
+ def get_selection(evt: gr.SelectData, *args):
37
+ # args: radios (N) + scales (N) + mds (N) + selected_loras (1) + gallery (1)
38
+
39
+ num_radios = NUM_LORAS
40
+ radio_vals = args[:num_radios]
41
+ scale_vals = args[num_radios:num_radios*2]
42
+ md_vals = args[num_radios*2:num_radios*3]
43
+ current_state = args[-2]
44
+ gallery_val = args[-1]
45
+
46
+ selected_val = next((val for val in radio_vals if val is not None), None)
47
+
48
+ # Identify index of selected radio
49
+ selected_index = -1
50
+ for i, val in enumerate(radio_vals):
51
+ if val is not None:
52
+ selected_index = i
53
+ break
54
+
55
+ # Prepare outputs: [output_text] + [md_0_update, ...] + [selected_loras_update] + [scale_0_update, ...] + [del_btn_0_update, ...] + [thumb_0_update, ...]
56
+
57
+ md_updates = [gr.update() for _ in range(num_radios)]
58
+ scale_updates = [gr.update() for _ in range(num_radios)]
59
+ del_btn_updates = [gr.update() for _ in range(num_radios)]
60
+
61
+ new_state = current_state
62
+
63
+ selected_image_info = ""
64
+
65
+ if selected_index != -1:
66
+ # Get LoRA details
67
+ lora = LORA_LIST[evt.index]
68
+ lora_name = lora["title"]
69
+ lora_image = lora["image"]
70
+
71
+ # Update specific markdown with image and name
72
+ selected_image_info = f"Selected LoRA: {lora_name}"
73
+ markdown_content = f"<img src='{lora_image}' style='height:100px; display:block; margin-bottom:10px;' />\n\n**{lora_name}**"
74
+ md_updates[selected_index] = gr.update(value=markdown_content)
75
+ scale_updates[selected_index] = gr.update(visible=True)
76
+ del_btn_updates[selected_index] = gr.update(visible=True)
77
+
78
+ # Update state for this column: (image_index, scale)
79
+ new_state = list(current_state)
80
+ new_state[selected_index] = (evt.index, scale_vals[selected_index])
81
+
82
+ return [f"Selected Radio: {selected_val}. {selected_image_info}. State: {new_state}"] + md_updates + [new_state] + scale_updates + del_btn_updates
83
+
84
+
85
+
86
+
87
+
88
+ def greet(*args):
89
+ # args will contain: radio_0_val, radio_1_val, ..., radio_N-1_val, name, intensity
90
+ radio_vals = args[:NUM_LORAS]
91
+ name = args[-2]
92
+ intensity = args[-1]
93
+
94
+ selected = next((val for val in radio_vals if val is not None), None)
95
+ return "Hola, qué tal " + name + "!" * int(intensity) + f". Selected: {selected}"
96
+
97
+
98
+
99
+
100
+ css = """
101
+ .disabled {
102
+ pointer-events: none;
103
+ opacity: 0.5;
104
+ }
105
+ """
106
+
107
+ with gr.Blocks(css=css) as demo:
108
+ selected_loras = gr.State([(None, 1.0)] * NUM_LORAS)
109
+ radios = []
110
+ scales = []
111
+ mds = []
112
+ delete_btns = []
113
+ with gr.Row():
114
+ for i in range(NUM_LORAS):
115
+ with gr.Column():
116
+ # Each radio has a single choice which is its column number
117
+ r = gr.Radio([str(i + 1)], label=f"Option {i + 1}")
118
+ radios.append(r)
119
+
120
+ md = gr.Markdown("")
121
+ mds.append(md)
122
+
123
+ lora_scale = gr.Slider(label="LoRA Scale", minimum=0, maximum=3, step=0.1, value=1.0, interactive=True, visible=False)
124
+ scales.append(lora_scale)
125
+
126
+ del_btn = gr.Button("🗑️", visible=False)
127
+ delete_btns.append(del_btn)
128
+
129
+ name = gr.Textbox(label="Name")
130
+ intensity = gr.Slider(label="Intensity", minimum=1, maximum=10, step=1)
131
+ output = gr.Textbox(label="Output")
132
+
133
+
134
+ gallery = gr.Gallery(
135
+ label="Generated Images",
136
+ value=[(item["image"], item["title"]) for item in LORA_LIST],
137
+ columns=5,
138
+ height="auto",
139
+ interactive=False,
140
+ allow_preview=False,
141
+ elem_classes=["disabled"],
142
+ elem_id="gallery"
143
+ )
144
+
145
+ btn = gr.Button("Submit")
146
+ btn.click(fn=greet, inputs=radios + [name, intensity], outputs=output)
147
+
148
+ for i, r in enumerate(radios):
149
+ others = radios[:i] + radios[i+1:]
150
+ # JS: if val is selected (true), return nulls for all others.
151
+ # Otherwise return current values (no change).
152
+ js_code = f"(val, ...args) => val ? args.map(_ => null) : args"
153
+ r.change(fn=None, inputs=[r] + others, outputs=others, js=js_code)
154
+
155
+ # JS toggle for gallery class
156
+ js_gallery_toggle = "(...args) => { const gallery = document.getElementById('gallery'); const anySelected = args.some(v => v !== null && v !== ''); if (gallery) { if (anySelected) gallery.classList.remove('disabled'); else gallery.classList.add('disabled'); } }"
157
+ r.change(fn=None, inputs=radios, outputs=None, js=js_gallery_toggle)
158
+
159
+ # Bind slider changes separately to ensure all inputs/outputs are available
160
+ for i, scale in enumerate(scales):
161
+ scale.change(fn=partial(update_slider_state, idx=i), inputs=[scale, selected_loras], outputs=[selected_loras, output])
162
+
163
+ # Bind delete buttons
164
+ for i, del_btn in enumerate(delete_btns):
165
+ del_btn.click(
166
+ fn=partial(remove_lora, idx=i),
167
+ inputs=[selected_loras],
168
+ outputs=[selected_loras, radios[i], scales[i], mds[i], del_btn, output]
169
+ )
170
+
171
+ gallery.select(fn=get_selection, inputs=radios + scales + mds + [selected_loras, gallery], outputs=[output] + mds + [selected_loras] + scales + delete_btns)
172
+
173
+ demo.launch()