prithivMLmods commited on
Commit
dee857b
·
verified ·
1 Parent(s): 18115c6

update app

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