Spaces:
Running on Zero
Running on Zero
update app
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 197 |
-
if name in ["I1.jpg", "I2.jpg"]:
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 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 {{
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
-
/* Layout
|
| 341 |
.layout {{
|
| 342 |
display: grid;
|
| 343 |
grid-template-columns: 400px 1fr;
|
| 344 |
gap: 24px;
|
| 345 |
-
align-items: stretch; /*
|
|
|
|
| 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:
|
| 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
|
| 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
|
| 400 |
-
.upload-
|
| 401 |
background: var(--ub-input);
|
| 402 |
border: 1px dashed var(--ub-muted);
|
| 403 |
border-radius: 4px;
|
| 404 |
-
padding:
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
| 407 |
transition: border-color 0.2s, background 0.2s;
|
| 408 |
}}
|
| 409 |
-
.upload-
|
| 410 |
border-color: var(--ub-orange);
|
| 411 |
background: rgba(233,84,32,0.05);
|
| 412 |
}}
|
| 413 |
-
.upload-
|
|
|
|
| 414 |
padding: 10px;
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
}}
|
| 417 |
-
.upload-
|
|
|
|
| 418 |
|
| 419 |
-
.
|
| 420 |
-
display:
|
| 421 |
-
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
|
| 422 |
-
gap: 10px;
|
| 423 |
}}
|
|
|
|
|
|
|
|
|
|
| 424 |
.thumb {{
|
| 425 |
-
position: relative;
|
| 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:
|
| 435 |
display: flex; align-items: center; justify-content: center;
|
| 436 |
-
cursor: pointer; font-size:
|
| 437 |
}}
|
|
|
|
|
|
|
| 438 |
.add-more-btn {{
|
|
|
|
|
|
|
| 439 |
display: flex; align-items: center; justify-content: center;
|
| 440 |
-
font-size:
|
| 441 |
-
|
| 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;
|
| 468 |
-
flex-shrink: 0;
|
| 469 |
}}
|
| 470 |
-
.advanced-
|
| 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; /*
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| 538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
}}
|
| 540 |
|
| 541 |
.loader {{
|
| 542 |
-
position: absolute; inset: 0;
|
|
|
|
| 543 |
display: none; flex-direction: column;
|
| 544 |
-
align-items: center; justify-content: center;
|
|
|
|
| 545 |
}}
|
| 546 |
.spinner {{
|
| 547 |
-
width: 40px; height: 40px;
|
| 548 |
-
border
|
| 549 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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 |
-
.
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 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 |
-
.
|
| 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-
|
| 600 |
<input type="file" id="fileInput" multiple accept="image/*" />
|
| 601 |
-
|
| 602 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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"
|
| 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;
|
| 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
|
| 691 |
const fileInput = document.getElementById('fileInput');
|
| 692 |
-
const
|
| 693 |
-
const
|
|
|
|
| 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 |
-
|
|
|
|
| 716 |
if(filesState.length > 0) {{
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
//
|
| 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.
|
| 730 |
btn.onclick = (e) => {{ e.stopPropagation(); filesState.splice(i, 1); renderPreviews(); }};
|
| 731 |
-
|
| 732 |
-
|
|
|
|
|
|
|
| 733 |
}});
|
| 734 |
-
|
| 735 |
-
//
|
| 736 |
const addMore = document.createElement('div');
|
| 737 |
-
addMore.className = '
|
| 738 |
-
addMore.
|
| 739 |
-
addMore.
|
| 740 |
-
|
| 741 |
-
|
|
|
|
| 742 |
}} else {{
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
}}
|
| 747 |
}}
|
| 748 |
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
}};
|
| 757 |
|
| 758 |
// --- Examples Logic ---
|
| 759 |
-
async function loadExample(urls,
|
| 760 |
try {{
|
| 761 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 767 |
}}
|
|
|
|
| 768 |
renderPreviews();
|
| 769 |
-
promptInput.value =
|
| 770 |
window.scrollTo({{top: 0, behavior: 'smooth'}});
|
| 771 |
-
}} catch (e) {{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 780 |
-
|
| 781 |
-
|
| 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 = `${{
|
| 789 |
-
|
|
|
|
|
|
|
| 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
|
| 844 |
-
imgSmall.src = data.small_url
|
| 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 = '×';
|
| 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 |
}};
|