Ultronprime commited on
Commit
b66e17e
Β·
verified Β·
1 Parent(s): 89485cd

Add batch processing app.py - upload multiple images, same prompt, view/download all results

Browse files
Files changed (1) hide show
  1. app.py +400 -0
app.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import random
4
+ import tempfile
5
+ import zipfile
6
+ import gradio as gr
7
+ import numpy as np
8
+ import spaces
9
+ import torch
10
+ from typing import Iterable
11
+ from PIL import Image
12
+ from gradio.themes import Soft
13
+ from gradio.themes.utils import colors, fonts, sizes
14
+
15
+ # ── Theme ──────────────────────────────────────────────────────────────────────
16
+ colors.steel_blue = colors.Color(
17
+ name="steel_blue",
18
+ c50="#EBF3F8", c100="#D3E5F0", c200="#A8CCE1", c300="#7DB3D2",
19
+ c400="#529AC3", c500="#4682B4", c600="#3E72A0", c700="#36638C",
20
+ c800="#2E5378", c900="#264364", c950="#1E3450",
21
+ )
22
+
23
+ class SteelBlueTheme(Soft):
24
+ def __init__(
25
+ self,
26
+ *,
27
+ primary_hue: colors.Color | str = colors.gray,
28
+ secondary_hue: colors.Color | str = colors.steel_blue,
29
+ neutral_hue: colors.Color | str = colors.slate,
30
+ text_size: sizes.Size | str = sizes.text_lg,
31
+ font: fonts.Font | str | Iterable[fonts.Font | str] = (
32
+ fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
33
+ ),
34
+ font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
35
+ fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
36
+ ),
37
+ ):
38
+ super().__init__(
39
+ primary_hue=primary_hue, secondary_hue=secondary_hue,
40
+ neutral_hue=neutral_hue, text_size=text_size, font=font, font_mono=font_mono,
41
+ )
42
+ super().set(
43
+ body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
44
+ body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
45
+ button_primary_text_color="white",
46
+ button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
47
+ button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
48
+ slider_color="*secondary_500",
49
+ block_title_text_weight="600",
50
+ block_border_width="3px",
51
+ block_shadow="*shadow_drop_lg",
52
+ )
53
+
54
+ steel_blue_theme = SteelBlueTheme()
55
+
56
+ # ── Device / dtype ─────────────────────────────────────────────────────────────
57
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
58
+ dtype = torch.bfloat16
59
+
60
+ print("CUDA available:", torch.cuda.is_available())
61
+ print("Using device:", device)
62
+
63
+ # ── Model loading ──────────────────────────────────────────────────────────────
64
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
65
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
66
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
67
+
68
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
69
+ "Qwen/Qwen-Image-Edit-2509",
70
+ transformer=QwenImageTransformer2DModel.from_pretrained(
71
+ "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V4",
72
+ torch_dtype=dtype,
73
+ device_map="cuda",
74
+ ),
75
+ torch_dtype=dtype,
76
+ ).to(device)
77
+
78
+ pipe.vae.enable_tiling(tile_sample_min_width=256, tile_sample_min_height=256)
79
+ pipe.vae.enable_slicing()
80
+
81
+ try:
82
+ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
83
+ print("Flash Attention 3 Processor set successfully.")
84
+ except Exception as e:
85
+ print(f"Warning: Could not set FA3 processor: {e}")
86
+
87
+ # ── LoRA catalog ──────────────────────────────────────────────────────────────
88
+ LORA_REPO = "wiikoo/Qwen-lora-nsfw"
89
+ LORA_CONFIGS = {
90
+ "CockQwen_v3": "loras/CockQwen-v3.safetensors",
91
+ "Eva_Qwen_V3": "loras/Eva_Qwen_V3.safetensors",
92
+ "Facial_Cumshots_V1": "loras/Facial_Cumshots_For_Qwen_Image_V1.safetensors",
93
+ "HearmemanAI_V3_Breasts": "loras/HearmemanAI_V3_Rank64_BreastsLoRA_Epoch60.safetensors",
94
+ "HearmemanAI_V4_Breasts": "loras/HearmemanAI_V4_Rank128_BreastsLoRA_Epoch80.safetensors",
95
+ "InniePussy": "loras/InniePussy.safetensors",
96
+ "JTT2_5": "loras/[QWEN] JTT2_5.safetensors",
97
+ "LumiNude01a": "loras/LumiNude01a_CE_QWEN_AIT3k.safetensors",
98
+ "MEXX_QWEN_TG300": "loras/MEXX_QWEN_TG300_23.safetensors",
99
+ "Meta4": "loras/Meta4.safetensors",
100
+ "MysticXXX": "loras/Qwen-MysticXXX-v1.safetensors",
101
+ "Nsfw_Body_V10": "loras/Qwen_Nsfw_Body_V10-4K.safetensors",
102
+ "Nsfw_Body_V14": "loras/Qwen_Nsfw_Body_V14-10K.safetensors",
103
+ "OilySkin_V2": "loras/Oily Skin QWEN V2-GMR.safetensors",
104
+ "PillowHump_2509": "loras/PillowHump_2509.safetensors",
105
+ "PutItHere_V2": "loras/Put it here_Qwen edit_V2.0.safetensors",
106
+ "PutItHere_V01": "loras/put it here_QwenEdit_V0.1.safetensors",
107
+ "Qwen4Play_v2": "loras/Qwen4Play_v2.safetensors",
108
+ "QwenHentai_v3": "loras/QwenImageHentaiPIV_v3.1.safetensors",
109
+ "Qwen_Helm": "loras/Qwen-Image-Helm_v0.1.safetensors",
110
+ "Qwen_NSFW_Beta1": "loras/Qwen-NSFW.safetensors",
111
+ "Qwen_NSFW_Beta2": "loras/Qwen-NSFW-Beta2.safetensors",
112
+ "Qwen_NSFW_Beta4": "loras/Qwen-NSFW-Beta4.safetensors",
113
+ "Qwen_NSFW_Beta5": "loras/Qwen-NSFW-Beta5.safetensors",
114
+ "Qwen_Real_Nud3s": "loras/Qwen_Real_Nud3s.safetensors",
115
+ "Qwen_Real_PS": "loras/Qwen-Real PS_v1_83K.safetensors",
116
+ "QwenSnofs_v1": "loras/qwen_snofs.safetensors",
117
+ "QwenSnofs_v1_1": "loras/QwenSnofs1_1.safetensors",
118
+ "Real_Breast_Nipples": "loras/Real Breast Nipples-QWEN-[rbn]-GMR.safetensors",
119
+ "SendDudes": "loras/[QWEN] SendDudes.safetensors",
120
+ "SendNudesLite": "loras/SendNudesLite (Qwen).safetensors",
121
+ "SendNudesPro_Beta": "loras/[QWEN] Send Nudes Pro - Beta v1.safetensors",
122
+ "Ultimate_Breast_Nipples": "loras/Ultimate Realistic Breast NIPPLES-QWEN-[rab]-GMR.safetensors",
123
+ "ass_up_QWEN": "loras/ass_up_QWEN.safetensors",
124
+ "barbell_nipples_QWEN": "loras/QWEN_jtn_barbell.safetensors",
125
+ "bfs_v2_face": "loras-sfw/face_swap_5500_qwen_image_edit_2509_v1.safetensors",
126
+ "bfs_v2_focus_face": "loras-sfw/bfs_v2_000005000.safetensors",
127
+ "bfs_v2_head": "loras-sfw/bfs_v2_head_000007000.safetensors",
128
+ "big_nipples_QWEN": "loras/big_nipples_QWEN.safetensors",
129
+ "bumpynipples": "loras/bumpynipples1.safetensors",
130
+ "cmslt_cum_on_her": "loras/cmslt_2509_2.safetensors",
131
+ "consistence_edit_v1": "loras-2/consistence_edit_v1.safetensors",
132
+ "consistence_edit_v2": "loras2/consistence_edit_v2.safetensors",
133
+ "d33p7hroa7": "loras/d33p7hroa7_qwen.safetensors",
134
+ "d1ck_p3n1s_V1_1": "loras/qwen-image_d!ck_P3N1S_LoRA_V1.1.safetensors",
135
+ "goblin_anal_v1": "loras/goblin_anal_v1_qwen.safetensors",
136
+ "horseshoe_nipple_rings": "loras/horseshoe_nipple_rings_QWEN.safetensors",
137
+ "jib_nudity_fixer": "loras/jib_qwen_fix_000002750.safetensors",
138
+ "jillin": "loras/jillin1.safetensors",
139
+ "male_nude": "loras/lora_nudenan_v1.safetensors",
140
+ "milk_juggs": "loras/milk_juggs_QWEN.safetensors",
141
+ "n00d_b": "loras/n00d-b-qwen.safetensors",
142
+ "nsfw_adv_v1": "loras/qwen-image_nsfw_adv_v1.0.safetensors",
143
+ "p0ssy_lora_v1": "loras/p0ssy_lora_v1.safetensors",
144
+ "p3nis": "loras/p3nis.safetensors",
145
+ "qwen_MCNL": "loras/qwen_MCNL_v1.0.safetensors",
146
+ "qwen_PENISLORA": "loras/qwen-PENISLORA.safetensors",
147
+ "qwen_hand_grab": "loras/qwen_hand_grab_6000s.safetensors",
148
+ "qwen_uncensor": "loras/qwen_uncensor_000014928.safetensors",
149
+ "reclining_nude": "loras/reclining_nude_v1_000003500.safetensors",
150
+ "remove_clothing": "loras/qwen_image_edit_remove-clothing_v1.0.safetensors",
151
+ "royal_treatment_V3": "loras/royal+treatment+V3.safetensors",
152
+ "sabi_character": "loras-2/sabi_character_v1.safetensors",
153
+ "snapchat_selfie": "loras/qwen_image_snapchat.safetensors",
154
+ "uka_qwen": "loras/uka_1_qwen.safetensors",
155
+ "ultimate_realistic_breast":"loras/ultimate realistic breast.safetensors",
156
+ }
157
+
158
+ LORA_TRIGGER_WORDS = {
159
+ "remove_clothing": "remove her clothing",
160
+ "Qwen_Real_Nud3s": "nud3",
161
+ "HearmemanAI_V4_Breasts": "large breasts, hard nipples, erect nipples",
162
+ "bfs_v2_head": "head swap, transfer head from image 1 to image 2",
163
+ "bfs_v2_face": "keep the face consistent, preserve facial identity",
164
+ "bfs_v2_focus_face": "head swap from Image 1 to Image 2",
165
+ "OilySkin_V2": "oilski",
166
+ "MysticXXX": "nsfw",
167
+ "SendNudesLite": "nude",
168
+ }
169
+
170
+ LOADED_ADAPTERS: set[str] = set()
171
+
172
+ # ── Helpers ────────────────────────────────────────────────────────────────────
173
+ def append_triggers(current_prompt: str, lora_name: str) -> str:
174
+ if lora_name == "None":
175
+ return current_prompt
176
+ triggers = LORA_TRIGGER_WORDS.get(lora_name, "")
177
+ if not triggers:
178
+ return current_prompt
179
+ existing = {w.strip().lower() for w in current_prompt.replace(",", " ").split()}
180
+ new_words = [w.strip() for w in triggers.split(",")
181
+ if w.strip().lower() not in existing and w.strip()]
182
+ if not new_words:
183
+ return current_prompt
184
+ sep = ", " if current_prompt.strip() else ""
185
+ return current_prompt.rstrip(", ") + sep + ", ".join(new_words)
186
+
187
+
188
+ def load_and_apply_stack(extra_adapters: list[str], extra_weights: list[float]):
189
+ if not extra_adapters:
190
+ pipe.disable_lora()
191
+ return [], []
192
+
193
+ loaded, weights_out = [], []
194
+ for name, weight in zip(extra_adapters, extra_weights):
195
+ if name not in LORA_CONFIGS:
196
+ continue
197
+ if name not in LOADED_ADAPTERS:
198
+ try:
199
+ pipe.load_lora_weights(LORA_REPO, weight_name=LORA_CONFIGS[name], adapter_name=name)
200
+ LOADED_ADAPTERS.add(name)
201
+ except Exception as e:
202
+ print(f"WARNING: Failed to load LoRA '{name}': {e}")
203
+ continue
204
+ loaded.append(name)
205
+ weights_out.append(weight)
206
+
207
+ if loaded:
208
+ pipe.enable_lora()
209
+ pipe.set_adapters(loaded, adapter_weights=weights_out)
210
+ else:
211
+ pipe.disable_lora()
212
+ return loaded, weights_out
213
+
214
+
215
+ # ── Inference ──────────────────────────────────────────────────────────────────
216
+ MAX_SEED = np.iinfo(np.int32).max
217
+ NEGATIVE_PROMPT = (
218
+ "worst quality, low quality, bad anatomy, bad hands, text, error, "
219
+ "missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, "
220
+ "signature, watermark, username, blurry"
221
+ )
222
+
223
+
224
+ @spaces.GPU(duration=300)
225
+ def infer_batch(
226
+ input_images,
227
+ prompt,
228
+ seed,
229
+ randomize_seed,
230
+ guidance_scale,
231
+ steps,
232
+ *lora_params,
233
+ progress=gr.Progress(track_tqdm=True),
234
+ ):
235
+ """Process multiple images with the same prompt, one at a time."""
236
+ gc.collect()
237
+ torch.cuda.empty_cache()
238
+ LOADED_ADAPTERS.clear()
239
+
240
+ if not input_images:
241
+ raise gr.Error("Please upload at least one image.")
242
+
243
+ # Parse LoRA params
244
+ extra_adapters, extra_weights = [], []
245
+ for i in range(0, len(lora_params), 2):
246
+ name, strength = lora_params[i], lora_params[i + 1]
247
+ if name != "None" and float(strength) > 0.05:
248
+ extra_adapters.append(name)
249
+ extra_weights.append(float(strength))
250
+
251
+ loaded_adapters, _ = load_and_apply_stack(extra_adapters, extra_weights)
252
+
253
+ results = []
254
+ current_seed = seed
255
+
256
+ for idx, img_path in enumerate(progress.tqdm(input_images, desc="Processing images")):
257
+ # Load image
258
+ if isinstance(img_path, str):
259
+ image = Image.open(img_path).convert("RGB")
260
+ elif isinstance(img_path, Image.Image):
261
+ image = img_path.convert("RGB")
262
+ else:
263
+ image = Image.open(img_path).convert("RGB")
264
+
265
+ # Validate aspect ratio
266
+ w, h = image.size
267
+ ratio = max(w, h) / max(min(w, h), 1)
268
+ if ratio > 4.0:
269
+ print(f"Skipping image {idx+1}: aspect ratio too extreme ({w}x{h})")
270
+ continue
271
+
272
+ if randomize_seed:
273
+ current_seed = random.randint(0, MAX_SEED)
274
+
275
+ generator = torch.Generator(device=device).manual_seed(current_seed)
276
+
277
+ try:
278
+ result = pipe(
279
+ image=image,
280
+ prompt=prompt,
281
+ negative_prompt=NEGATIVE_PROMPT if guidance_scale > 1.0 else None,
282
+ num_inference_steps=steps,
283
+ generator=generator,
284
+ true_cfg_scale=guidance_scale,
285
+ ).images[0]
286
+ results.append(result)
287
+ except torch.cuda.OutOfMemoryError:
288
+ gc.collect()
289
+ torch.cuda.empty_cache()
290
+ print(f"OOM on image {idx+1}, skipping...")
291
+ continue
292
+ except Exception as e:
293
+ print(f"Error on image {idx+1}: {e}")
294
+ continue
295
+
296
+ # Clean between images
297
+ gc.collect()
298
+ torch.cuda.empty_cache()
299
+
300
+ # Cleanup
301
+ if loaded_adapters:
302
+ pipe.disable_lora()
303
+ gc.collect()
304
+ torch.cuda.empty_cache()
305
+
306
+ if not results:
307
+ raise gr.Error("All images failed to process.")
308
+
309
+ # Create zip file for download
310
+ zip_path = create_zip(results)
311
+
312
+ return results, zip_path, f"βœ… Successfully processed {len(results)}/{len(input_images)} images"
313
+
314
+
315
+ def create_zip(images: list) -> str:
316
+ """Create a zip file of all processed images for download."""
317
+ tmp_dir = tempfile.mkdtemp()
318
+ zip_path = os.path.join(tmp_dir, "edited_images.zip")
319
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
320
+ for i, img in enumerate(images):
321
+ img_path = os.path.join(tmp_dir, f"edited_{i+1:03d}.png")
322
+ img.save(img_path, "PNG")
323
+ zf.write(img_path, f"edited_{i+1:03d}.png")
324
+ return zip_path
325
+
326
+
327
+ # ── UI ─────────────────────────────────────────────────────────────────────────
328
+ css = """
329
+ #col-container { margin: 0 auto; max-width: 1100px; }
330
+ #main-title h1 { font-size: 2.1em !important; }
331
+ """
332
+
333
+ LORA_NAMES = ["None"] + sorted(LORA_CONFIGS.keys())
334
+
335
+ with gr.Blocks(css=css, theme=steel_blue_theme) as demo:
336
+ with gr.Column(elem_id="col-container"):
337
+ gr.Markdown("# πŸ–ΌοΈ **Qwen Image Edit β€” Batch Processing**", elem_id="main-title")
338
+ gr.Markdown(
339
+ "Upload **multiple images**, apply the **same prompt** to all of them, "
340
+ "view results in a gallery, and **download all as a ZIP**.\n\n"
341
+ "Base: `Qwen-Image-Edit-2509` + `Qwen-Image-Edit-Rapid-AIO-V4` | Optional LoRA stacking"
342
+ )
343
+
344
+ with gr.Row():
345
+ # LEFT: Inputs
346
+ with gr.Column(scale=1):
347
+ input_images = gr.File(
348
+ label="πŸ“ Upload Images (select multiple)",
349
+ file_count="multiple",
350
+ file_types=["image"],
351
+ type="filepath",
352
+ )
353
+ prompt = gr.Textbox(
354
+ label="✏️ Edit Prompt (applied to ALL images)",
355
+ placeholder="e.g. remove background, change hair color to blonde, add sunglasses...",
356
+ lines=3,
357
+ )
358
+ run_button = gr.Button("πŸš€ Process All Images", variant="primary", size="lg")
359
+
360
+ with gr.Accordion("βž• Extra LoRAs (optional)", open=False):
361
+ gr.Markdown("Stack up to 4 LoRAs.")
362
+ lora_stack = []
363
+ for i in range(4):
364
+ with gr.Row():
365
+ dd = gr.Dropdown(choices=LORA_NAMES, value="None", label=f"LoRA {i+1}", scale=3)
366
+ sl = gr.Slider(0.0, 1.5, value=0.75, step=0.05, label="Strength", scale=2)
367
+ lora_stack.extend([dd, sl])
368
+
369
+ with gr.Accordion("βš™οΈ Advanced Settings", open=False):
370
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
371
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
372
+ guidance_scale = gr.Slider(label="CFG Scale", minimum=1.0, maximum=5.0, step=0.1, value=1.0)
373
+ steps = gr.Slider(label="Steps", minimum=1, maximum=30, step=1, value=4)
374
+
375
+ # RIGHT: Outputs
376
+ with gr.Column(scale=1):
377
+ status_text = gr.Markdown("*Upload images and click Process to start*")
378
+ output_gallery = gr.Gallery(
379
+ label="πŸ“Έ Processed Results",
380
+ columns=2,
381
+ rows=3,
382
+ height="auto",
383
+ object_fit="contain",
384
+ show_download_button=True,
385
+ )
386
+ download_zip = gr.File(label="⬇️ Download All (ZIP)", visible=True)
387
+
388
+ # Wire up the batch processing
389
+ run_button.click(
390
+ fn=infer_batch,
391
+ inputs=[input_images, prompt, seed, randomize_seed, guidance_scale, steps] + lora_stack,
392
+ outputs=[output_gallery, download_zip, status_text],
393
+ )
394
+
395
+ # Auto-fill trigger words when LoRA selected
396
+ for i in range(0, len(lora_stack), 2):
397
+ lora_stack[i].change(fn=append_triggers, inputs=[prompt, lora_stack[i]], outputs=[prompt])
398
+
399
+ if __name__ == "__main__":
400
+ demo.queue(max_size=10).launch(ssr_mode=False, show_error=True)