prithivMLmods commited on
Commit
eaa26b4
·
verified ·
1 Parent(s): 3d7021d

Update app.py

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