prithivMLmods commited on
Commit
efb5305
·
verified ·
1 Parent(s): 611377e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1134 -341
app.py CHANGED
@@ -5,91 +5,24 @@ import numpy as np
5
  import random
6
  import spaces
7
  import torch
 
 
 
 
 
8
  from diffusers import Flux2KleinPipeline, AutoencoderKLFlux2
9
  from PIL import Image
10
  from pathlib import Path
11
- import threading
12
  from typing import Iterable
13
- from gradio.themes import Soft
14
- from gradio.themes.utils import colors, fonts, sizes
15
-
16
- # ── Theme ─────────────────────────────────────────────────────────────────────
17
-
18
- colors.orange_red = colors.Color(
19
- name="orange_red",
20
- c50="#FFF0E5",
21
- c100="#FFE0CC",
22
- c200="#FFC299",
23
- c300="#FFA366",
24
- c400="#FF8533",
25
- c500="#FF4500",
26
- c600="#E63E00",
27
- c700="#CC3700",
28
- c800="#B33000",
29
- c900="#992900",
30
- c950="#802200",
31
- )
32
-
33
- class OrangeRedTheme(Soft):
34
- def __init__(
35
- self,
36
- *,
37
- primary_hue: colors.Color | str = colors.gray,
38
- secondary_hue: colors.Color | str = colors.orange_red,
39
- neutral_hue: colors.Color | str = colors.slate,
40
- text_size: sizes.Size | str = sizes.text_lg,
41
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
42
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
43
- ),
44
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
45
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
46
- ),
47
- ):
48
- super().__init__(
49
- primary_hue=primary_hue,
50
- secondary_hue=secondary_hue,
51
- neutral_hue=neutral_hue,
52
- text_size=text_size,
53
- font=font,
54
- font_mono=font_mono,
55
- )
56
- super().set(
57
- background_fill_primary="*primary_50",
58
- background_fill_primary_dark="*primary_900",
59
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
60
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
61
- button_primary_text_color="white",
62
- button_primary_text_color_hover="white",
63
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
64
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
65
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
66
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
67
- button_secondary_text_color="black",
68
- button_secondary_text_color_hover="white",
69
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
70
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
71
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
72
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
73
- slider_color="*secondary_500",
74
- slider_color_dark="*secondary_600",
75
- block_title_text_weight="600",
76
- block_border_width="3px",
77
- block_shadow="*shadow_drop_lg",
78
- button_primary_shadow="*shadow_drop_lg",
79
- button_large_padding="11px",
80
- color_accent_soft="*primary_100",
81
- block_label_background_fill="*primary_200",
82
- )
83
-
84
- orange_red_theme = OrangeRedTheme()
85
 
86
  # ── Config ────────────────────────────────────────────────────────────────────
87
 
88
- dtype = torch.bfloat16
89
- device = "cuda" if torch.cuda.is_available() else "cpu"
90
 
91
  MAX_SEED = np.iinfo(np.int32).max
92
  MAX_IMAGE_SIZE = 1024
 
93
 
94
  # ── Models ────────────────────────────────────────────────────────────────────
95
 
@@ -114,68 +47,166 @@ pipe_small_decoder = Flux2KleinPipeline.from_pretrained(
114
  )
115
  pipe_small_decoder.enable_model_cpu_offload()
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  # ── Helpers ───────────────────────────────────────────────────────────────────
118
 
119
- def update_dimensions_from_image(image_list):
120
- if image_list is None or len(image_list) == 0:
121
- return 1024, 1024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- item = image_list[0]
124
- img = item[0] if isinstance(item, tuple) else item
125
 
126
- if isinstance(img, str):
127
- img = Image.open(img).convert("RGB")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- iw, ih = img.size
130
- aspect_ratio = iw / ih
131
 
132
- if aspect_ratio >= 1:
133
- new_width = 1024
134
- new_height = int(1024 / aspect_ratio)
 
 
 
 
 
135
  else:
136
- new_height = 1024
137
- new_width = int(1024 * aspect_ratio)
138
-
139
- new_width = max(256, min(1024, round(new_width / 8) * 8))
140
- new_height = max(256, min(1024, round(new_height / 8) * 8))
141
- return new_width, new_height
142
-
143
-
144
- def parse_input_images(input_images):
145
- """Safely parse gallery / filepath / PIL inputs → list[PIL.Image] or None."""
146
- if input_images is None:
147
- return None
148
- if isinstance(input_images, str):
149
- return [Image.open(input_images).convert("RGB")] if os.path.exists(input_images) else None
150
- if isinstance(input_images, list) and len(input_images) > 0:
151
- parsed = []
152
- for item in input_images:
153
- try:
154
- src = item[0] if isinstance(item, tuple) else item
155
- if isinstance(src, str):
156
- parsed.append(Image.open(src).convert("RGB"))
157
- elif isinstance(src, Image.Image):
158
- parsed.append(src.convert("RGB"))
159
- elif hasattr(src, "name"):
160
- parsed.append(Image.open(src.name).convert("RGB"))
161
- except Exception as e:
162
- print(f"Skipping invalid image: {e}")
163
- return parsed or None
164
- return None
165
 
 
 
 
166
 
167
  # ── Inference ─────────────────────────────────────────────────────────────────
168
 
169
  @spaces.GPU(duration=240)
170
  def infer(
 
171
  prompt,
172
- input_images=None,
173
- seed=42,
174
- randomize_seed=False,
175
- width=1024,
176
- height=1024,
177
- num_inference_steps=4,
178
- guidance_scale=1.0,
179
  progress=gr.Progress(track_tqdm=True),
180
  ):
181
  gc.collect()
@@ -187,7 +218,11 @@ def infer(
187
  if randomize_seed:
188
  seed = random.randint(0, MAX_SEED)
189
 
190
- image_list = parse_input_images(input_images)
 
 
 
 
191
 
192
  shared_kwargs = dict(
193
  prompt=prompt,
@@ -199,249 +234,1007 @@ def infer(
199
  if image_list is not None:
200
  shared_kwargs["image"] = image_list
201
 
202
- # ── Step 1: Standard VAE ──────────────────────────────────────────────────
203
- progress(0.05, desc="🟦 Running Standard VAE generation...")
204
- print("Starting Standard VAE generation...")
205
-
206
- gen_std = torch.Generator(device="cpu").manual_seed(seed)
207
  out_standard = pipe_standard(**shared_kwargs, generator=gen_std).images[0]
208
-
209
- print("Standard VAE generation complete.")
210
- progress(0.55, desc="🟦 Standard VAE done — now running 🟩 Small Decoder VAE...")
211
 
212
  gc.collect()
213
  torch.cuda.empty_cache()
214
 
215
- # ── Step 2: Small Decoder VAE ─────────────────────────────────────────────
216
- print("Starting Small Decoder VAE generation...")
217
-
218
  gen_small = torch.Generator(device="cpu").manual_seed(seed)
219
  out_small = pipe_small_decoder(**shared_kwargs, generator=gen_small).images[0]
220
-
221
- print("Small Decoder VAE generation complete.")
222
- progress(1.0, desc="✅ Both generations complete!")
223
 
224
  gc.collect()
225
  torch.cuda.empty_cache()
226
 
227
- return out_standard, out_small, seed
 
228
 
 
 
 
 
 
 
 
 
229
 
230
- @spaces.GPU(duration=240)
231
- def infer_example(images, prompt):
232
- if not images:
233
- images_list = None
234
- elif isinstance(images, str):
235
- images_list = [images]
236
- else:
237
- images_list = images
238
-
239
- out_std, out_small, seed_used = infer(
240
- prompt=prompt,
241
- input_images=images_list,
242
- seed=0,
243
- randomize_seed=True,
244
- width=1024,
245
- height=1024,
246
- num_inference_steps=4,
247
- guidance_scale=1.0,
248
- )
249
- return out_std, out_small, seed_used
250
 
251
 
252
  # ── CSS ────────────────────��──────────────────────────────────────────────────
253
 
254
- css = """
255
- #col-container {
256
- margin: 0 auto;
257
- max-width: 1000px;
258
- }
259
- #main-title h1 {
260
- font-size: 2.4em !important;
261
- }
262
- .vae-badge {
263
- font-weight: 700;
264
- font-size: 0.95em;
265
- text-align: center;
266
- padding: 4px 16px;
267
- border-radius: 20px;
268
- display: block;
269
- margin-bottom: 6px;
270
- }
271
- .output-order-note {
272
- text-align: center;
273
- font-size: 0.85em;
274
- color: #777;
275
- font-style: italic;
276
- margin-top: 2px;
277
- margin-bottom: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
  """
280
 
281
- # ── UI ────────────────────────────────────────────────────────────────────────
282
-
283
- with gr.Blocks() as demo:
284
-
285
- with gr.Column(elem_id="col-container"):
286
-
287
- gr.Markdown(
288
- "# **Flux.2-4B-Encoder-Comparator**",
289
- elem_id="main-title",
290
- )
291
- gr.Markdown(
292
- "Compare **FLUX.2-klein-4B** side-by-side with two VAE decoders — generated **sequentially** from the **same seed**.\n\n"
293
- "🟦 **Standard VAE** is generated **first**, then 🟩 **Small Decoder VAE** ([FLUX.2-small-decoder](https://huggingface.co/black-forest-labs/FLUX.2-small-decoder)) · "
294
- "[[model](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B)]"
295
- )
296
-
297
- # ── Main two-column row ───────────────────────────────────────────────
298
- with gr.Row(equal_height=True):
299
-
300
- # ── Left: inputs ─────────────────────────────────────────────────
301
- with gr.Column():
302
- input_images = gr.Gallery(
303
- label="Input Image(s) for Editing (optional)",
304
- type="filepath",
305
- columns=2,
306
- rows=1,
307
- height=300,
308
- allow_preview=True,
309
- )
310
-
311
- prompt = gr.Text(
312
- label="Edit Prompt",
313
- show_label=True,
314
- placeholder="e.g., Transform the scene into a snowy winter day...",
315
- )
316
-
317
- run_button = gr.Button("⚡ Run Comparison", variant="primary")
318
-
319
- # ── Right: outputs ────────────────────────────────────────────────
320
- with gr.Column():
321
- with gr.Row():
322
-
323
- # Standard VAE — Generated First
324
- with gr.Column():
325
- gr.HTML(
326
- '<span class="vae-badge" '
327
- 'style="background:#FFE0CC;color:#CC3700;">'
328
- '🟦 Standard VAE</span>'
329
- '<p class="output-order-note">① Generated First</p>'
330
- )
331
- result_standard = gr.Image(
332
- label="Standard VAE",
333
- show_label=False,
334
- interactive=False,
335
- format="png",
336
- height=280,
337
- )
338
-
339
- # Small Decoder VAE — Generated Second
340
- with gr.Column():
341
- gr.HTML(
342
- '<span class="vae-badge" '
343
- 'style="background:#FFF0E5;color:#E63E00;">'
344
- '🟩 Small Decoder VAE</span>'
345
- '<p class="output-order-note">② Generated Second</p>'
346
- )
347
- result_small = gr.Image(
348
- label="Small Decoder VAE",
349
- show_label=False,
350
- interactive=False,
351
- format="png",
352
- height=280,
353
- )
354
-
355
- with gr.Accordion("Advanced Settings", open=False):
356
- seed_output = gr.Number(label="Seed Used", precision=0)
357
- seed = gr.Slider(
358
- label="Seed",
359
- minimum=0,
360
- maximum=MAX_SEED,
361
- step=1,
362
- value=0,
363
- )
364
- randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
365
-
366
- with gr.Row():
367
- width = gr.Slider(
368
- label="Width",
369
- minimum=256,
370
- maximum=MAX_IMAGE_SIZE,
371
- step=8,
372
- value=1024,
373
- )
374
- height_slider = gr.Slider(
375
- label="Height",
376
- minimum=256,
377
- maximum=MAX_IMAGE_SIZE,
378
- step=8,
379
- value=1024,
380
- )
381
-
382
- with gr.Row():
383
- num_inference_steps = gr.Slider(
384
- label="Inference Steps",
385
- minimum=1,
386
- maximum=20,
387
- step=1,
388
- value=4,
389
- )
390
- guidance_scale = gr.Slider(
391
- label="Guidance Scale",
392
- minimum=0.0,
393
- maximum=10.0,
394
- step=0.1,
395
- value=1.0,
396
- )
397
-
398
- # ── Examples ──────────────────────────────────────────────────────────
399
- gr.Examples(
400
- examples=[
401
- [["examples/1.jpg"], "Change the weather to stormy."],
402
- [["examples/2.jpg"], "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition."],
403
- [["examples/3.jpg"], "Relight the image with soft golden sunset lighting while keeping all structures and subject details consistent."],
404
- [["examples/4.jpg"], "Make the texture high-resolution."],
405
- ],
406
- inputs=[input_images, prompt],
407
- outputs=[result_standard, result_small, seed_output],
408
- fn=infer_example,
409
- cache_examples=False,
410
- label="Examples",
411
- )
412
-
413
- gr.Markdown(
414
- "[*](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) "
415
- "Experimental Space — FLUX.2 [klein] 4B VAE Decoder Comparison."
416
- )
417
-
418
- # ── Events ────────────────────────────────────────────────────────────────
419
-
420
- input_images.upload(
421
- fn=update_dimensions_from_image,
422
- inputs=[input_images],
423
- outputs=[width, height_slider],
424
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- gr.on(
427
- triggers=[run_button.click, prompt.submit],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  fn=infer,
429
- inputs=[
430
- prompt,
431
- input_images,
432
- seed,
433
- randomize_seed,
434
- width,
435
- height_slider,
436
- num_inference_steps,
437
- guidance_scale,
438
- ],
439
- outputs=[result_standard, result_small, seed_output],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  )
441
 
442
  if __name__ == "__main__":
443
  demo.queue(max_size=20).launch(
444
- css=css, theme=orange_red_theme,
445
  ssr_mode=False,
446
  show_error=True,
 
447
  )
 
5
  import random
6
  import spaces
7
  import torch
8
+ import time
9
+ import base64
10
+ import json
11
+ import html as html_lib
12
+ from io import BytesIO
13
  from diffusers import Flux2KleinPipeline, AutoencoderKLFlux2
14
  from PIL import Image
15
  from pathlib import Path
 
16
  from typing import Iterable
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  # ── Config ────────────────────────────────────────────────────────────────────
19
 
20
+ dtype = torch.bfloat16
21
+ device = "cuda" if torch.cuda.is_available() else "cpu"
22
 
23
  MAX_SEED = np.iinfo(np.int32).max
24
  MAX_IMAGE_SIZE = 1024
25
+ LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
26
 
27
  # ── Models ────────────────────────────────────────────────────────────────────
28
 
 
47
  )
48
  pipe_small_decoder.enable_model_cpu_offload()
49
 
50
+ # ── Examples ──────────────────────────────────────────────────────────────────
51
+
52
+ EXAMPLES_CONFIG = [
53
+ {
54
+ "images": ["examples/1.jpg"],
55
+ "prompt": "Change the weather to stormy.",
56
+ },
57
+ {
58
+ "images": ["examples/2.jpg"],
59
+ "prompt": "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition.",
60
+ },
61
+ {
62
+ "images": ["examples/3.jpg"],
63
+ "prompt": "Relight the image with soft golden sunset lighting while keeping all structures and subject details consistent.",
64
+ },
65
+ {
66
+ "images": ["examples/4.jpg"],
67
+ "prompt": "Make the texture high-resolution.",
68
+ },
69
+ ]
70
+
71
  # ── Helpers ───────────────────────────────────────────────────────────────────
72
 
73
+ def make_thumb_b64(path, max_dim=220):
74
+ if not os.path.exists(path):
75
+ return ""
76
+ try:
77
+ img = Image.open(path).convert("RGB")
78
+ img.thumbnail((max_dim, max_dim), LANCZOS)
79
+ buf = BytesIO()
80
+ img.save(buf, format="JPEG", quality=65)
81
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
82
+ except Exception as e:
83
+ print(f"Thumbnail error for {path}: {e}")
84
+ return ""
85
+
86
+
87
+ def encode_full_image(path):
88
+ if not os.path.exists(path):
89
+ return ""
90
+ try:
91
+ with open(path, "rb") as f:
92
+ data = f.read()
93
+ ext = path.rsplit(".", 1)[-1].lower()
94
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg",
95
+ "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
96
+ return f"data:{mime};base64,{base64.b64encode(data).decode()}"
97
+ except Exception as e:
98
+ print(f"Encode error for {path}: {e}")
99
+ return ""
100
+
101
+
102
+ def build_example_cards_html():
103
+ cards = ""
104
+ for i, ex in enumerate(EXAMPLES_CONFIG):
105
+ thumbs_html = ""
106
+ for path in ex["images"]:
107
+ thumb = make_thumb_b64(path)
108
+ if thumb:
109
+ thumbs_html += f'<img src="{thumb}" alt="">'
110
+ else:
111
+ thumbs_html += '<div class="example-thumb-placeholder">Preview</div>'
112
+ n = len(ex["images"])
113
+ badge = f'{n} image{"s" if n > 1 else ""}'
114
+ prompt_short = html_lib.escape(ex["prompt"][:90])
115
+ if len(ex["prompt"]) > 90:
116
+ prompt_short += "..."
117
+ cards += f'''<div class="example-card" data-idx="{i}">
118
+ <div class="example-thumbs">{thumbs_html}</div>
119
+ <div class="example-meta"><span class="example-badge">{badge}</span></div>
120
+ <div class="example-prompt-text">{prompt_short}</div>
121
+ </div>'''
122
+ return cards
123
+
124
+
125
+ def load_example_data(idx_str):
126
+ try:
127
+ idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
128
+ except (ValueError, TypeError):
129
+ idx = -1
130
+ if idx < 0 or idx >= len(EXAMPLES_CONFIG):
131
+ return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"})
132
+ ex = EXAMPLES_CONFIG[idx]
133
+ b64_list, names = [], []
134
+ for path in ex["images"]:
135
+ b64 = encode_full_image(path)
136
+ if b64:
137
+ b64_list.append(b64)
138
+ names.append(os.path.basename(path))
139
+ return json.dumps({"images": b64_list, "prompt": ex["prompt"],
140
+ "names": names, "status": "ok"})
141
 
 
 
142
 
143
+ def b64_to_pil_list(b64_json_str):
144
+ if not b64_json_str or b64_json_str.strip() in ("", "[]"):
145
+ return []
146
+ try:
147
+ b64_list = json.loads(b64_json_str)
148
+ except Exception:
149
+ return []
150
+ pil_images = []
151
+ for b64_str in b64_list:
152
+ if not b64_str or not isinstance(b64_str, str):
153
+ continue
154
+ try:
155
+ if b64_str.startswith("data:image"):
156
+ _, data = b64_str.split(",", 1)
157
+ else:
158
+ data = b64_str
159
+ image_data = base64.b64decode(data)
160
+ pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
161
+ except Exception as e:
162
+ print(f"Error decoding image: {e}")
163
+ return pil_images
164
 
 
 
165
 
166
+ def update_dimensions_on_upload(pil_images):
167
+ if not pil_images:
168
+ return 1024, 1024
169
+ img = pil_images[0]
170
+ iw, ih = img.size
171
+ if iw >= ih:
172
+ nw = 1024
173
+ nh = int(1024 * ih / iw)
174
  else:
175
+ nh = 1024
176
+ nw = int(1024 * iw / ih)
177
+ return max(256, (nw // 8) * 8), max(256, (nh // 8) * 8)
178
+
179
+
180
+ def format_time(seconds: float) -> str:
181
+ if seconds < 60:
182
+ return f"{seconds:.2f}s"
183
+ minutes = int(seconds // 60)
184
+ secs = seconds % 60
185
+ return f"{minutes}m {secs:.2f}s"
186
+
187
+
188
+ def pil_to_b64(img: Image.Image) -> str:
189
+ buf = BytesIO()
190
+ img.save(buf, format="PNG")
191
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
192
+
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ print("Building example thumbnails...")
195
+ EXAMPLE_CARDS_HTML = build_example_cards_html()
196
+ print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
197
 
198
  # ── Inference ─────────────────────────────────────────────────────────────────
199
 
200
  @spaces.GPU(duration=240)
201
  def infer(
202
+ images_b64_json,
203
  prompt,
204
+ seed,
205
+ randomize_seed,
206
+ width,
207
+ height,
208
+ num_inference_steps,
209
+ guidance_scale,
 
210
  progress=gr.Progress(track_tqdm=True),
211
  ):
212
  gc.collect()
 
218
  if randomize_seed:
219
  seed = random.randint(0, MAX_SEED)
220
 
221
+ pil_images = b64_to_pil_list(images_b64_json)
222
+ image_list = pil_images if pil_images else None
223
+
224
+ if image_list:
225
+ width, height = update_dimensions_on_upload(image_list)
226
 
227
  shared_kwargs = dict(
228
  prompt=prompt,
 
234
  if image_list is not None:
235
  shared_kwargs["image"] = image_list
236
 
237
+ # Standard VAE
238
+ progress(0.05, desc="Running Standard VAE generation...")
239
+ t0 = time.perf_counter()
240
+ gen_std = torch.Generator(device="cpu").manual_seed(seed)
 
241
  out_standard = pipe_standard(**shared_kwargs, generator=gen_std).images[0]
242
+ time_std = format_time(time.perf_counter() - t0)
243
+ progress(0.55, desc=f"Standard VAE done in {time_std} — now running Small Decoder VAE...")
 
244
 
245
  gc.collect()
246
  torch.cuda.empty_cache()
247
 
248
+ # Small Decoder VAE
249
+ t0 = time.perf_counter()
 
250
  gen_small = torch.Generator(device="cpu").manual_seed(seed)
251
  out_small = pipe_small_decoder(**shared_kwargs, generator=gen_small).images[0]
252
+ time_small = format_time(time.perf_counter() - t0)
253
+ progress(1.0, desc=f"Both done! Standard: {time_std} | Small Decoder: {time_small}")
 
254
 
255
  gc.collect()
256
  torch.cuda.empty_cache()
257
 
258
+ std_b64 = pil_to_b64(out_standard)
259
+ small_b64 = pil_to_b64(out_small)
260
 
261
+ result_json = json.dumps({
262
+ "standard_b64": std_b64,
263
+ "small_b64": small_b64,
264
+ "seed": seed,
265
+ "time_std": time_std,
266
+ "time_small": time_small,
267
+ "status": "ok",
268
+ })
269
 
270
+ return out_standard, out_small, seed, result_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
 
273
  # ── CSS ────────────────────��──────────────────────────────────────────────────
274
 
275
+ css = r"""
276
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
277
+ *{box-sizing:border-box;margin:0;padding:0}
278
+ body,.gradio-container{
279
+ background:#0f0f13!important;font-family:'Inter',system-ui,-apple-system,sans-serif!important;
280
+ font-size:14px!important;color:#e4e4e7!important;min-height:100vh;
281
+ }
282
+ footer{display:none!important}
283
+ .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
284
+
285
+ #example-load-btn{
286
+ position:absolute!important;left:-9999px!important;top:-9999px!important;
287
+ width:1px!important;height:1px!important;opacity:0.01!important;pointer-events:none!important;
288
+ }
289
+ #gradio-run-btn{
290
+ position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;
291
+ opacity:0.01;pointer-events:none;overflow:hidden;
292
+ }
293
+
294
+ /* ── Shell ── */
295
+ .app-shell{
296
+ background:#18181b;border:1px solid #27272a;border-radius:16px;
297
+ margin:12px auto;max-width:1400px;overflow:hidden;
298
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
299
+ }
300
+ .app-header{
301
+ background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
302
+ padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
303
+ }
304
+ .app-header-left{display:flex;align-items:center;gap:12px}
305
+ .app-logo{
306
+ width:36px;height:36px;background:linear-gradient(135deg,#FF6B00,#FF4500,#FF8C00);
307
+ border-radius:10px;display:flex;align-items:center;justify-content:center;
308
+ box-shadow:0 4px 12px rgba(255,69,0,.35);
309
+ }
310
+ .app-logo svg{width:20px;height:20px;fill:#fff;flex-shrink:0}
311
+ .app-title{
312
+ font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
313
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
314
+ }
315
+ .app-badge{
316
+ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
317
+ background:rgba(255,69,0,.15);color:#FF6B35;border:1px solid rgba(255,69,0,.25);letter-spacing:.3px;
318
+ }
319
+ .app-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
320
+
321
+ /* ── Toolbar ── */
322
+ .app-toolbar{
323
+ background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;
324
+ display:flex;gap:4px;align-items:center;flex-wrap:wrap;
325
+ }
326
+ .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
327
+ .modern-tb-btn{
328
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
329
+ min-width:32px;height:34px;background:transparent;border:1px solid transparent;
330
+ border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;
331
+ font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
332
+ transition:all .15s ease;
333
+ }
334
+ .modern-tb-btn:hover{background:rgba(255,69,0,.15);border-color:rgba(255,69,0,.3)}
335
+ .modern-tb-btn:active{background:rgba(255,69,0,.25);border-color:rgba(255,69,0,.45)}
336
+ .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600}
337
+ .modern-tb-btn .tb-svg{width:15px;height:15px;flex-shrink:0}
338
+ .modern-tb-btn .tb-svg,.modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important}
339
+ .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center}
340
+
341
+ /* ── Layout ── */
342
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
343
+ .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
344
+ .app-main-right{width:460px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
345
+
346
+ /* ── Drop Zone ── */
347
+ #gallery-drop-zone{position:relative;background:#09090b;min-height:300px;overflow:auto}
348
+ #gallery-drop-zone.drag-over{outline:2px solid #FF4500;outline-offset:-2px;background:rgba(255,69,0,.04)}
349
+ .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20}
350
+ .upload-click-area{
351
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
352
+ cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;border-radius:16px;
353
+ background:rgba(255,69,0,.03);transition:all .2s ease;gap:8px;
354
+ }
355
+ .upload-click-area:hover{background:rgba(255,69,0,.08);border-color:#FF4500;transform:scale(1.03)}
356
+ .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px}
357
+ .upload-sub-text{color:#52525b;font-size:12px;text-align:center}
358
+
359
+ /* ── Gallery ── */
360
+ .image-gallery-grid{
361
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
362
+ gap:12px;padding:16px;align-content:start;
363
+ }
364
+ .gallery-thumb{
365
+ position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
366
+ cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b;
367
+ }
368
+ .gallery-thumb:hover{border-color:#3f3f46;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}
369
+ .gallery-thumb.selected{border-color:#FF4500!important;box-shadow:0 0 0 3px rgba(255,69,0,.2)}
370
+ .gallery-thumb img{width:100%;height:100%;object-fit:cover}
371
+ .thumb-badge{
372
+ position:absolute;top:6px;left:6px;background:#FF4500;color:#fff;
373
+ padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;
374
+ }
375
+ .thumb-remove{
376
+ position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.75);
377
+ color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:50%;cursor:pointer;
378
+ display:none;align-items:center;justify-content:center;font-size:12px;transition:all .15s;line-height:1;
379
+ }
380
+ .gallery-thumb:hover .thumb-remove{display:flex}
381
+ .thumb-remove:hover{background:#FF4500;border-color:#FF4500}
382
+ .gallery-add-card{
383
+ aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46;
384
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
385
+ cursor:pointer;transition:all .2s ease;background:rgba(255,69,0,.03);gap:4px;
386
+ }
387
+ .gallery-add-card:hover{border-color:#FF4500;background:rgba(255,69,0,.08)}
388
+ .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
389
+ .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
390
+
391
+ /* ── Hint Bar ── */
392
+ .hint-bar{
393
+ background:rgba(255,69,0,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
394
+ padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
395
+ }
396
+ .hint-bar b{color:#FF8C55;font-weight:600}
397
+ .hint-bar kbd{
398
+ display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
399
+ border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
400
+ }
401
+
402
+ /* ── Suggestions ── */
403
+ .suggestions-section{border-top:1px solid #27272a;padding:12px 16px}
404
+ .suggestions-title,.examples-title{
405
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
406
+ letter-spacing:.8px;margin-bottom:10px;
407
+ }
408
+ .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px}
409
+ .suggestion-chip{
410
+ display:inline-flex;align-items:center;gap:4px;padding:5px 12px;
411
+ background:rgba(255,69,0,.08);border:1px solid rgba(255,69,0,.2);border-radius:20px;
412
+ color:#FF8C55;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;
413
+ cursor:pointer;transition:all .15s;white-space:nowrap;
414
+ }
415
+ .suggestion-chip:hover{background:rgba(255,69,0,.15);border-color:rgba(255,69,0,.35);color:#FF6B35;transform:translateY(-1px)}
416
+
417
+ /* ── Examples ── */
418
+ .examples-section{border-top:1px solid #27272a;padding:12px 16px}
419
+ .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
420
+ .examples-scroll::-webkit-scrollbar{height:6px}
421
+ .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
422
+ .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
423
+ .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
424
+ .example-card{
425
+ flex-shrink:0;width:210px;background:#09090b;border:1px solid #27272a;
426
+ border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
427
+ }
428
+ .example-card:hover{border-color:#FF4500;transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,69,0,.15)}
429
+ .example-card.loading{opacity:.5;pointer-events:none}
430
+ .example-thumbs{display:flex;height:110px;overflow:hidden;background:#18181b}
431
+ .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a}
432
+ .example-thumb-placeholder{
433
+ flex:1;display:flex;align-items:center;justify-content:center;
434
+ background:#18181b;color:#3f3f46;font-size:11px;min-width:0;
435
+ }
436
+ .example-meta{padding:6px 10px;display:flex;align-items:center;gap:6px}
437
+ .example-badge{
438
+ display:inline-flex;padding:2px 7px;background:rgba(255,69,0,.1);border-radius:4px;
439
+ font-size:10px;font-weight:600;color:#FF6B35;font-family:'JetBrains Mono',monospace;white-space:nowrap;
440
+ }
441
+ .example-prompt-text{
442
+ padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;
443
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
444
+ }
445
+
446
+ /* ── Panel Cards ── */
447
+ .panel-card{border-bottom:1px solid #27272a}
448
+ .panel-card-title{
449
+ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
450
+ text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
451
+ }
452
+ .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
453
+ .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
454
+ .modern-textarea{
455
+ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
456
+ padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
457
+ resize:vertical;outline:none;min-height:42px;transition:border-color .2s;
458
+ }
459
+ .modern-textarea:focus{border-color:#FF4500;box-shadow:0 0 0 3px rgba(255,69,0,.15)}
460
+ .modern-textarea::placeholder{color:#3f3f46}
461
+ .modern-textarea.error-flash{
462
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;
463
+ animation:shake .4s ease;
464
+ }
465
+ @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
466
+
467
+ /* ── Toast ── */
468
+ .toast-notification{
469
+ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
470
+ z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
471
+ font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
472
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
473
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
474
+ }
475
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1}
476
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
477
+ .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
478
+ .toast-notification.info{background:linear-gradient(135deg,#FF4500,#CC3700);color:#fff;border:1px solid rgba(255,255,255,.15)}
479
+
480
+ /* ── Run Button ── */
481
+ .btn-run{
482
+ display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
483
+ background:linear-gradient(135deg,#FF4500,#CC3700);border:none;border-radius:10px;
484
+ padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
485
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;transition:all .2s ease;
486
+ box-shadow:0 4px 16px rgba(255,69,0,.3),inset 0 1px 0 rgba(255,255,255,.1);
487
+ }
488
+ .btn-run:hover{
489
+ background:linear-gradient(135deg,#FF6B35,#FF4500);transform:translateY(-1px);
490
+ box-shadow:0 6px 24px rgba(255,69,0,.45),inset 0 1px 0 rgba(255,255,255,.15);
491
+ }
492
+ .btn-run:active{transform:translateY(0)}
493
+ .btn-run svg{width:18px;height:18px;fill:#ffffff!important}
494
+
495
+ /* ── Output Frame ── */
496
+ .output-section{padding:16px 20px;display:flex;flex-direction:column;gap:12px;flex:1}
497
+ .output-section-title{
498
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
499
+ }
500
+ .output-compare-row{display:flex;gap:12px;flex:1}
501
+ .output-col{flex:1;display:flex;flex-direction:column;gap:8px;min-width:0}
502
+ .output-col-label{
503
+ display:flex;align-items:center;justify-content:space-between;
504
+ padding:6px 10px;border-radius:8px;font-size:12px;font-weight:600;
505
+ }
506
+ .output-col-label.std{background:rgba(30,100,255,.1);color:#6BA3FF;border:1px solid rgba(30,100,255,.2)}
507
+ .output-col-label.small{background:rgba(34,197,94,.1);color:#4ade80;border:1px solid rgba(34,197,94,.2)}
508
+ .output-img-wrap{
509
+ flex:1;background:#09090b;border:1px solid #27272a;border-radius:10px;
510
+ overflow:hidden;min-height:220px;display:flex;align-items:center;justify-content:center;
511
+ position:relative;
512
+ }
513
+ .output-img-wrap img{max-width:100%;max-height:340px;object-fit:contain;display:block}
514
+ .output-placeholder{color:#3f3f46;font-size:12px;text-align:center;padding:20px}
515
+ .timing-chip{
516
+ display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;
517
+ font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;
518
+ background:#18181b;border:1px solid #27272a;color:#71717a;
519
+ }
520
+ .timing-chip.done{color:#4ade80;border-color:rgba(34,197,94,.3);background:rgba(34,197,94,.08)}
521
+ .out-download-btn{
522
+ display:none;align-items:center;justify-content:center;background:rgba(255,69,0,.1);
523
+ border:1px solid rgba(255,69,0,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
524
+ font-size:11px;font-weight:500;color:#FF8C55!important;gap:4px;height:24px;transition:all .15s;
525
+ }
526
+ .out-download-btn:hover{background:rgba(255,69,0,.2);border-color:rgba(255,69,0,.35);color:#fff!important}
527
+ .out-download-btn.visible{display:inline-flex}
528
+ .out-download-btn svg{width:12px;height:12px;fill:#FF8C55}
529
+
530
+ /* ── Loader ── */
531
+ .modern-loader{
532
+ position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
533
+ z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:12px;
534
+ backdrop-filter:blur(4px);display:none;
535
+ }
536
+ .modern-loader.active{display:flex}
537
+ .loader-spinner{
538
+ width:32px;height:32px;border:3px solid #27272a;border-top-color:#FF4500;
539
+ border-radius:50%;animation:spin .8s linear infinite;
540
+ }
541
+ @keyframes spin{to{transform:rotate(360deg)}}
542
+ .loader-text{font-size:12px;color:#a1a1aa;font-weight:500}
543
+ .loader-bar-track{width:180px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
544
+ .loader-bar-fill{
545
+ height:100%;background:linear-gradient(90deg,#FF4500,#FF8C00,#FF4500);
546
+ background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
547
+ }
548
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
549
+
550
+ /* ── Settings ── */
551
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;overflow:hidden}
552
+ .settings-group-title{
553
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
554
+ padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
555
+ }
556
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
557
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
558
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:80px;flex-shrink:0}
559
+ .slider-row input[type="range"]{
560
+ flex:1;-webkit-appearance:none;height:6px;background:#27272a;border-radius:3px;outline:none;
561
+ }
562
+ .slider-row input[type="range"]::-webkit-slider-thumb{
563
+ -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#FF4500,#CC3700);
564
+ border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,69,0,.4);transition:transform .15s;
565
+ }
566
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
567
+ .slider-row .slider-val{
568
+ min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;
569
+ padding:3px 8px;background:#09090b;border:1px solid #27272a;border-radius:6px;color:#a1a1aa;flex-shrink:0;
570
+ }
571
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
572
+ .checkbox-row input[type="checkbox"]{accent-color:#FF4500;width:16px;height:16px;cursor:pointer}
573
+
574
+ /* ── Statusbar ── */
575
+ .app-statusbar{
576
+ background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
577
+ display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
578
+ }
579
+ .sb-section{
580
+ padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
581
+ font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
582
+ }
583
+ .sb-section.sb-fixed{
584
+ flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
585
+ padding:3px 12px;background:rgba(255,69,0,.08);border-radius:6px;color:#FF6B35;font-weight:500;
586
+ }
587
+ .exp-note{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center}
588
+ .exp-note a{color:#FF8C55;text-decoration:none}
589
+ .exp-note a:hover{text-decoration:underline}
590
+
591
+ ::-webkit-scrollbar{width:8px;height:8px}
592
+ ::-webkit-scrollbar-track{background:#09090b}
593
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
594
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
595
+
596
+ @media(max-width:900px){
597
+ .app-main-row{flex-direction:column}
598
+ .app-main-right{width:100%}
599
+ .app-main-left{border-right:none;border-bottom:1px solid #27272a}
600
+ .output-compare-row{flex-direction:column}
601
  }
602
  """
603
 
604
+ # ── SVGs ──────────────────────────────────────────────────────────────────────
605
+
606
+ FLUX_LOGO_SVG = '''<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
607
+ <path d="M13 2L4.09 12.11a2 2 0 0 0-.09 2.16L8 21h8l4-6.73a2 2 0 0 0-.09-2.16L13 2z"/>
608
+ </svg>'''
609
+
610
+ UPLOAD_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>'
611
+ REMOVE_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
612
+ CLEAR_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>'
613
+ DOWNLOAD_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 16l-5-5h3V4h4v7h3l-5 5z"/><path d="M20 18H4v2h16v-2z"/></svg>'
614
+ RUN_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#ffffff"><path d="M13 2L4.09 12.11a2 2 0 0 0-.09 2.16L8 21h8l4-6.73a2 2 0 0 0-.09-2.16L13 2z"/></svg>'
615
+
616
+ # ── Gallery JS ────────────────────────────────────────────────────────────────
617
+
618
+ gallery_js = r"""
619
+ () => {
620
+ function init() {
621
+ if (window.__fluxInitDone) return;
622
+
623
+ const galleryGrid = document.getElementById('image-gallery-grid');
624
+ const dropZone = document.getElementById('gallery-drop-zone');
625
+ const uploadPrompt = document.getElementById('upload-prompt');
626
+ const uploadClick = document.getElementById('upload-click-area');
627
+ const fileInput = document.getElementById('custom-file-input');
628
+ const btnUpload = document.getElementById('tb-upload');
629
+ const btnRemove = document.getElementById('tb-remove');
630
+ const btnClear = document.getElementById('tb-clear');
631
+ const promptInput = document.getElementById('custom-prompt-input');
632
+ const runBtnEl = document.getElementById('custom-run-btn');
633
+ const imgCountTb = document.getElementById('tb-image-count');
634
+ const imgCountSb = document.getElementById('sb-image-count');
635
+
636
+ if (!galleryGrid || !fileInput || !dropZone) { setTimeout(init, 250); return; }
637
+ window.__fluxInitDone = true;
638
+
639
+ let images = [];
640
+ window.__uploadedImages = images;
641
+ let selectedIdx = -1;
642
+ let toastTimer = null;
643
+
644
+ function showToast(message, type) {
645
+ let toast = document.getElementById('app-toast');
646
+ if (!toast) {
647
+ toast = document.createElement('div');
648
+ toast.id = 'app-toast';
649
+ toast.className = 'toast-notification';
650
+ toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
651
+ document.body.appendChild(toast);
652
+ }
653
+ const icon = toast.querySelector('.toast-icon');
654
+ const text = toast.querySelector('.toast-text');
655
+ toast.className = 'toast-notification ' + (type || 'error');
656
+ icon.textContent = type === 'warning' ? '\u26A0' : type === 'info' ? '\u2139' : '\u2717';
657
+ text.textContent = message;
658
+ if (toastTimer) clearTimeout(toastTimer);
659
+ void toast.offsetWidth;
660
+ toast.classList.add('visible');
661
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
662
+ }
663
+ window.__showToast = showToast;
664
+
665
+ function flashPromptError() {
666
+ if (!promptInput) return;
667
+ promptInput.classList.add('error-flash');
668
+ promptInput.focus();
669
+ setTimeout(() => promptInput.classList.remove('error-flash'), 800);
670
+ }
671
+
672
+ function setGradioValue(containerId, value) {
673
+ const container = document.getElementById(containerId);
674
+ if (!container) return;
675
+ container.querySelectorAll('input, textarea').forEach(el => {
676
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
677
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
678
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
679
+ if (ns && ns.set) {
680
+ ns.set.call(el, value);
681
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
682
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
683
+ }
684
+ });
685
+ }
686
+ window.__setGradioValue = setGradioValue;
687
+
688
+ function syncImagesToGradio() {
689
+ window.__uploadedImages = images;
690
+ const b64Array = images.map(img => img.b64);
691
+ setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
692
+ updateCounts();
693
+ }
694
+
695
+ function syncPromptToGradio() {
696
+ if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
697
+ }
698
+
699
+ function updateCounts() {
700
+ const n = images.length;
701
+ const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
702
+ if (imgCountTb) imgCountTb.textContent = txt;
703
+ if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
704
+ }
705
+
706
+ function addImage(b64, name) {
707
+ images.push({id: Date.now() + Math.random(), b64, name});
708
+ renderGallery();
709
+ syncImagesToGradio();
710
+ }
711
+ window.__addImage = addImage;
712
+
713
+ function removeImage(idx) {
714
+ images.splice(idx, 1);
715
+ if (selectedIdx === idx) selectedIdx = -1;
716
+ else if (selectedIdx > idx) selectedIdx--;
717
+ renderGallery();
718
+ syncImagesToGradio();
719
+ }
720
+
721
+ function clearAll() {
722
+ images = [];
723
+ window.__uploadedImages = images;
724
+ selectedIdx = -1;
725
+ renderGallery();
726
+ syncImagesToGradio();
727
+ }
728
+ window.__clearAll = clearAll;
729
+
730
+ function renderGallery() {
731
+ if (images.length === 0) {
732
+ galleryGrid.innerHTML = '';
733
+ galleryGrid.style.display = 'none';
734
+ if (uploadPrompt) uploadPrompt.style.display = '';
735
+ return;
736
+ }
737
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
738
+ galleryGrid.style.display = 'grid';
739
+ let html = '';
740
+ images.forEach((img, i) => {
741
+ const sel = i === selectedIdx ? ' selected' : '';
742
+ html += '<div class="gallery-thumb' + sel + '" data-idx="' + i + '">'
743
+ + '<img src="' + img.b64 + '" alt="">'
744
+ + '<span class="thumb-badge">#' + (i+1) + '</span>'
745
+ + '<button class="thumb-remove" data-remove="' + i + '">\u2715</button>'
746
+ + '</div>';
747
+ });
748
+ html += '<div class="gallery-add-card" id="gallery-add-card"><span class="add-icon">+</span><span class="add-text">Add</span></div>';
749
+ galleryGrid.innerHTML = html;
750
+
751
+ galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
752
+ thumb.addEventListener('click', (e) => {
753
+ if (e.target.closest('.thumb-remove')) return;
754
+ selectedIdx = selectedIdx === parseInt(thumb.dataset.idx) ? -1 : parseInt(thumb.dataset.idx);
755
+ renderGallery();
756
+ });
757
+ });
758
+ galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
759
+ btn.addEventListener('click', (e) => { e.stopPropagation(); removeImage(parseInt(btn.dataset.remove)); });
760
+ });
761
+ const addCard = document.getElementById('gallery-add-card');
762
+ if (addCard) addCard.addEventListener('click', () => fileInput.click());
763
+ }
764
+
765
+ function processFiles(files) {
766
+ Array.from(files).forEach(file => {
767
+ if (!file.type.startsWith('image/')) return;
768
+ const reader = new FileReader();
769
+ reader.onload = (e) => addImage(e.target.result, file.name);
770
+ reader.readAsDataURL(file);
771
+ });
772
+ }
773
+
774
+ fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
775
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
776
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
777
+ if (btnRemove) btnRemove.addEventListener('click', () => { if (selectedIdx >= 0) removeImage(selectedIdx); });
778
+ if (btnClear) btnClear.addEventListener('click', clearAll);
779
+
780
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
781
+ dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
782
+ dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files); });
783
+
784
+ if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
785
+ window.__setPrompt = function(text) { if (promptInput) { promptInput.value = text; syncPromptToGradio(); } };
786
+
787
+ // Example cards
788
+ document.querySelectorAll('.example-card[data-idx]').forEach(card => {
789
+ card.addEventListener('click', () => {
790
+ const idx = card.getAttribute('data-idx');
791
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
792
+ card.classList.add('loading');
793
+ showToast('Loading example...', 'info');
794
+ setGradioValue('example-result-data', '');
795
+ setGradioValue('example-idx-input', idx);
796
+ setTimeout(() => {
797
+ const btn = document.getElementById('example-load-btn');
798
+ if (btn) { const b = btn.querySelector('button'); if (b) b.click(); else btn.click(); }
799
+ }, 150);
800
+ setTimeout(() => card.classList.remove('loading'), 12000);
801
+ });
802
+ });
803
+
804
+ // Sliders
805
+ function syncSlider(customId, gradioId) {
806
+ const slider = document.getElementById(customId);
807
+ const valSpan = document.getElementById(customId + '-val');
808
+ if (!slider) return;
809
+ slider.addEventListener('input', () => {
810
+ if (valSpan) valSpan.textContent = slider.value;
811
+ const container = document.getElementById(gradioId);
812
+ if (!container) return;
813
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
814
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
815
+ if (ns && ns.set) {
816
+ ns.set.call(el, slider.value);
817
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
818
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
819
+ }
820
+ });
821
+ });
822
+ }
823
+ syncSlider('custom-seed', 'gradio-seed');
824
+ syncSlider('custom-guidance', 'gradio-guidance');
825
+ syncSlider('custom-steps', 'gradio-steps');
826
+ syncSlider('custom-width', 'gradio-width');
827
+ syncSlider('custom-height', 'gradio-height');
828
+
829
+ const randCheck = document.getElementById('custom-randomize');
830
+ if (randCheck) {
831
+ randCheck.addEventListener('change', () => {
832
+ const container = document.getElementById('gradio-randomize');
833
+ if (!container) return;
834
+ const cb = container.querySelector('input[type="checkbox"]');
835
+ if (cb && cb.checked !== randCheck.checked) cb.click();
836
+ });
837
+ }
838
+
839
+ function showLoader(id) { const l = document.getElementById(id); if (l) l.classList.add('active'); }
840
+ function hideLoader(id) { const l = document.getElementById(id); if (l) l.classList.remove('active'); }
841
+ window.__showLoaders = () => { showLoader('loader-std'); showLoader('loader-small'); const sb = document.querySelector('.sb-fixed'); if (sb) sb.textContent = 'Processing...'; };
842
+ window.__hideLoaders = () => { hideLoader('loader-std'); hideLoader('loader-small'); const sb = document.querySelector('.sb-fixed'); if (sb) sb.textContent = 'Done'; };
843
+
844
+ function validateBeforeRun() {
845
+ const promptVal = promptInput ? promptInput.value.trim() : '';
846
+ if (!promptVal) { showToast('Please enter a prompt', 'warning'); flashPromptError(); return false; }
847
+ return true;
848
+ }
849
+
850
+ window.__clickGradioRunBtn = function() {
851
+ if (!validateBeforeRun()) return;
852
+ syncPromptToGradio(); syncImagesToGradio();
853
+ window.__showLoaders();
854
+ setTimeout(() => {
855
+ const gradioBtn = document.getElementById('gradio-run-btn');
856
+ if (!gradioBtn) return;
857
+ const btn = gradioBtn.querySelector('button');
858
+ if (btn) btn.click(); else gradioBtn.click();
859
+ }, 200);
860
+ };
861
+
862
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
863
+
864
+ renderGallery();
865
+ updateCounts();
866
+ }
867
+ init();
868
+ }
869
+ """
870
+
871
+ wire_outputs_js = r"""
872
+ () => {
873
+ function watchOutputs() {
874
+ const resultContainer = document.getElementById('gradio-result-json');
875
+ if (!resultContainer) { setTimeout(watchOutputs, 500); return; }
876
+
877
+ let lastVal = '';
878
+
879
+ function checkResult() {
880
+ const el = resultContainer.querySelector('textarea') || resultContainer.querySelector('input');
881
+ if (!el || !el.value || el.value === lastVal || el.value.length < 20) return;
882
+ lastVal = el.value;
883
 
884
+ try {
885
+ const data = JSON.parse(el.value);
886
+ if (data.status !== 'ok') return;
887
+
888
+ // Standard VAE output
889
+ const stdWrap = document.getElementById('output-img-std');
890
+ const stdPh = document.getElementById('output-ph-std');
891
+ const dlStd = document.getElementById('dl-btn-std');
892
+ if (stdWrap && data.standard_b64) {
893
+ if (stdPh) stdPh.style.display = 'none';
894
+ let img = stdWrap.querySelector('img.out-img');
895
+ if (!img) { img = document.createElement('img'); img.className = 'out-img'; stdWrap.appendChild(img); }
896
+ img.src = data.standard_b64;
897
+ if (dlStd) { dlStd.classList.add('visible'); dlStd.onclick = () => { const a = document.createElement('a'); a.href = data.standard_b64; a.download = 'flux_standard.png'; a.click(); }; }
898
+ }
899
+
900
+ // Small Decoder output
901
+ const smWrap = document.getElementById('output-img-small');
902
+ const smPh = document.getElementById('output-ph-small');
903
+ const dlSm = document.getElementById('dl-btn-small');
904
+ if (smWrap && data.small_b64) {
905
+ if (smPh) smPh.style.display = 'none';
906
+ let img = smWrap.querySelector('img.out-img');
907
+ if (!img) { img = document.createElement('img'); img.className = 'out-img'; smWrap.appendChild(img); }
908
+ img.src = data.small_b64;
909
+ if (dlSm) { dlSm.classList.add('visible'); dlSm.onclick = () => { const a = document.createElement('a'); a.href = data.small_b64; a.download = 'flux_small_decoder.png'; a.click(); }; }
910
+ }
911
+
912
+ // Timing chips
913
+ const chipStd = document.getElementById('timing-chip-std');
914
+ const chipSm = document.getElementById('timing-chip-small');
915
+ if (chipStd && data.time_std) { chipStd.textContent = '⏱ ' + data.time_std; chipStd.classList.add('done'); }
916
+ if (chipSm && data.time_small) { chipSm.textContent = '⏱ ' + data.time_small; chipSm.classList.add('done'); }
917
+
918
+ // Seed
919
+ const seedSb = document.getElementById('sb-seed');
920
+ if (seedSb && data.seed !== undefined) seedSb.textContent = 'seed: ' + data.seed;
921
+
922
+ if (window.__hideLoaders) window.__hideLoaders();
923
+
924
+ } catch(e) { console.error('Output parse error:', e); }
925
+ }
926
+
927
+ const obs = new MutationObserver(checkResult);
928
+ obs.observe(resultContainer, {childList:true, subtree:true, characterData:true, attributes:true});
929
+ setInterval(checkResult, 600);
930
+ }
931
+ watchOutputs();
932
+
933
+ function watchExampleResults() {
934
+ const container = document.getElementById('example-result-data');
935
+ if (!container) { setTimeout(watchExampleResults, 500); return; }
936
+ let lastProcessed = '';
937
+ function checkResult() {
938
+ const el = container.querySelector('textarea') || container.querySelector('input');
939
+ if (!el || !el.value || el.value === lastProcessed || el.value.length < 20) return;
940
+ try {
941
+ const data = JSON.parse(el.value);
942
+ if (data.status === 'ok' && data.images && data.images.length > 0) {
943
+ lastProcessed = el.value;
944
+ if (window.__clearAll) window.__clearAll();
945
+ if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
946
+ data.images.forEach((b64, i) => {
947
+ if (b64 && window.__addImage) {
948
+ const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i+1) + '.jpg');
949
+ window.__addImage(b64, name);
950
+ }
951
+ });
952
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
953
+ if (window.__showToast) window.__showToast('Example loaded — ' + data.images.length + ' image(s)', 'info');
954
+ }
955
+ } catch(e) {}
956
+ }
957
+ const obs = new MutationObserver(checkResult);
958
+ obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
959
+ setInterval(checkResult, 500);
960
+ }
961
+ watchExampleResults();
962
+ }
963
+ """
964
+
965
+ # ── Gradio App ────────────────────────────────────────────────────────────────
966
+
967
+ with gr.Blocks(css=css) as demo:
968
+
969
+ # Hidden Gradio state components
970
+ hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
971
+ prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
972
+ seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0,
973
+ elem_id="gradio-seed", elem_classes="hidden-input", container=False)
974
+ randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
975
+ guidance_scale = gr.Slider(minimum=0.0, maximum=10.0, step=0.1, value=1.0,
976
+ elem_id="gradio-guidance", elem_classes="hidden-input", container=False)
977
+ steps = gr.Slider(minimum=1, maximum=20, step=1, value=4,
978
+ elem_id="gradio-steps", elem_classes="hidden-input", container=False)
979
+ width_gr = gr.Slider(minimum=256, maximum=1024, step=8, value=1024,
980
+ elem_id="gradio-width", elem_classes="hidden-input", container=False)
981
+ height_gr = gr.Slider(minimum=256, maximum=1024, step=8, value=1024,
982
+ elem_id="gradio-height", elem_classes="hidden-input", container=False)
983
+
984
+ result_std_gr = gr.Image(elem_id="gradio-result-std", elem_classes="hidden-input", container=False, format="png")
985
+ result_small_gr = gr.Image(elem_id="gradio-result-small", elem_classes="hidden-input", container=False, format="png")
986
+ seed_out_gr = gr.Number(elem_id="gradio-seed-out", elem_classes="hidden-input", container=False)
987
+ result_json_gr = gr.Textbox(value="", elem_id="gradio-result-json", elem_classes="hidden-input", container=False)
988
+
989
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
990
+ example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
991
+ example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
992
+
993
+ gr.HTML(f"""
994
+ <div class="app-shell">
995
+
996
+ <!-- Header -->
997
+ <div class="app-header">
998
+ <div class="app-header-left">
999
+ <div class="app-logo">{FLUX_LOGO_SVG}</div>
1000
+ <span class="app-title">FLUX.2-klein-4B VAE Comparator</span>
1001
+ <span class="app-badge">4B Distilled</span>
1002
+ <span class="app-badge fast">4-Step Fast</span>
1003
+ </div>
1004
+ </div>
1005
+
1006
+ <!-- Toolbar -->
1007
+ <div class="app-toolbar">
1008
+ <button id="tb-upload" class="modern-tb-btn" title="Upload images">
1009
+ {UPLOAD_SVG}<span class="tb-label">Upload</span>
1010
+ </button>
1011
+ <button id="tb-remove" class="modern-tb-btn" title="Remove selected">
1012
+ {REMOVE_SVG}<span class="tb-label">Remove</span>
1013
+ </button>
1014
+ <button id="tb-clear" class="modern-tb-btn" title="Clear all">
1015
+ {CLEAR_SVG}<span class="tb-label">Clear All</span>
1016
+ </button>
1017
+ <div class="tb-sep"></div>
1018
+ <span id="tb-image-count" class="tb-info">No images</span>
1019
+ <div class="tb-sep"></div>
1020
+ <span class="tb-info" style="color:#71717a;">
1021
+ Compare 🟦 Standard VAE vs 🟩 Small Decoder VAE — same seed, sequential generation
1022
+ </span>
1023
+ </div>
1024
+
1025
+ <!-- Main -->
1026
+ <div class="app-main-row">
1027
+
1028
+ <!-- Left: Upload + Examples -->
1029
+ <div class="app-main-left">
1030
+ <div id="gallery-drop-zone">
1031
+ <div id="upload-prompt" class="upload-prompt-modern">
1032
+ <div id="upload-click-area" class="upload-click-area">
1033
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1034
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#FF4500" stroke-width="2" stroke-dasharray="4 3"/>
1035
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,69,0,0.15)" stroke="#FF4500" stroke-width="1.5"/>
1036
+ <circle cx="28" cy="30" r="6" fill="rgba(255,69,0,0.2)" stroke="#FF4500" stroke-width="1.5"/>
1037
+ </svg>
1038
+ <span class="upload-main-text">Click or drag reference image(s) here</span>
1039
+ <span class="upload-sub-text">Optional — leave empty for text-to-image generation</span>
1040
+ </div>
1041
+ </div>
1042
+ <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
1043
+ <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
1044
+ </div>
1045
+
1046
+ <div class="hint-bar">
1047
+ <b>Optional:</b> Upload reference image(s) for image-guided generation &nbsp;&middot;&nbsp;
1048
+ Leave empty for <b>text-to-image</b> mode &nbsp;&middot;&nbsp;
1049
+ <kbd>Remove</kbd> deletes selected &nbsp;&middot;&nbsp; <kbd>Clear All</kbd> removes everything
1050
+ </div>
1051
+
1052
+ <div class="suggestions-section">
1053
+ <div class="suggestions-title">Quick Prompts</div>
1054
+ <div class="suggestions-wrap">
1055
+ <button class="suggestion-chip" onclick="window.__setPrompt('Change the weather to stormy.')">Stormy Weather</button>
1056
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform the scene into a snowy winter day.')">Snowy Winter</button>
1057
+ <button class="suggestion-chip" onclick="window.__setPrompt('Relight with soft golden sunset lighting.')">Golden Sunset</button>
1058
+ <button class="suggestion-chip" onclick="window.__setPrompt('Make the texture high-resolution and ultra detailed.')">HD Texture</button>
1059
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to cinematic film look with grain and vignette.')">Cinematic Film</button>
1060
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime illustration style.')">Anime Style</button>
1061
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply oil painting effect with visible brush strokes.')">Oil Painting</button>
1062
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add dramatic neon glow with cyberpunk aesthetic.')">Neon Cyberpunk</button>
1063
+ <button class="suggestion-chip" onclick="window.__setPrompt('A futuristic city at night with flying cars and neon lights.')">Futuristic City</button>
1064
+ <button class="suggestion-chip" onclick="window.__setPrompt('A serene mountain landscape at dawn with mist.')">Mountain Dawn</button>
1065
+ <button class="suggestion-chip" onclick="window.__setPrompt('Portrait of a warrior in ancient armor, epic lighting.')">Epic Portrait</button>
1066
+ <button class="suggestion-chip" onclick="window.__setPrompt('Abstract geometric art with vibrant colors and patterns.')">Abstract Art</button>
1067
+ </div>
1068
+ </div>
1069
+
1070
+ <div class="examples-section">
1071
+ <div class="examples-title">Quick Examples</div>
1072
+ <div class="examples-scroll">
1073
+ {EXAMPLE_CARDS_HTML}
1074
+ </div>
1075
+ </div>
1076
+ </div>
1077
+
1078
+ <!-- Right: Prompt + Outputs + Settings -->
1079
+ <div class="app-main-right">
1080
+
1081
+ <div class="panel-card">
1082
+ <div class="panel-card-title">Generation Prompt</div>
1083
+ <div class="panel-card-body">
1084
+ <label class="modern-label" for="custom-prompt-input">Prompt</label>
1085
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="3"
1086
+ placeholder="e.g., Transform the scene into a snowy winter day..."></textarea>
1087
+ </div>
1088
+ </div>
1089
+
1090
+ <div style="padding:12px 20px;">
1091
+ <button id="custom-run-btn" class="btn-run">
1092
+ {RUN_SVG}
1093
+ <span>⚡ Run Comparison</span>
1094
+ </button>
1095
+ </div>
1096
+
1097
+ <!-- Output Compare -->
1098
+ <div class="output-section">
1099
+ <div class="output-section-title">Output Comparison</div>
1100
+ <div class="output-compare-row">
1101
+
1102
+ <!-- Standard VAE -->
1103
+ <div class="output-col">
1104
+ <div class="output-col-label std">
1105
+ <span>🟦 Standard VAE</span>
1106
+ <span id="dl-btn-std" class="out-download-btn" title="Download">{DOWNLOAD_SVG}</span>
1107
+ </div>
1108
+ <div class="output-img-wrap" id="output-img-std">
1109
+ <div class="modern-loader" id="loader-std">
1110
+ <div class="loader-spinner"></div>
1111
+ <div class="loader-text">Generating...</div>
1112
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1113
+ </div>
1114
+ <div class="output-placeholder" id="output-ph-std">Result will appear here</div>
1115
+ </div>
1116
+ <span class="timing-chip" id="timing-chip-std">⏱ Waiting...</span>
1117
+ </div>
1118
+
1119
+ <!-- Small Decoder VAE -->
1120
+ <div class="output-col">
1121
+ <div class="output-col-label small">
1122
+ <span>🟩 Small Decoder</span>
1123
+ <span id="dl-btn-small" class="out-download-btn" title="Download">{DOWNLOAD_SVG}</span>
1124
+ </div>
1125
+ <div class="output-img-wrap" id="output-img-small">
1126
+ <div class="modern-loader" id="loader-small">
1127
+ <div class="loader-spinner"></div>
1128
+ <div class="loader-text">Generating...</div>
1129
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1130
+ </div>
1131
+ <div class="output-placeholder" id="output-ph-small">Result will appear here</div>
1132
+ </div>
1133
+ <span class="timing-chip" id="timing-chip-small">⏱ Waiting...</span>
1134
+ </div>
1135
+
1136
+ </div>
1137
+ </div>
1138
+
1139
+ <!-- Advanced Settings -->
1140
+ <div class="settings-group">
1141
+ <div class="settings-group-title">Advanced Settings</div>
1142
+ <div class="settings-group-body">
1143
+ <div class="slider-row">
1144
+ <label>Seed</label>
1145
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1146
+ <span class="slider-val" id="custom-seed-val">0</span>
1147
+ </div>
1148
+ <div class="checkbox-row">
1149
+ <input type="checkbox" id="custom-randomize" checked>
1150
+ <label for="custom-randomize">Randomize seed</label>
1151
+ </div>
1152
+ <div class="slider-row">
1153
+ <label>Guidance</label>
1154
+ <input type="range" id="custom-guidance" min="0" max="10" step="0.1" value="1.0">
1155
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
1156
+ </div>
1157
+ <div class="slider-row">
1158
+ <label>Steps</label>
1159
+ <input type="range" id="custom-steps" min="1" max="20" step="1" value="4">
1160
+ <span class="slider-val" id="custom-steps-val">4</span>
1161
+ </div>
1162
+ <div class="slider-row">
1163
+ <label>Width</label>
1164
+ <input type="range" id="custom-width" min="256" max="1024" step="8" value="1024">
1165
+ <span class="slider-val" id="custom-width-val">1024</span>
1166
+ </div>
1167
+ <div class="slider-row">
1168
+ <label>Height</label>
1169
+ <input type="range" id="custom-height" min="256" max="1024" step="8" value="1024">
1170
+ <span class="slider-val" id="custom-height-val">1024</span>
1171
+ </div>
1172
+ </div>
1173
+ </div>
1174
+
1175
+ </div>
1176
+ </div>
1177
+
1178
+ <div class="exp-note">
1179
+ Experimental Space — <a href="https://huggingface.co/black-forest-labs/FLUX.2-klein-4B" target="_blank">FLUX.2-klein-4B</a>
1180
+ &nbsp;·&nbsp; <a href="https://huggingface.co/black-forest-labs/FLUX.2-small-decoder" target="_blank">FLUX.2-small-decoder</a>
1181
+ &nbsp;·&nbsp; Comparing Standard VAE vs Small Decoder VAE from the same seed
1182
+ </div>
1183
+
1184
+ <div class="app-statusbar">
1185
+ <div class="sb-section" id="sb-image-count">No images uploaded</div>
1186
+ <div class="sb-section" id="sb-seed" style="color:#52525b;font-family:'JetBrains Mono',monospace;font-size:12px;">seed: —</div>
1187
+ <div class="sb-section sb-fixed">Ready</div>
1188
+ </div>
1189
+
1190
+ </div>
1191
+ """)
1192
+
1193
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1194
+
1195
+ demo.load(fn=None, js=gallery_js)
1196
+ demo.load(fn=None, js=wire_outputs_js)
1197
+
1198
+ run_btn.click(
1199
  fn=infer,
1200
+ inputs=[hidden_images_b64, prompt, seed, randomize_seed,
1201
+ width_gr, height_gr, steps, guidance_scale],
1202
+ outputs=[result_std_gr, result_small_gr, seed_out_gr, result_json_gr],
1203
+ js=r"""(imgs, p, s, rs, w, h, st, gs) => {
1204
+ const images = window.__uploadedImages || [];
1205
+ const b64Array = images.map(img => img.b64);
1206
+ const imgsJson = JSON.stringify(b64Array);
1207
+ const promptEl = document.getElementById('custom-prompt-input');
1208
+ const promptVal = promptEl ? promptEl.value : p;
1209
+ const seedEl = document.getElementById('custom-seed');
1210
+ const guidEl = document.getElementById('custom-guidance');
1211
+ const stepsEl = document.getElementById('custom-steps');
1212
+ const wEl = document.getElementById('custom-width');
1213
+ const hEl = document.getElementById('custom-height');
1214
+ const randEl = document.getElementById('custom-randomize');
1215
+ return [
1216
+ imgsJson, promptVal,
1217
+ seedEl ? parseFloat(seedEl.value) : s,
1218
+ randEl ? randEl.checked : rs,
1219
+ wEl ? parseFloat(wEl.value) : w,
1220
+ hEl ? parseFloat(hEl.value) : h,
1221
+ stepsEl ? parseFloat(stepsEl.value) : st,
1222
+ guidEl ? parseFloat(guidEl.value) : gs,
1223
+ ];
1224
+ }""",
1225
+ )
1226
+
1227
+ example_load_btn.click(
1228
+ fn=load_example_data,
1229
+ inputs=[example_idx],
1230
+ outputs=[example_result],
1231
+ queue=False,
1232
  )
1233
 
1234
  if __name__ == "__main__":
1235
  demo.queue(max_size=20).launch(
1236
+ css=css,
1237
  ssr_mode=False,
1238
  show_error=True,
1239
+ allowed_paths=["examples"],
1240
  )