prithivMLmods commited on
Commit
33aa2ab
·
verified ·
1 Parent(s): 4c20639

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +607 -1717
app.py CHANGED
@@ -1,10 +1,7 @@
1
-
2
  import os
3
  import gc
4
- import io
5
  import uuid
6
  import json
7
- import base64
8
  import random
9
  import threading
10
  import concurrent.futures
@@ -19,9 +16,9 @@ from PIL import Image
19
  from gradio import Server
20
  from fastapi import Request, UploadFile, File, Form
21
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
22
-
23
  from diffusers import Flux2KleinPipeline, AutoencoderKLFlux2
24
 
 
25
  app = Server()
26
 
27
  BASE_DIR = Path(__file__).resolve().parent
@@ -35,78 +32,70 @@ OUTPUT_DIR.mkdir(exist_ok=True)
35
  MAX_SEED = np.iinfo(np.int32).max
36
  MAX_IMAGE_SIZE = 1024
37
 
38
- dtype = torch.bfloat16
39
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
40
 
41
  if torch.cuda.is_available():
 
 
42
  DEVICE_LABEL = torch.cuda.get_device_name(torch.cuda.current_device()).lower()
43
  else:
44
- DEVICE_LABEL = DEVICE
45
 
 
46
  print("Loading 4B Distilled model (Standard VAE)...")
47
  pipe_standard = Flux2KleinPipeline.from_pretrained(
48
  "black-forest-labs/FLUX.2-klein-4B",
49
  torch_dtype=dtype,
50
- )
51
  pipe_standard.enable_model_cpu_offload()
52
 
53
  print("Loading Small Decoder VAE...")
54
  vae_small = AutoencoderKLFlux2.from_pretrained(
55
  "black-forest-labs/FLUX.2-small-decoder",
56
  torch_dtype=dtype,
57
- )
58
 
59
  print("Loading 4B Distilled model (Small Decoder VAE)...")
60
  pipe_small_decoder = Flux2KleinPipeline.from_pretrained(
61
  "black-forest-labs/FLUX.2-klein-4B",
62
  vae=vae_small,
63
  torch_dtype=dtype,
64
- )
65
  pipe_small_decoder.enable_model_cpu_offload()
66
 
67
  pipe_lock_standard = threading.Lock()
68
- pipe_lock_small = threading.Lock()
69
-
70
-
71
- def image_to_base64(img: Image.Image) -> str:
72
- buf = io.BytesIO()
73
- img.save(buf, format="PNG")
74
- return base64.b64encode(buf.getvalue()).decode("utf-8")
75
-
76
-
77
- def save_image(img: Image.Image, prefix: str = "output") -> str:
78
- filename = f"{prefix}_{uuid.uuid4().hex}.png"
79
- path = OUTPUT_DIR / filename
80
- img.save(path, format="PNG")
81
- return filename
82
-
83
 
 
84
  def calc_dimensions(pil_img: Image.Image):
85
  iw, ih = pil_img.size
86
  aspect = iw / ih
 
87
  if aspect >= 1:
88
- new_width = 1024
89
  new_height = int(round(1024 / aspect))
90
  else:
91
  new_height = 1024
92
- new_width = int(round(1024 * aspect))
93
- new_width = max(256, min(1024, round(new_width / 8) * 8))
 
94
  new_height = max(256, min(1024, round(new_height / 8) * 8))
95
  return new_width, new_height
96
 
97
-
98
- def parse_and_resize_images(image_paths: list, width: int, height: int):
99
- raw_list = []
 
 
100
  for path in image_paths:
101
  try:
102
  img = Image.open(path).convert("RGB")
103
- raw_list.append(img)
104
  except Exception as e:
105
  print(f"Skipping invalid image: {e}")
106
- if not raw_list:
107
- return None
108
- return [img.resize((width, height), Image.LANCZOS) for img in raw_list]
109
-
110
 
111
  def run_pipeline(pipe, lock, kwargs, seed):
112
  with lock:
@@ -114,80 +103,67 @@ def run_pipeline(pipe, lock, kwargs, seed):
114
  result = pipe(**kwargs, generator=gen).images[0]
115
  return result
116
 
117
-
118
- def get_example_items():
119
- example_prompts = {
120
- "1.jpg": "Change the weather to stormy.",
121
- "2.jpg": "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition.",
122
- "3.jpg": "Relight the image with soft golden sunset lighting while keeping all structures and subject details consistent.",
123
- "4.jpg": "Make the texture high-resolution.",
124
- }
125
- items = []
126
- if EXAMPLES_DIR.exists():
127
- for name in sorted(os.listdir(EXAMPLES_DIR)):
128
- if name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
129
- items.append({
130
- "file": name,
131
- "url": f"/example-file/{name}",
132
- "prompt": example_prompts.get(name, "Edit this image while preserving composition."),
133
- })
134
- return items
135
-
136
-
137
- @app.get("/example-file/{filename}")
138
- async def example_file(filename: str):
139
- path = EXAMPLES_DIR / filename
140
- if not path.exists():
141
- return JSONResponse({"error": "Example not found"}, status_code=404)
142
- return FileResponse(path)
143
-
144
-
145
- @app.get("/download/{filename}")
146
- async def download_file(filename: str):
147
  path = OUTPUT_DIR / filename
148
- if not path.exists():
149
- return JSONResponse({"error": "File not found"}, status_code=404)
150
- return FileResponse(path, filename=filename, media_type="image/png")
151
-
152
 
 
153
  @spaces.GPU(duration=120)
154
- def run_inference(image_paths, prompt, seed, randomize_seed, width, height, steps, guidance_scale):
 
 
 
 
 
 
 
 
 
155
  gc.collect()
156
  if torch.cuda.is_available():
157
  torch.cuda.empty_cache()
158
 
 
 
 
159
  if randomize_seed:
160
  seed = random.randint(0, MAX_SEED)
161
 
162
  image_list = None
163
- if image_paths:
164
  try:
165
  first_pil = Image.open(image_paths[0]).convert("RGB")
166
  width, height = calc_dimensions(first_pil)
167
- except Exception:
168
- pass
169
- image_list = parse_and_resize_images(image_paths, width, height)
170
 
171
- width = max(256, min(MAX_IMAGE_SIZE, round(int(width) / 8) * 8))
172
  height = max(256, min(MAX_IMAGE_SIZE, round(int(height) / 8) * 8))
173
 
174
  shared_kwargs = dict(
175
  prompt=prompt,
176
  height=height,
177
  width=width,
178
- num_inference_steps=steps,
179
  guidance_scale=guidance_scale,
180
  )
181
  if image_list is not None:
182
  shared_kwargs["image"] = image_list
183
 
184
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
185
- future_std = executor.submit(run_pipeline, pipe_standard, pipe_lock_standard, shared_kwargs, seed)
186
  future_small = executor.submit(run_pipeline, pipe_small_decoder, pipe_lock_small, shared_kwargs, seed)
187
- concurrent.futures.wait([future_std, future_small], return_when=concurrent.futures.ALL_COMPLETED)
 
 
 
 
188
 
189
  out_standard = future_std.result()
190
- out_small = future_small.result()
191
 
192
  gc.collect()
193
  if torch.cuda.is_available():
@@ -196,6 +172,51 @@ def run_inference(image_paths, prompt, seed, randomize_seed, width, height, step
196
  return out_standard, out_small, seed
197
 
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  @app.post("/api/compare")
200
  async def compare_images(
201
  prompt: str = Form(...),
@@ -204,7 +225,7 @@ async def compare_images(
204
  width: str = Form("1024"),
205
  height: str = Form("1024"),
206
  steps: str = Form("4"),
207
- guidance_scale: str = Form("1.0"),
208
  images: Optional[List[UploadFile]] = File(None),
209
  ):
210
  temp_paths = []
@@ -212,40 +233,34 @@ async def compare_images(
212
  image_paths = []
213
  if images:
214
  for upload in images:
 
215
  suffix = Path(upload.filename).suffix or ".png"
216
- temp_name = f"upload_{uuid.uuid4().hex}{suffix}"
217
- temp_path = OUTPUT_DIR / temp_name
218
  content = await upload.read()
219
  with open(temp_path, "wb") as f:
220
  f.write(content)
221
  temp_paths.append(str(temp_path))
222
  image_paths.append(str(temp_path))
223
 
224
- out_standard, out_small, used_seed = run_inference(
225
- image_paths=image_paths,
226
  prompt=prompt,
 
227
  seed=int(seed),
228
- randomize_seed=randomize_seed.lower() == "true",
229
  width=int(width),
230
  height=int(height),
231
- steps=int(steps),
232
- guidance_scale=float(guidance_scale),
233
  )
234
 
235
- std_filename = save_image(out_standard, prefix="standard")
236
- small_filename = save_image(out_small, prefix="small")
237
 
238
  return JSONResponse({
239
  "success": True,
240
  "seed": used_seed,
241
- "standard": {
242
- "image_url": f"/download/{std_filename}",
243
- "download_url": f"/download/{std_filename}",
244
- },
245
- "small": {
246
- "image_url": f"/download/{small_filename}",
247
- "download_url": f"/download/{small_filename}",
248
- },
249
  "device": DEVICE_LABEL,
250
  })
251
 
@@ -253,1729 +268,604 @@ async def compare_images(
253
  return JSONResponse({"success": False, "error": str(e)}, status_code=500)
254
  finally:
255
  for p in temp_paths:
256
- try:
257
- if os.path.exists(p):
258
- os.remove(p)
259
- except Exception:
260
- pass
261
-
262
 
 
263
  @app.get("/", response_class=HTMLResponse)
264
  async def homepage(request: Request):
265
  examples = get_example_items()
266
  examples_json = json.dumps(examples)
267
 
268
- return f"""<!DOCTYPE html>
 
269
  <html lang="en">
270
  <head>
271
- <meta charset="UTF-8"/>
272
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
273
- <title>FLUX.2 Decoder Comparator — Terminal</title>
 
274
  <style>
275
- @import url('https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400&family=Ubuntu:wght@300;400;500;700&display=swap');
276
-
277
  :root {{
278
- --term-bg: #300a24;
279
- --term-bg2: #2c0a20;
280
- --term-bg3: #3a0f2e;
281
- --term-win: #1e0a18;
282
- --term-chrome: #3c1535;
283
- --term-border: #5a1e48;
284
- --term-border2: #7a2860;
285
- --green: #4edd91;
286
- --green2: #2ec27e;
287
- --orange: #e95420;
288
- --orange2: #ff6b35;
289
- --orange-soft: rgba(233,84,32,0.18);
290
- --orange-border: rgba(233,84,32,0.45);
291
- --yellow: #f4c21b;
292
- --cyan: #4fd8e0;
293
- --pink: #e06dc0;
294
- --white: #f8f8f2;
295
- --gray: #a89db8;
296
- --gray2: #6d5f7d;
297
- --prompt-color: #4edd91;
298
- --cursor-color: #f8f8f2;
299
- --scrollbar: #5a1e48;
300
- --font-mono: 'Ubuntu Mono', 'Courier New', monospace;
301
- --font-ui: 'Ubuntu', sans-serif;
302
- --radius: 0px;
303
- --shadow: 0 8px 32px rgba(0,0,0,0.6);
304
- --shadow-lg: 0 16px 48px rgba(0,0,0,0.75);
305
- --glow-green: 0 0 12px rgba(78,221,145,0.35);
306
- --glow-orange: 0 0 12px rgba(233,84,32,0.45);
307
- }}
308
-
309
- *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
310
-
311
- html, body {{
312
- height: 100%;
313
- background: #1a0514;
314
- font-family: var(--font-mono);
315
- overflow: hidden;
316
- }}
317
-
318
- ::-webkit-scrollbar {{ width: 5px; height: 5px; }}
319
- ::-webkit-scrollbar-track {{ background: var(--term-bg2); }}
320
- ::-webkit-scrollbar-thumb {{ background: var(--scrollbar); border-radius: 2px; }}
321
-
322
- /* ══════════════════════════════════════════
323
- DESKTOP WALLPAPER
324
- ══════════════════════════════════════════ */
325
- .desktop {{
326
- width: 100vw;
327
- height: 100vh;
328
- background:
329
- radial-gradient(ellipse at 20% 50%, rgba(233,84,32,0.12) 0%, transparent 60%),
330
- radial-gradient(ellipse at 80% 20%, rgba(124,58,237,0.10) 0%, transparent 55%),
331
- radial-gradient(ellipse at 60% 80%, rgba(78,221,145,0.07) 0%, transparent 50%),
332
- #1a0514;
333
- display: flex;
334
- align-items: center;
335
- justify-content: center;
336
- position: relative;
337
- overflow: hidden;
338
- }}
339
-
340
- /* subtle grid */
341
- .desktop::before {{
342
- content: '';
343
- position: absolute;
344
- inset: 0;
345
- background-image:
346
- linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
347
- linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
348
- background-size: 40px 40px;
349
- pointer-events: none;
350
- }}
351
-
352
- /* ══════════════════════════════════════════
353
- TERMINAL WINDOW
354
- ══════════════════════════════════════════ */
355
- .terminal {{
356
- width: min(1380px, 97vw);
357
- height: min(920px, 96vh);
358
- background: var(--term-bg);
359
- border: 1px solid var(--term-border);
360
- border-radius: 10px;
361
  display: flex;
362
  flex-direction: column;
363
- box-shadow: var(--shadow-lg), 0 0 0 1px rgba(255,255,255,0.04) inset;
364
- overflow: hidden;
365
- position: relative;
366
- }}
367
-
368
- /* ── title bar ── */
369
- .titlebar {{
370
- height: 38px;
371
- min-height: 38px;
372
- background: linear-gradient(180deg, #4a1a38 0%, #3c1535 100%);
373
- border-bottom: 1px solid var(--term-border);
374
- display: flex;
375
- align-items: center;
376
- padding: 0 14px;
377
- gap: 0;
378
- user-select: none;
379
- border-radius: 10px 10px 0 0;
380
- }}
381
-
382
- .tb-buttons {{
383
- display: flex;
384
- gap: 8px;
385
- margin-right: 14px;
386
- }}
387
-
388
- .tb-btn {{
389
- width: 13px;
390
- height: 13px;
391
- border-radius: 50%;
392
- border: none;
393
- cursor: pointer;
394
- position: relative;
395
- display: flex;
396
- align-items: center;
397
- justify-content: center;
398
- font-size: 0;
399
- transition: filter 0.15s;
400
  }}
401
- .tb-btn:hover {{ filter: brightness(1.25); font-size: 9px; color: rgba(0,0,0,0.6); font-weight: 900; }}
402
- .tb-close {{ background: #e95420; }}
403
- .tb-min {{ background: #f4c21b; }}
404
- .tb-max {{ background: #4edd91; }}
405
 
406
- .tb-title {{
407
- flex: 1;
 
 
408
  text-align: center;
409
- font-size: 13px;
410
- color: #c8a8c0;
411
- letter-spacing: 0.04em;
412
- font-family: var(--font-mono);
413
- }}
414
-
415
- .tb-title strong {{ color: var(--white); }}
416
-
417
- .tb-right {{
418
- display: flex;
419
- align-items: center;
420
- gap: 10px;
421
- font-size: 11px;
422
- color: var(--gray2);
423
- font-family: var(--font-mono);
424
- }}
425
-
426
- .tb-badge {{
427
- padding: 2px 8px;
428
- background: var(--orange-soft);
429
- border: 1px solid var(--orange-border);
430
- border-radius: 20px;
431
- color: var(--orange2);
432
- font-size: 11px;
433
- }}
434
-
435
- /* ── tab bar ── */
436
- .tabbar {{
437
- height: 34px;
438
- min-height: 34px;
439
- background: var(--term-win);
440
- border-bottom: 1px solid var(--term-border);
441
- display: flex;
442
- align-items: flex-end;
443
- padding: 0 12px;
444
- gap: 2px;
445
- }}
446
-
447
- .tab {{
448
- height: 28px;
449
- padding: 0 16px;
450
- background: var(--term-chrome);
451
- border: 1px solid var(--term-border);
452
- border-bottom: none;
453
- border-radius: 5px 5px 0 0;
454
- display: flex;
455
- align-items: center;
456
- gap: 7px;
457
- font-size: 12px;
458
- color: var(--gray);
459
- cursor: pointer;
460
- font-family: var(--font-mono);
461
- transition: background 0.15s;
462
  }}
463
 
464
- .tab.active {{
465
- background: var(--term-bg);
466
- color: var(--white);
467
- border-color: var(--term-border2);
 
 
468
  }}
469
 
470
- .tab-dot {{
471
- width: 7px;
472
- height: 7px;
473
- border-radius: 50%;
474
- background: var(--gray2);
475
  }}
 
 
476
 
477
- .tab.active .tab-dot {{ background: var(--green); box-shadow: var(--glow-green); }}
478
-
479
- /* ── body layout ── */
480
- .term-body {{
481
- flex: 1;
482
- min-height: 0;
483
  display: grid;
484
- grid-template-columns: 420px 1fr;
 
 
485
  }}
486
 
487
- /* ── LEFT PANE (input) ── */
488
- .pane-left {{
489
- border-right: 1px solid var(--term-border);
 
490
  display: flex;
491
  flex-direction: column;
 
492
  overflow: hidden;
493
  }}
494
 
495
- .pane-header {{
496
- height: 32px;
497
- min-height: 32px;
498
- background: var(--term-win);
499
- border-bottom: 1px solid var(--term-border);
500
- display: flex;
501
- align-items: center;
502
- padding: 0 14px;
503
- gap: 8px;
504
- font-size: 11px;
505
- color: var(--gray2);
506
- font-family: var(--font-mono);
507
- }}
508
-
509
- .pane-header-label {{ color: var(--green); font-weight: 700; font-size: 12px; }}
510
- .pane-header-path {{ color: var(--cyan); font-size: 11px; }}
511
-
512
- .pane-scroll {{
513
- flex: 1;
514
- min-height: 0;
515
- overflow-y: auto;
516
- overflow-x: hidden;
517
- padding: 14px;
518
- display: flex;
519
- flex-direction: column;
520
- gap: 14px;
521
- }}
522
-
523
- /* ── RIGHT PANE ── */
524
- .pane-right {{
525
- display: flex;
526
- flex-direction: column;
527
- overflow: hidden;
528
- background: var(--term-win);
529
  }}
530
 
531
- /* ── TERMINAL SECTIONS ── */
532
- .term-section {{
 
 
533
  display: flex;
534
  flex-direction: column;
535
- gap: 6px;
536
  }}
537
 
538
- .term-label {{
539
- display: flex;
540
- align-items: center;
541
- gap: 6px;
542
- font-size: 11px;
543
- font-family: var(--font-mono);
544
- color: var(--gray);
545
- padding-left: 2px;
546
- }}
547
 
548
- .term-label::before {{
549
- content: '#';
550
- color: var(--gray2);
 
 
551
  }}
552
 
553
- .term-label .lbl {{ color: var(--cyan); font-weight: 700; }}
554
- .term-label .lbl-opt {{ color: var(--gray2); margin-left: 4px; }}
555
-
556
- /* prompt input */
557
- .term-prompt-wrap {{
558
- background: var(--term-win);
559
- border: 1px solid var(--term-border);
560
  border-radius: 4px;
561
- display: flex;
562
- align-items: flex-start;
563
- padding: 10px 12px;
564
- gap: 8px;
565
- transition: border-color 0.2s;
566
- }}
567
-
568
- .term-prompt-wrap:focus-within {{ border-color: var(--green2); box-shadow: 0 0 0 2px rgba(78,221,145,0.1); }}
569
-
570
- .term-prompt-prefix {{
571
- color: var(--prompt-color);
572
- font-size: 14px;
573
- font-family: var(--font-mono);
574
- padding-top: 1px;
575
- flex-shrink: 0;
576
- font-weight: 700;
577
- }}
578
-
579
- .term-textarea {{
580
- flex: 1;
581
- background: transparent;
582
- border: none;
583
  outline: none;
584
- color: var(--white);
585
- font-family: var(--font-mono);
586
- font-size: 13.5px;
587
- line-height: 1.55;
588
- resize: none;
589
- min-height: 62px;
590
- caret-color: var(--cursor-color);
591
  }}
 
 
592
 
593
- .term-textarea::placeholder {{ color: var(--gray2); }}
594
-
595
- /* upload drop zone */
596
  .upload-zone {{
597
- background: var(--term-win);
598
- border: 1px dashed var(--term-border2);
599
  border-radius: 4px;
 
 
600
  cursor: pointer;
601
  transition: border-color 0.2s, background 0.2s;
602
  }}
603
-
604
  .upload-zone:hover, .upload-zone.dragover {{
605
- border-color: var(--orange);
606
- background: rgba(233,84,32,0.06);
 
 
 
 
607
  }}
608
-
609
  .upload-zone input[type="file"] {{ display: none; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
- .upload-placeholder {{
612
- padding: 20px 16px;
613
- display: flex;
614
- flex-direction: column;
615
- align-items: center;
616
- gap: 8px;
617
- text-align: center;
618
- background: transparent;
619
- border: none;
620
- width: 100%;
621
- cursor: pointer;
622
- color: var(--white);
623
  }}
 
 
 
 
624
 
625
- .upload-icon-term {{
626
- width: 42px;
627
- height: 42px;
628
- border: 1px solid var(--term-border2);
629
- background: var(--term-chrome);
 
 
630
  border-radius: 4px;
 
631
  display: flex;
632
  align-items: center;
633
  justify-content: center;
634
- color: var(--orange2);
635
- margin-bottom: 2px;
636
  }}
637
-
638
- .upload-main {{
639
- font-size: 12.5px;
640
- font-weight: 700;
641
- color: var(--white);
642
- font-family: var(--font-mono);
 
 
 
 
 
 
 
643
  }}
 
644
 
645
- .upload-sub {{
646
- font-size: 11px;
647
- color: var(--gray2);
648
- font-family: var(--font-mono);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  }}
650
 
651
- /* preview grid */
652
- .preview-bar {{
653
- display: flex;
654
- align-items: center;
655
  justify-content: space-between;
656
- padding: 0 2px;
 
657
  }}
658
-
659
- .preview-count {{
660
- font-size: 11px;
661
- font-family: var(--font-mono);
662
- color: var(--green);
663
- font-weight: 700;
664
  }}
665
 
666
- .preview-count::before {{ content: '$ ls -1 | wc -l → '; color: var(--gray2); }}
667
-
668
- .preview-clear-btn {{
669
- background: transparent;
670
- border: 1px solid var(--term-border);
671
- border-radius: 3px;
672
- color: var(--gray2);
673
- font-size: 11px;
674
- font-family: var(--font-mono);
675
- cursor: pointer;
676
- padding: 2px 8px;
677
- transition: all 0.15s;
678
  }}
679
-
680
- .preview-clear-btn:hover {{ border-color: var(--orange); color: var(--orange2); }}
681
-
682
- .preview-grid {{
683
- display: grid;
684
- grid-template-columns: repeat(4, 1fr);
685
- gap: 6px;
686
  }}
687
 
688
- .thumb {{
689
- position: relative;
690
- aspect-ratio: 1/1;
691
- border: 1px solid var(--term-border);
692
- border-radius: 3px;
693
- overflow: hidden;
694
- background: #0d0510;
695
- cursor: pointer;
696
- transition: border-color 0.15s;
 
 
 
 
 
 
697
  }}
 
 
 
 
 
 
698
 
699
- .thumb:hover {{ border-color: var(--orange); }}
700
 
701
- .thumb img {{
702
- width: 100%;
703
- height: 100%;
704
- object-fit: cover;
705
- display: block;
706
  }}
 
 
 
707
 
708
- .thumb-overlay {{
709
- position: absolute;
710
- inset: 0;
711
- background: rgba(0,0,0,0);
712
- transition: background 0.15s;
713
- display: flex;
714
- align-items: flex-start;
715
- justify-content: flex-end;
716
- padding: 4px;
717
- }}
718
 
719
- .thumb:hover .thumb-overlay {{ background: rgba(0,0,0,0.45); }}
 
 
 
 
720
 
721
- .thumb-rm {{
722
- width: 20px;
723
- height: 20px;
724
- border-radius: 50%;
725
- background: rgba(233,84,32,0.9);
726
- border: none;
727
- color: white;
728
- font-size: 13px;
729
- cursor: pointer;
730
- display: flex;
731
- align-items: center;
732
- justify-content: center;
733
- opacity: 0;
734
- transition: opacity 0.15s;
735
- }}
736
 
737
- .thumb:hover .thumb-rm {{ opacity: 1; }}
 
 
 
738
 
739
- .thumb-idx {{
740
- position: absolute;
741
- bottom: 3px;
742
- left: 3px;
743
- background: rgba(233,84,32,0.85);
744
- color: white;
745
- font-size: 9px;
746
- font-weight: 700;
747
- font-family: var(--font-mono);
748
- width: 16px;
749
- height: 16px;
750
- border-radius: 50%;
751
- display: flex;
752
- align-items: center;
753
- justify-content: center;
754
- }}
755
-
756
- .thumb-add {{
757
- aspect-ratio: 1/1;
758
- border: 1px dashed var(--term-border2);
759
- border-radius: 3px;
760
- background: transparent;
761
- display: flex;
762
- align-items: center;
763
- justify-content: center;
764
- color: var(--gray2);
765
- font-size: 20px;
766
- cursor: pointer;
767
- transition: all 0.15s;
768
- }}
769
-
770
- .thumb-add:hover {{ border-color: var(--orange); color: var(--orange2); }}
771
-
772
- /* advanced toggle */
773
- .adv-toggle {{
774
- width: 100%;
775
- background: var(--term-win);
776
- border: 1px solid var(--term-border);
777
- border-radius: 4px;
778
- color: var(--gray);
779
- font-family: var(--font-mono);
780
- font-size: 12px;
781
- cursor: pointer;
782
- display: flex;
783
- align-items: center;
784
- justify-content: space-between;
785
- padding: 8px 12px;
786
- transition: all 0.15s;
787
- }}
788
-
789
- .adv-toggle:hover {{ border-color: var(--term-border2); color: var(--white); }}
790
-
791
- .adv-toggle-left {{ display: flex; align-items: center; gap: 8px; }}
792
- .adv-toggle-left::before {{ content: '$'; color: var(--prompt-color); }}
793
- .adv-chevron {{ font-size: 10px; transition: transform 0.25s; color: var(--gray2); }}
794
- .adv-chevron.open {{ transform: rotate(180deg); }}
795
-
796
- .adv-body {{
797
- display: none;
798
- flex-direction: column;
799
- gap: 12px;
800
- background: var(--term-win);
801
- border: 1px solid var(--term-border);
802
- border-radius: 4px;
803
- padding: 14px;
804
- margin-top: -8px;
805
- }}
806
-
807
- .adv-body.open {{ display: flex; }}
808
-
809
- .adv-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }}
810
-
811
- .adv-field {{ display: flex; flex-direction: column; gap: 5px; }}
812
-
813
- .adv-field-label {{
814
- display: flex;
815
- justify-content: space-between;
816
- font-size: 11px;
817
- font-family: var(--font-mono);
818
- color: var(--gray2);
819
- }}
820
-
821
- .adv-field-label .adv-key {{ color: var(--cyan); }}
822
- .adv-field-label .adv-val {{ color: var(--yellow); font-weight: 700; }}
823
-
824
- input[type="range"] {{
825
- -webkit-appearance: none;
826
- width: 100%;
827
- height: 3px;
828
- background: var(--term-border);
829
- border-radius: 2px;
830
- outline: none;
831
- }}
832
-
833
- input[type="range"]::-webkit-slider-thumb {{
834
- -webkit-appearance: none;
835
- width: 14px;
836
- height: 14px;
837
- border-radius: 50%;
838
- background: var(--orange);
839
- cursor: pointer;
840
- box-shadow: 0 0 8px rgba(233,84,32,0.6);
841
- }}
842
-
843
- .adv-checkbox {{
844
- display: flex;
845
- align-items: center;
846
- gap: 8px;
847
- font-size: 12px;
848
- font-family: var(--font-mono);
849
- color: var(--gray);
850
- cursor: pointer;
851
- }}
852
-
853
- .adv-checkbox::before {{ content: '$'; color: var(--prompt-color); }}
854
-
855
- .adv-checkbox input {{
856
- width: 14px;
857
- height: 14px;
858
- accent-color: var(--orange);
859
- cursor: pointer;
860
- }}
861
-
862
- /* run button */
863
- .run-btn {{
864
- width: 100%;
865
- height: 42px;
866
- background: linear-gradient(135deg, var(--orange) 0%, #c74119 100%);
867
- border: none;
868
- border-radius: 4px;
869
- color: white;
870
- font-family: var(--font-mono);
871
- font-size: 14px;
872
- font-weight: 700;
873
- cursor: pointer;
874
- display: flex;
875
- align-items: center;
876
- justify-content: center;
877
- gap: 10px;
878
- letter-spacing: 0.06em;
879
- transition: all 0.2s;
880
- box-shadow: 0 3px 14px rgba(233,84,32,0.4);
881
- margin-top: 4px;
882
- }}
883
-
884
- .run-btn:hover {{ filter: brightness(1.12); transform: translateY(-1px); }}
885
- .run-btn:active {{ transform: translateY(0); }}
886
- .run-btn:disabled {{
887
- background: var(--term-chrome);
888
- box-shadow: none;
889
- cursor: not-allowed;
890
- transform: none;
891
- filter: none;
892
- color: var(--gray2);
893
- }}
894
-
895
- /* ── OUTPUT AREA ── */
896
- .output-header {{
897
- height: 32px;
898
- min-height: 32px;
899
- background: var(--term-win);
900
- border-bottom: 1px solid var(--term-border);
901
- display: flex;
902
- align-items: center;
903
- padding: 0 14px;
904
- gap: 10px;
905
- font-size: 11px;
906
- font-family: var(--font-mono);
907
- color: var(--gray2);
908
- }}
909
-
910
- .out-label {{ color: var(--pink); font-weight: 700; font-size: 12px; }}
911
-
912
- .out-meta-chips {{
913
- margin-left: auto;
914
- display: flex;
915
- gap: 8px;
916
- }}
917
-
918
- .out-chip {{
919
- padding: 2px 8px;
920
- background: var(--term-chrome);
921
- border: 1px solid var(--term-border);
922
- border-radius: 3px;
923
- font-size: 10px;
924
- font-family: var(--font-mono);
925
- color: var(--gray2);
926
- }}
927
-
928
- .out-chip .val {{ color: var(--yellow); font-weight: 700; }}
929
-
930
- /* slider stage */
931
- .slider-stage {{
932
- flex: 1;
933
- min-height: 0;
934
- position: relative;
935
- background: #0a0410;
936
- display: flex;
937
- align-items: center;
938
- justify-content: center;
939
- overflow: hidden;
940
- }}
941
-
942
- /* empty */
943
- .empty-state {{
944
- display: flex;
945
- flex-direction: column;
946
- align-items: center;
947
- gap: 16px;
948
- text-align: center;
949
- padding: 40px;
950
- color: var(--gray2);
951
- }}
952
-
953
- .empty-ascii {{
954
- font-family: var(--font-mono);
955
- font-size: 11px;
956
- color: var(--term-border2);
957
- line-height: 1.3;
958
- white-space: pre;
959
- }}
960
-
961
- .empty-msg {{
962
- font-family: var(--font-mono);
963
- font-size: 13px;
964
- color: var(--gray2);
965
- }}
966
-
967
- .empty-msg .em-cmd {{ color: var(--green); }}
968
- .empty-msg .em-dim {{ color: var(--gray2); }}
969
-
970
- .cursor-blink {{
971
- display: inline-block;
972
- width: 8px;
973
- height: 14px;
974
- background: var(--cursor-color);
975
- margin-left: 2px;
976
- animation: blink 1.1s step-end infinite;
977
- vertical-align: middle;
978
- }}
979
-
980
- @keyframes blink {{ 0%,100% {{ opacity:1; }} 50% {{ opacity:0; }} }}
981
-
982
- /* compare widget */
983
- .compare-wrap {{
984
- position: relative;
985
- width: 100%;
986
- height: 100%;
987
- user-select: none;
988
- display: none;
989
- }}
990
-
991
- .compare-wrap.visible {{ display: block; }}
992
-
993
- .compare-img-base {{
994
- display: block;
995
- width: 100%;
996
- height: 100%;
997
- object-fit: contain;
998
- }}
999
-
1000
- .compare-overlay {{
1001
- position: absolute;
1002
- top: 0;
1003
- left: 0;
1004
- height: 100%;
1005
- overflow: hidden;
1006
- pointer-events: none;
1007
- }}
1008
-
1009
- .compare-overlay img {{
1010
- display: block;
1011
- height: 100%;
1012
- width: 100vw;
1013
- max-width: none;
1014
- object-fit: contain;
1015
- object-position: left center;
1016
- }}
1017
-
1018
- .compare-handle {{
1019
- position: absolute;
1020
- top: 0;
1021
- height: 100%;
1022
- width: 3px;
1023
- background: rgba(255,255,255,0.9);
1024
- cursor: ew-resize;
1025
- z-index: 10;
1026
- box-shadow: 0 0 10px rgba(255,255,255,0.4);
1027
- }}
1028
-
1029
- .compare-handle::before {{
1030
- content: '';
1031
- position: absolute;
1032
- top: 50%;
1033
- left: 50%;
1034
- transform: translate(-50%, -50%);
1035
- width: 34px;
1036
- height: 34px;
1037
- border-radius: 50%;
1038
- background: white;
1039
- box-shadow: 0 2px 12px rgba(0,0,0,0.5);
1040
- }}
1041
-
1042
- .compare-handle::after {{
1043
- content: '◀▶';
1044
- position: absolute;
1045
- top: 50%;
1046
- left: 50%;
1047
- transform: translate(-50%, -50%);
1048
- font-size: 9px;
1049
- color: #333;
1050
- font-weight: 900;
1051
- pointer-events: none;
1052
- letter-spacing: -1px;
1053
- }}
1054
-
1055
- .cmp-label {{
1056
- position: absolute;
1057
- top: 12px;
1058
- padding: 4px 10px;
1059
- border-radius: 3px;
1060
- font-size: 11px;
1061
- font-weight: 700;
1062
- font-family: var(--font-mono);
1063
- pointer-events: none;
1064
- z-index: 5;
1065
- letter-spacing: 0.05em;
1066
- }}
1067
-
1068
- .cmp-label-left {{ left: 12px; background: rgba(233,84,32,0.9); color: white; }}
1069
- .cmp-label-right {{ right: 12px; background: rgba(20,10,18,0.85); color: var(--cyan); border: 1px solid var(--term-border); }}
1070
-
1071
- /* loader */
1072
- .loader-overlay {{
1073
- position: absolute;
1074
- inset: 0;
1075
- background: rgba(10,4,16,0.82);
1076
- backdrop-filter: blur(10px);
1077
- -webkit-backdrop-filter: blur(10px);
1078
- display: none;
1079
- align-items: center;
1080
- justify-content: center;
1081
- flex-direction: column;
1082
- gap: 18px;
1083
- z-index: 20;
1084
- }}
1085
-
1086
- .loader-overlay.active {{ display: flex; }}
1087
-
1088
- .loader-terminal {{
1089
- background: var(--term-win);
1090
- border: 1px solid var(--term-border2);
1091
- border-radius: 6px;
1092
- padding: 20px 28px;
1093
- min-width: 320px;
1094
- display: flex;
1095
- flex-direction: column;
1096
- gap: 12px;
1097
- box-shadow: var(--shadow-lg);
1098
- }}
1099
-
1100
- .loader-title-bar {{
1101
- display: flex;
1102
- align-items: center;
1103
- gap: 8px;
1104
- margin-bottom: 4px;
1105
- }}
1106
-
1107
- .loader-dots {{ display: flex; gap: 6px; }}
1108
- .loader-dot {{ width: 10px; height: 10px; border-radius: 50%; }}
1109
- .ld-r {{ background: var(--orange); animation: pulse 1s ease-in-out infinite; }}
1110
- .ld-y {{ background: var(--yellow); animation: pulse 1s ease-in-out 0.2s infinite; }}
1111
- .ld-g {{ background: var(--green); animation: pulse 1s ease-in-out 0.4s infinite; }}
1112
-
1113
- @keyframes pulse {{ 0%,100% {{ opacity:1; }} 50% {{ opacity:0.3; }} }}
1114
-
1115
- .loader-terminal-title {{ font-size: 11px; color: var(--gray2); font-family: var(--font-mono); }}
1116
-
1117
- .loader-line {{
1118
- font-family: var(--font-mono);
1119
- font-size: 13px;
1120
- color: var(--green);
1121
- display: flex;
1122
- align-items: center;
1123
- gap: 8px;
1124
- }}
1125
-
1126
- .loader-line .ll-prefix {{ color: var(--gray2); }}
1127
-
1128
- .loader-spinner {{
1129
- display: inline-block;
1130
- font-size: 16px;
1131
- animation: spin 0.7s linear infinite;
1132
- }}
1133
-
1134
- @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
1135
-
1136
- .loader-progress {{
1137
- height: 3px;
1138
- background: var(--term-border);
1139
- border-radius: 2px;
1140
- overflow: hidden;
1141
- }}
1142
-
1143
- .loader-progress-bar {{
1144
- height: 100%;
1145
- background: linear-gradient(90deg, var(--orange), var(--orange2));
1146
- border-radius: 2px;
1147
- animation: progress-anim 2s ease-in-out infinite;
1148
- box-shadow: 0 0 8px rgba(233,84,32,0.6);
1149
- }}
1150
-
1151
- @keyframes progress-anim {{
1152
- 0% {{ width: 0%; margin-left: 0%; }}
1153
- 50% {{ width: 70%; margin-left: 15%; }}
1154
- 100% {{ width: 0%; margin-left: 100%; }}
1155
- }}
1156
-
1157
- /* output footer */
1158
- .output-footer {{
1159
- height: 36px;
1160
- min-height: 36px;
1161
- background: var(--term-win);
1162
- border-top: 1px solid var(--term-border);
1163
- display: flex;
1164
- align-items: center;
1165
- padding: 0 14px;
1166
- gap: 10px;
1167
- font-size: 11px;
1168
- font-family: var(--font-mono);
1169
- }}
1170
-
1171
- .dl-link {{
1172
- display: inline-flex;
1173
- align-items: center;
1174
- gap: 5px;
1175
- padding: 3px 10px;
1176
- background: var(--term-chrome);
1177
- border: 1px solid var(--term-border);
1178
- border-radius: 3px;
1179
- color: var(--gray);
1180
- text-decoration: none;
1181
- font-size: 11px;
1182
- font-family: var(--font-mono);
1183
- cursor: pointer;
1184
- transition: all 0.15s;
1185
- }}
1186
-
1187
- .dl-link:hover {{ border-color: var(--green2); color: var(--green); }}
1188
- .dl-link.hidden {{ display: none; }}
1189
-
1190
- .footer-seed {{
1191
- margin-left: auto;
1192
- color: var(--gray2);
1193
- font-family: var(--font-mono);
1194
- font-size: 11px;
1195
- }}
1196
-
1197
- .footer-seed .seed-val {{ color: var(--yellow); font-weight: 700; }}
1198
-
1199
- .footer-device {{
1200
- color: var(--gray2);
1201
- font-family: var(--font-mono);
1202
- font-size: 11px;
1203
- }}
1204
-
1205
- .footer-device .dev-val {{ color: var(--cyan); }}
1206
-
1207
- /* ── BOTTOM: EXAMPLES TAB ── */
1208
- .bottom-bar {{
1209
- border-top: 1px solid var(--term-border);
1210
- background: var(--term-bg2);
1211
- display: flex;
1212
- flex-direction: column;
1213
- }}
1214
-
1215
- .bottom-tab-row {{
1216
- height: 32px;
1217
- display: flex;
1218
- align-items: center;
1219
- padding: 0 14px;
1220
- gap: 12px;
1221
- border-bottom: 1px solid var(--term-border);
1222
- }}
1223
-
1224
- .bottom-tab {{
1225
- font-size: 11px;
1226
- font-family: var(--font-mono);
1227
- color: var(--gray2);
1228
- cursor: pointer;
1229
- padding: 3px 0;
1230
- border-bottom: 2px solid transparent;
1231
- transition: all 0.15s;
1232
- }}
1233
-
1234
- .bottom-tab.active {{ color: var(--green); border-bottom-color: var(--green); }}
1235
-
1236
- .examples-scroll {{
1237
- padding: 12px 14px;
1238
- overflow-x: auto;
1239
- overflow-y: hidden;
1240
- display: flex;
1241
- gap: 12px;
1242
- }}
1243
-
1244
- .example-card {{
1245
- flex-shrink: 0;
1246
- width: 200px;
1247
- background: var(--term-win);
1248
- border: 1px solid var(--term-border);
1249
- border-radius: 4px;
1250
- overflow: hidden;
1251
- cursor: pointer;
1252
- transition: border-color 0.15s, transform 0.15s;
1253
- }}
1254
-
1255
- .example-card:hover {{
1256
- border-color: var(--orange);
1257
- transform: translateY(-2px);
1258
- }}
1259
-
1260
- .example-img {{
1261
- width: 100%;
1262
- aspect-ratio: 4/3;
1263
- object-fit: cover;
1264
- display: block;
1265
- border-bottom: 1px solid var(--term-border);
1266
- }}
1267
-
1268
- .example-body {{
1269
- padding: 8px 10px;
1270
- }}
1271
-
1272
- .example-prompt {{
1273
- font-size: 11px;
1274
- font-family: var(--font-mono);
1275
- color: var(--gray);
1276
- line-height: 1.45;
1277
- display: -webkit-box;
1278
- -webkit-line-clamp: 2;
1279
- -webkit-box-orient: vertical;
1280
- overflow: hidden;
1281
- margin-bottom: 7px;
1282
- }}
1283
-
1284
- .example-use-btn {{
1285
- width: 100%;
1286
- padding: 4px 0;
1287
- background: var(--orange-soft);
1288
- border: 1px solid var(--orange-border);
1289
- border-radius: 3px;
1290
- color: var(--orange2);
1291
- font-size: 10px;
1292
- font-weight: 700;
1293
- font-family: var(--font-mono);
1294
- cursor: pointer;
1295
- letter-spacing: 0.04em;
1296
- transition: background 0.15s;
1297
- }}
1298
-
1299
- .example-use-btn:hover {{ background: rgba(233,84,32,0.3); }}
1300
-
1301
- /* ── STATUS BAR ── */
1302
- .statusbar {{
1303
- height: 22px;
1304
- min-height: 22px;
1305
- background: var(--orange);
1306
- display: flex;
1307
- align-items: center;
1308
- padding: 0 12px;
1309
- gap: 16px;
1310
- border-radius: 0 0 10px 10px;
1311
- }}
1312
-
1313
- .sb-item {{
1314
- font-size: 11px;
1315
- font-family: var(--font-mono);
1316
- color: rgba(255,255,255,0.85);
1317
- display: flex;
1318
- align-items: center;
1319
- gap: 5px;
1320
- }}
1321
-
1322
- .sb-item strong {{ color: white; font-weight: 700; }}
1323
-
1324
- .sb-sep {{ color: rgba(255,255,255,0.35); }}
1325
-
1326
- .sb-right {{ margin-left: auto; display: flex; gap: 12px; }}
1327
-
1328
- .sb-status-dot {{
1329
- width: 7px;
1330
- height: 7px;
1331
- border-radius: 50%;
1332
- background: rgba(255,255,255,0.5);
1333
- transition: all 0.3s;
1334
- }}
1335
-
1336
- .sb-status-dot.running {{
1337
- background: white;
1338
- box-shadow: 0 0 6px rgba(255,255,255,0.8);
1339
- animation: pulse 0.8s ease-in-out infinite;
1340
- }}
1341
-
1342
- .sb-status-dot.done {{ background: var(--green); box-shadow: 0 0 6px rgba(78,221,145,0.6); }}
1343
-
1344
- /* ── TOAST ── */
1345
- .toast-wrap {{
1346
- position: fixed;
1347
- bottom: 30px;
1348
- right: 24px;
1349
- z-index: 9999;
1350
- display: flex;
1351
- flex-direction: column;
1352
- gap: 8px;
1353
- }}
1354
-
1355
- .toast {{
1356
- background: var(--term-win);
1357
- border: 1px solid var(--term-border2);
1358
- border-radius: 4px;
1359
- padding: 10px 14px;
1360
- display: flex;
1361
- align-items: flex-start;
1362
- gap: 10px;
1363
- min-width: 240px;
1364
- max-width: 340px;
1365
- box-shadow: var(--shadow-lg);
1366
- font-family: var(--font-mono);
1367
- font-size: 12px;
1368
- animation: slideUp 0.25s ease;
1369
- }}
1370
-
1371
- .toast-err {{ border-color: rgba(233,84,32,0.6); }}
1372
- .toast-ok {{ border-color: rgba(78,221,145,0.5); }}
1373
-
1374
- .toast-prefix {{ color: var(--prompt-color); flex-shrink: 0; }}
1375
- .toast-err .toast-prefix {{ color: var(--orange2); }}
1376
- .toast-msg {{ color: var(--white); flex: 1; }}
1377
- .toast-x {{ background: transparent; border: none; color: var(--gray2); cursor: pointer; font-size: 14px; }}
1378
-
1379
- @keyframes slideUp {{
1380
- from {{ transform: translateY(12px); opacity: 0; }}
1381
- to {{ transform: translateY(0); opacity: 1; }}
1382
- }}
1383
-
1384
- /* ── RESPONSIVE ── */
1385
- @media (max-width: 900px) {{
1386
- .terminal {{ width: 100vw; height: 100vh; border-radius: 0; }}
1387
- .term-body {{ grid-template-columns: 1fr; grid-template-rows: auto 1fr; }}
1388
- .pane-left {{ border-right: none; border-bottom: 1px solid var(--term-border); max-height: 52vh; }}
1389
- .statusbar {{ border-radius: 0; }}
1390
- }}
1391
-
1392
- @media (max-width: 600px) {{
1393
- .adv-grid {{ grid-template-columns: 1fr; }}
1394
- .preview-grid {{ grid-template-columns: repeat(3, 1fr); }}
1395
- }}
1396
- </style>
1397
- </head>
1398
- <body>
1399
-
1400
- <div class="toast-wrap" id="toastWrap"></div>
1401
-
1402
- <div class="desktop">
1403
- <div class="terminal">
1404
-
1405
- <!-- TITLE BAR -->
1406
- <div class="titlebar">
1407
- <div class="tb-buttons">
1408
- <button class="tb-btn tb-close" title="Close">×</button>
1409
- <button class="tb-btn tb-min" title="Minimize">−</button>
1410
- <button class="tb-btn tb-max" title="Maximize">+</button>
1411
- </div>
1412
- <div class="tb-title">
1413
- <strong>flux2-decoder-comparator</strong> — bash
1414
- </div>
1415
- <div class="tb-right">
1416
- <span class="tb-badge">GPU: {DEVICE_LABEL}</span>
1417
- </div>
1418
- </div>
1419
-
1420
- <!-- TAB BAR -->
1421
- <div class="tabbar">
1422
- <div class="tab active">
1423
- <div class="tab-dot"></div>
1424
- flux2-comparator
1425
- </div>
1426
- <div class="tab">
1427
- <div class="tab-dot"></div>
1428
- model-logs
1429
- </div>
1430
- </div>
1431
-
1432
- <!-- BODY -->
1433
- <div class="term-body">
1434
-
1435
- <!-- ═══ LEFT: INPUT PANE ═══ -->
1436
- <div class="pane-left">
1437
- <div class="pane-header">
1438
- <span class="pane-header-label">INPUT</span>
1439
- <span style="color:var(--gray2);">//</span>
1440
- <span class="pane-header-path">~/flux2/configure</span>
1441
- </div>
1442
- <div class="pane-scroll">
1443
-
1444
- <!-- upload -->
1445
- <div class="term-section">
1446
- <div class="term-label"><span class="lbl">images</span><span class="lbl-opt">(optional)</span></div>
1447
- <div class="upload-zone" id="uploadZone">
1448
- <input id="fileInput" type="file" accept="image/*" multiple />
1449
- <button class="upload-placeholder" id="uploadPlaceholder" type="button">
1450
- <div class="upload-icon-term">
1451
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1452
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
1453
- <polyline points="17 8 12 3 7 8"/>
1454
- <line x1="12" y1="3" x2="12" y2="15"/>
1455
- </svg>
1456
- </div>
1457
- <div class="upload-main">drop images or click to browse</div>
1458
- <div class="upload-sub">PNG · JPG · WEBP — multiple files</div>
1459
- </button>
1460
- </div>
1461
-
1462
- <div id="previewContainer" style="display:none; flex-direction:column; gap:6px;">
1463
- <div class="preview-bar">
1464
- <span class="preview-count" id="previewCount">0</span>
1465
- <button class="preview-clear-btn" id="clearImagesBtn" type="button">rm -rf *</button>
1466
  </div>
1467
- <div class="preview-grid" id="previewGrid"></div>
1468
- </div>
1469
- </div>
1470
-
1471
- <!-- prompt -->
1472
- <div class="term-section">
1473
- <div class="term-label"><span class="lbl">prompt</span></div>
1474
- <div class="term-prompt-wrap">
1475
- <span class="term-prompt-prefix">$&gt;</span>
1476
- <textarea id="prompt" class="term-textarea" placeholder="describe the edit or generation..." rows="3"></textarea>
1477
- </div>
1478
- </div>
1479
-
1480
- <!-- advanced -->
1481
- <div class="term-section">
1482
- <button class="adv-toggle" id="advToggle" type="button">
1483
- <span class="adv-toggle-left">advanced --options</span>
1484
- <span class="adv-chevron" id="advChevron">▼</span>
1485
- </button>
1486
- <div class="adv-body" id="advBody">
1487
- <div class="adv-grid">
1488
- <div class="adv-field">
1489
- <div class="adv-field-label">
1490
- <span class="adv-key">--seed</span>
1491
- <span class="adv-val" id="seedVal">0</span>
1492
- </div>
1493
- <input type="range" id="seed" min="0" max="{MAX_SEED}" step="1" value="0"
1494
- oninput="document.getElementById('seedVal').textContent=this.value" />
1495
- </div>
1496
- <div class="adv-field">
1497
- <div class="adv-field-label">
1498
- <span class="adv-key">--steps</span>
1499
- <span class="adv-val" id="stepsVal">4</span>
1500
- </div>
1501
- <input type="range" id="steps" min="1" max="20" step="1" value="4"
1502
- oninput="document.getElementById('stepsVal').textContent=this.value" />
1503
- </div>
1504
- <div class="adv-field">
1505
- <div class="adv-field-label">
1506
- <span class="adv-key">--width</span>
1507
- <span class="adv-val" id="widthVal">1024</span>
1508
- </div>
1509
- <input type="range" id="width" min="256" max="{MAX_IMAGE_SIZE}" step="8" value="1024"
1510
- oninput="document.getElementById('widthVal').textContent=this.value" />
1511
- </div>
1512
- <div class="adv-field">
1513
- <div class="adv-field-label">
1514
- <span class="adv-key">--height</span>
1515
- <span class="adv-val" id="heightVal">1024</span>
1516
- </div>
1517
- <input type="range" id="height" min="256" max="{MAX_IMAGE_SIZE}" step="8" value="1024"
1518
- oninput="document.getElementById('heightVal').textContent=this.value" />
1519
- </div>
1520
- <div class="adv-field">
1521
- <div class="adv-field-label">
1522
- <span class="adv-key">--guidance</span>
1523
- <span class="adv-val" id="guidanceVal">1.0</span>
1524
- </div>
1525
- <input type="range" id="guidance" min="0" max="10" step="0.1" value="1.0"
1526
- oninput="document.getElementById('guidanceVal').textContent=parseFloat(this.value).toFixed(1)" />
1527
- </div>
1528
  </div>
1529
- <label class="adv-checkbox">
1530
- <input type="checkbox" id="randomizeSeed" checked />
1531
- randomize_seed=True
1532
- </label>
1533
  </div>
1534
  </div>
1535
 
1536
- <!-- run -->
1537
- <button class="run-btn" id="runBtn" type="button">
1538
- <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1539
- <polygon points="5 3 19 12 5 21 5 3"/>
1540
- </svg>
1541
- ./run_comparison.sh
1542
- </button>
1543
-
1544
- </div><!-- /pane-scroll -->
1545
- </div><!-- /pane-left -->
1546
-
1547
- <!-- ═══ RIGHT: OUTPUT PANE ═══ -->
1548
- <div class="pane-right">
1549
- <div class="output-header">
1550
- <span class="out-label">OUTPUT</span>
1551
- <span style="color:var(--gray2);">//</span>
1552
- <span style="color:var(--pink); font-size:11px;">standard_decoder ◀▶ small_decoder</span>
1553
- <div class="out-meta-chips">
1554
- <span class="out-chip">seed: <span class="val" id="chipSeed">—</span></span>
1555
- <span class="out-chip" id="chipStatus">idle</span>
1556
- </div>
1557
  </div>
 
1558
 
1559
- <!-- stage -->
1560
- <div class="slider-stage" id="sliderStage">
1561
-
1562
- <!-- empty -->
1563
- <div class="empty-state" id="emptyState">
1564
- <div class="empty-ascii">┌─────────────────────────────────┐
1565
- │ flux2 decoder comparator v1.0
1566
- │ standard vae ◀▶ small vae │
1567
- └─────────────────────────────────┘</div>
1568
- <div class="empty-msg">
1569
- <span class="em-dim">$ </span><span class="em-cmd">./run_comparison.sh</span><span class="em-dim"> --waiting</span><span class="cursor-blink"></span>
1570
- </div>
1571
- <div style="font-size:11px; color:var(--gray2); font-family:var(--font-mono);">
1572
- configure inputs on the left, then click run
1573
  </div>
1574
- </div>
1575
-
1576
- <!-- compare widget -->
1577
- <div class="compare-wrap" id="compareWrap">
1578
- <img class="compare-img-base" id="imgSmall" alt="Small Decoder" />
1579
- <span class="cmp-label cmp-label-right">small_decoder</span>
1580
 
1581
- <div class="compare-overlay" id="compareOverlay">
1582
- <img id="imgStandard" alt="Standard Decoder" />
 
 
 
 
1583
  </div>
1584
- <span class="cmp-label cmp-label-left">standard_vae</span>
1585
 
1586
- <div class="compare-handle" id="compareHandle"></div>
1587
- </div>
1588
 
1589
- <!-- loader -->
1590
- <div class="loader-overlay" id="loaderOverlay">
1591
- <div class="loader-terminal">
1592
- <div class="loader-title-bar">
1593
- <div class="loader-dots">
1594
- <div class="loader-dot ld-r"></div>
1595
- <div class="loader-dot ld-y"></div>
1596
- <div class="loader-dot ld-g"></div>
1597
- </div>
1598
- <span class="loader-terminal-title">flux2-inference — running</span>
1599
- </div>
1600
- <div class="loader-line">
1601
- <span class="ll-prefix">$&gt;</span>
1602
- <span class="loader-spinner">⠋</span>
1603
- <span id="loaderMsg">spawning parallel pipelines...</span>
1604
- </div>
1605
- <div class="loader-line" style="color:var(--cyan); font-size:11px;">
1606
- <span class="ll-prefix"> </span>
1607
- standard_vae ──┐
1608
- </div>
1609
- <div class="loader-line" style="color:var(--pink); font-size:11px;">
1610
- <span class="ll-prefix"> </span>
1611
- small_decoder ─┘ running concurrently
1612
- </div>
1613
- <div class="loader-progress">
1614
- <div class="loader-progress-bar"></div>
1615
- </div>
1616
  </div>
1617
  </div>
1618
  </div>
1619
-
1620
- <!-- output footer -->
1621
- <div class="output-footer">
1622
- <a class="dl-link hidden" id="dlStd" download="standard_decoder.png">
1623
- ↓ standard
1624
- </a>
1625
- <a class="dl-link hidden" id="dlSmall" download="small_decoder.png">
1626
- ↓ small
1627
- </a>
1628
- <span class="footer-seed">seed=<span class="seed-val" id="footerSeed">—</span></span>
1629
- <span class="footer-device">device=<span class="dev-val">{DEVICE_LABEL}</span></span>
1630
- </div>
1631
- </div><!-- /pane-right -->
1632
- </div><!-- /term-body -->
1633
-
1634
- <!-- EXAMPLES BOTTOM BAR -->
1635
- <div class="bottom-bar">
1636
- <div class="bottom-tab-row">
1637
- <span class="bottom-tab active">examples</span>
1638
- <span style="font-size:11px; color:var(--gray2); font-family:var(--font-mono); margin-left:auto;">
1639
- click a card to load · drag-compare after run
1640
- </span>
1641
  </div>
1642
- <div class="examples-scroll" id="examplesScroll"></div>
1643
  </div>
1644
 
1645
- <!-- STATUS BAR -->
1646
- <div class="statusbar">
1647
- <div class="sb-item"><div class="sb-status-dot" id="sbDot"></div><span id="sbText">idle</span></div>
1648
- <span class="sb-sep">|</span>
1649
- <div class="sb-item">branch: <strong>main</strong></div>
1650
- <span class="sb-sep">|</span>
1651
- <div class="sb-item">model: <strong>flux.2-klein-4b</strong></div>
1652
- <div class="sb-right">
1653
- <div class="sb-item">standard_vae + small_decoder</div>
1654
- <span class="sb-sep">|</span>
1655
- <div class="sb-item">4-step distilled</div>
1656
- </div>
1657
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1658
 
1659
- </div><!-- /terminal -->
1660
- </div><!-- /desktop -->
1661
-
1662
- <script>
1663
- const examples = {examples_json};
1664
-
1665
- /* ── state ── */
1666
- const S = {{ files: [], advOpen: false, running: false }};
1667
-
1668
- /* ── refs ── */
1669
- const uploadZone = document.getElementById('uploadZone');
1670
- const fileInput = document.getElementById('fileInput');
1671
- const uploadPlaceholder= document.getElementById('uploadPlaceholder');
1672
- const previewContainer = document.getElementById('previewContainer');
1673
- const previewGrid = document.getElementById('previewGrid');
1674
- const previewCount = document.getElementById('previewCount');
1675
- const clearImagesBtn = document.getElementById('clearImagesBtn');
1676
-
1677
- const promptEl = document.getElementById('prompt');
1678
- const seedEl = document.getElementById('seed');
1679
- const stepsEl = document.getElementById('steps');
1680
- const widthEl = document.getElementById('width');
1681
- const heightEl = document.getElementById('height');
1682
- const guidanceEl = document.getElementById('guidance');
1683
- const randomizeSeedEl = document.getElementById('randomizeSeed');
1684
-
1685
- const advToggle = document.getElementById('advToggle');
1686
- const advBody = document.getElementById('advBody');
1687
- const advChevron = document.getElementById('advChevron');
1688
-
1689
- const runBtn = document.getElementById('runBtn');
1690
-
1691
- const sliderStage = document.getElementById('sliderStage');
1692
- const emptyState = document.getElementById('emptyState');
1693
- const compareWrap = document.getElementById('compareWrap');
1694
- const imgSmall = document.getElementById('imgSmall');
1695
- const imgStandard = document.getElementById('imgStandard');
1696
- const compareOverlay = document.getElementById('compareOverlay');
1697
- const compareHandle = document.getElementById('compareHandle');
1698
-
1699
- const loaderOverlay = document.getElementById('loaderOverlay');
1700
- const loaderMsg = document.getElementById('loaderMsg');
1701
- const chipSeed = document.getElementById('chipSeed');
1702
- const chipStatus = document.getElementById('chipStatus');
1703
-
1704
- const dlStd = document.getElementById('dlStd');
1705
- const dlSmall = document.getElementById('dlSmall');
1706
- const footerSeed = document.getElementById('footerSeed');
1707
-
1708
- const examplesScroll = document.getElementById('examplesScroll');
1709
- const toastWrap = document.getElementById('toastWrap');
1710
-
1711
- const sbDot = document.getElementById('sbDot');
1712
- const sbText = document.getElementById('sbText');
1713
-
1714
- /* ── spinner frames ── */
1715
- const spinFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
1716
- let spinIdx = 0, spinTimer = null;
1717
-
1718
- function startSpinner() {{
1719
- spinTimer = setInterval(() => {{
1720
- const sp = loaderOverlay.querySelector('.loader-spinner');
1721
- if (sp) sp.textContent = spinFrames[spinIdx++ % spinFrames.length];
1722
- }}, 80);
1723
- }}
1724
-
1725
- function stopSpinner() {{
1726
- clearInterval(spinTimer);
1727
- }}
1728
-
1729
- /* ── toast ── */
1730
- function toast(msg, type='info') {{
1731
- const el = document.createElement('div');
1732
- el.className = 'toast' + (type==='error' ? ' toast-err' : type==='ok' ? ' toast-ok' : '');
1733
- el.innerHTML = `<span class="toast-prefix">${{type==='error' ? '✗' : '✓'}}</span>
1734
- <span class="toast-msg">${{msg}}</span>
1735
- <button class="toast-x" onclick="this.parentElement.remove()">×</button>`;
1736
- toastWrap.appendChild(el);
1737
- setTimeout(() => el.remove(), 4500);
1738
- }}
1739
-
1740
- /* ── status ── */
1741
- function setRunning(running) {{
1742
- S.running = running;
1743
- runBtn.disabled = running;
1744
- sbDot.className = 'sb-status-dot' + (running ? ' running' : '');
1745
- sbText.textContent = running ? 'running...' : 'idle';
1746
- chipStatus.textContent = running ? 'processing...' : (compareWrap.classList.contains('visible') ? 'done' : 'idle');
1747
- if (running) {{
1748
- loaderOverlay.classList.add('active');
1749
- startSpinner();
1750
- }} else {{
1751
- loaderOverlay.classList.remove('active');
1752
- stopSpinner();
1753
- if (compareWrap.classList.contains('visible')) sbDot.classList.add('done');
1754
- }}
1755
- }}
1756
-
1757
- /* ── advanced ── */
1758
- advToggle.addEventListener('click', () => {{
1759
- S.advOpen = !S.advOpen;
1760
- advBody.classList.toggle('open', S.advOpen);
1761
- advChevron.classList.toggle('open', S.advOpen);
1762
- }});
1763
-
1764
- /* ── file upload ── */
1765
- function addFiles(fl) {{
1766
- const valid = Array.from(fl).filter(f => f.type.startsWith('image/'));
1767
- if (!valid.length) {{ toast('Please upload valid image files.', 'error'); return; }}
1768
- S.files = [...S.files, ...valid];
1769
- renderPreviews();
1770
- }}
1771
-
1772
- function renderPreviews() {{
1773
- previewGrid.innerHTML = '';
1774
- if (!S.files.length) {{
1775
- previewContainer.style.display = 'none';
1776
- uploadPlaceholder.style.display = 'flex';
1777
- return;
1778
- }}
1779
- previewContainer.style.display = 'flex';
1780
- uploadPlaceholder.style.display = 'none';
1781
- previewCount.textContent = S.files.length;
1782
-
1783
- S.files.forEach((file, i) => {{
1784
- const wrap = document.createElement('div');
1785
- wrap.className = 'thumb';
1786
-
1787
- const img = document.createElement('img');
1788
- img.src = URL.createObjectURL(file);
1789
- img.alt = file.name;
1790
-
1791
- const ov = document.createElement('div');
1792
- ov.className = 'thumb-overlay';
1793
-
1794
- const rm = document.createElement('button');
1795
- rm.type = 'button';
1796
- rm.className = 'thumb-rm';
1797
- rm.innerHTML = '×';
1798
- rm.addEventListener('click', e => {{
1799
- e.stopPropagation();
1800
- S.files.splice(i, 1);
1801
- renderPreviews();
1802
- }});
1803
- ov.appendChild(rm);
1804
-
1805
- const idx = document.createElement('div');
1806
- idx.className = 'thumb-idx';
1807
- idx.textContent = i + 1;
1808
-
1809
- wrap.appendChild(img);
1810
- wrap.appendChild(ov);
1811
- wrap.appendChild(idx);
1812
- previewGrid.appendChild(wrap);
1813
- }});
1814
-
1815
- const addMore = document.createElement('div');
1816
- addMore.className = 'thumb-add';
1817
- addMore.title = 'Add more';
1818
- addMore.textContent = '+';
1819
- addMore.addEventListener('click', () => fileInput.click());
1820
- previewGrid.appendChild(addMore);
1821
- }}
1822
-
1823
- uploadPlaceholder.addEventListener('click', () => fileInput.click());
1824
- uploadZone.addEventListener('click', e => {{ if (e.target === uploadZone) fileInput.click(); }});
1825
- fileInput.addEventListener('change', e => {{ addFiles(e.target.files); fileInput.value = ''; }});
1826
- uploadZone.addEventListener('dragover', e => {{ e.preventDefault(); uploadZone.classList.add('dragover'); }});
1827
- uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
1828
- uploadZone.addEventListener('drop', e => {{
1829
- e.preventDefault();
1830
- uploadZone.classList.remove('dragover');
1831
- if (e.dataTransfer.files?.length) addFiles(e.dataTransfer.files);
1832
- }});
1833
- clearImagesBtn.addEventListener('click', () => {{ S.files = []; renderPreviews(); }});
1834
-
1835
- /* ── compare slider ── */
1836
- let dragging = false;
1837
-
1838
- function setSlider(pct) {{
1839
- pct = Math.max(0, Math.min(100, pct));
1840
- compareHandle.style.left = pct + '%';
1841
- compareOverlay.style.width = pct + '%';
1842
- }}
1843
-
1844
- compareHandle.addEventListener('mousedown', e => {{ dragging = true; e.preventDefault(); }});
1845
- window.addEventListener('mousemove', e => {{
1846
- if (!dragging) return;
1847
- const r = compareWrap.getBoundingClientRect();
1848
- setSlider(((e.clientX - r.left) / r.width) * 100);
1849
- }});
1850
- window.addEventListener('mouseup', () => dragging = false);
1851
-
1852
- compareHandle.addEventListener('touchstart', () => dragging = true, {{passive:true}});
1853
- window.addEventListener('touchmove', e => {{
1854
- if (!dragging) return;
1855
- const r = compareWrap.getBoundingClientRect();
1856
- setSlider(((e.touches[0].clientX - r.left) / r.width) * 100);
1857
- }}, {{passive:true}});
1858
- window.addEventListener('touchend', () => dragging = false);
1859
-
1860
- /* ── show comparison ── */
1861
- function showComparison(stdUrl, smallUrl) {{
1862
- imgSmall.src = smallUrl + '?t=' + Date.now();
1863
- imgStandard.onload = () => {{
1864
- emptyState.style.display = 'none';
1865
- compareWrap.classList.add('visible');
1866
- setSlider(50);
1867
  }};
1868
- imgStandard.src = stdUrl + '?t=' + Date.now();
1869
- dlStd.href = stdUrl; dlStd.classList.remove('hidden');
1870
- dlSmall.href = smallUrl; dlSmall.classList.remove('hidden');
1871
- }}
1872
-
1873
- /* ── submit ── */
1874
- async function submit() {{
1875
- if (S.running) return;
1876
- const prompt = promptEl.value.trim();
1877
- if (!prompt) {{ toast('Please enter a prompt.', 'error'); return; }}
1878
-
1879
- const fd = new FormData();
1880
- fd.append('prompt', prompt);
1881
- fd.append('seed', seedEl.value);
1882
- fd.append('randomize_seed', String(randomizeSeedEl.checked));
1883
- fd.append('width', widthEl.value);
1884
- fd.append('height', heightEl.value);
1885
- fd.append('steps', stepsEl.value);
1886
- fd.append('guidance_scale', parseFloat(guidanceEl.value).toFixed(1));
1887
- S.files.forEach(f => fd.append('images', f));
1888
-
1889
- setRunning(true);
1890
-
1891
- try {{
1892
- const res = await fetch('/api/compare', {{ method: 'POST', body: fd }});
1893
- const data = await res.json();
1894
- if (!res.ok || !data.success) throw new Error(data.error || 'Processing failed.');
1895
-
1896
- showComparison(data.standard.image_url, data.small.image_url);
1897
- chipSeed.textContent = data.seed;
1898
- footerSeed.textContent = data.seed;
1899
- chipStatus.textContent = 'done ✓';
1900
- toast('Comparison ready — drag the slider to compare!', 'ok');
1901
- }} catch(err) {{
1902
- toast(err.message || 'Unexpected error.', 'error');
1903
- }} finally {{
1904
- setRunning(false);
1905
- }}
1906
- }}
1907
-
1908
- runBtn.addEventListener('click', submit);
1909
- promptEl.addEventListener('keydown', e => {{ if (e.ctrlKey && e.key === 'Enter') submit(); }});
1910
-
1911
- /* ── examples ── */
1912
- async function fileFromUrl(url, name) {{
1913
- const res = await fetch(url);
1914
- if (!res.ok) throw new Error('Failed to fetch example.');
1915
- const blob = await res.blob();
1916
- return new File([blob], name, {{ type: blob.type || 'image/jpeg' }});
1917
- }}
1918
-
1919
- function renderExamples() {{
1920
- examplesScroll.innerHTML = '';
1921
- if (!examples.length) {{
1922
- examplesScroll.innerHTML = '<span style="font-size:12px;color:var(--gray2);font-family:var(--font-mono);padding:12px;">// no examples found in ./examples/</span>';
1923
- return;
1924
  }}
1925
- examples.forEach(item => {{
 
 
1926
  const card = document.createElement('div');
1927
- card.className = 'example-card';
1928
-
1929
- const img = document.createElement('img');
1930
- img.className = 'example-img';
1931
- img.src = item.url;
1932
- img.alt = item.file;
1933
- img.loading = 'lazy';
1934
-
1935
- const body = document.createElement('div');
1936
- body.className = 'example-body';
1937
-
1938
- const p = document.createElement('p');
1939
- p.className = 'example-prompt';
1940
- p.textContent = item.prompt;
1941
-
1942
- const btn = document.createElement('button');
1943
- btn.type = 'button';
1944
- btn.className = 'example-use-btn';
1945
- btn.textContent = '$ load --example';
1946
-
1947
- btn.addEventListener('click', async e => {{
1948
- e.stopPropagation();
1949
- try {{
1950
- btn.textContent = 'fetching...';
1951
- btn.disabled = true;
1952
- const file = await fileFromUrl(item.url, item.file);
1953
- S.files = [file];
1954
- renderPreviews();
1955
- promptEl.value = item.prompt;
1956
- toast('Example loaded.', 'ok');
1957
- }} catch(err) {{
1958
- toast(err.message, 'error');
1959
- }} finally {{
1960
- btn.textContent = '$ load --example';
1961
- btn.disabled = false;
1962
- }}
1963
- }});
1964
 
1965
- body.appendChild(p);
1966
- body.appendChild(btn);
1967
- card.appendChild(img);
1968
- card.appendChild(body);
1969
- examplesScroll.appendChild(card);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1970
  }});
1971
- }}
1972
 
1973
- /* ── init ── */
1974
- renderPreviews();
1975
- renderExamples();
1976
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1977
  </body>
1978
- </html>"""
1979
-
1980
 
1981
  app.launch()
 
 
1
  import os
2
  import gc
 
3
  import uuid
4
  import json
 
5
  import random
6
  import threading
7
  import concurrent.futures
 
16
  from gradio import Server
17
  from fastapi import Request, UploadFile, File, Form
18
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
 
19
  from diffusers import Flux2KleinPipeline, AutoencoderKLFlux2
20
 
21
+ # --- App Configuration & Directories ---
22
  app = Server()
23
 
24
  BASE_DIR = Path(__file__).resolve().parent
 
32
  MAX_SEED = np.iinfo(np.int32).max
33
  MAX_IMAGE_SIZE = 1024
34
 
35
+ dtype = torch.bfloat16
36
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
37
 
38
  if torch.cuda.is_available():
39
+ print("current device:", torch.cuda.current_device())
40
+ print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))
41
  DEVICE_LABEL = torch.cuda.get_device_name(torch.cuda.current_device()).lower()
42
  else:
43
+ DEVICE_LABEL = str(device).lower()
44
 
45
+ # --- Model Loading ---
46
  print("Loading 4B Distilled model (Standard VAE)...")
47
  pipe_standard = Flux2KleinPipeline.from_pretrained(
48
  "black-forest-labs/FLUX.2-klein-4B",
49
  torch_dtype=dtype,
50
+ ).to(device)
51
  pipe_standard.enable_model_cpu_offload()
52
 
53
  print("Loading Small Decoder VAE...")
54
  vae_small = AutoencoderKLFlux2.from_pretrained(
55
  "black-forest-labs/FLUX.2-small-decoder",
56
  torch_dtype=dtype,
57
+ ).to(device)
58
 
59
  print("Loading 4B Distilled model (Small Decoder VAE)...")
60
  pipe_small_decoder = Flux2KleinPipeline.from_pretrained(
61
  "black-forest-labs/FLUX.2-klein-4B",
62
  vae=vae_small,
63
  torch_dtype=dtype,
64
+ ).to(device)
65
  pipe_small_decoder.enable_model_cpu_offload()
66
 
67
  pipe_lock_standard = threading.Lock()
68
+ pipe_lock_small = threading.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ # --- Utility Functions ---
71
  def calc_dimensions(pil_img: Image.Image):
72
  iw, ih = pil_img.size
73
  aspect = iw / ih
74
+
75
  if aspect >= 1:
76
+ new_width = 1024
77
  new_height = int(round(1024 / aspect))
78
  else:
79
  new_height = 1024
80
+ new_width = int(round(1024 * aspect))
81
+
82
+ new_width = max(256, min(1024, round(new_width / 8) * 8))
83
  new_height = max(256, min(1024, round(new_height / 8) * 8))
84
  return new_width, new_height
85
 
86
+ def parse_and_resize_images(image_paths: List[str], width: int, height: int):
87
+ if not image_paths:
88
+ return None
89
+
90
+ resized = []
91
  for path in image_paths:
92
  try:
93
  img = Image.open(path).convert("RGB")
94
+ resized.append(img.resize((width, height), Image.LANCZOS))
95
  except Exception as e:
96
  print(f"Skipping invalid image: {e}")
97
+
98
+ return resized if resized else None
 
 
99
 
100
  def run_pipeline(pipe, lock, kwargs, seed):
101
  with lock:
 
103
  result = pipe(**kwargs, generator=gen).images[0]
104
  return result
105
 
106
+ def save_image(img: Image.Image, prefix: str = "output") -> str:
107
+ filename = f"{prefix}_{uuid.uuid4().hex}.png"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  path = OUTPUT_DIR / filename
109
+ img.save(path, format="PNG")
110
+ return filename
 
 
111
 
112
+ # --- Inference Function ---
113
  @spaces.GPU(duration=120)
114
+ def infer(
115
+ prompt: str,
116
+ image_paths: List[str] = None,
117
+ seed: int = 42,
118
+ randomize_seed: bool = False,
119
+ width: int = 1024,
120
+ height: int = 1024,
121
+ num_inference_steps: int = 4,
122
+ guidance_scale: float = 1.0,
123
+ ):
124
  gc.collect()
125
  if torch.cuda.is_available():
126
  torch.cuda.empty_cache()
127
 
128
+ if not prompt or not prompt.strip():
129
+ raise ValueError("Please enter a prompt.")
130
+
131
  if randomize_seed:
132
  seed = random.randint(0, MAX_SEED)
133
 
134
  image_list = None
135
+ if image_paths and len(image_paths) > 0:
136
  try:
137
  first_pil = Image.open(image_paths[0]).convert("RGB")
138
  width, height = calc_dimensions(first_pil)
139
+ image_list = parse_and_resize_images(image_paths, width, height)
140
+ except Exception as e:
141
+ print(f"Error processing upload: {e}")
142
 
143
+ width = max(256, min(MAX_IMAGE_SIZE, round(int(width) / 8) * 8))
144
  height = max(256, min(MAX_IMAGE_SIZE, round(int(height) / 8) * 8))
145
 
146
  shared_kwargs = dict(
147
  prompt=prompt,
148
  height=height,
149
  width=width,
150
+ num_inference_steps=num_inference_steps,
151
  guidance_scale=guidance_scale,
152
  )
153
  if image_list is not None:
154
  shared_kwargs["image"] = image_list
155
 
156
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
157
+ future_std = executor.submit(run_pipeline, pipe_standard, pipe_lock_standard, shared_kwargs, seed)
158
  future_small = executor.submit(run_pipeline, pipe_small_decoder, pipe_lock_small, shared_kwargs, seed)
159
+
160
+ concurrent.futures.wait(
161
+ [future_std, future_small],
162
+ return_when=concurrent.futures.ALL_COMPLETED,
163
+ )
164
 
165
  out_standard = future_std.result()
166
+ out_small = future_small.result()
167
 
168
  gc.collect()
169
  if torch.cuda.is_available():
 
172
  return out_standard, out_small, seed
173
 
174
 
175
+ # --- FastAPI Endpoints ---
176
+ def get_example_items():
177
+ items = []
178
+
179
+ # 1. Multi-image example (Explicitly Handled)
180
+ items.append({
181
+ "files": ["I1.jpg", "I2.jpg"],
182
+ "urls": ["/example-file/I1.jpg", "/example-file/I2.jpg"],
183
+ "prompt": "Make her wear these glasses in Image 2."
184
+ })
185
+
186
+ # 2. Single image examples
187
+ example_prompts = {
188
+ "1.jpg": "Change the weather to stormy.",
189
+ "2.jpg": "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition.",
190
+ "3.jpg": "Relight the image with soft golden sunset lighting while keeping all structures and subject details consistent.",
191
+ "4.jpg": "Make the texture high-resolution.",
192
+ }
193
+
194
+ if EXAMPLES_DIR.exists():
195
+ for name in sorted(os.listdir(EXAMPLES_DIR)):
196
+ # Skip the ones explicitly handled above to avoid duplicates
197
+ if name in ["I1.jpg", "I2.jpg"]: continue
198
+ if name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
199
+ items.append({
200
+ "files": [name],
201
+ "urls": [f"/example-file/{name}"],
202
+ "prompt": example_prompts.get(name, "Edit this image while preserving composition.")
203
+ })
204
+ return items
205
+
206
+ @app.get("/example-file/{filename}")
207
+ async def example_file(filename: str):
208
+ path = EXAMPLES_DIR / filename
209
+ if not path.exists():
210
+ return JSONResponse({"error": "Example not found"}, status_code=404)
211
+ return FileResponse(path)
212
+
213
+ @app.get("/download/{filename}")
214
+ async def download_file(filename: str):
215
+ path = OUTPUT_DIR / filename
216
+ if not path.exists():
217
+ return JSONResponse({"error": "File not found"}, status_code=404)
218
+ return FileResponse(path, filename=filename, media_type="image/png")
219
+
220
  @app.post("/api/compare")
221
  async def compare_images(
222
  prompt: str = Form(...),
 
225
  width: str = Form("1024"),
226
  height: str = Form("1024"),
227
  steps: str = Form("4"),
228
+ guidance: str = Form("1.0"),
229
  images: Optional[List[UploadFile]] = File(None),
230
  ):
231
  temp_paths = []
 
233
  image_paths = []
234
  if images:
235
  for upload in images:
236
+ if not upload.filename: continue
237
  suffix = Path(upload.filename).suffix or ".png"
238
+ temp_path = OUTPUT_DIR / f"upload_{uuid.uuid4().hex}{suffix}"
 
239
  content = await upload.read()
240
  with open(temp_path, "wb") as f:
241
  f.write(content)
242
  temp_paths.append(str(temp_path))
243
  image_paths.append(str(temp_path))
244
 
245
+ result_std, result_small, used_seed = infer(
 
246
  prompt=prompt,
247
+ image_paths=image_paths,
248
  seed=int(seed),
249
+ randomize_seed=(randomize_seed.lower() == "true"),
250
  width=int(width),
251
  height=int(height),
252
+ num_inference_steps=int(steps),
253
+ guidance_scale=float(guidance),
254
  )
255
 
256
+ std_filename = save_image(result_std, prefix="std")
257
+ small_filename = save_image(result_small, prefix="small")
258
 
259
  return JSONResponse({
260
  "success": True,
261
  "seed": used_seed,
262
+ "std_url": f"/download/{std_filename}",
263
+ "small_url": f"/download/{small_filename}",
 
 
 
 
 
 
264
  "device": DEVICE_LABEL,
265
  })
266
 
 
268
  return JSONResponse({"success": False, "error": str(e)}, status_code=500)
269
  finally:
270
  for p in temp_paths:
271
+ if os.path.exists(p):
272
+ os.remove(p)
 
 
 
 
273
 
274
+ # --- Frontend ---
275
  @app.get("/", response_class=HTMLResponse)
276
  async def homepage(request: Request):
277
  examples = get_example_items()
278
  examples_json = json.dumps(examples)
279
 
280
+ return f"""
281
+ <!DOCTYPE html>
282
  <html lang="en">
283
  <head>
284
+ <meta charset="UTF-8" />
285
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
286
+ <title>Flux.2-4B-Decoder-Comparator</title>
287
+ <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
288
  <style>
 
 
289
  :root {{
290
+ --ub-aubergine: #2C001E;
291
+ --ub-aubergine-dark: #1f0015;
292
+ --ub-orange: #E95420;
293
+ --ub-orange-hover: #c4461a;
294
+ --ub-panel: #3D3D3D;
295
+ --ub-panel-light: #4f4f4f;
296
+ --ub-border: rgba(255,255,255,0.1);
297
+ --ub-text: #FFFFFF;
298
+ --ub-muted: #b0b0b0;
299
+ --ub-input: #2b2b2b;
300
+ --panel-radius: 8px;
301
+ --panel-height: 700px; /* Locked equal height */
302
+ }}
303
+
304
+ * {{ box-sizing: border-box; font-family: 'Ubuntu', sans-serif; }}
305
+
306
+ body {{
307
+ margin: 0; padding: 0;
308
+ background: var(--ub-aubergine);
309
+ color: var(--ub-text);
310
+ min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  display: flex;
312
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  }}
 
 
 
 
314
 
315
+ .topbar {{
316
+ background: var(--ub-aubergine-dark);
317
+ padding: 16px 24px;
318
+ border-bottom: 1px solid var(--ub-border);
319
  text-align: center;
320
+ font-weight: 700;
321
+ letter-spacing: 0.5px;
322
+ color: var(--ub-orange);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  }}
324
 
325
+ .container {{
326
+ max-width: 1300px;
327
+ margin: 0 auto;
328
+ padding: 30px 20px;
329
+ flex: 1;
330
+ width: 100%;
331
  }}
332
 
333
+ .header-text {{
334
+ text-align: center;
335
+ margin-bottom: 30px;
 
 
336
  }}
337
+ .header-text h1 {{ margin: 0 0 10px 0; font-size: 2.2rem; }}
338
+ .header-text p {{ color: var(--ub-muted); margin: 0; }}
339
 
340
+ /* Layout & Panels */
341
+ .layout {{
 
 
 
 
342
  display: grid;
343
+ grid-template-columns: 400px 1fr;
344
+ gap: 24px;
345
+ align-items: stretch; /* Ensures columns match height natively if unset, but we use fixed height */
346
  }}
347
 
348
+ .panel {{
349
+ background: var(--ub-panel);
350
+ border-radius: var(--panel-radius);
351
+ box-shadow: 0 8px 24px rgba(0,0,0,0.2);
352
  display: flex;
353
  flex-direction: column;
354
+ height: var(--panel-height); /* Enforces exact equal height */
355
  overflow: hidden;
356
  }}
357
 
358
+ .panel-header {{
359
+ padding: 16px 20px;
360
+ background: rgba(0,0,0,0.2);
361
+ border-bottom: 1px solid var(--ub-border);
362
+ font-weight: 500;
363
+ font-size: 1.1rem;
364
+ flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  }}
366
 
367
+ .panel-body {{
368
+ padding: 20px;
369
+ flex: 1;
370
+ overflow-y: auto; /* Allows left settings panel to scroll without expanding container */
371
  display: flex;
372
  flex-direction: column;
 
373
  }}
374
 
375
+ .panel-body::-webkit-scrollbar {{ width: 8px; }}
376
+ .panel-body::-webkit-scrollbar-track {{ background: var(--ub-panel); }}
377
+ .panel-body::-webkit-scrollbar-thumb {{ background: var(--ub-panel-light); border-radius: 4px; }}
 
 
 
 
 
 
378
 
379
+ /* Input Forms */
380
+ .form-group {{ margin-bottom: 20px; flex-shrink: 0; }}
381
+ .label {{
382
+ display: block; font-weight: 500; font-size: 14px;
383
+ color: var(--ub-muted); margin-bottom: 8px;
384
  }}
385
 
386
+ .textarea, .input {{
387
+ width: 100%;
388
+ background: var(--ub-input);
389
+ border: 1px solid var(--ub-border);
390
+ color: var(--ub-text);
391
+ padding: 12px;
 
392
  border-radius: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  outline: none;
394
+ font-size: 14px;
 
 
 
 
 
 
395
  }}
396
+ .textarea:focus, .input:focus {{ border-color: var(--ub-orange); }}
397
+ .textarea {{ min-height: 100px; resize: vertical; }}
398
 
399
+ /* Upload Zone & Plus Button */
 
 
400
  .upload-zone {{
401
+ background: var(--ub-input);
402
+ border: 1px dashed var(--ub-muted);
403
  border-radius: 4px;
404
+ padding: 20px;
405
+ text-align: center;
406
  cursor: pointer;
407
  transition: border-color 0.2s, background 0.2s;
408
  }}
 
409
  .upload-zone:hover, .upload-zone.dragover {{
410
+ border-color: var(--ub-orange);
411
+ background: rgba(233,84,32,0.05);
412
+ }}
413
+ .upload-zone.has-files {{
414
+ padding: 10px;
415
+ border-style: solid;
416
  }}
 
417
  .upload-zone input[type="file"] {{ display: none; }}
418
+
419
+ .preview-grid {{
420
+ display: grid;
421
+ grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
422
+ gap: 10px;
423
+ }}
424
+ .thumb {{
425
+ position: relative; aspect-ratio: 1;
426
+ border-radius: 4px; overflow: hidden;
427
+ border: 1px solid var(--ub-border);
428
+ background: rgba(0,0,0,0.2);
429
+ }}
430
+ .thumb img {{ width: 100%; height: 100%; object-fit: cover; display: block; }}
431
+ .thumb-remove {{
432
+ position: absolute; top: 4px; right: 4px;
433
+ background: rgba(0,0,0,0.7); color: white;
434
+ border: none; border-radius: 50%; width: 20px; height: 20px;
435
+ display: flex; align-items: center; justify-content: center;
436
+ cursor: pointer; font-size: 12px;
437
+ }}
438
+ .add-more-btn {{
439
+ display: flex; align-items: center; justify-content: center;
440
+ font-size: 28px; font-weight: 300; color: var(--ub-muted);
441
+ cursor: pointer; transition: 0.2s;
442
+ border: 1px dashed var(--ub-muted); background: transparent;
443
+ }}
444
+ .add-more-btn:hover {{
445
+ color: var(--ub-orange); border-color: var(--ub-orange);
446
+ background: rgba(233,84,32,0.05);
447
+ }}
448
+
449
+ /* Buttons */
450
+ .btn {{
451
+ width: 100%; padding: 14px; border: none; border-radius: 4px;
452
+ font-size: 16px; font-weight: 700; cursor: pointer;
453
+ transition: opacity 0.2s, background 0.2s;
454
+ flex-shrink: 0;
455
+ }}
456
+ .btn-primary {{
457
+ background: var(--ub-orange); color: white;
458
+ box-shadow: 0 4px 12px rgba(233,84,32,0.3);
459
+ }}
460
+ .btn-primary:hover {{ background: var(--ub-orange-hover); }}
461
+ .btn:disabled {{ opacity: 0.6; cursor: not-allowed; }}
462
 
463
+ /* Advanced Accordion */
464
+ .advanced-toggle {{
465
+ width: 100%; background: none; border: none; color: var(--ub-orange);
466
+ text-align: left; padding: 10px 0; font-weight: 500; cursor: pointer;
467
+ display: flex; justify-content: space-between; align-items: center;
468
+ flex-shrink: 0;
 
 
 
 
 
 
469
  }}
470
+ .advanced-toggle span.icon {{ font-family: monospace; font-size: 18px; font-weight: bold; }}
471
+ .advanced-body {{ display: none; padding-top: 10px; flex-shrink: 0; }}
472
+ .advanced-body.open {{ display: block; }}
473
+ .grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }}
474
 
475
+ /* SLIDER CONTAINER */
476
+ .slider-stage {{
477
+ position: relative;
478
+ width: 100%;
479
+ flex: 1; /* Automatically takes remaining height to hit 700px */
480
+ min-height: 0;
481
+ background: #111;
482
  border-radius: 4px;
483
+ overflow: hidden;
484
  display: flex;
485
  align-items: center;
486
  justify-content: center;
 
 
487
  }}
488
+ .slider-empty {{
489
+ color: var(--ub-muted);
490
+ text-align: center;
491
+ }}
492
+
493
+ .slider-img {{
494
+ position: absolute;
495
+ top: 0; left: 0;
496
+ width: 100%; height: 100%;
497
+ object-fit: contain;
498
+ display: none;
499
+ user-select: none;
500
+ -webkit-user-drag: none;
501
  }}
502
+ #imgSmall {{ clip-path: inset(0 50% 0 0); }}
503
 
504
+ .slider-handle {{
505
+ position: absolute;
506
+ left: 50%;
507
+ top: 0; bottom: 0;
508
+ width: 4px;
509
+ background: var(--ub-orange);
510
+ cursor: ew-resize;
511
+ display: none;
512
+ z-index: 10;
513
+ }}
514
+ .slider-handle::after {{
515
+ content: '◀ ▶';
516
+ position: absolute;
517
+ top: 50%; left: 50%;
518
+ transform: translate(-50%, -50%);
519
+ width: 40px; height: 30px;
520
+ background: var(--ub-orange);
521
+ color: white;
522
+ border-radius: 15px;
523
+ display: flex; align-items: center; justify-content: center;
524
+ font-size: 10px; font-weight: bold;
525
+ box-shadow: 0 2px 6px rgba(0,0,0,0.5);
526
  }}
527
 
528
+ .slider-labels {{
529
+ position: absolute;
530
+ top: 15px; left: 15px; right: 15px;
531
+ display: none;
532
  justify-content: space-between;
533
+ pointer-events: none;
534
+ z-index: 5;
535
  }}
536
+ .badge {{
537
+ background: rgba(0,0,0,0.6); color: white; padding: 6px 12px;
538
+ border-radius: 20px; font-size: 13px; backdrop-filter: blur(4px);
 
 
 
539
  }}
540
 
541
+ .loader {{
542
+ position: absolute; inset: 0; background: rgba(0,0,0,0.7);
543
+ display: none; flex-direction: column;
544
+ align-items: center; justify-content: center; z-index: 20;
 
 
 
 
 
 
 
 
545
  }}
546
+ .spinner {{
547
+ width: 40px; height: 40px; border: 4px solid rgba(255,255,255,0.2);
548
+ border-top-color: var(--ub-orange); border-radius: 50%;
549
+ animation: spin 1s linear infinite; margin-bottom: 15px;
 
 
 
550
  }}
551
 
552
+ /* Examples */
553
+ .examples-section {{ margin-top: 40px; }}
554
+ .examples-section h3 {{ border-bottom: 1px solid var(--ub-border); padding-bottom: 10px; }}
555
+ .examples-grid {{
556
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 20px;
557
+ }}
558
+ .ex-card {{
559
+ background: var(--ub-panel); border-radius: 4px; overflow: hidden;
560
+ cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;
561
+ display: flex; flex-direction: column;
562
+ }}
563
+ .ex-card:hover {{ transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0,0,0,0.3); }}
564
+
565
+ .ex-card-images {{
566
+ display: flex; width: 100%; aspect-ratio: 1;
567
  }}
568
+ .ex-card-images img {{
569
+ flex: 1; min-width: 0; object-fit: cover;
570
+ }}
571
+ .ex-card-images img:nth-child(2) {{ border-left: 1px solid #111; }}
572
+
573
+ .ex-card p {{ padding: 12px; margin: 0; font-size: 13px; color: var(--ub-muted); line-height: 1.4; }}
574
 
575
+ @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
576
 
577
+ @media (max-width: 900px) {{
578
+ .layout {{ grid-template-columns: 1fr; }}
579
+ .panel {{ height: auto; min-height: 500px; }}
 
 
580
  }}
581
+ </style>
582
+ </head>
583
+ <body>
584
 
585
+ <div class="topbar">Flux.2-4B VAE Decoder Comparator</div>
 
 
 
 
 
 
 
 
 
586
 
587
+ <div class="container">
588
+ <div class="header-text">
589
+ <h1>Standard vs. Small Decoder</h1>
590
+ <p>Upload an image, enter a prompt, and use the slider to compare outputs in real-time.</p>
591
+ </div>
592
 
593
+ <div class="layout">
594
+ <div class="panel">
595
+ <div class="panel-header">Settings</div>
596
+ <div class="panel-body">
597
+ <div class="form-group">
598
+ <label class="label">Input Images (Optional)</label>
599
+ <div class="upload-zone" id="dropZone">
600
+ <input type="file" id="fileInput" multiple accept="image/*" />
601
+ <div id="uploadText">Click or Drag & Drop images here</div>
602
+ <div class="preview-grid" id="previewGrid" style="display:none;"></div>
603
+ </div>
604
+ </div>
 
 
 
605
 
606
+ <div class="form-group">
607
+ <label class="label">Prompt</label>
608
+ <textarea id="promptInput" class="textarea" placeholder="Describe the edit or generation..."></textarea>
609
+ </div>
610
 
611
+ <button class="advanced-toggle" id="advToggle">
612
+ <span>Advanced Settings</span> <span class="icon" id="advIcon">+</span>
613
+ </button>
614
+
615
+ <div class="advanced-body" id="advBody">
616
+ <div class="grid-2">
617
+ <div class="form-group">
618
+ <label class="label">Seed</label>
619
+ <input type="number" id="seed" class="input" value="0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  </div>
621
+ <div class="form-group">
622
+ <label class="label">Steps</label>
623
+ <input type="number" id="steps" class="input" value="4">
624
+ </div>
625
+ <div class="form-group">
626
+ <label class="label">Width</label>
627
+ <input type="number" id="width" class="input" value="1024" step="8">
628
+ </div>
629
+ <div class="form-group">
630
+ <label class="label">Height</label>
631
+ <input type="number" id="height" class="input" value="1024" step="8">
632
+ </div>
633
+ <div class="form-group" style="grid-column: span 2;">
634
+ <label class="label">Guidance Scale</label>
635
+ <input type="number" id="guidance" class="input" value="1.0" step="0.1">
636
+ </div>
637
+ <div class="form-group" style="grid-column: span 2;">
638
+ <label style="display:flex; align-items:center; gap:8px; font-size:14px;">
639
+ <input type="checkbox" id="randomize" checked> Randomize Seed
640
+ </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  </div>
 
 
 
 
642
  </div>
643
  </div>
644
 
645
+ <button class="btn btn-primary" id="runBtn" style="margin-top: 20px;">Run Comparison</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  </div>
647
+ </div>
648
 
649
+ <div class="panel">
650
+ <div class="panel-header">Comparison View</div>
651
+ <div class="panel-body" style="padding:0;">
652
+ <div class="slider-stage" id="sliderStage">
653
+ <div class="slider-empty" id="sliderEmpty">
654
+ <svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="margin-bottom:10px; opacity:0.5; display:inline-block;">
655
+ <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
656
+ </svg>
657
+ <div>Results will appear here</div>
 
 
 
 
 
658
  </div>
 
 
 
 
 
 
659
 
660
+ <img id="imgStd" class="slider-img" alt="Standard Decoder" />
661
+ <img id="imgSmall" class="slider-img" alt="Small Decoder" />
662
+
663
+ <div class="slider-labels" id="sliderLabels">
664
+ <div class="badge">Standard Decoder</div>
665
+ <div class="badge">Small Decoder</div>
666
  </div>
 
667
 
668
+ <div class="slider-handle" id="sliderHandle"></div>
 
669
 
670
+ <div class="loader" id="loader">
671
+ <div class="spinner"></div>
672
+ <div style="font-weight: 500;">Running both models...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  </div>
674
  </div>
675
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  </div>
 
677
  </div>
678
 
679
+ <div class="examples-section">
680
+ <h3>Examples</h3>
681
+ <div class="examples-grid" id="examplesGrid"></div>
 
 
 
 
 
 
 
 
 
682
  </div>
683
+ </div>
684
+
685
+ <script>
686
+ const examples = {examples_json};
687
+ let filesState = [];
688
+
689
+ // UI Elements
690
+ const dropZone = document.getElementById('dropZone');
691
+ const fileInput = document.getElementById('fileInput');
692
+ const previewGrid = document.getElementById('previewGrid');
693
+ const uploadText = document.getElementById('uploadText');
694
+ const promptInput = document.getElementById('promptInput');
695
+ const runBtn = document.getElementById('runBtn');
696
+
697
+ // Slider Elements
698
+ const sliderStage = document.getElementById('sliderStage');
699
+ const imgStd = document.getElementById('imgStd');
700
+ const imgSmall = document.getElementById('imgSmall');
701
+ const sliderHandle = document.getElementById('sliderHandle');
702
+ const sliderLabels = document.getElementById('sliderLabels');
703
+ const sliderEmpty = document.getElementById('sliderEmpty');
704
+ const loader = document.getElementById('loader');
705
+
706
+ // Advanced Toggle
707
+ document.getElementById('advToggle').onclick = function() {{
708
+ const body = document.getElementById('advBody');
709
+ body.classList.toggle('open');
710
+ document.getElementById('advIcon').innerText = body.classList.contains('open') ? '-' : '+';
711
+ }};
712
 
713
+ // --- File Upload Logic ---
714
+ function renderPreviews() {{
715
+ previewGrid.innerHTML = '';
716
+ if(filesState.length > 0) {{
717
+ uploadText.style.display = 'none';
718
+ previewGrid.style.display = 'grid';
719
+ dropZone.classList.add('has-files');
720
+
721
+ // 1. Render actual thumbnails
722
+ filesState.forEach((f, i) => {{
723
+ const div = document.createElement('div');
724
+ div.className = 'thumb';
725
+ const img = document.createElement('img');
726
+ img.src = URL.createObjectURL(f);
727
+ const btn = document.createElement('button');
728
+ btn.className = 'thumb-remove';
729
+ btn.innerText = '×';
730
+ btn.onclick = (e) => {{ e.stopPropagation(); filesState.splice(i, 1); renderPreviews(); }};
731
+ div.appendChild(img); div.appendChild(btn);
732
+ previewGrid.appendChild(div);
733
+ }});
734
+
735
+ // 2. Add the "+" box to append more
736
+ const addMore = document.createElement('div');
737
+ addMore.className = 'thumb add-more-btn';
738
+ addMore.innerText = '+';
739
+ addMore.onclick = (e) => {{ e.stopPropagation(); fileInput.click(); }};
740
+ previewGrid.appendChild(addMore);
741
+
742
+ }} else {{
743
+ uploadText.style.display = 'block';
744
+ previewGrid.style.display = 'none';
745
+ dropZone.classList.remove('has-files');
746
+ }}
747
+ }}
748
+
749
+ dropZone.onclick = (e) => {{ if(e.target === dropZone || e.target === uploadText) fileInput.click(); }};
750
+ fileInput.onchange = (e) => {{ filesState.push(...Array.from(e.target.files)); renderPreviews(); fileInput.value=''; }};
751
+ dropZone.ondragover = (e) => {{ e.preventDefault(); dropZone.classList.add('dragover'); }};
752
+ dropZone.ondragleave = () => dropZone.classList.remove('dragover');
753
+ dropZone.ondrop = (e) => {{
754
+ e.preventDefault(); dropZone.classList.remove('dragover');
755
+ if(e.dataTransfer.files.length) {{ filesState.push(...Array.from(e.dataTransfer.files)); renderPreviews(); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  }};
757
+
758
+ // --- Examples Logic ---
759
+ async function loadExample(urls, filenames, text) {{
760
+ try {{
761
+ // Reset state and load all provided files
762
+ filesState = [];
763
+ for(let i=0; i<urls.length; i++) {{
764
+ const res = await fetch(urls[i]);
765
+ const blob = await res.blob();
766
+ filesState.push(new File([blob], filenames[i], {{type: blob.type}}));
767
+ }}
768
+ renderPreviews();
769
+ promptInput.value = text;
770
+ window.scrollTo({{top: 0, behavior: 'smooth'}});
771
+ }} catch (e) {{ alert('Failed to load example image.'); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  }}
773
+
774
+ const exGrid = document.getElementById('examplesGrid');
775
+ examples.forEach(ex => {{
776
  const card = document.createElement('div');
777
+ card.className = 'ex-card';
778
+
779
+ // Split view logic for multi-image examples
780
+ let imgHtml = '';
781
+ if (ex.urls.length > 1) {{
782
+ const imgs = ex.urls.map(u => `<img src="${{u}}">`).join('');
783
+ imgHtml = `<div class="ex-card-images">${{imgs}}</div>`;
784
+ }} else {{
785
+ imgHtml = `<div class="ex-card-images"><img src="${{ex.urls[0]}}"></div>`;
786
+ }}
787
+
788
+ card.innerHTML = `${{imgHtml}}<p>${{ex.prompt}}</p>`;
789
+ card.onclick = () => loadExample(ex.urls, ex.files, ex.prompt);
790
+ exGrid.appendChild(card);
791
+ }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
 
793
+ // --- Image Slider Logic ---
794
+ let isDragging = false;
795
+
796
+ function updateSlider(clientX) {{
797
+ const rect = sliderStage.getBoundingClientRect();
798
+ let pos = Math.max(0, Math.min(clientX - rect.left, rect.width));
799
+ let percent = (pos / rect.width) * 100;
800
+
801
+ sliderHandle.style.left = percent + '%';
802
+ imgSmall.style.clipPath = `inset(0 ${{100 - percent}}% 0 0)`;
803
+ }}
804
+
805
+ sliderHandle.addEventListener('mousedown', () => isDragging = true);
806
+ window.addEventListener('mouseup', () => isDragging = false);
807
+ window.addEventListener('mousemove', (e) => {{
808
+ if (!isDragging) return;
809
+ updateSlider(e.clientX);
810
+ }});
811
+
812
+ sliderHandle.addEventListener('touchstart', () => isDragging = true);
813
+ window.addEventListener('touchend', () => isDragging = false);
814
+ window.addEventListener('touchmove', (e) => {{
815
+ if (!isDragging) return;
816
+ updateSlider(e.touches[0].clientX);
817
  }});
 
818
 
819
+ // --- Form Submission ---
820
+ runBtn.onclick = async () => {{
821
+ const prompt = promptInput.value.trim();
822
+ if(!prompt) return alert("Enter a prompt");
823
+
824
+ const fd = new FormData();
825
+ fd.append('prompt', prompt);
826
+ fd.append('seed', document.getElementById('seed').value);
827
+ fd.append('randomize_seed', document.getElementById('randomize').checked);
828
+ fd.append('width', document.getElementById('width').value);
829
+ fd.append('height', document.getElementById('height').value);
830
+ fd.append('steps', document.getElementById('steps').value);
831
+ fd.append('guidance', document.getElementById('guidance').value);
832
+
833
+ filesState.forEach(f => fd.append('images', f));
834
+
835
+ loader.style.display = 'flex';
836
+ runBtn.disabled = true;
837
+
838
+ try {{
839
+ const res = await fetch('/api/compare', {{ method: 'POST', body: fd }});
840
+ const data = await res.json();
841
+
842
+ if(data.success) {{
843
+ imgStd.src = data.std_url + "?t=" + Date.now();
844
+ imgSmall.src = data.small_url + "?t=" + Date.now();
845
+
846
+ imgStd.onload = () => {{
847
+ sliderEmpty.style.display = 'none';
848
+ imgStd.style.display = 'block';
849
+ imgSmall.style.display = 'block';
850
+ sliderHandle.style.display = 'block';
851
+ sliderLabels.style.display = 'flex';
852
+
853
+ const rect = sliderStage.getBoundingClientRect();
854
+ updateSlider(rect.left + rect.width / 2);
855
+ }};
856
+ }} else {{
857
+ alert('Error: ' + data.error);
858
+ }}
859
+ }} catch(e) {{
860
+ alert('Failed to connect to server.');
861
+ }} finally {{
862
+ loader.style.display = 'none';
863
+ runBtn.disabled = false;
864
+ }}
865
+ }};
866
+ </script>
867
  </body>
868
+ </html>
869
+ """
870
 
871
  app.launch()