9ine commited on
Commit
8595af2
·
0 Parent(s):

Initial commit: Forked and cleaned from FLUX.2-Klein-LoRA-Studio

Browse files
.gitattributes ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/1.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/cloth.jpg filter=lfs diff=lfs merge=lfs -text
38
+ examples/mc.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/Snow-Klein-consistency.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/C.jpg filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FLUX.2 Klein LoRA Studio
3
+ emoji: 🥚
4
+ colorFrom: indigo
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 6.9.0
8
+ app_file: app.py
9
+ pinned: true
10
+ license: apache-2.0
11
+ short_description: Demo of a Collection of FLUX.2-Klein Model LoRAs
12
+ ---
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import gradio as gr
4
+ import numpy as np
5
+ import spaces
6
+ import torch
7
+ import random
8
+ from PIL import Image
9
+ from typing import Iterable
10
+
11
+ from diffusers import Flux2KleinPipeline
12
+ from diffusers.utils import load_image
13
+ from huggingface_hub import hf_hub_download
14
+
15
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
+
17
+ from gradio.themes import Soft
18
+ from gradio.themes.utils import colors, fonts, sizes
19
+
20
+ colors.orange_red = colors.Color(
21
+ name="orange_red", c50="#FFF0E5", c100="#FFE0CC", c200="#FFC299", c300="#FFA366",
22
+ c400="#FF8533", c500="#FF4500", c600="#E63E00", c700="#CC3700", c800="#B33000",
23
+ c900="#992900", c950="#802200",
24
+ )
25
+
26
+ class OrangeRedTheme(Soft):
27
+ def __init__(
28
+ self, *, primary_hue: colors.Color | str = colors.gray,
29
+ secondary_hue: colors.Color | str = colors.orange_red,
30
+ neutral_hue: colors.Color | str = colors.slate, 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, neutral_hue=neutral_hue,
40
+ text_size=text_size, font=font, font_mono=font_mono,
41
+ )
42
+ super().set(
43
+ background_fill_primary="*primary_50",
44
+ background_fill_primary_dark="*primary_900",
45
+ body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
46
+ body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
47
+ button_primary_text_color="white",
48
+ button_primary_text_color_hover="white",
49
+ button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
50
+ button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
51
+ button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
52
+ button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
53
+ slider_color="*secondary_500",
54
+ slider_color_dark="*secondary_600",
55
+ block_title_text_weight="600", block_border_width="3px",
56
+ block_shadow="*shadow_drop_lg", button_primary_shadow="*shadow_drop_lg",
57
+ button_large_padding="11px", color_accent_soft="*primary_100",
58
+ block_label_background_fill="*primary_200",
59
+ )
60
+
61
+ orange_red_theme = OrangeRedTheme()
62
+ MAX_SEED = np.iinfo(np.int32).max
63
+
64
+ # Face Swap Prompt Template
65
+ 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.
66
+ FROM PICTURE 1 (strictly preserve):
67
+ - Scene: lighting conditions, shadows, highlights, color temperature, environment, background
68
+ - Head positioning: exact rotation angle, tilt, direction the head is facing
69
+ - Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion
70
+ FROM PICTURE 2 (strictly preserve identity):
71
+ - Facial structure: face shape, bone structure, jawline, chin
72
+ - All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows
73
+ - Hair: color, style, texture, hairline
74
+ - Skin: texture, tone, complexion
75
+ 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."""
76
+
77
+ LORA_STYLES = [
78
+ {
79
+ "image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/image.webp",
80
+ "title": "None",
81
+ "adapter_name": None,
82
+ "repo": None,
83
+ "weights": None,
84
+ "default_prompt": None
85
+ },
86
+ {
87
+ "image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/Snow-Klein-consistency.png",
88
+ "title": "Klein-Consistency",
89
+ "adapter_name": "klein-consistency",
90
+ "repo": "dx8152/Flux2-Klein-9B-Consistency",
91
+ "weights": "Klein-consistency.safetensors",
92
+ "default_prompt": None
93
+ },
94
+ {
95
+ "image": "https://huggingface.co/linoyts/Flux2-Klein-Delight-LoRA/resolve/main/image_3.png",
96
+ "title": "Klein-Delight-Style",
97
+ "adapter_name": "klein-delight",
98
+ "repo": "linoyts/Flux2-Klein-Delight-LoRA",
99
+ "weights": "pytorch_lora_weights.safetensors",
100
+ "default_prompt": None
101
+ },
102
+ {
103
+ "image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/face-swap.jpg",
104
+ "title": "Best-Face-Swap",
105
+ "adapter_name": "face-swap",
106
+ "repo": "Alissonerdx/BFS-Best-Face-Swap",
107
+ "weights": "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors",
108
+ "default_prompt": FACE_SWAP_PROMPT
109
+ },
110
+ {
111
+ "image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/mc.png",
112
+ "title": "Ghost-Mannequin",
113
+ "adapter_name": "ghost-mannequin",
114
+ "repo": "nhathoangfoto/FLUX.2-klein-ghost-mannequin",
115
+ "weights": "3D-GhosMannequinRank-256_000005000.safetensors",
116
+ "default_prompt": None
117
+ },
118
+ ]
119
+
120
+ LOADED_ADAPTERS = set()
121
+
122
+ print("Loading FLUX.2 Klein 9B model base...")
123
+ pipe = Flux2KleinPipeline.from_pretrained(
124
+ "black-forest-labs/FLUX.2-klein-9B",
125
+ torch_dtype=torch.bfloat16,
126
+ ).to(device)
127
+ print("Base Model loaded successfully.")
128
+
129
+ def update_dimensions_on_upload(image):
130
+ """Resizes image to be divisible by 16 to avoid tensor mismatch errors in FLUX."""
131
+ if image is None:
132
+ return 1024, 1024
133
+
134
+ original_width, original_height = image.size
135
+
136
+ scale = min(1024 / original_width, 1024 / original_height)
137
+ new_width = int(original_width * scale)
138
+ new_height = int(original_height * scale)
139
+
140
+ new_width = (new_width // 16) * 16
141
+ new_height = (new_height // 16) * 16
142
+
143
+ return new_width, new_height
144
+
145
+ def process_gallery_images(images):
146
+ """Process images from gallery input and return list of PIL images."""
147
+ if not images:
148
+ return []
149
+
150
+ pil_images = []
151
+ for item in images:
152
+ try:
153
+ if isinstance(item, tuple) or isinstance(item, list):
154
+ path_or_img = item[0]
155
+ else:
156
+ path_or_img = item
157
+
158
+ if isinstance(path_or_img, str):
159
+ pil_images.append(Image.open(path_or_img).convert("RGB"))
160
+ elif isinstance(path_or_img, Image.Image):
161
+ pil_images.append(path_or_img.convert("RGB"))
162
+ else:
163
+ pil_images.append(Image.open(path_or_img.name).convert("RGB"))
164
+ except Exception as e:
165
+ print(f"Skipping invalid image item: {e}")
166
+ continue
167
+
168
+ return pil_images
169
+
170
+ def get_style_by_name(name):
171
+ """Retrieve the style dictionary by its title."""
172
+ for style in LORA_STYLES:
173
+ if style["title"] == name:
174
+ return style
175
+ return LORA_STYLES[0] # Default to None
176
+
177
+ def update_style_selection(evt: gr.SelectData):
178
+ """Update selected style based on gallery click."""
179
+ selected_style = LORA_STYLES[evt.index]
180
+ default_prompt = selected_style.get("default_prompt", None)
181
+ # Return the title string and optional prompt update
182
+ return selected_style["title"], default_prompt if default_prompt else gr.update()
183
+
184
+ def update_style_info(style_name):
185
+ """Update the info text based on the selected style name."""
186
+ return f"### Selected: {style_name} ✅"
187
+
188
+ def get_image_count_info(images):
189
+ """Return info about uploaded images"""
190
+ if not images:
191
+ return "📷 No images uploaded"
192
+
193
+ count = len(images)
194
+ if count == 1:
195
+ return "📷 1 image uploaded (Picture 1 - Base)"
196
+ elif count == 2:
197
+ return "📷 2 images uploaded (Picture 1 - Base, Picture 2 - Face Source)"
198
+ else:
199
+ return f"📷 {count} images uploaded"
200
+
201
+ @spaces.GPU
202
+ def infer(
203
+ input_images,
204
+ prompt,
205
+ style_name,
206
+ seed=42,
207
+ randomize_seed=True,
208
+ guidance_scale=1.0,
209
+ steps=4,
210
+ progress=gr.Progress(track_tqdm=True)
211
+ ):
212
+ gc.collect()
213
+ torch.cuda.empty_cache()
214
+
215
+ if not input_images:
216
+ raise gr.Error("Please upload at least one image to apply a style to.")
217
+
218
+ # Process gallery images
219
+ pil_images = process_gallery_images(input_images)
220
+
221
+ if not pil_images:
222
+ raise gr.Error("Could not process uploaded images.")
223
+
224
+ # Find the selected style configuration
225
+ selected_style = get_style_by_name(style_name)
226
+
227
+ # Check if Face Swap is selected and validate image count
228
+ if selected_style["adapter_name"] == "face-swap":
229
+ if len(pil_images) < 2:
230
+ raise gr.Error("Face Swap requires exactly 2 images: Picture 1 (base/body) and Picture 2 (face source). Please upload 2 images.")
231
+ elif len(pil_images) > 2:
232
+ gr.Warning("Face Swap uses only the first 2 images. Additional images will be ignored.")
233
+ pil_images = pil_images[:2]
234
+
235
+ if selected_style["adapter_name"] is None:
236
+ print("Selection is None. Disabling LoRA adapters.")
237
+ pipe.disable_lora()
238
+ else:
239
+ adapter_name = selected_style["adapter_name"]
240
+
241
+ if adapter_name not in LOADED_ADAPTERS:
242
+ print(f"--- Downloading and Loading Adapter: {selected_style['title']} ---")
243
+ try:
244
+ pipe.load_lora_weights(
245
+ selected_style["repo"],
246
+ weight_name=selected_style["weights"],
247
+ adapter_name=adapter_name
248
+ )
249
+ LOADED_ADAPTERS.add(adapter_name)
250
+ except Exception as e:
251
+ raise gr.Error(f"Failed to load adapter {selected_style['title']}: {e}")
252
+ else:
253
+ print(f"--- Adapter {selected_style['title']} is already loaded. ---")
254
+
255
+ print(f"Activating LoRA: {adapter_name}")
256
+ pipe.set_adapters([adapter_name], adapter_weights=[1.0])
257
+
258
+ if randomize_seed:
259
+ seed = random.randint(0, MAX_SEED)
260
+
261
+ # Get dimensions from first image
262
+ width, height = update_dimensions_on_upload(pil_images[0])
263
+
264
+ # Process all images to the same dimensions
265
+ processed_images = [
266
+ img.resize((width, height), Image.LANCZOS).convert("RGB")
267
+ for img in pil_images
268
+ ]
269
+
270
+ try:
271
+ # Pass single image or list based on count
272
+ image_input = processed_images if len(processed_images) > 1 else processed_images[0]
273
+
274
+ image = pipe(
275
+ image=image_input,
276
+ prompt=prompt,
277
+ guidance_scale=guidance_scale,
278
+ width=width,
279
+ height=height,
280
+ num_inference_steps=steps,
281
+ generator=torch.Generator(device=device).manual_seed(seed),
282
+ ).images[0]
283
+
284
+ return image, seed
285
+
286
+ except Exception as e:
287
+ raise gr.Error(f"Inference failed: {e}")
288
+ finally:
289
+ gc.collect()
290
+ torch.cuda.empty_cache()
291
+
292
+ @spaces.GPU
293
+ def infer_example(input_images, prompt, style_name):
294
+ if not input_images:
295
+ return None, 0
296
+
297
+ # Handle examples where inputs might be paths
298
+ if isinstance(input_images, str):
299
+ input_images = [input_images]
300
+
301
+ image, seed = infer(
302
+ input_images=input_images,
303
+ prompt=prompt,
304
+ style_name=style_name,
305
+ seed=0,
306
+ randomize_seed=True,
307
+ guidance_scale=1.0,
308
+ steps=4
309
+ )
310
+ return image, seed
311
+
312
+ css = """
313
+ #col-container { margin: 0 auto; max-width: 960px; }
314
+ #main-title h1 { font-size: 2.4em !important; }
315
+ #style_gallery .grid-wrap { height: 10vh }
316
+ #input_gallery .grid-wrap { min-height: 200px }
317
+ """
318
+
319
+ with gr.Blocks() as demo:
320
+ with gr.Column(elem_id="col-container"):
321
+ gr.Markdown("# **FLUX.2-Klein-LoRA-Studio**", elem_id="main-title")
322
+ gr.Markdown("Perform diverse image edits using specialized [LoRAs](https://huggingface.co/models?other=base_model:adapter:black-forest-labs/FLUX.2-klein-9B) adapters for the [FLUX.2-Klein-Distilled](https://huggingface.co/black-forest-labs/FLUX.2-klein-9B) model. Open on [GitHub](https://github.com/PRITHIVSAKTHIUR/FLUX.2-Klein-LoRA-Studio)")
323
+
324
+ selected_style_name = gr.Textbox(value="None", visible=False, label="Selected Style Name")
325
+
326
+ with gr.Row(equal_height=True):
327
+ with gr.Column():
328
+ input_images = gr.Gallery(
329
+ label="Upload Images",
330
+ type="filepath",
331
+ columns=2,
332
+ rows=1,
333
+ height=290,
334
+ allow_preview=True,
335
+ elem_id="input_gallery"
336
+ )
337
+
338
+ with gr.Row():
339
+ prompt = gr.Text(
340
+ label="Edit Prompt",
341
+ max_lines=1,
342
+ show_label=True,
343
+ placeholder="e.g., a man with a red superhero mask"
344
+ )
345
+
346
+ run_button = gr.Button("Apply Style", variant="primary")
347
+
348
+ with gr.Accordion("Advanced Settings", open=False, visible=False):
349
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
350
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
351
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=0.0, maximum=10.0, step=0.1, value=1.0)
352
+ steps = gr.Slider(label="Steps", minimum=1, maximum=50, value=4, step=1)
353
+
354
+ with gr.Column():
355
+ output_image = gr.Image(label="Output Image", interactive=False, format="png", height=358)
356
+ used_seed = gr.Textbox(label="Used Seed", interactive=False, visible=False)
357
+
358
+ selected_style_info = gr.Markdown("### Selected: None (FLUX.2-klein-9B) ✅")
359
+
360
+ style_gallery = gr.Gallery(
361
+ [(item["image"], item["title"]) for item in LORA_STYLES],
362
+ label="Edit Style Gallery",
363
+ allow_preview=False,
364
+ columns=3,
365
+ elem_id="style_gallery",
366
+ )
367
+
368
+ gr.Examples(
369
+ examples=[
370
+ [
371
+ ["examples/C.jpg"],
372
+ "Change the weather to stormy.",
373
+ "Klein-Consistency"
374
+ ],
375
+ [
376
+ ["examples/2.jpg"],
377
+ "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.",
378
+ "Klein-Delight-Style"
379
+ ],
380
+ [
381
+ ["examples/1.jpg", "examples/2.jpg"],
382
+ FACE_SWAP_PROMPT,
383
+ "Best-Face-Swap"
384
+ ],
385
+ [
386
+ ["examples/1.jpg"],
387
+ "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed by prithivMLmods preserving realistic texture and details",
388
+ "None"
389
+ ],
390
+ [
391
+ ["examples/cloth.jpg"],
392
+ "3Dghostmannequin",
393
+ "Ghost-Mannequin"
394
+ ],
395
+ ],
396
+ inputs=[input_images, prompt, selected_style_name],
397
+ outputs=[output_image, used_seed],
398
+ fn=infer_example,
399
+ cache_examples=False,
400
+ label="Examples"
401
+ )
402
+
403
+ gr.Markdown("[*](https://huggingface.co/black-forest-labs/FLUX.2-klein-9B)This is still an experimental Space for FLUX.2-Klein-9B. More adapters will be added soon.")
404
+
405
+ input_images.change(
406
+ fn=get_image_count_info,
407
+ inputs=[input_images],
408
+ )
409
+
410
+ style_gallery.select(
411
+ fn=update_style_selection,
412
+ outputs=[selected_style_name, prompt]
413
+ )
414
+
415
+ selected_style_name.change(
416
+ fn=update_style_info,
417
+ inputs=[selected_style_name],
418
+ outputs=[selected_style_info]
419
+ )
420
+
421
+ run_button.click(
422
+ fn=infer,
423
+ inputs=[input_images, prompt, selected_style_name, seed, randomize_seed, guidance_scale, steps],
424
+ outputs=[output_image, used_seed]
425
+ )
426
+
427
+ if __name__ == "__main__":
428
+ demo.queue().launch(css=css, theme=orange_red_theme, mcp_server=True, ssr_mode=False, show_error=True)
examples/1.jpg ADDED

Git LFS Details

  • SHA256: 4c13fa9ae6a11e56020df6a2f4d4284915c4ca83b369c14f467521a84b59c165
  • Pointer size: 131 Bytes
  • Size of remote file: 265 kB
examples/2.jpg ADDED
examples/C.jpg ADDED

Git LFS Details

  • SHA256: 195095d2febe2b7cf7e7e54be1fd0ac035e349da6fe0f096d525289fcaac6df4
  • Pointer size: 131 Bytes
  • Size of remote file: 245 kB
examples/Snow-Klein-consistency.png ADDED

Git LFS Details

  • SHA256: 64ac154c872d105f49d10d82ec5d0cb00395d568b8bdeb6f657b7b366702e623
  • Pointer size: 131 Bytes
  • Size of remote file: 957 kB
examples/cloth.jpg ADDED

Git LFS Details

  • SHA256: 8944a3a13f848a973a94c56df6c7f34427b0655631b0dd60846b2743bd47e0ca
  • Pointer size: 131 Bytes
  • Size of remote file: 365 kB
examples/face-swap.jpg ADDED
examples/image.webp ADDED
examples/mc.png ADDED

Git LFS Details

  • SHA256: 149345408fdc6bbc485b78a5e0c312c43584b1611454e59616057dcf1811bb3c
  • Pointer size: 131 Bytes
  • Size of remote file: 698 kB
optimization.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces / ZeroGPU AoTI helper for Qwen image pipelines.
3
+
4
+ Usage:
5
+ from optimization_optimized import optimize_pipeline_
6
+
7
+ optimize_pipeline_(
8
+ pipe,
9
+ image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))],
10
+ prompt="warmup prompt",
11
+ num_inference_steps=4,
12
+ true_cfg_scale=1.0,
13
+ height=1024,
14
+ width=1024,
15
+ )
16
+
17
+ Design goals:
18
+ - Keep the same in-place API as your original optimize_pipeline_.
19
+ - Avoid crashing the Space if AoTI export/compile fails.
20
+ - Only mark transformer dimensions as dynamic when the captured call actually contains them.
21
+ - Keep FP8 quantization optional, because it can change quality and may break export on some setups.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from contextlib import nullcontext
28
+ from typing import Any, Callable, Mapping, Optional, ParamSpec
29
+
30
+ import torch
31
+ from torch.utils._pytree import tree_map
32
+
33
+ try:
34
+ import spaces
35
+ except Exception: # Local/dev environment without Hugging Face Spaces.
36
+ spaces = None # type: ignore[assignment]
37
+
38
+ try:
39
+ from torchao.quantization import Float8DynamicActivationFloat8WeightConfig, quantize_
40
+ except Exception:
41
+ Float8DynamicActivationFloat8WeightConfig = None # type: ignore[assignment]
42
+ quantize_ = None # type: ignore[assignment]
43
+
44
+
45
+ P = ParamSpec("P")
46
+ LOGGER = logging.getLogger(__name__)
47
+
48
+
49
+ # Qwen Image transformer forward usually has:
50
+ # hidden_states: [batch, image_seq, channels]
51
+ # encoder_hidden_states: [batch, text_seq, channels]
52
+ # encoder_hidden_states_mask: [batch, text_seq]
53
+ # image_rotary_emb: tuple(image_rotary_emb, text_rotary_emb)
54
+ TRANSFORMER_IMAGE_SEQ_LENGTH_DIM = torch.export.Dim("image_seq_length", min=1)
55
+ TRANSFORMER_TEXT_SEQ_LENGTH_DIM = torch.export.Dim("text_seq_length", min=1)
56
+
57
+ DEFAULT_TRANSFORMER_DYNAMIC_SHAPES: dict[str, Any] = {
58
+ "hidden_states": {1: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM},
59
+ "encoder_hidden_states": {1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM},
60
+ "encoder_hidden_states_mask": {1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM},
61
+ "image_rotary_emb": (
62
+ {0: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM},
63
+ {0: TRANSFORMER_TEXT_SEQ_LENGTH_DIM},
64
+ ),
65
+ }
66
+
67
+
68
+ # Conservative-but-fast defaults for ZeroGPU/H200 AoTI.
69
+ # You can override any key through optimize_pipeline_(..., inductor_configs={...}).
70
+ DEFAULT_INDUCTOR_CONFIGS: dict[str, Any] = {
71
+ "conv_1x1_as_mm": True,
72
+ "epilogue_fusion": False,
73
+ "coordinate_descent_tuning": True,
74
+ "coordinate_descent_check_all_directions": True,
75
+ "max_autotune": True,
76
+ "triton.cudagraphs": True,
77
+ }
78
+
79
+
80
+ def _has_zero_gpu_aoti() -> bool:
81
+ return (
82
+ spaces is not None
83
+ and hasattr(spaces, "GPU")
84
+ and hasattr(spaces, "aoti_capture")
85
+ and hasattr(spaces, "aoti_compile")
86
+ and hasattr(spaces, "aoti_apply")
87
+ )
88
+
89
+
90
+ def _supports_float8() -> bool:
91
+ """FP8 is mainly useful on Hopper-class GPUs such as H100/H200."""
92
+ if not torch.cuda.is_available():
93
+ return False
94
+ major, _minor = torch.cuda.get_device_capability()
95
+ return major >= 9
96
+
97
+
98
+ def _maybe_disable_progress_bar(pipeline: Callable[..., Any]) -> None:
99
+ """Avoid tqdm/progress side effects during graph capture."""
100
+ setter = getattr(pipeline, "set_progress_bar_config", None)
101
+ if callable(setter):
102
+ try:
103
+ setter(disable=True)
104
+ except Exception:
105
+ pass
106
+
107
+
108
+ def _build_dynamic_shapes(
109
+ captured_kwargs: Mapping[str, Any],
110
+ shape_specs: Optional[Mapping[str, Any]] = None,
111
+ ) -> dict[str, Any]:
112
+ """
113
+ Build a dynamic_shapes dict matching the captured transformer kwargs.
114
+
115
+ torch.export expects the dynamic_shapes tree to mirror args/kwargs. The original
116
+ code unconditionally OR'ed every known Qwen key into the captured tree. This
117
+ version only injects a dynamic spec when that key is present in the actual
118
+ captured transformer call, which makes the helper more robust across Qwen /
119
+ Diffusers versions.
120
+ """
121
+ dynamic_shapes = tree_map(lambda _leaf: None, dict(captured_kwargs))
122
+ specs = dict(shape_specs or DEFAULT_TRANSFORMER_DYNAMIC_SHAPES)
123
+
124
+ for name, spec in specs.items():
125
+ if name not in captured_kwargs:
126
+ continue
127
+
128
+ # image_rotary_emb is expected to be a pair. Skip if a future pipeline
129
+ # changes its structure instead of giving torch.export a mismatched tree.
130
+ if name == "image_rotary_emb":
131
+ value = captured_kwargs[name]
132
+ if not isinstance(value, (tuple, list)) or len(value) != 2:
133
+ LOGGER.warning(
134
+ "Skipping dynamic shape for image_rotary_emb: expected a 2-item tuple/list, got %s",
135
+ type(value).__name__,
136
+ )
137
+ continue
138
+
139
+ dynamic_shapes[name] = spec
140
+
141
+ return dynamic_shapes
142
+
143
+
144
+ def _maybe_quantize_transformer(transformer: torch.nn.Module, enable_float8: bool) -> None:
145
+ """
146
+ Optional in-place FP8 quantization.
147
+
148
+ Keep disabled by default:
149
+ - It can affect image quality.
150
+ - It requires torchao.
151
+ - It is most beneficial on H100/H200-class GPUs.
152
+ - If export fails after quantization, the module has still been mutated.
153
+ """
154
+ if not enable_float8:
155
+ return
156
+
157
+ if quantize_ is None or Float8DynamicActivationFloat8WeightConfig is None:
158
+ raise RuntimeError("enable_float8=True requires torchao to be installed.")
159
+
160
+ if not _supports_float8():
161
+ raise RuntimeError("enable_float8=True requires a Hopper-class CUDA GPU such as H100/H200.")
162
+
163
+ LOGGER.info("Applying experimental FP8 dynamic activation + FP8 weight quantization.")
164
+ quantize_(transformer, Float8DynamicActivationFloat8WeightConfig())
165
+
166
+
167
+ def optimize_pipeline_(
168
+ pipeline: Callable[P, Any],
169
+ *args: P.args,
170
+ duration: int = 1500,
171
+ inductor_configs: Optional[Mapping[str, Any]] = None,
172
+ dynamic_shape_specs: Optional[Mapping[str, Any]] = None,
173
+ enable_float8: bool = False,
174
+ strict_export: bool = False,
175
+ capture_autocast_dtype: Optional[torch.dtype] = None,
176
+ fail_silently: bool = True,
177
+ **kwargs: P.kwargs,
178
+ ) -> Callable[P, Any]:
179
+ """
180
+ Compile and apply AoTI to pipeline.transformer in-place.
181
+
182
+ Parameters:
183
+ pipeline:
184
+ Diffusers-style pipeline with a `.transformer` module.
185
+ *args, **kwargs:
186
+ Warmup call passed to the pipeline during AoTI capture.
187
+ Use representative image size, prompt length, step count, and guidance.
188
+ duration:
189
+ ZeroGPU allocation duration for compilation.
190
+ inductor_configs:
191
+ Optional overrides for DEFAULT_INDUCTOR_CONFIGS.
192
+ dynamic_shape_specs:
193
+ Optional overrides for dynamic-shape specs.
194
+ enable_float8:
195
+ Optional experimental torchao FP8 quantization before export.
196
+ strict_export:
197
+ Passed to torch.export.export(..., strict=...).
198
+ False is more permissive for complex model code.
199
+ capture_autocast_dtype:
200
+ Set to torch.bfloat16 or torch.float16 if your real inference path uses autocast.
201
+ Leave None when the pipeline/model is already loaded in the desired dtype.
202
+ fail_silently:
203
+ If True, log and keep the original transformer on compile failure.
204
+ If False, re-raise the error.
205
+
206
+ Returns:
207
+ The same pipeline object, mutated in-place if compile/apply succeeds.
208
+ """
209
+ if not _has_zero_gpu_aoti():
210
+ LOGGER.warning("Hugging Face Spaces AoTI APIs are unavailable; skipping optimization.")
211
+ return pipeline
212
+
213
+ transformer = getattr(pipeline, "transformer", None)
214
+ if transformer is None:
215
+ message = "pipeline has no `.transformer` attribute; cannot apply transformer AoTI."
216
+ if fail_silently:
217
+ LOGGER.warning(message)
218
+ return pipeline
219
+ raise AttributeError(message)
220
+
221
+ if isinstance(transformer, torch.nn.Module):
222
+ transformer.eval()
223
+
224
+ _maybe_disable_progress_bar(pipeline)
225
+
226
+ configs = dict(DEFAULT_INDUCTOR_CONFIGS)
227
+ if inductor_configs:
228
+ configs.update(inductor_configs)
229
+
230
+ # Define inside optimize_pipeline_ so it closes over the actual pipeline and warmup args.
231
+ @spaces.GPU(duration=duration) # type: ignore[union-attr]
232
+ def compile_transformer():
233
+ LOGGER.info("Capturing transformer call for AoTI export.")
234
+
235
+ autocast_ctx = (
236
+ torch.autocast(device_type="cuda", dtype=capture_autocast_dtype)
237
+ if capture_autocast_dtype is not None and torch.cuda.is_available()
238
+ else nullcontext()
239
+ )
240
+
241
+ with torch.inference_mode(), autocast_ctx:
242
+ with spaces.aoti_capture(transformer) as call: # type: ignore[union-attr]
243
+ pipeline(*args, **kwargs)
244
+
245
+ dynamic_shapes = _build_dynamic_shapes(call.kwargs, dynamic_shape_specs)
246
+
247
+ _maybe_quantize_transformer(transformer, enable_float8)
248
+
249
+ LOGGER.info("Exporting transformer with torch.export; strict=%s", strict_export)
250
+ exported = torch.export.export(
251
+ mod=transformer,
252
+ args=call.args,
253
+ kwargs=call.kwargs,
254
+ dynamic_shapes=dynamic_shapes,
255
+ strict=strict_export,
256
+ )
257
+
258
+ LOGGER.info("Compiling exported transformer with AoTI.")
259
+ return spaces.aoti_compile(exported, configs) # type: ignore[union-attr]
260
+
261
+ try:
262
+ compiled_transformer = compile_transformer()
263
+ spaces.aoti_apply(compiled_transformer, transformer) # type: ignore[union-attr]
264
+ LOGGER.info("AoTI transformer optimization applied successfully.")
265
+ except Exception as exc:
266
+ LOGGER.exception("AoTI transformer optimization failed.")
267
+ if not fail_silently:
268
+ raise
269
+ LOGGER.warning("Continuing with the original uncompiled transformer. Error: %s", exc)
270
+
271
+ return pipeline
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/huggingface/diffusers.git
2
+ transformers==4.57.6
3
+ huggingface_hub
4
+ sentencepiece
5
+ bitsandbytes
6
+ torchvision
7
+ accelerate
8
+ torchao
9
+ spaces
10
+ hf_xet
11
+ gradio
12
+ numpy
13
+ torch
14
+ peft
15
+ av