prithivMLmods commited on
Commit
4741a7b
·
verified ·
1 Parent(s): 33aa2ab

update app

Browse files
Files changed (1) hide show
  1. app.py +189 -138
app.py CHANGED
@@ -1,7 +1,9 @@
1
  import os
 
2
  import gc
3
  import uuid
4
  import json
 
5
  import random
6
  import threading
7
  import concurrent.futures
@@ -174,16 +176,15 @@ def infer(
174
 
175
  # --- FastAPI Endpoints ---
176
  def get_example_items():
177
- items = []
 
 
 
 
 
 
 
178
 
179
- # 1. Multi-image example (Explicitly Handled)
180
- items.append({
181
- "files": ["I1.jpg", "I2.jpg"],
182
- "urls": ["/example-file/I1.jpg", "/example-file/I2.jpg"],
183
- "prompt": "Make her wear these glasses in Image 2."
184
- })
185
-
186
- # 2. Single image examples
187
  example_prompts = {
188
  "1.jpg": "Change the weather to stormy.",
189
  "2.jpg": "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition.",
@@ -193,14 +194,15 @@ def get_example_items():
193
 
194
  if EXAMPLES_DIR.exists():
195
  for name in sorted(os.listdir(EXAMPLES_DIR)):
196
- # Skip the ones explicitly handled above to avoid duplicates
197
- if name in ["I1.jpg", "I2.jpg"]: continue
198
- if name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
199
- items.append({
200
- "files": [name],
201
- "urls": [f"/example-file/{name}"],
202
- "prompt": example_prompts.get(name, "Edit this image while preserving composition.")
203
- })
 
204
  return items
205
 
206
  @app.get("/example-file/{filename}")
@@ -298,7 +300,6 @@ async def homepage(request: Request):
298
  --ub-muted: #b0b0b0;
299
  --ub-input: #2b2b2b;
300
  --panel-radius: 8px;
301
- --panel-height: 700px; /* Locked equal height */
302
  }}
303
 
304
  * {{ box-sizing: border-box; font-family: 'Ubuntu', sans-serif; }}
@@ -334,15 +335,22 @@ async def homepage(request: Request):
334
  text-align: center;
335
  margin-bottom: 30px;
336
  }}
337
- .header-text h1 {{ margin: 0 0 10px 0; font-size: 2.2rem; }}
338
- .header-text p {{ color: var(--ub-muted); margin: 0; }}
 
 
 
 
 
 
339
 
340
- /* Layout & Panels */
341
  .layout {{
342
  display: grid;
343
  grid-template-columns: 400px 1fr;
344
  gap: 24px;
345
- align-items: stretch; /* Ensures columns match height natively if unset, but we use fixed height */
 
346
  }}
347
 
348
  .panel {{
@@ -351,7 +359,7 @@ async def homepage(request: Request):
351
  box-shadow: 0 8px 24px rgba(0,0,0,0.2);
352
  display: flex;
353
  flex-direction: column;
354
- height: var(--panel-height); /* Enforces exact equal height */
355
  overflow: hidden;
356
  }}
357
 
@@ -367,15 +375,11 @@ async def homepage(request: Request):
367
  .panel-body {{
368
  padding: 20px;
369
  flex: 1;
370
- overflow-y: auto; /* Allows left settings panel to scroll without expanding container */
371
  display: flex;
372
  flex-direction: column;
373
  }}
374
 
375
- .panel-body::-webkit-scrollbar {{ width: 8px; }}
376
- .panel-body::-webkit-scrollbar-track {{ background: var(--ub-panel); }}
377
- .panel-body::-webkit-scrollbar-thumb {{ background: var(--ub-panel-light); border-radius: 4px; }}
378
-
379
  /* Input Forms */
380
  .form-group {{ margin-bottom: 20px; flex-shrink: 0; }}
381
  .label {{
@@ -396,66 +400,74 @@ async def homepage(request: Request):
396
  .textarea:focus, .input:focus {{ border-color: var(--ub-orange); }}
397
  .textarea {{ min-height: 100px; resize: vertical; }}
398
 
399
- /* Upload Zone & Plus Button */
400
- .upload-zone {{
401
  background: var(--ub-input);
402
  border: 1px dashed var(--ub-muted);
403
  border-radius: 4px;
404
- padding: 20px;
405
- text-align: center;
406
- cursor: pointer;
 
 
407
  transition: border-color 0.2s, background 0.2s;
408
  }}
409
- .upload-zone:hover, .upload-zone.dragover {{
410
  border-color: var(--ub-orange);
411
  background: rgba(233,84,32,0.05);
412
  }}
413
- .upload-zone.has-files {{
 
414
  padding: 10px;
415
- border-style: solid;
 
 
 
 
 
416
  }}
417
- .upload-zone input[type="file"] {{ display: none; }}
 
418
 
419
- .preview-grid {{
420
- display: grid;
421
- grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
422
- gap: 10px;
423
  }}
 
 
 
424
  .thumb {{
425
- position: relative; aspect-ratio: 1;
426
- border-radius: 4px; overflow: hidden;
427
- border: 1px solid var(--ub-border);
428
- background: rgba(0,0,0,0.2);
429
  }}
430
  .thumb img {{ width: 100%; height: 100%; object-fit: cover; display: block; }}
431
  .thumb-remove {{
432
  position: absolute; top: 4px; right: 4px;
433
  background: rgba(0,0,0,0.7); color: white;
434
- border: none; border-radius: 50%; width: 20px; height: 20px;
435
  display: flex; align-items: center; justify-content: center;
436
- cursor: pointer; font-size: 12px;
437
  }}
 
 
438
  .add-more-btn {{
 
 
439
  display: flex; align-items: center; justify-content: center;
440
- font-size: 28px; font-weight: 300; color: var(--ub-muted);
441
- cursor: pointer; transition: 0.2s;
442
- border: 1px dashed var(--ub-muted); background: transparent;
443
- }}
444
- .add-more-btn:hover {{
445
- color: var(--ub-orange); border-color: var(--ub-orange);
446
- background: rgba(233,84,32,0.05);
447
  }}
 
448
 
449
  /* Buttons */
450
  .btn {{
451
  width: 100%; padding: 14px; border: none; border-radius: 4px;
452
  font-size: 16px; font-weight: 700; cursor: pointer;
453
  transition: opacity 0.2s, background 0.2s;
454
- flex-shrink: 0;
455
  }}
456
  .btn-primary {{
457
  background: var(--ub-orange); color: white;
458
  box-shadow: 0 4px 12px rgba(233,84,32,0.3);
 
459
  }}
460
  .btn-primary:hover {{ background: var(--ub-orange-hover); }}
461
  .btn:disabled {{ opacity: 0.6; cursor: not-allowed; }}
@@ -464,11 +476,9 @@ async def homepage(request: Request):
464
  .advanced-toggle {{
465
  width: 100%; background: none; border: none; color: var(--ub-orange);
466
  text-align: left; padding: 10px 0; font-weight: 500; cursor: pointer;
467
- display: flex; justify-content: space-between; align-items: center;
468
- flex-shrink: 0;
469
  }}
470
- .advanced-toggle span.icon {{ font-family: monospace; font-size: 18px; font-weight: bold; }}
471
- .advanced-body {{ display: none; padding-top: 10px; flex-shrink: 0; }}
472
  .advanced-body.open {{ display: block; }}
473
  .grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }}
474
 
@@ -476,8 +486,7 @@ async def homepage(request: Request):
476
  .slider-stage {{
477
  position: relative;
478
  width: 100%;
479
- flex: 1; /* Automatically takes remaining height to hit 700px */
480
- min-height: 0;
481
  background: #111;
482
  border-radius: 4px;
483
  overflow: hidden;
@@ -499,7 +508,11 @@ async def homepage(request: Request):
499
  user-select: none;
500
  -webkit-user-drag: none;
501
  }}
502
- #imgSmall {{ clip-path: inset(0 50% 0 0); }}
 
 
 
 
503
 
504
  .slider-handle {{
505
  position: absolute;
@@ -534,49 +547,55 @@ async def homepage(request: Request):
534
  z-index: 5;
535
  }}
536
  .badge {{
537
- background: rgba(0,0,0,0.6); color: white; padding: 6px 12px;
538
- border-radius: 20px; font-size: 13px; backdrop-filter: blur(4px);
 
 
 
 
539
  }}
540
 
541
  .loader {{
542
- position: absolute; inset: 0; background: rgba(0,0,0,0.7);
 
543
  display: none; flex-direction: column;
544
- align-items: center; justify-content: center; z-index: 20;
 
545
  }}
546
  .spinner {{
547
- width: 40px; height: 40px; border: 4px solid rgba(255,255,255,0.2);
548
- border-top-color: var(--ub-orange); border-radius: 50%;
549
- animation: spin 1s linear infinite; margin-bottom: 15px;
 
 
 
550
  }}
551
 
552
  /* Examples */
553
  .examples-section {{ margin-top: 40px; }}
554
  .examples-section h3 {{ border-bottom: 1px solid var(--ub-border); padding-bottom: 10px; }}
555
  .examples-grid {{
556
- display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 20px;
557
  }}
558
  .ex-card {{
559
- background: var(--ub-panel); border-radius: 4px; overflow: hidden;
560
  cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;
561
- display: flex; flex-direction: column;
562
  }}
563
  .ex-card:hover {{ transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0,0,0,0.3); }}
 
 
564
 
565
- .ex-card-images {{
566
- display: flex; width: 100%; aspect-ratio: 1;
567
- }}
568
- .ex-card-images img {{
569
- flex: 1; min-width: 0; object-fit: cover;
570
  }}
571
- .ex-card-images img:nth-child(2) {{ border-left: 1px solid #111; }}
572
-
573
- .ex-card p {{ padding: 12px; margin: 0; font-size: 13px; color: var(--ub-muted); line-height: 1.4; }}
574
 
575
  @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
576
 
577
  @media (max-width: 900px) {{
578
- .layout {{ grid-template-columns: 1fr; }}
579
- .panel {{ height: auto; min-height: 500px; }}
580
  }}
581
  </style>
582
  </head>
@@ -596,10 +615,15 @@ async def homepage(request: Request):
596
  <div class="panel-body">
597
  <div class="form-group">
598
  <label class="label">Input Images (Optional)</label>
599
- <div class="upload-zone" id="dropZone">
600
  <input type="file" id="fileInput" multiple accept="image/*" />
601
- <div id="uploadText">Click or Drag & Drop images here</div>
602
- <div class="preview-grid" id="previewGrid" style="display:none;"></div>
 
 
 
 
 
603
  </div>
604
  </div>
605
 
@@ -609,7 +633,7 @@ async def homepage(request: Request):
609
  </div>
610
 
611
  <button class="advanced-toggle" id="advToggle">
612
- <span>Advanced Settings</span> <span class="icon" id="advIcon">+</span>
613
  </button>
614
 
615
  <div class="advanced-body" id="advBody">
@@ -634,24 +658,24 @@ async def homepage(request: Request):
634
  <label class="label">Guidance Scale</label>
635
  <input type="number" id="guidance" class="input" value="1.0" step="0.1">
636
  </div>
637
- <div class="form-group" style="grid-column: span 2;">
638
- <label style="display:flex; align-items:center; gap:8px; font-size:14px;">
639
  <input type="checkbox" id="randomize" checked> Randomize Seed
640
  </label>
641
  </div>
642
  </div>
643
  </div>
644
 
645
- <button class="btn btn-primary" id="runBtn" style="margin-top: 20px;">Run Comparison</button>
646
  </div>
647
  </div>
648
 
649
  <div class="panel">
650
  <div class="panel-header">Comparison View</div>
651
- <div class="panel-body" style="padding:0;">
652
  <div class="slider-stage" id="sliderStage">
653
  <div class="slider-empty" id="sliderEmpty">
654
- <svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="margin-bottom:10px; opacity:0.5; display:inline-block;">
655
  <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
656
  </svg>
657
  <div>Results will appear here</div>
@@ -687,10 +711,11 @@ async def homepage(request: Request):
687
  let filesState = [];
688
 
689
  // UI Elements
690
- const dropZone = document.getElementById('dropZone');
691
  const fileInput = document.getElementById('fileInput');
692
- const previewGrid = document.getElementById('previewGrid');
693
- const uploadText = document.getElementById('uploadText');
 
694
  const promptInput = document.getElementById('promptInput');
695
  const runBtn = document.getElementById('runBtn');
696
 
@@ -712,81 +737,105 @@ async def homepage(request: Request):
712
 
713
  // --- File Upload Logic ---
714
  function renderPreviews() {{
715
- previewGrid.innerHTML = '';
 
716
  if(filesState.length > 0) {{
717
- uploadText.style.display = 'none';
718
- previewGrid.style.display = 'grid';
719
- dropZone.classList.add('has-files');
720
-
721
- // 1. Render actual thumbnails
722
  filesState.forEach((f, i) => {{
723
  const div = document.createElement('div');
724
  div.className = 'thumb';
725
  const img = document.createElement('img');
726
  img.src = URL.createObjectURL(f);
 
727
  const btn = document.createElement('button');
728
  btn.className = 'thumb-remove';
729
- btn.innerText = '×';
730
  btn.onclick = (e) => {{ e.stopPropagation(); filesState.splice(i, 1); renderPreviews(); }};
731
- div.appendChild(img); div.appendChild(btn);
732
- previewGrid.appendChild(div);
 
 
733
  }});
734
-
735
- // 2. Add the "+" box to append more
736
  const addMore = document.createElement('div');
737
- addMore.className = 'thumb add-more-btn';
738
- addMore.innerText = '+';
739
- addMore.onclick = (e) => {{ e.stopPropagation(); fileInput.click(); }};
740
- previewGrid.appendChild(addMore);
741
-
 
742
  }} else {{
743
- uploadText.style.display = 'block';
744
- previewGrid.style.display = 'none';
745
- dropZone.classList.remove('has-files');
746
  }}
747
  }}
748
 
749
- dropZone.onclick = (e) => {{ if(e.target === dropZone || e.target === uploadText) fileInput.click(); }};
750
- fileInput.onchange = (e) => {{ filesState.push(...Array.from(e.target.files)); renderPreviews(); fileInput.value=''; }};
751
- dropZone.ondragover = (e) => {{ e.preventDefault(); dropZone.classList.add('dragover'); }};
752
- dropZone.ondragleave = () => dropZone.classList.remove('dragover');
753
- dropZone.ondrop = (e) => {{
754
- e.preventDefault(); dropZone.classList.remove('dragover');
755
- if(e.dataTransfer.files.length) {{ filesState.push(...Array.from(e.dataTransfer.files)); renderPreviews(); }}
 
 
 
 
 
 
 
 
 
 
 
756
  }};
757
 
758
  // --- Examples Logic ---
759
- async function loadExample(urls, filenames, text) {{
760
  try {{
761
- // Reset state and load all provided files
 
 
762
  filesState = [];
763
  for(let i=0; i<urls.length; i++) {{
764
  const res = await fetch(urls[i]);
 
765
  const blob = await res.blob();
766
- filesState.push(new File([blob], filenames[i], {{type: blob.type}}));
 
767
  }}
 
768
  renderPreviews();
769
- promptInput.value = text;
770
  window.scrollTo({{top: 0, behavior: 'smooth'}});
771
- }} catch (e) {{ alert('Failed to load example image.'); }}
 
 
 
 
 
772
  }}
773
 
774
  const exGrid = document.getElementById('examplesGrid');
775
  examples.forEach(ex => {{
776
  const card = document.createElement('div');
777
  card.className = 'ex-card';
778
-
779
- // Split view logic for multi-image examples
780
- let imgHtml = '';
781
- if (ex.urls.length > 1) {{
782
- const imgs = ex.urls.map(u => `<img src="${{u}}">`).join('');
783
- imgHtml = `<div class="ex-card-images">${{imgs}}</div>`;
784
- }} else {{
785
- imgHtml = `<div class="ex-card-images"><img src="${{ex.urls[0]}}"></div>`;
786
  }}
787
-
788
- card.innerHTML = `${{imgHtml}}<p>${{ex.prompt}}</p>`;
789
- card.onclick = () => loadExample(ex.urls, ex.files, ex.prompt);
 
 
790
  exGrid.appendChild(card);
791
  }});
792
 
@@ -809,6 +858,7 @@ async def homepage(request: Request):
809
  updateSlider(e.clientX);
810
  }});
811
 
 
812
  sliderHandle.addEventListener('touchstart', () => isDragging = true);
813
  window.addEventListener('touchend', () => isDragging = false);
814
  window.addEventListener('touchmove', (e) => {{
@@ -840,8 +890,8 @@ async def homepage(request: Request):
840
  const data = await res.json();
841
 
842
  if(data.success) {{
843
- imgStd.src = data.std_url + "?t=" + Date.now();
844
- imgSmall.src = data.small_url + "?t=" + Date.now();
845
 
846
  imgStd.onload = () => {{
847
  sliderEmpty.style.display = 'none';
@@ -850,6 +900,7 @@ async def homepage(request: Request):
850
  sliderHandle.style.display = 'block';
851
  sliderLabels.style.display = 'flex';
852
 
 
853
  const rect = sliderStage.getBoundingClientRect();
854
  updateSlider(rect.left + rect.width / 2);
855
  }};
 
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
 
176
 
177
  # --- FastAPI Endpoints ---
178
  def get_example_items():
179
+ # Hardcode the multi-image example
180
+ items = [
181
+ {
182
+ "files": ["/example-file/I1.jpg", "/example-file/I2.jpg"],
183
+ "prompt": "Make her wear these glasses in Image 2.",
184
+ "thumb": "/example-file/I1.jpg"
185
+ }
186
+ ]
187
 
 
 
 
 
 
 
 
 
188
  example_prompts = {
189
  "1.jpg": "Change the weather to stormy.",
190
  "2.jpg": "Transform the scene into a snowy winter day while preserving the original subject identity, framing, and composition.",
 
194
 
195
  if EXAMPLES_DIR.exists():
196
  for name in sorted(os.listdir(EXAMPLES_DIR)):
197
+ # Ignore the specific I1/I2 files since they are bundled in the first example manually
198
+ if name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")) and name not in ["I1.jpg", "I2.jpg"]:
199
+ items.append(
200
+ {
201
+ "files": [f"/example-file/{name}"],
202
+ "prompt": example_prompts.get(name, "Edit this image while preserving composition."),
203
+ "thumb": f"/example-file/{name}"
204
+ }
205
+ )
206
  return items
207
 
208
  @app.get("/example-file/{filename}")
 
300
  --ub-muted: #b0b0b0;
301
  --ub-input: #2b2b2b;
302
  --panel-radius: 8px;
 
303
  }}
304
 
305
  * {{ box-sizing: border-box; font-family: 'Ubuntu', sans-serif; }}
 
335
  text-align: center;
336
  margin-bottom: 30px;
337
  }}
338
+ .header-text h1 {{
339
+ margin: 0 0 10px 0;
340
+ font-size: 2.2rem;
341
+ }}
342
+ .header-text p {{
343
+ color: var(--ub-muted);
344
+ margin: 0;
345
+ }}
346
 
347
+ /* Layout enforces identical heights for both panels */
348
  .layout {{
349
  display: grid;
350
  grid-template-columns: 400px 1fr;
351
  gap: 24px;
352
+ align-items: stretch; /* Forces equal height */
353
+ height: 700px; /* Fixed height for the main interactive area */
354
  }}
355
 
356
  .panel {{
 
359
  box-shadow: 0 8px 24px rgba(0,0,0,0.2);
360
  display: flex;
361
  flex-direction: column;
362
+ height: 100%;
363
  overflow: hidden;
364
  }}
365
 
 
375
  .panel-body {{
376
  padding: 20px;
377
  flex: 1;
378
+ overflow-y: auto; /* Allows scrolling inside if settings get too tall */
379
  display: flex;
380
  flex-direction: column;
381
  }}
382
 
 
 
 
 
383
  /* Input Forms */
384
  .form-group {{ margin-bottom: 20px; flex-shrink: 0; }}
385
  .label {{
 
400
  .textarea:focus, .input:focus {{ border-color: var(--ub-orange); }}
401
  .textarea {{ min-height: 100px; resize: vertical; }}
402
 
403
+ /* Upload Container: Handles both empty state and populated state gracefully */
404
+ .upload-container {{
405
  background: var(--ub-input);
406
  border: 1px dashed var(--ub-muted);
407
  border-radius: 4px;
408
+ padding: 15px;
409
+ min-height: 120px;
410
+ display: flex;
411
+ flex-direction: column;
412
+ justify-content: center;
413
  transition: border-color 0.2s, background 0.2s;
414
  }}
415
+ .upload-container.dragover {{
416
  border-color: var(--ub-orange);
417
  background: rgba(233,84,32,0.05);
418
  }}
419
+ .upload-container.has-files {{
420
+ border: 1px solid var(--ub-border);
421
  padding: 10px;
422
+ justify-content: flex-start;
423
+ }}
424
+
425
+ .upload-placeholder {{
426
+ text-align: center; cursor: pointer; color: var(--ub-muted);
427
+ padding: 20px 0;
428
  }}
429
+ .upload-placeholder:hover {{ color: var(--ub-orange); }}
430
+ .upload-container input[type="file"] {{ display: none; }}
431
 
432
+ .image-list {{
433
+ display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px;
 
 
434
  }}
435
+ .image-list::-webkit-scrollbar {{ height: 6px; }}
436
+ .image-list::-webkit-scrollbar-thumb {{ background: var(--ub-panel-light); border-radius: 3px; }}
437
+
438
  .thumb {{
439
+ position: relative; width: 85px; height: 85px; flex-shrink: 0;
440
+ border-radius: 4px; overflow: hidden; border: 1px solid var(--ub-border);
 
 
441
  }}
442
  .thumb img {{ width: 100%; height: 100%; object-fit: cover; display: block; }}
443
  .thumb-remove {{
444
  position: absolute; top: 4px; right: 4px;
445
  background: rgba(0,0,0,0.7); color: white;
446
+ border: none; border-radius: 50%; width: 22px; height: 22px;
447
  display: flex; align-items: center; justify-content: center;
448
+ cursor: pointer; font-size: 14px; line-height: 1;
449
  }}
450
+ .thumb-remove:hover {{ background: rgba(233,84,32,0.9); }}
451
+
452
  .add-more-btn {{
453
+ width: 85px; height: 85px; flex-shrink: 0;
454
+ border: 1px dashed var(--ub-orange); border-radius: 4px;
455
  display: flex; align-items: center; justify-content: center;
456
+ font-size: 32px; color: var(--ub-orange); cursor: pointer;
457
+ background: rgba(233,84,32,0.05); transition: background 0.2s;
 
 
 
 
 
458
  }}
459
+ .add-more-btn:hover {{ background: rgba(233,84,32,0.15); }}
460
 
461
  /* Buttons */
462
  .btn {{
463
  width: 100%; padding: 14px; border: none; border-radius: 4px;
464
  font-size: 16px; font-weight: 700; cursor: pointer;
465
  transition: opacity 0.2s, background 0.2s;
 
466
  }}
467
  .btn-primary {{
468
  background: var(--ub-orange); color: white;
469
  box-shadow: 0 4px 12px rgba(233,84,32,0.3);
470
+ margin-top: auto; /* Pushes button to bottom if space allows */
471
  }}
472
  .btn-primary:hover {{ background: var(--ub-orange-hover); }}
473
  .btn:disabled {{ opacity: 0.6; cursor: not-allowed; }}
 
476
  .advanced-toggle {{
477
  width: 100%; background: none; border: none; color: var(--ub-orange);
478
  text-align: left; padding: 10px 0; font-weight: 500; cursor: pointer;
479
+ display: flex; justify-content: space-between; font-size: 15px;
 
480
  }}
481
+ .advanced-body {{ display: none; padding-top: 10px; margin-bottom: 20px; }}
 
482
  .advanced-body.open {{ display: block; }}
483
  .grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }}
484
 
 
486
  .slider-stage {{
487
  position: relative;
488
  width: 100%;
489
+ flex: 1; /* Fills remaining space in the equal height panel */
 
490
  background: #111;
491
  border-radius: 4px;
492
  overflow: hidden;
 
508
  user-select: none;
509
  -webkit-user-drag: none;
510
  }}
511
+
512
+ /* The Small Decoder image sits on top and gets clipped */
513
+ #imgSmall {{
514
+ clip-path: inset(0 50% 0 0);
515
+ }}
516
 
517
  .slider-handle {{
518
  position: absolute;
 
547
  z-index: 5;
548
  }}
549
  .badge {{
550
+ background: rgba(0,0,0,0.6);
551
+ color: white;
552
+ padding: 6px 12px;
553
+ border-radius: 20px;
554
+ font-size: 13px;
555
+ backdrop-filter: blur(4px);
556
  }}
557
 
558
  .loader {{
559
+ position: absolute; inset: 0;
560
+ background: rgba(0,0,0,0.7);
561
  display: none; flex-direction: column;
562
+ align-items: center; justify-content: center;
563
+ z-index: 20;
564
  }}
565
  .spinner {{
566
+ width: 40px; height: 40px;
567
+ border: 4px solid rgba(255,255,255,0.2);
568
+ border-top-color: var(--ub-orange);
569
+ border-radius: 50%;
570
+ animation: spin 1s linear infinite;
571
+ margin-bottom: 15px;
572
  }}
573
 
574
  /* Examples */
575
  .examples-section {{ margin-top: 40px; }}
576
  .examples-section h3 {{ border-bottom: 1px solid var(--ub-border); padding-bottom: 10px; }}
577
  .examples-grid {{
578
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px;
579
  }}
580
  .ex-card {{
581
+ background: var(--ub-panel); border-radius: 4px; overflow: hidden; position: relative;
582
  cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;
 
583
  }}
584
  .ex-card:hover {{ transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0,0,0,0.3); }}
585
+ .ex-card img {{ width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }}
586
+ .ex-card p {{ padding: 12px; margin: 0; font-size: 13px; color: var(--ub-muted); line-height: 1.4; }}
587
 
588
+ .img-count-badge {{
589
+ position: absolute; top: 8px; right: 8px;
590
+ background: rgba(0,0,0,0.7); color: white;
591
+ font-size: 11px; padding: 3px 8px; border-radius: 12px; font-weight: bold;
 
592
  }}
 
 
 
593
 
594
  @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
595
 
596
  @media (max-width: 900px) {{
597
+ .layout {{ grid-template-columns: 1fr; height: auto; }}
598
+ .slider-stage {{ height: 500px; flex: none; }}
599
  }}
600
  </style>
601
  </head>
 
615
  <div class="panel-body">
616
  <div class="form-group">
617
  <label class="label">Input Images (Optional)</label>
618
+ <div class="upload-container" id="uploadContainer">
619
  <input type="file" id="fileInput" multiple accept="image/*" />
620
+
621
+ <div class="upload-placeholder" id="uploadPlaceholder">
622
+ Click or Drag & Drop images here
623
+ </div>
624
+
625
+ <div class="image-list" id="imageList" style="display: none;">
626
+ </div>
627
  </div>
628
  </div>
629
 
 
633
  </div>
634
 
635
  <button class="advanced-toggle" id="advToggle">
636
+ <span>Advanced Settings</span> <span id="advIcon" style="font-weight:700; font-size:18px; line-height:1;">+</span>
637
  </button>
638
 
639
  <div class="advanced-body" id="advBody">
 
658
  <label class="label">Guidance Scale</label>
659
  <input type="number" id="guidance" class="input" value="1.0" step="0.1">
660
  </div>
661
+ <div class="form-group" style="grid-column: span 2; margin-bottom: 0;">
662
+ <label style="display:flex; align-items:center; gap:8px; font-size:14px; cursor: pointer;">
663
  <input type="checkbox" id="randomize" checked> Randomize Seed
664
  </label>
665
  </div>
666
  </div>
667
  </div>
668
 
669
+ <button class="btn btn-primary" id="runBtn">Run Comparison</button>
670
  </div>
671
  </div>
672
 
673
  <div class="panel">
674
  <div class="panel-header">Comparison View</div>
675
+ <div class="panel-body" style="padding: 0;">
676
  <div class="slider-stage" id="sliderStage">
677
  <div class="slider-empty" id="sliderEmpty">
678
+ <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;">
679
  <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>
680
  </svg>
681
  <div>Results will appear here</div>
 
711
  let filesState = [];
712
 
713
  // UI Elements
714
+ const uploadContainer = document.getElementById('uploadContainer');
715
  const fileInput = document.getElementById('fileInput');
716
+ const uploadPlaceholder = document.getElementById('uploadPlaceholder');
717
+ const imageList = document.getElementById('imageList');
718
+
719
  const promptInput = document.getElementById('promptInput');
720
  const runBtn = document.getElementById('runBtn');
721
 
 
737
 
738
  // --- File Upload Logic ---
739
  function renderPreviews() {{
740
+ imageList.innerHTML = '';
741
+
742
  if(filesState.length > 0) {{
743
+ uploadContainer.classList.add('has-files');
744
+ uploadPlaceholder.style.display = 'none';
745
+ imageList.style.display = 'flex';
746
+
747
+ // Render Thumbnails
748
  filesState.forEach((f, i) => {{
749
  const div = document.createElement('div');
750
  div.className = 'thumb';
751
  const img = document.createElement('img');
752
  img.src = URL.createObjectURL(f);
753
+
754
  const btn = document.createElement('button');
755
  btn.className = 'thumb-remove';
756
+ btn.innerHTML = '&times;';
757
  btn.onclick = (e) => {{ e.stopPropagation(); filesState.splice(i, 1); renderPreviews(); }};
758
+
759
+ div.appendChild(img);
760
+ div.appendChild(btn);
761
+ imageList.appendChild(div);
762
  }});
763
+
764
+ // Add the "+" box to add more images
765
  const addMore = document.createElement('div');
766
+ addMore.className = 'add-more-btn';
767
+ addMore.innerHTML = '+';
768
+ addMore.title = "Add more images";
769
+ addMore.onclick = () => fileInput.click();
770
+ imageList.appendChild(addMore);
771
+
772
  }} else {{
773
+ uploadContainer.classList.remove('has-files');
774
+ uploadPlaceholder.style.display = 'block';
775
+ imageList.style.display = 'none';
776
  }}
777
  }}
778
 
779
+ uploadPlaceholder.onclick = () => fileInput.click();
780
+
781
+ fileInput.onchange = (e) => {{
782
+ if(e.target.files.length) {{
783
+ filesState.push(...Array.from(e.target.files));
784
+ renderPreviews();
785
+ }}
786
+ fileInput.value = '';
787
+ }};
788
+
789
+ uploadContainer.ondragover = (e) => {{ e.preventDefault(); uploadContainer.classList.add('dragover'); }};
790
+ uploadContainer.ondragleave = () => uploadContainer.classList.remove('dragover');
791
+ uploadContainer.ondrop = (e) => {{
792
+ e.preventDefault(); uploadContainer.classList.remove('dragover');
793
+ if(e.dataTransfer.files.length) {{
794
+ filesState.push(...Array.from(e.dataTransfer.files));
795
+ renderPreviews();
796
+ }}
797
  }};
798
 
799
  // --- Examples Logic ---
800
+ async function loadExample(urls, prompt) {{
801
  try {{
802
+ loader.style.display = 'flex';
803
+ loader.querySelector('div:nth-child(2)').innerText = "Loading Example Images...";
804
+
805
  filesState = [];
806
  for(let i=0; i<urls.length; i++) {{
807
  const res = await fetch(urls[i]);
808
+ if (!res.ok) throw new Error("File not found");
809
  const blob = await res.blob();
810
+ const ext = urls[i].split('.').pop() || 'jpg';
811
+ filesState.push(new File([blob], `example_image_${{i}}.${{ext}}`, {{type: blob.type}}));
812
  }}
813
+
814
  renderPreviews();
815
+ promptInput.value = prompt;
816
  window.scrollTo({{top: 0, behavior: 'smooth'}});
817
+ }} catch (e) {{
818
+ alert('Failed to load example images. They might be missing from the server.');
819
+ }} finally {{
820
+ loader.style.display = 'none';
821
+ loader.querySelector('div:nth-child(2)').innerText = "Running both models...";
822
+ }}
823
  }}
824
 
825
  const exGrid = document.getElementById('examplesGrid');
826
  examples.forEach(ex => {{
827
  const card = document.createElement('div');
828
  card.className = 'ex-card';
829
+
830
+ let badgeHtml = '';
831
+ if (ex.files.length > 1) {{
832
+ badgeHtml = `<div class="img-count-badge">${{ex.files.length}} Images</div>`;
 
 
 
 
833
  }}
834
+
835
+ card.innerHTML = `${{badgeHtml}}<img src="${{ex.thumb}}"><p>${{ex.prompt}}</p>`;
836
+
837
+ // Load all files in the array when clicked
838
+ card.onclick = () => loadExample(ex.files, ex.prompt);
839
  exGrid.appendChild(card);
840
  }});
841
 
 
858
  updateSlider(e.clientX);
859
  }});
860
 
861
+ // Touch support for slider
862
  sliderHandle.addEventListener('touchstart', () => isDragging = true);
863
  window.addEventListener('touchend', () => isDragging = false);
864
  window.addEventListener('touchmove', (e) => {{
 
890
  const data = await res.json();
891
 
892
  if(data.success) {{
893
+ imgStd.src = data.std_url;
894
+ imgSmall.src = data.small_url;
895
 
896
  imgStd.onload = () => {{
897
  sliderEmpty.style.display = 'none';
 
900
  sliderHandle.style.display = 'block';
901
  sliderLabels.style.display = 'flex';
902
 
903
+ // Reset slider to center
904
  const rect = sliderStage.getBoundingClientRect();
905
  updateSlider(rect.left + rect.width / 2);
906
  }};