prithivMLmods commited on
Commit
645bfee
·
verified ·
1 Parent(s): efb5305

update app

Browse files
Files changed (1) hide show
  1. app.py +536 -538
app.py CHANGED
@@ -12,19 +12,14 @@ 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
 
29
  print("Loading 4B Distilled model (Standard VAE)...")
30
  pipe_standard = Flux2KleinPipeline.from_pretrained(
@@ -47,28 +42,13 @@ pipe_small_decoder = Flux2KleinPipeline.from_pretrained(
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):
@@ -90,9 +70,8 @@ def encode_full_image(path):
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}")
@@ -109,7 +88,7 @@ def build_example_cards_html():
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:
@@ -129,15 +108,19 @@ def load_example_data(idx_str):
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):
@@ -163,6 +146,14 @@ def b64_to_pil_list(b64_json_str):
163
  return pil_images
164
 
165
 
 
 
 
 
 
 
 
 
166
  def update_dimensions_on_upload(pil_images):
167
  if not pil_images:
168
  return 1024, 1024
@@ -174,28 +165,8 @@ def update_dimensions_on_upload(pil_images):
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(
@@ -205,7 +176,7 @@ def infer(
205
  randomize_seed,
206
  width,
207
  height,
208
- num_inference_steps,
209
  guidance_scale,
210
  progress=gr.Progress(track_tqdm=True),
211
  ):
@@ -219,58 +190,55 @@ def infer(
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,
229
  height=height,
230
  width=width,
231
- num_inference_steps=num_inference_steps,
232
  guidance_scale=guidance_scale,
233
  )
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');
@@ -287,14 +255,12 @@ footer{display:none!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{
@@ -314,11 +280,11 @@ footer{display:none!important}
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;
@@ -338,28 +304,26 @@ footer{display:none!important}
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;
@@ -388,96 +352,35 @@ footer{display:none!important}
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;
@@ -486,106 +389,176 @@ footer{display:none!important}
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}
@@ -595,26 +568,12 @@ footer{display:none!important}
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() {
@@ -634,6 +593,7 @@ function init() {
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 = [];
@@ -736,27 +696,30 @@ function init() {
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());
@@ -766,23 +729,29 @@ function init() {
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 => {
@@ -827,19 +796,26 @@ function init() {
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() : '';
@@ -849,8 +825,7 @@ function init() {
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;
@@ -871,76 +846,110 @@ init();
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) => {
@@ -951,6 +960,9 @@ function watchExampleResults() {
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
  }
@@ -962,230 +974,222 @@ 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
  """)
@@ -1197,30 +1201,24 @@ with gr.Blocks(css=css) as demo:
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
 
 
12
  from io import BytesIO
13
  from diffusers import Flux2KleinPipeline, AutoencoderKLFlux2
14
  from PIL import Image
 
15
  from typing import Iterable
16
 
 
 
 
 
 
17
  MAX_SEED = np.iinfo(np.int32).max
18
  MAX_IMAGE_SIZE = 1024
19
+ dtype = torch.bfloat16
20
+ device = "cuda" if torch.cuda.is_available() else "cpu"
21
 
22
+ LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
23
 
24
  print("Loading 4B Distilled model (Standard VAE)...")
25
  pipe_standard = Flux2KleinPipeline.from_pretrained(
 
42
  )
43
  pipe_small_decoder.enable_model_cpu_offload()
44
 
 
 
45
  EXAMPLES_CONFIG = [
46
+ {"images": ["examples/1.jpg"], "prompt": "Change the weather to stormy."},
47
+ {"images": ["examples/2.jpg"], "prompt": "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition."},
48
+ {"images": ["examples/3.jpg"], "prompt": "Relight the image with soft golden sunset lighting while keeping all structures and subject details consistent."},
49
+ {"images": ["examples/4.jpg"], "prompt": "Make the texture high-resolution."},
 
 
 
 
 
 
 
 
 
 
 
 
50
  ]
51
 
 
52
 
53
  def make_thumb_b64(path, max_dim=220):
54
  if not os.path.exists(path):
 
70
  try:
71
  with open(path, "rb") as f:
72
  data = f.read()
73
+ ext = path.rsplit(".", 1)[-1].lower()
74
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
 
75
  return f"data:{mime};base64,{base64.b64encode(data).decode()}"
76
  except Exception as e:
77
  print(f"Encode error for {path}: {e}")
 
88
  thumbs_html += f'<img src="{thumb}" alt="">'
89
  else:
90
  thumbs_html += '<div class="example-thumb-placeholder">Preview</div>'
91
+ n = len(ex["images"])
92
  badge = f'{n} image{"s" if n > 1 else ""}'
93
  prompt_short = html_lib.escape(ex["prompt"][:90])
94
  if len(ex["prompt"]) > 90:
 
108
  idx = -1
109
  if idx < 0 or idx >= len(EXAMPLES_CONFIG):
110
  return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"})
111
+ ex = EXAMPLES_CONFIG[idx]
112
  b64_list, names = [], []
113
  for path in ex["images"]:
114
  b64 = encode_full_image(path)
115
  if b64:
116
  b64_list.append(b64)
117
  names.append(os.path.basename(path))
118
+ return json.dumps({"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"})
119
+
120
+
121
+ print("Building example thumbnails...")
122
+ EXAMPLE_CARDS_HTML = build_example_cards_html()
123
+ print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
124
 
125
 
126
  def b64_to_pil_list(b64_json_str):
 
146
  return pil_images
147
 
148
 
149
+ def format_time(seconds: float) -> str:
150
+ if seconds < 60:
151
+ return f"{seconds:.2f}s"
152
+ minutes = int(seconds // 60)
153
+ secs = seconds % 60
154
+ return f"{minutes}m {secs:.2f}s"
155
+
156
+
157
  def update_dimensions_on_upload(pil_images):
158
  if not pil_images:
159
  return 1024, 1024
 
165
  else:
166
  nh = 1024
167
  nw = int(1024 * iw / ih)
168
+ return max(256, min(1024, (nw // 8) * 8)), max(256, min(1024, (nh // 8) * 8))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
 
170
 
171
  @spaces.GPU(duration=240)
172
  def infer(
 
176
  randomize_seed,
177
  width,
178
  height,
179
+ steps,
180
  guidance_scale,
181
  progress=gr.Progress(track_tqdm=True),
182
  ):
 
190
  seed = random.randint(0, MAX_SEED)
191
 
192
  pil_images = b64_to_pil_list(images_b64_json)
193
+ if pil_images:
194
+ width, height = update_dimensions_on_upload(pil_images)
 
 
195
 
196
  shared_kwargs = dict(
197
  prompt=prompt,
198
  height=height,
199
  width=width,
200
+ num_inference_steps=steps,
201
  guidance_scale=guidance_scale,
202
  )
203
+ if pil_images:
204
+ shared_kwargs["image"] = pil_images
205
 
 
206
  progress(0.05, desc="Running Standard VAE generation...")
207
+ t0_std = time.perf_counter()
208
+ gen_std = torch.Generator(device="cpu").manual_seed(seed)
209
  out_standard = pipe_standard(**shared_kwargs, generator=gen_std).images[0]
210
+ time_std_str = format_time(time.perf_counter() - t0_std)
211
+ print(f"Standard VAE done in {time_std_str}")
212
 
213
  gc.collect()
214
  torch.cuda.empty_cache()
215
 
216
+ progress(0.55, desc=f"Standard VAE done in {time_std_str} — now running Small Decoder VAE...")
217
+ t0_small = time.perf_counter()
218
+ gen_small = torch.Generator(device="cpu").manual_seed(seed)
219
+ out_small = pipe_small_decoder(**shared_kwargs, generator=gen_small).images[0]
220
+ time_small_str = format_time(time.perf_counter() - t0_small)
221
+ print(f"Small Decoder VAE done in {time_small_str}")
222
 
223
  gc.collect()
224
  torch.cuda.empty_cache()
225
 
226
+ label_std = f"Standard VAE — {time_std_str}"
227
+ label_small = f"Small Decoder VAE — {time_small_str}"
228
 
229
+ return out_standard, out_small, seed, label_std, label_small
 
 
 
 
 
 
 
230
 
 
231
 
232
+ # ── SVG / Icon constants ───────────────────────────────────────────────────────
233
 
234
+ FLUX_LOGO_SVG = '''<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
235
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
236
+ </svg>'''
237
+
238
+ 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>'
239
+ 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>'
240
+ 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>'
241
+ 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>'
242
 
243
  css = r"""
244
  @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');
 
255
  width:1px!important;height:1px!important;opacity:0.01!important;pointer-events:none!important;
256
  }
257
  #gradio-run-btn{
258
+ position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0.01;pointer-events:none;
 
259
  }
260
 
 
261
  .app-shell{
262
  background:#18181b;border:1px solid #27272a;border-radius:16px;
263
+ margin:12px auto;max-width:1500px;overflow:hidden;
264
  box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
265
  }
266
  .app-header{
 
280
  }
281
  .app-badge{
282
  font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
283
+ background:rgba(255,69,0,.15);color:#FF8C00;border:1px solid rgba(255,69,0,.25);letter-spacing:.3px;
284
  }
285
+ .app-badge.std{background:rgba(30,100,255,.12);color:#60A5FA;border:1px solid rgba(30,100,255,.25)}
286
+ .app-badge.small{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
287
 
 
288
  .app-toolbar{
289
  background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;
290
  display:flex;gap:4px;align-items:center;flex-wrap:wrap;
 
304
  .modern-tb-btn .tb-svg,.modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important}
305
  .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center}
306
 
 
307
  .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
308
+ .app-main-left{flex:0 0 360px;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a;background:#18181b}
309
+ .app-main-right{flex:1;display:flex;flex-direction:column;min-width:0;background:#0f0f13}
310
 
311
+ /* ── Input panel ── */
312
+ #gallery-drop-zone{position:relative;background:#09090b;min-height:260px;overflow:auto}
313
  #gallery-drop-zone.drag-over{outline:2px solid #FF4500;outline-offset:-2px;background:rgba(255,69,0,.04)}
314
+ .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20;width:90%}
315
  .upload-click-area{
316
  display:flex;flex-direction:column;align-items:center;justify-content:center;
317
+ cursor:pointer;padding:28px 24px;border:2px dashed #3f3f46;border-radius:16px;
318
  background:rgba(255,69,0,.03);transition:all .2s ease;gap:8px;
319
  }
320
+ .upload-click-area:hover{background:rgba(255,69,0,.08);border-color:#FF4500;transform:scale(1.02)}
321
+ .upload-click-area svg{width:64px;height:64px}
322
+ .upload-main-text{color:#71717a;font-size:13px;font-weight:500;margin-top:4px;text-align:center}
323
+ .upload-sub-text{color:#52525b;font-size:11px;text-align:center}
 
324
  .image-gallery-grid{
325
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));
326
+ gap:10px;padding:12px;align-content:start;
327
  }
328
  .gallery-thumb{
329
  position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
 
352
  .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
353
  .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
354
 
 
355
  .hint-bar{
356
  background:rgba(255,69,0,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
357
+ padding:8px 16px;font-size:12px;color:#a1a1aa;line-height:1.6;
358
  }
359
+ .hint-bar b{color:#FF8C00;font-weight:600}
360
  .hint-bar kbd{
361
  display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
362
  border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
363
  }
364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  .panel-card{border-bottom:1px solid #27272a}
366
  .panel-card-title{
367
+ padding:10px 16px;font-size:12px;font-weight:600;color:#71717a;
368
  text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
369
  }
370
+ .panel-card-body{padding:12px 16px;display:flex;flex-direction:column;gap:8px}
371
  .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
372
  .modern-textarea{
373
  width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
374
+ padding:10px 14px;font-family:'Inter',sans-serif;font-size:13px;color:#e4e4e7;
375
+ resize:vertical;outline:none;min-height:80px;transition:border-color .2s;
376
  }
377
  .modern-textarea:focus{border-color:#FF4500;box-shadow:0 0 0 3px rgba(255,69,0,.15)}
378
  .modern-textarea::placeholder{color:#3f3f46}
379
  .modern-textarea.error-flash{
380
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
 
381
  }
382
  @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  .btn-run{
385
  display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
386
  background:linear-gradient(135deg,#FF4500,#CC3700);border:none;border-radius:10px;
 
389
  box-shadow:0 4px 16px rgba(255,69,0,.3),inset 0 1px 0 rgba(255,255,255,.1);
390
  }
391
  .btn-run:hover{
392
+ background:linear-gradient(135deg,#FF6B00,#FF4500);transform:translateY(-1px);
393
  box-shadow:0 6px 24px rgba(255,69,0,.45),inset 0 1px 0 rgba(255,255,255,.15);
394
  }
395
+ .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(255,69,0,.3)}
396
  .btn-run svg{width:18px;height:18px;fill:#ffffff!important}
397
 
398
+ /* ── Output panels ── */
399
+ .output-area{flex:1;display:flex;flex-direction:column;gap:0;overflow:hidden}
400
+ .output-cols{display:flex;flex:1;gap:0;overflow:hidden}
401
+ .output-col{
402
+ flex:1;display:flex;flex-direction:column;border-left:1px solid #27272a;
403
+ background:#0f0f13;min-width:0;
404
+ }
405
+ .output-col:first-child{border-left:none}
406
+ .out-col-header{
407
+ padding:10px 16px;border-bottom:1px solid #27272a;
408
+ display:flex;align-items:center;justify-content:space-between;background:#18181b;
409
+ }
410
+ .out-col-title{font-size:12px;font-weight:700;letter-spacing:.5px;text-transform:uppercase}
411
+ .out-col-title.std{color:#60A5FA}
412
+ .out-col-title.small{color:#4ade80}
413
+ .out-col-badge{
414
+ font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px;
415
+ font-family:'JetBrains Mono',monospace;white-space:nowrap;
416
  }
417
+ .out-col-badge.std{background:rgba(30,100,255,.12);color:#60A5FA;border:1px solid rgba(30,100,255,.25)}
418
+ .out-col-badge.small{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
419
+ .out-col-body{
420
+ flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;
421
+ overflow:hidden;min-height:300px;position:relative;
422
  }
423
+ .out-col-body img.modern-out-img{max-width:100%;max-height:500px;image-rendering:auto;display:block}
424
+ .out-col-placeholder{color:#3f3f46;font-size:12px;text-align:center;padding:16px}
425
+ .out-col-footer{
426
+ padding:8px 16px;border-top:1px solid #27272a;background:#18181b;
427
+ display:flex;align-items:center;justify-content:space-between;min-height:38px;
 
428
  }
429
+ .timing-display{
430
+ font-family:'JetBrains Mono',monospace;font-size:11px;color:#71717a;
 
 
 
 
431
  }
 
432
  .out-download-btn{
433
  display:none;align-items:center;justify-content:center;background:rgba(255,69,0,.1);
434
  border:1px solid rgba(255,69,0,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
435
+ font-size:11px;font-weight:500;color:#FF8C00!important;gap:4px;height:24px;transition:all .15s;
436
  }
437
+ .out-download-btn:hover{background:rgba(255,69,0,.2);border-color:rgba(255,69,0,.35);color:#ffffff!important}
438
  .out-download-btn.visible{display:inline-flex}
439
+ .out-download-btn svg{width:12px;height:12px;fill:#FF8C00}
440
 
441
  /* ── Loader ── */
442
  .modern-loader{
443
+ display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
444
+ z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
 
445
  }
446
  .modern-loader.active{display:flex}
447
+ .modern-loader .loader-spinner{
448
+ width:36px;height:36px;border:3px solid #27272a;border-top-color:#FF4500;
449
  border-radius:50%;animation:spin .8s linear infinite;
450
  }
451
  @keyframes spin{to{transform:rotate(360deg)}}
452
+ .modern-loader .loader-text{font-size:12px;color:#a1a1aa;font-weight:500;text-align:center}
453
+ .loader-bar-track{width:160px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
454
  .loader-bar-fill{
455
  height:100%;background:linear-gradient(90deg,#FF4500,#FF8C00,#FF4500);
456
  background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
457
  }
458
  @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
459
 
460
+ /* ── Toast ── */
461
+ .toast-notification{
462
+ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
463
+ z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
464
+ font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
465
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
466
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
467
+ }
468
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
469
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
470
+ .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
471
+ .toast-notification.info{background:linear-gradient(135deg,#FF4500,#CC3700);color:#fff;border:1px solid rgba(255,255,255,.15)}
472
+ .toast-notification .toast-icon{font-size:16px;line-height:1}
473
+ .toast-notification .toast-text{line-height:1.3}
474
+
475
  /* ── Settings ── */
476
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
477
  .settings-group-title{
478
  font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
479
+ padding:8px 14px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
480
  }
481
+ .settings-group-body{padding:12px 14px;display:flex;flex-direction:column;gap:10px}
482
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:26px}
483
+ .slider-row label{font-size:12px;font-weight:500;color:#a1a1aa;min-width:80px;flex-shrink:0}
484
  .slider-row input[type="range"]{
485
+ flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;
486
+ border-radius:3px;outline:none;min-width:0;
487
  }
488
  .slider-row input[type="range"]::-webkit-slider-thumb{
489
  -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#FF4500,#CC3700);
490
  border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,69,0,.4);transition:transform .15s;
491
  }
492
  .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
493
+ .slider-row input[type="range"]::-moz-range-thumb{
494
+ width:16px;height:16px;background:linear-gradient(135deg,#FF4500,#CC3700);
495
+ border-radius:50%;cursor:pointer;border:none;
496
+ }
497
  .slider-row .slider-val{
498
+ min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px;
499
+ font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;
500
+ border-radius:6px;color:#a1a1aa;flex-shrink:0;
501
  }
502
  .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
503
  .checkbox-row input[type="checkbox"]{accent-color:#FF4500;width:16px;height:16px;cursor:pointer}
504
 
505
+ /* ── Suggestions / Examples ── */
506
+ .suggestions-section{border-top:1px solid #27272a;padding:10px 14px}
507
+ .suggestions-title,.examples-title{
508
+ font-size:11px;font-weight:600;color:#71717a;text-transform:uppercase;
509
+ letter-spacing:.8px;margin-bottom:8px;
510
+ }
511
+ .suggestions-wrap{display:flex;flex-wrap:wrap;gap:5px}
512
+ .suggestion-chip{
513
+ display:inline-flex;align-items:center;gap:4px;padding:4px 10px;
514
+ background:rgba(255,69,0,.08);border:1px solid rgba(255,69,0,.2);border-radius:20px;
515
+ color:#FF8C00;font-size:11px;font-weight:500;font-family:'Inter',sans-serif;
516
+ cursor:pointer;transition:all .15s;white-space:nowrap;
517
+ }
518
+ .suggestion-chip:hover{background:rgba(255,69,0,.15);border-color:rgba(255,69,0,.35);color:#FFB366;transform:translateY(-1px)}
519
+
520
+ .examples-section{border-top:1px solid #27272a;padding:10px 14px}
521
+ .examples-scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:6px}
522
+ .examples-scroll::-webkit-scrollbar{height:4px}
523
+ .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
524
+ .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
525
+ .example-card{
526
+ flex-shrink:0;width:180px;background:#09090b;border:1px solid #27272a;
527
+ border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
528
+ }
529
+ .example-card:hover{border-color:#FF4500;transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,69,0,.15)}
530
+ .example-card.loading{opacity:.5;pointer-events:none}
531
+ .example-thumbs{display:flex;height:90px;overflow:hidden;background:#18181b}
532
+ .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a}
533
+ .example-thumb-placeholder{
534
+ flex:1;display:flex;align-items:center;justify-content:center;
535
+ background:#18181b;color:#3f3f46;font-size:11px;min-width:0;
536
+ }
537
+ .example-meta{padding:5px 8px;display:flex;align-items:center;gap:6px}
538
+ .example-badge{
539
+ display:inline-flex;padding:2px 6px;background:rgba(255,69,0,.1);border-radius:4px;
540
+ font-size:10px;font-weight:600;color:#FF8C00;font-family:'JetBrains Mono',monospace;white-space:nowrap;
541
+ }
542
+ .example-prompt-text{
543
+ padding:0 8px 7px;font-size:10px;color:#a1a1aa;line-height:1.4;
544
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
545
+ }
546
+
547
  /* ── Statusbar ── */
548
  .app-statusbar{
549
  background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
550
  display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
551
  }
552
+ .app-statusbar .sb-section{
553
  padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
554
  font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
555
  }
556
+ .app-statusbar .sb-section.sb-fixed{
557
  flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
558
+ padding:3px 12px;background:rgba(255,69,0,.08);border-radius:6px;color:#FF8C00;font-weight:500;
559
  }
560
  .exp-note{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center}
561
+ .exp-note a{color:#FF8C00;text-decoration:none}
562
  .exp-note a:hover{text-decoration:underline}
563
 
564
  ::-webkit-scrollbar{width:8px;height:8px}
 
568
 
569
  @media(max-width:900px){
570
  .app-main-row{flex-direction:column}
571
+ .app-main-left{flex:0 0 auto;border-right:none;border-bottom:1px solid #27272a}
572
+ .output-cols{flex-direction:column}
573
+ .output-col{border-left:none;border-top:1px solid #27272a}
574
  }
575
  """
576
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  gallery_js = r"""
578
  () => {
579
  function init() {
 
593
  const imgCountSb = document.getElementById('sb-image-count');
594
 
595
  if (!galleryGrid || !fileInput || !dropZone) { setTimeout(init, 250); return; }
596
+
597
  window.__fluxInitDone = true;
598
 
599
  let images = [];
 
696
  }
697
  if (uploadPrompt) uploadPrompt.style.display = 'none';
698
  galleryGrid.style.display = 'grid';
699
+
700
  let html = '';
701
  images.forEach((img, i) => {
702
  const sel = i === selectedIdx ? ' selected' : '';
703
+ html += `<div class="gallery-thumb${sel}" data-idx="${i}">
704
+ <img src="${img.b64}" alt="${img.name||'image'}">
705
+ <span class="thumb-badge">#${i+1}</span>
706
+ <button class="thumb-remove" data-remove="${i}">\u2715</button>
707
+ </div>`;
708
  });
709
+ html += `<div class="gallery-add-card" id="gallery-add-card">
710
+ <span class="add-icon">+</span><span class="add-text">Add</span>
711
+ </div>`;
712
  galleryGrid.innerHTML = html;
713
 
714
  galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
715
+ thumb.addEventListener('click', e => {
716
  if (e.target.closest('.thumb-remove')) return;
717
  selectedIdx = selectedIdx === parseInt(thumb.dataset.idx) ? -1 : parseInt(thumb.dataset.idx);
718
  renderGallery();
719
  });
720
  });
721
  galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
722
+ btn.addEventListener('click', e => { e.stopPropagation(); removeImage(parseInt(btn.dataset.remove)); });
723
  });
724
  const addCard = document.getElementById('gallery-add-card');
725
  if (addCard) addCard.addEventListener('click', () => fileInput.click());
 
729
  Array.from(files).forEach(file => {
730
  if (!file.type.startsWith('image/')) return;
731
  const reader = new FileReader();
732
+ reader.onload = e => addImage(e.target.result, file.name);
733
  reader.readAsDataURL(file);
734
  });
735
  }
736
 
737
+ fileInput.addEventListener('change', e => { processFiles(e.target.files); e.target.value = ''; });
738
  if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
739
  if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
740
  if (btnRemove) btnRemove.addEventListener('click', () => { if (selectedIdx >= 0) removeImage(selectedIdx); });
741
  if (btnClear) btnClear.addEventListener('click', clearAll);
742
 
743
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
744
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
745
+ dropZone.addEventListener('drop', e => {
746
+ e.preventDefault(); dropZone.classList.remove('drag-over');
747
+ if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
748
+ });
749
 
750
  if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
751
+
752
+ window.__setPrompt = function(text) {
753
+ if (promptInput) { promptInput.value = text; syncPromptToGradio(); }
754
+ };
755
 
756
  // Example cards
757
  document.querySelectorAll('.example-card[data-idx]').forEach(card => {
 
796
  syncSlider('custom-height', 'gradio-height');
797
 
798
  const randCheck = document.getElementById('custom-randomize');
799
+ if (randCheck) randCheck.addEventListener('change', () => {
800
+ const container = document.getElementById('gradio-randomize');
801
+ if (!container) return;
802
+ const cb = container.querySelector('input[type="checkbox"]');
803
+ if (cb && cb.checked !== randCheck.checked) cb.click();
804
+ });
 
 
805
 
806
+ // Loader
807
+ function showLoader() {
808
+ document.querySelectorAll('.modern-loader').forEach(l => l.classList.add('active'));
809
+ const sb = document.querySelector('.sb-fixed');
810
+ if (sb) sb.textContent = 'Processing...';
811
+ }
812
+ function hideLoader() {
813
+ document.querySelectorAll('.modern-loader').forEach(l => l.classList.remove('active'));
814
+ const sb = document.querySelector('.sb-fixed');
815
+ if (sb) sb.textContent = 'Done';
816
+ }
817
+ window.__showLoader = showLoader;
818
+ window.__hideLoader = hideLoader;
819
 
820
  function validateBeforeRun() {
821
  const promptVal = promptInput ? promptInput.value.trim() : '';
 
825
 
826
  window.__clickGradioRunBtn = function() {
827
  if (!validateBeforeRun()) return;
828
+ syncPromptToGradio(); syncImagesToGradio(); showLoader();
 
829
  setTimeout(() => {
830
  const gradioBtn = document.getElementById('gradio-run-btn');
831
  if (!gradioBtn) return;
 
846
  wire_outputs_js = r"""
847
  () => {
848
  function watchOutputs() {
849
+ // Watch both result containers
850
+ const containers = [
851
+ {id: 'gradio-result-std', bodyId: 'output-std-body', dlId: 'dl-btn-std', timingId: 'timing-std', suffix: 'std_output.png'},
852
+ {id: 'gradio-result-small', bodyId: 'output-small-body', dlId: 'dl-btn-small', timingId: 'timing-small', suffix: 'small_output.png'},
853
+ ];
854
+
855
+ let ready = containers.every(c => document.getElementById(c.id) && document.getElementById(c.bodyId));
856
+ if (!ready) { setTimeout(watchOutputs, 500); return; }
857
+
858
+ containers.forEach(c => {
859
+ const dlBtn = document.getElementById(c.dlId);
860
+ if (dlBtn) {
861
+ dlBtn.addEventListener('click', e => {
862
+ e.stopPropagation();
863
+ const img = document.getElementById(c.bodyId).querySelector('img.modern-out-img');
864
+ if (img && img.src) {
865
+ const a = document.createElement('a');
866
+ a.href = img.src; a.download = c.suffix;
867
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
868
+ }
869
+ });
870
+ }
871
+ });
 
 
872
 
873
+ function syncImage(c) {
874
+ const resultContainer = document.getElementById(c.id);
875
+ const outBody = document.getElementById(c.bodyId);
876
+ if (!resultContainer || !outBody) return;
877
+ const resultImg = resultContainer.querySelector('img');
878
+ if (resultImg && resultImg.src) {
879
+ const ph = outBody.querySelector('.out-col-placeholder');
880
+ if (ph) ph.style.display = 'none';
881
+ let existing = outBody.querySelector('img.modern-out-img');
882
+ if (!existing) { existing = document.createElement('img'); existing.className = 'modern-out-img'; outBody.appendChild(existing); }
883
+ if (existing.src !== resultImg.src) {
884
+ existing.src = resultImg.src;
885
+ const dlBtn = document.getElementById(c.dlId);
886
+ if (dlBtn) dlBtn.classList.add('visible');
887
+ if (window.__hideLoader) window.__hideLoader();
888
  }
889
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  }
891
 
892
+ containers.forEach(c => {
893
+ const resultContainer = document.getElementById(c.id);
894
+ if (!resultContainer) return;
895
+ const observer = new MutationObserver(() => syncImage(c));
896
+ observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
897
+ setInterval(() => syncImage(c), 800);
898
+ });
899
  }
900
  watchOutputs();
901
 
902
+ function watchTimings() {
903
+ const timingMap = [
904
+ {gradioId: 'gradio-timing-std', displayId: 'timing-std-display'},
905
+ {gradioId: 'gradio-timing-small', displayId: 'timing-small-display'},
906
+ ];
907
+ timingMap.forEach(t => {
908
+ function sync() {
909
+ const container = document.getElementById(t.gradioId);
910
+ const display = document.getElementById(t.displayId);
911
+ if (!container || !display) return;
912
+ const el = container.querySelector('textarea, input');
913
+ if (el && el.value && el.value.trim()) display.textContent = el.value;
914
+ }
915
+ const container = document.getElementById(t.gradioId);
916
+ if (container) {
917
+ const obs = new MutationObserver(sync);
918
+ obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
919
+ }
920
+ setInterval(sync, 600);
921
+ });
922
+ }
923
+ watchTimings();
924
+
925
+ function watchSeed() {
926
+ const seedContainer = document.getElementById('gradio-seed');
927
+ const seedSlider = document.getElementById('custom-seed');
928
+ const seedVal = document.getElementById('custom-seed-val');
929
+ if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
930
+ function sync() {
931
+ const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
932
+ if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; }
933
+ }
934
+ const obs = new MutationObserver(sync);
935
+ obs.observe(seedContainer, {childList:true, subtree:true, attributes:true});
936
+ setInterval(sync, 1000);
937
+ }
938
+ watchSeed();
939
+
940
  function watchExampleResults() {
941
  const container = document.getElementById('example-result-data');
942
  if (!container) { setTimeout(watchExampleResults, 500); return; }
943
  let lastProcessed = '';
944
  function checkResult() {
945
  const el = container.querySelector('textarea') || container.querySelector('input');
946
+ if (!el) return;
947
+ const val = el.value;
948
+ if (!val || val === lastProcessed || val.length < 20) return;
949
  try {
950
+ const data = JSON.parse(val);
951
  if (data.status === 'ok' && data.images && data.images.length > 0) {
952
+ lastProcessed = val;
953
  if (window.__clearAll) window.__clearAll();
954
  if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
955
  data.images.forEach((b64, i) => {
 
960
  });
961
  document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
962
  if (window.__showToast) window.__showToast('Example loaded — ' + data.images.length + ' image(s)', 'info');
963
+ } else if (data.status === 'error') {
964
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
965
+ if (window.__showToast) window.__showToast('Could not load example images', 'error');
966
  }
967
  } catch(e) {}
968
  }
 
974
  }
975
  """
976
 
 
 
977
  with gr.Blocks(css=css) as demo:
978
 
979
+ # ── Hidden Gradio state ───────────────────────────────────────────────────
980
+ hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
981
+ prompt_hidden = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
982
+ seed_slider = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
983
+ randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
984
+ guidance_scale = gr.Slider(minimum=0.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False)
985
+ steps_slider = gr.Slider(minimum=1, maximum=20, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
986
+ width_slider = gr.Slider(minimum=256, maximum=1024, step=8, value=1024, elem_id="gradio-width", elem_classes="hidden-input", container=False)
987
+ height_slider = gr.Slider(minimum=256, maximum=1024, step=8, value=1024, elem_id="gradio-height", elem_classes="hidden-input", container=False)
988
+
989
+ result_std = gr.Image(elem_id="gradio-result-std", elem_classes="hidden-input", container=False, format="png")
990
+ result_small_dec = gr.Image(elem_id="gradio-result-small", elem_classes="hidden-input", container=False, format="png")
991
+ seed_used = gr.Number(value=0, elem_id="gradio-seed-used", elem_classes="hidden-input", container=False)
992
+ timing_std_hidden = gr.Textbox(value="", elem_id="gradio-timing-std", elem_classes="hidden-input", container=False)
993
+ timing_small_hidden= gr.Textbox(value="", elem_id="gradio-timing-small", elem_classes="hidden-input", container=False)
994
+
995
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
 
 
 
 
996
  example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
997
  example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
998
 
999
  gr.HTML(f"""
1000
  <div class="app-shell">
1001
 
1002
+ <div class="app-header">
1003
+ <div class="app-header-left">
1004
+ <div class="app-logo">{FLUX_LOGO_SVG}</div>
1005
+ <span class="app-title">FLUX.2-klein-4B Comparator</span>
1006
+ <span class="app-badge">v1.0</span>
1007
+ <span class="app-badge std">Standard VAE</span>
1008
+ <span class="app-badge small">Small Decoder</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  </div>
1010
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1011
 
1012
+ <div class="app-toolbar">
1013
+ <button id="tb-upload" class="modern-tb-btn" title="Upload images">
1014
+ {UPLOAD_SVG}<span class="tb-label">Upload</span>
1015
+ </button>
1016
+ <button id="tb-remove" class="modern-tb-btn" title="Remove selected">
1017
+ {REMOVE_SVG}<span class="tb-label">Remove</span>
1018
+ </button>
1019
+ <button id="tb-clear" class="modern-tb-btn" title="Clear all">
1020
+ {CLEAR_SVG}<span class="tb-label">Clear All</span>
1021
+ </button>
1022
+ <div class="tb-sep"></div>
1023
+ <span id="tb-image-count" class="tb-info">No images</span>
1024
  </div>
1025
 
1026
+ <div class="app-main-row">
1027
+
1028
+ <!-- LEFT: Input Panel -->
1029
+ <div class="app-main-left">
1030
+
1031
+ <div id="gallery-drop-zone">
1032
+ <div id="upload-prompt" class="upload-prompt-modern">
1033
+ <div id="upload-click-area" class="upload-click-area">
1034
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1035
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#FF4500" stroke-width="2" stroke-dasharray="4 3"/>
1036
+ <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"/>
1037
+ <circle cx="28" cy="30" r="6" fill="rgba(255,69,0,0.2)" stroke="#FF4500" stroke-width="1.5"/>
1038
+ </svg>
1039
+ <span class="upload-main-text">Click or drag image here</span>
1040
+ <span class="upload-sub-text">Optional reference image for editing tasks</span>
1041
+ </div>
1042
+ </div>
1043
+ <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
1044
+ <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
1045
+ </div>
1046
 
1047
+ <div class="hint-bar">
1048
+ <b>Optional:</b> Upload a reference image for editing &nbsp;&middot;&nbsp;
1049
+ Text-to-image also works without any input &nbsp;&middot;&nbsp;
1050
+ <kbd>Clear All</kbd> to reset
1051
+ </div>
 
 
 
1052
 
1053
+ <div class="panel-card">
1054
+ <div class="panel-card-title">Prompt</div>
1055
+ <div class="panel-card-body">
1056
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="4"
1057
+ placeholder="e.g., Transform the scene into a snowy winter day..."></textarea>
1058
+ </div>
 
 
 
 
 
 
 
 
 
 
 
1059
  </div>
1060
+
1061
+ <div style="padding:12px 16px;">
1062
+ <button id="custom-run-btn" class="btn-run">
1063
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1064
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="white"/>
1065
+ </svg>
1066
+ <span>Run Comparison</span>
1067
+ </button>
1068
  </div>
1069
+
1070
+ <div class="suggestions-section">
1071
+ <div class="suggestions-title">Quick Prompts</div>
1072
+ <div class="suggestions-wrap">
1073
+ <button class="suggestion-chip" onclick="window.__setPrompt('Change the weather to stormy.')">Stormy Weather</button>
1074
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform the scene into a snowy winter day.')">Snowy Winter</button>
1075
+ <button class="suggestion-chip" onclick="window.__setPrompt('Relight with soft golden sunset lighting.')">Golden Sunset</button>
1076
+ <button class="suggestion-chip" onclick="window.__setPrompt('Make the texture high-resolution and detailed.')">HD Texture</button>
1077
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to oil painting style.')">Oil Painting</button>
1078
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add cinematic film grain and color grading.')">Cinematic</button>
1079
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime illustration style.')">Anime Style</button>
1080
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply dramatic noir black and white style.')">Noir B&W</button>
1081
+ </div>
1082
  </div>
1083
+
1084
+ <div class="examples-section">
1085
+ <div class="examples-title">Quick Examples</div>
1086
+ <div class="examples-scroll">
1087
+ {EXAMPLE_CARDS_HTML}
1088
+ </div>
1089
+ </div>
1090
+
1091
+ <div class="settings-group">
1092
+ <div class="settings-group-title">Advanced Settings</div>
1093
+ <div class="settings-group-body">
1094
+ <div class="slider-row">
1095
+ <label>Seed</label>
1096
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1097
+ <span class="slider-val" id="custom-seed-val">0</span>
1098
+ </div>
1099
+ <div class="checkbox-row">
1100
+ <input type="checkbox" id="custom-randomize" checked>
1101
+ <label for="custom-randomize">Randomize seed</label>
1102
+ </div>
1103
+ <div class="slider-row">
1104
+ <label>Guidance</label>
1105
+ <input type="range" id="custom-guidance" min="0" max="10" step="0.1" value="1.0">
1106
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
1107
+ </div>
1108
+ <div class="slider-row">
1109
+ <label>Steps</label>
1110
+ <input type="range" id="custom-steps" min="1" max="20" step="1" value="4">
1111
+ <span class="slider-val" id="custom-steps-val">4</span>
1112
+ </div>
1113
+ <div class="slider-row">
1114
+ <label>Width</label>
1115
+ <input type="range" id="custom-width" min="256" max="1024" step="8" value="1024">
1116
+ <span class="slider-val" id="custom-width-val">1024</span>
1117
+ </div>
1118
+ <div class="slider-row">
1119
+ <label>Height</label>
1120
+ <input type="range" id="custom-height" min="256" max="1024" step="8" value="1024">
1121
+ <span class="slider-val" id="custom-height-val">1024</span>
1122
+ </div>
1123
+ </div>
1124
  </div>
 
 
1125
 
1126
  </div>
1127
+
1128
+ <!-- RIGHT: Output Area -->
1129
+ <div class="app-main-right">
1130
+ <div class="output-area">
1131
+
1132
+ <div class="output-cols">
1133
+
1134
+ <!-- Standard VAE -->
1135
+ <div class="output-col">
1136
+ <div class="out-col-header">
1137
+ <span class="out-col-title std">&#128310; Standard VAE</span>
1138
+ <span class="out-col-badge std">Generated First</span>
1139
+ </div>
1140
+ <div class="out-col-body" id="output-std-body">
1141
+ <div class="modern-loader" id="output-loader-std">
1142
+ <div class="loader-spinner"></div>
1143
+ <div class="loader-text">Running Standard VAE...</div>
1144
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1145
+ </div>
1146
+ <div class="out-col-placeholder">Standard VAE output will appear here</div>
1147
+ </div>
1148
+ <div class="out-col-footer">
1149
+ <span class="timing-display" id="timing-std-display">⏱ Waiting...</span>
1150
+ <span id="dl-btn-std" class="out-download-btn" title="Download">
1151
+ {DOWNLOAD_SVG} Save
1152
+ </span>
1153
+ </div>
1154
+ </div>
1155
+
1156
+ <!-- Small Decoder VAE -->
1157
+ <div class="output-col">
1158
+ <div class="out-col-header">
1159
+ <span class="out-col-title small">&#128312; Small Decoder VAE</span>
1160
+ <span class="out-col-badge small">Generated Second</span>
1161
+ </div>
1162
+ <div class="out-col-body" id="output-small-body">
1163
+ <div class="modern-loader" id="output-loader-small">
1164
+ <div class="loader-spinner"></div>
1165
+ <div class="loader-text">Running Small Decoder VAE...</div>
1166
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1167
+ </div>
1168
+ <div class="out-col-placeholder">Small Decoder VAE output will appear here</div>
1169
+ </div>
1170
+ <div class="out-col-footer">
1171
+ <span class="timing-display" id="timing-small-display">⏱ Waiting...</span>
1172
+ <span id="dl-btn-small" class="out-download-btn" title="Download">
1173
+ {DOWNLOAD_SVG} Save
1174
+ </span>
1175
+ </div>
1176
+ </div>
1177
+
1178
+ </div>
1179
+ </div>
1180
  </div>
 
1181
 
1182
  </div>
 
1183
 
1184
+ <div class="exp-note">
1185
+ Experimental Space — <a href="https://huggingface.co/black-forest-labs/FLUX.2-klein-4B" target="_blank">FLUX.2-klein-4B</a>
1186
+ &middot; <a href="https://huggingface.co/black-forest-labs/FLUX.2-small-decoder" target="_blank">FLUX.2-small-decoder</a>
1187
+ </div>
 
1188
 
1189
+ <div class="app-statusbar">
1190
+ <div class="sb-section" id="sb-image-count">No images uploaded</div>
1191
+ <div class="sb-section sb-fixed">Ready</div>
1192
+ </div>
 
1193
 
1194
  </div>
1195
  """)
 
1201
 
1202
  run_btn.click(
1203
  fn=infer,
1204
+ inputs=[
1205
+ hidden_images_b64,
1206
+ prompt_hidden,
1207
+ seed_slider,
1208
+ randomize_seed,
1209
+ width_slider,
1210
+ height_slider,
1211
+ steps_slider,
1212
+ guidance_scale,
1213
+ ],
1214
+ outputs=[result_std, result_small_dec, seed_used, timing_std_hidden, timing_small_hidden],
1215
  js=r"""(imgs, p, s, rs, w, h, st, gs) => {
1216
  const images = window.__uploadedImages || [];
1217
  const b64Array = images.map(img => img.b64);
1218
  const imgsJson = JSON.stringify(b64Array);
1219
  const promptEl = document.getElementById('custom-prompt-input');
1220
  const promptVal = promptEl ? promptEl.value : p;
1221
+ return [imgsJson, promptVal, s, rs, w, h, st, gs];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
  }""",
1223
  )
1224