Spaces:
Running on Zero
Running on Zero
Download studio.html from briaai/Fibo-2.0-editing: direct link, hf CLI and curl.
- Browser
- Download file 35.3 kB
-
https://huggingface.co/spaces/briaai/Fibo-2.0-editing/resolve/main/studio.html
- Command line
-
hf download hf://spaces/briaai/Fibo-2.0-editing/studio.html
-
curl -L -o studio.html https://huggingface.co/spaces/briaai/Fibo-2.0-editing/resolve/main/studio.html
35.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>FIBO 2.0 Edit Turbo Studio</title> | |
| <style> | |
| :root { | |
| --bg: #0e0e10; --panel: #16171a; --panel2: #101114; --raised: #1e2024; | |
| --border: rgba(255,255,255,0.07); | |
| --text: rgba(255,255,255,0.87); --muted: rgba(255,255,255,0.5); --dim: rgba(255,255,255,0.35); | |
| --accent: #7c5cff; --accent2: #8a3dff; --accent-soft: rgba(124,92,255,0.14); | |
| --radius: 10px; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| [hidden] { display: none ; } | |
| body { | |
| font-family: "Inter", ui-sans-serif, system-ui, sans-serif; | |
| background: var(--bg); color: var(--text); height: 100vh; | |
| display: flex; flex-direction: column; overflow: hidden; | |
| font-size: 13px; -webkit-font-smoothing: antialiased; | |
| } | |
| .topbar { | |
| display: flex; align-items: center; gap: 16px; padding: 8px 16px; | |
| background: var(--panel); border-bottom: 1px solid var(--border); flex-shrink: 0; | |
| } | |
| .brand { font-weight: 700; font-size: 0.92rem; white-space: nowrap; letter-spacing: -0.01em; } | |
| .mode-tabs { display: flex; gap: 2px; background: var(--panel2); border: 1px solid var(--border); border-radius: 9px; padding: 3px; } | |
| .mode-tabs button { | |
| border: none; border-radius: 7px; padding: 6px 16px; font-weight: 550; font-size: 0.82rem; | |
| background: transparent; color: var(--muted); cursor: pointer; transition: color 0.15s, background 0.15s; | |
| } | |
| .mode-tabs button:hover { color: var(--text); } | |
| .mode-tabs button.active { background: var(--raised); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.45); } | |
| .spacer { flex: 1; } | |
| .ml-badge { color: var(--muted); text-decoration: none; font-size: 0.8rem; font-weight: 500; transition: color 0.15s; white-space: nowrap; } | |
| .ml-badge:hover { color: var(--text); } | |
| .ml-cta { | |
| color: var(--accent); text-decoration: none; font-size: 0.8rem; font-weight: 600; | |
| padding: 5px 13px; border: 1px solid var(--accent); border-radius: 999px; | |
| background: var(--accent-soft); transition: filter 0.15s; white-space: nowrap; | |
| } | |
| .ml-cta:hover { filter: brightness(1.2); } | |
| .main { display: flex; flex: 1; min-height: 0; } | |
| .rail { | |
| width: 52px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; | |
| gap: 6px; padding: 10px 0; background: var(--panel); border-right: 1px solid var(--border); | |
| } | |
| .rail button { | |
| width: 38px; height: 38px; border: 1px solid transparent; border-radius: 8px; | |
| background: transparent; color: var(--muted); cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: color 0.15s, background 0.15s; | |
| } | |
| .rail button:hover { background: var(--panel2); color: var(--text); } | |
| .rail button.active { color: var(--accent); background: var(--accent-soft); } | |
| .viewport { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: auto; position: relative; } | |
| .workflow-view { position: absolute; inset: 0; padding: 12px; } | |
| .workflow-view iframe { width: 100%; height: 100%; border: none; border-radius: 10px; background: #0c0d10; } | |
| .doc-grid { flex: 1; min-height: 0; display: flex; gap: 16px; width: 100%; align-items: stretch; justify-content: center; padding: 20px 20px 10px; } | |
| .doc { | |
| flex: 1; min-width: 0; max-width: 640px; display: flex; flex-direction: column; | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; | |
| } | |
| .doc figcaption { | |
| padding: 8px 12px; font-size: 0.62rem; font-weight: 650; letter-spacing: 0.12em; | |
| color: var(--dim); border-bottom: 1px solid var(--border); text-transform: uppercase; | |
| display: flex; align-items: center; | |
| } | |
| .doc .stage { | |
| position: relative; flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; | |
| overflow: hidden; background-color: #0b0b0d; | |
| background-image: | |
| linear-gradient(45deg, #141419 25%, transparent 25%), | |
| linear-gradient(-45deg, #141419 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #141419 75%), | |
| linear-gradient(-45deg, transparent 75%, #141419 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0; | |
| } | |
| .doc img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; } | |
| .doc img.zoomed { max-width: none; max-height: none; } | |
| .doc .stage.scrollable { overflow: auto; justify-content: flex-start; align-items: flex-start; } | |
| #mask-canvas { position: absolute; inset: 0; cursor: crosshair; display: none; } | |
| #mask-canvas.active { display: block; } | |
| .drop-hint { color: var(--dim); font-size: 0.85rem; text-align: center; padding: 20px; } | |
| .drop-hint b { color: var(--muted); display: block; margin-bottom: 6px; } | |
| .zoom-btn { | |
| margin-left: auto; border: 1px solid var(--border); background: transparent; color: var(--muted); | |
| border-radius: 5px; font-size: 0.62rem; padding: 2px 8px; cursor: pointer; letter-spacing: 0.08em; | |
| } | |
| .zoom-btn:hover { color: var(--text); border-color: var(--accent); } | |
| .doc-bar { | |
| display: flex; align-items: center; gap: 10px; padding: 6px 12px; | |
| border-top: 1px solid var(--border); background: var(--panel); | |
| font-size: 0.72rem; color: var(--muted); flex-shrink: 0; | |
| } | |
| .doc-bar button { | |
| border: 1px solid var(--border); background: transparent; color: var(--muted); | |
| border-radius: 5px; padding: 3px 10px; font-size: 0.68rem; cursor: pointer; letter-spacing: 0.06em; | |
| } | |
| .doc-bar button:hover, .doc-bar button.active { color: var(--text); border-color: var(--accent); } | |
| .doc-bar button.active { background: var(--accent-soft); color: var(--accent); } | |
| .split-wrap { position: absolute; inset: 0; overflow: hidden; } | |
| .split-wrap img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; } | |
| .split-divider { | |
| position: absolute; top: 0; bottom: 0; width: 3px; background: var(--accent); | |
| cursor: ew-resize; z-index: 5; box-shadow: 0 0 8px rgba(124,92,255,0.6); | |
| } | |
| .split-divider::after { | |
| content: "◂ ▸"; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); | |
| background: var(--accent); color: white; border-radius: 999px; padding: 4px 7px; | |
| font-size: 0.6rem; white-space: nowrap; | |
| } | |
| .history-strip { | |
| display: flex; gap: 8px; padding: 8px 12px; overflow-x: auto; flex-shrink: 0; | |
| background: var(--panel); border-top: 1px solid var(--border); align-items: center; | |
| } | |
| .history-strip .hlabel { font-size: 0.62rem; font-weight: 650; letter-spacing: 0.14em; color: var(--dim); text-transform: uppercase; white-space: nowrap; } | |
| .history-chip { | |
| width: 52px; height: 52px; border-radius: 6px; border: 1px solid var(--border); | |
| overflow: hidden; cursor: pointer; flex-shrink: 0; position: relative; | |
| } | |
| .history-chip:hover { border-color: var(--accent); } | |
| .history-chip img { width: 100%; height: 100%; object-fit: cover; } | |
| .history-chip .hseed { | |
| position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.65); | |
| color: #ddd; font-size: 0.52rem; padding: 1px 3px; text-align: center; | |
| } | |
| .panels { | |
| width: 320px; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px; | |
| padding: 12px; overflow-y: auto; background: var(--panel); border-left: 1px solid var(--border); | |
| } | |
| .panel { background: var(--panel2); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; } | |
| .panel-title { font-size: 0.62rem; font-weight: 650; letter-spacing: 0.12em; color: var(--dim); text-transform: uppercase; margin-bottom: 10px; cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: space-between; } | |
| .panel .panel-title::after { content: "▾"; font-size: 0.7rem; color: var(--dim); } | |
| .panel.collapsed .panel-title::after { content: "▸"; } | |
| .panel.collapsed .panel-body { display: none; } | |
| .panel label { display: block; font-size: 0.72rem; font-weight: 550; color: var(--muted); margin: 10px 0 4px; } | |
| .panel label:first-of-type { margin-top: 0; } | |
| textarea, input[type="text"], input[type="number"] { | |
| width: 100%; background: var(--raised); color: var(--text); border: 1px solid var(--border); | |
| border-radius: 7px; padding: 8px 10px; font-family: inherit; font-size: 0.83rem; resize: vertical; | |
| transition: border-color 0.15s, box-shadow 0.15s; | |
| } | |
| textarea:focus, input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } | |
| textarea.code { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.75rem; } | |
| .row { display: flex; gap: 8px; align-items: center; } | |
| .row > * { flex: 1; } | |
| input[type="range"] { | |
| -webkit-appearance: none; appearance: none; width: 100%; height: 3px; | |
| border-radius: 2px; background: var(--raised); outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%; | |
| background: #fff; border: none; box-shadow: 0 1px 4px rgba(0,0,0,0.55); cursor: pointer; | |
| transition: transform 0.12s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 14px; height: 14px; border-radius: 50%; background: #fff; border: none; | |
| box-shadow: 0 1px 4px rgba(0,0,0,0.55); cursor: pointer; | |
| } | |
| .switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; } | |
| .switch input { display: none; } | |
| .switch .sw { | |
| display: inline-block; | |
| width: 34px; height: 20px; border-radius: 999px; background: var(--raised); | |
| border: 1px solid var(--border); position: relative; flex-shrink: 0; transition: background 0.18s, border-color 0.18s; | |
| } | |
| .switch .sw::after { | |
| content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; | |
| border-radius: 50%; background: var(--muted); transition: transform 0.18s, background 0.18s; | |
| } | |
| .switch input:checked + .sw { background: var(--accent); border-color: var(--accent); } | |
| .switch input:checked + .sw::after { transform: translateX(14px); background: #fff; } | |
| .switch .sw-label { font-size: 0.75rem; color: var(--muted); } | |
| .layer-row { | |
| display: flex; align-items: center; gap: 10px; padding: 6px 8px; | |
| border: 1px solid var(--border); border-radius: 6px; margin-top: 6px; | |
| background: var(--raised); cursor: pointer; | |
| } | |
| .layer-row:hover { border-color: var(--accent); } | |
| .layer-row .thumb { | |
| width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0; | |
| background: #101014; border: 1px solid var(--border); | |
| display: flex; align-items: center; justify-content: center; | |
| color: var(--dim); font-size: 0.65rem; text-align: center; | |
| } | |
| .layer-row .thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 3px; } | |
| .layer-row .lname { flex: 1; font-size: 0.78rem; color: var(--text); } | |
| .layer-row .lsub { font-size: 0.62rem; color: var(--dim); display: block; } | |
| .layer-row .ldel { | |
| border: none; background: transparent; color: var(--dim); cursor: pointer; | |
| font-size: 0.9rem; padding: 2px 6px; border-radius: 4px; | |
| } | |
| .layer-row .ldel:hover { color: #ff6f91; background: rgba(255,111,145,0.1); } | |
| .layer-row.mask-layer .thumb { border-color: var(--accent); } | |
| .ref-slot { | |
| height: 72px; border: 1px dashed var(--border); border-radius: 6px; margin-top: 8px; | |
| display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden; | |
| color: var(--dim); font-size: 0.72rem; background: var(--raised); | |
| } | |
| .ref-slot:hover { border-color: var(--accent); } | |
| .ref-slot img { width: 100%; height: 100%; object-fit: cover; } | |
| .hint { font-size: 0.68rem; color: var(--dim); margin-top: 8px; line-height: 1.4; } | |
| .ex-row { display: flex; gap: 8px; overflow-x: auto; } | |
| .ex-chip { | |
| flex: 1; min-width: 76px; border: 1px solid var(--border); border-radius: 8px; | |
| overflow: hidden; cursor: pointer; background: var(--raised); padding: 0; | |
| text-align: left; transition: border-color 0.15s; | |
| } | |
| .ex-chip:hover { border-color: var(--accent); } | |
| .ex-chip img { width: 100%; height: 64px; object-fit: cover; display: block; } | |
| .ex-chip span { display: block; padding: 5px 7px; font-size: 0.62rem; color: var(--muted); line-height: 1.25; } | |
| #run-btn { | |
| margin-top: 4px; padding: 12px; border: none; border-radius: 8px; cursor: pointer; | |
| background: var(--accent); color: white; | |
| font-weight: 600; font-size: 0.9rem; letter-spacing: 0.01em; | |
| transition: filter 0.15s, transform 0.05s; | |
| } | |
| #run-btn:hover { filter: brightness(1.12); } | |
| #run-btn:active { transform: translateY(1px); } | |
| #run-btn:disabled { opacity: 0.5; cursor: wait; } | |
| .statusbar { | |
| display: flex; align-items: center; gap: 16px; padding: 7px 16px; flex-shrink: 0; | |
| background: var(--panel); border-top: 1px solid var(--border); | |
| font-size: 0.75rem; color: var(--muted); | |
| } | |
| .statusbar .pulse { width: 8px; height: 8px; border-radius: 50%; background: #4fd1a5; } | |
| .statusbar.busy .pulse { background: var(--accent); animation: pulse 1s infinite alternate; } | |
| @keyframes pulse { from { opacity: 0.4; } to { opacity: 1; } } | |
| #file-input { display: none; } | |
| @media (max-width: 860px) { | |
| html, body { height: auto ; overflow: visible ; } | |
| body { display: block; } | |
| .topbar { flex-wrap: wrap; gap: 8px; padding: 8px 12px; } | |
| .brand { font-size: 0.88rem; } | |
| .topbar .ml-badge { display: none; } | |
| .main { display: block; height: auto; min-height: 0; } | |
| .rail { | |
| flex-direction: row; width: 100%; justify-content: center; gap: 10px; | |
| padding: 6px 10px; border-right: none; border-bottom: 1px solid var(--border); | |
| } | |
| .rail button { width: 44px; height: 44px; } | |
| .viewport { display: block; padding: 10px; overflow: visible; } | |
| .workflow-view { position: static; height: 78vh; padding: 0 0 10px; } | |
| .doc-grid { flex-direction: column; align-items: stretch; height: auto; } | |
| .doc { max-width: 100%; width: 100%; } | |
| .doc .stage { min-height: 220px; } | |
| .panels { | |
| width: 100%; border-left: none; border-top: 1px solid var(--border); | |
| padding: 10px 12px; overflow-y: visible; | |
| } | |
| .mode-tabs button { padding: 6px 13px; font-size: 0.8rem; } | |
| .statusbar { position: sticky; bottom: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="topbar"> | |
| <div class="brand"><img src="/bria_logo.png" alt="BRIA" style="height:22px;vertical-align:middle;margin-right:8px" />FIBO 2.0 Edit Turbo</div> | |
| <div class="mode-tabs"> | |
| <button id="mode-edit" class="active">Edit</button> | |
| <button id="mode-workflow">Workflow</button> | |
| </div> | |
| <div class="spacer"></div> | |
| <a class="ml-badge" href="https://huggingface.co/briaai/Fibo-2.0-edit-turbo" target="_blank" rel="noopener">Model</a> | |
| <a class="ml-cta" href="https://arxiv.org/abs/2511.06876" target="_blank" rel="noopener">arXiv</a> | |
| </header> | |
| <div class="main"> | |
| <div class="rail"> | |
| <button id="tool-upload" title="Upload source image"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></button> | |
| <button id="tool-brush" title="Brush mask (single reference)"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9.06 11.9l8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.07"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg></button> | |
| <button id="tool-clear-mask" title="Clear mask"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20H8L3 15a2 2 0 0 1 0-2.83l7.17-7.17a2 2 0 0 1 2.83 0l5.66 5.66a2 2 0 0 1 0 2.83L13 19"/><line x1="18" y1="11" x2="9" y2="20"/></svg></button> | |
| <button id="tool-clear-doc" title="Clear document"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button> | |
| </div> | |
| <div class="viewport"> | |
| <div class="workflow-view" id="workflow-view" hidden> | |
| <iframe src="/workflow/?ui=canvas" title="Workflow canvas"></iframe> | |
| </div> | |
| <div class="doc-grid"> | |
| <figure class="doc" id="doc-before"> | |
| <figcaption id="before-caption">Source</figcaption> | |
| <div class="stage" id="before-stage"> | |
| <img id="source-img" alt="" hidden /> | |
| <canvas id="mask-canvas"></canvas> | |
| <div class="drop-hint" id="source-hint"><b>No source image</b>Use the upload tool or drop a file here</div> | |
| </div> | |
| </figure> | |
| <figure class="doc" id="doc-after"> | |
| <figcaption id="after-caption">Result</figcaption> | |
| <div class="stage" id="after-stage"> | |
| <img id="result-img" alt="" hidden /> | |
| <div class="split-wrap" id="split-wrap" hidden> | |
| <img id="split-source" alt="" style="clip-path: inset(0 50% 0 0)" /> | |
| <div class="split-divider" id="split-divider" style="left:50%"></div> | |
| </div> | |
| <div class="drop-hint" id="result-hint"><b>Nothing yet</b>Run to see the result</div> | |
| </div> | |
| <div class="doc-bar"> | |
| <span id="result-meta">—</span> | |
| <div class="spacer"></div> | |
| <button id="split-toggle" hidden>SPLIT</button> | |
| </div> | |
| </figure> | |
| </div> | |
| <div class="history-strip" id="history-strip"><span class="hlabel">History</span></div> | |
| </div> | |
| <aside class="panels"> | |
| <section class="panel"> | |
| <div class="panel-title">Examples</div> | |
| <div class="ex-row"> | |
| <button class="ex-chip" data-img="/example_images/edit_cat.jpg" data-instruction="Put a small party hat on the cat"> | |
| <img src="/example_images/edit_cat.jpg" alt="" /><span>Party hat on the cat</span> | |
| </button> | |
| <button class="ex-chip" data-img="/example_images/edit_bicycle.jpg" data-instruction="Change the bicycle color to bright red"> | |
| <img src="/example_images/edit_bicycle.jpg" alt="" /><span>Red bicycle</span> | |
| </button> | |
| <button class="ex-chip" data-img="/example_images/edit_portrait.jpg" data-instruction="Add round glasses"> | |
| <img src="/example_images/edit_portrait.jpg" alt="" /><span>Round glasses</span> | |
| </button> | |
| </div> | |
| </section> | |
| <section class="panel"> | |
| <div class="panel-title">Instruction</div> | |
| <label for="instruction">What should change?</label> | |
| <textarea id="instruction" rows="3" placeholder="make the sky dramatic and stormy"></textarea> | |
| <label for="json-instruction">JSON override (optional; auto-filled by the VGL prompt writer)</label> | |
| <textarea id="json-instruction" class="code" rows="5" placeholder='{"edit_instruction": "…"}'></textarea> | |
| </section> | |
| <section class="panel"> | |
| <div class="panel-title">Layers</div> | |
| <div class="layer-row" id="layer-source"> | |
| <div class="thumb" id="layer-source-thumb">BG</div> | |
| <div class="lname">Background<span class="lsub">source image</span></div> | |
| </div> | |
| <div class="layer-row mask-layer" id="layer-mask" hidden> | |
| <div class="thumb" id="layer-mask-thumb"></div> | |
| <div class="lname">Mask<span class="lsub">repaint region</span></div> | |
| <button class="ldel" id="layer-mask-del" title="Clear mask">✕</button> | |
| </div> | |
| <div class="ref-slot" data-ref="2">+ Reference 2</div> | |
| <div class="ref-slot" data-ref="3">+ Reference 3</div> | |
| <div class="ref-slot" data-ref="4">+ Reference 4</div> | |
| <div class="hint">Slots map to <image_1>…<image_4> in the instruction. A mask only works with a single reference.</div> | |
| </section> | |
| <section class="panel"> | |
| <div class="panel-title">Advanced</div> | |
| <div class="row"> | |
| <div><label for="seed">Seed</label><input type="number" id="seed" value="42" min="1" /></div> | |
| <label class="switch"><input type="checkbox" id="randomize" checked /><span class="sw"></span><span class="sw-label">Randomize</span></label> | |
| </div> | |
| <div class="row" style="margin-top:10px"> | |
| <div><label>Steps <span id="steps-val">4</span></label><input type="range" id="steps" min="1" max="8" value="4" /></div> | |
| </div> | |
| <div class="hint">Edit Turbo is distilled for 4 guidance-free steps.</div> | |
| </section> | |
| <button id="run-btn">Generate Edit</button> | |
| </aside> | |
| </div> | |
| <footer class="statusbar" id="statusbar"> | |
| <div class="pulse"></div> | |
| <span id="status">Idle</span> | |
| <div class="spacer"></div> | |
| <span id="seed-out"></span> | |
| </footer> | |
| <input type="file" id="file-input" accept="image/*" /> | |
| <script type="module"> | |
| import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client@2.7.1/dist/index.min.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const state = { mode: "edit", sourcePath: null, refPaths: { 2: null, 3: null, 4: null }, app: null }; | |
| const docState = { source: null, result: null, history: [] }; | |
| async function getApp() { | |
| if (!state.app) state.app = await client(location.origin); | |
| return state.app; | |
| } | |
| async function uploadFile(file) { | |
| const form = new FormData(); | |
| form.append("files", file); | |
| const resp = await fetch("/gradio_api/upload", { method: "POST", body: form }); | |
| if (!resp.ok) throw new Error(`Upload failed (${resp.status})`); | |
| const paths = await resp.json(); | |
| return Array.isArray(paths) ? paths[0] : paths; | |
| } | |
| function setStatus(text, busy = false) { | |
| $("status").textContent = text; | |
| $("statusbar").classList.toggle("busy", busy); | |
| $("run-btn").disabled = busy; | |
| } | |
| function setMode(mode) { | |
| state.mode = mode; | |
| $("mode-edit").classList.toggle("active", mode === "edit"); | |
| $("mode-workflow").classList.toggle("active", mode === "workflow"); | |
| const workflow = mode === "workflow"; | |
| $("workflow-view").hidden = !workflow; | |
| document.querySelector(".doc-grid").style.display = workflow ? "none" : ""; | |
| document.querySelector(".rail").style.display = workflow ? "none" : ""; | |
| document.querySelector(".panels").style.display = workflow ? "none" : ""; | |
| document.getElementById("history-strip").style.display = workflow ? "none" : ""; | |
| } | |
| $("mode-edit").onclick = () => setMode("edit"); | |
| $("mode-workflow").onclick = () => setMode("workflow"); | |
| // ---------- uploads ---------- | |
| let uploadTarget = "source"; | |
| function pickFile(target) { | |
| uploadTarget = target; | |
| $("file-input").click(); | |
| } | |
| $("tool-upload").onclick = () => pickFile("source"); | |
| document.querySelectorAll(".ref-slot").forEach((slot) => { | |
| slot.onclick = () => pickFile(slot.dataset.ref); | |
| }); | |
| $("file-input").onchange = async (e) => { | |
| const file = e.target.files[0]; | |
| e.target.value = ""; | |
| if (!file) return; | |
| setStatus("Uploading…", true); | |
| try { | |
| const path = await uploadFile(file); | |
| if (uploadTarget === "source") { | |
| state.sourcePath = path; | |
| showSource(file); | |
| } else { | |
| state.refPaths[uploadTarget] = path; | |
| const slot = document.querySelector(`.ref-slot[data-ref="${uploadTarget}"]`); | |
| slot.innerHTML = ""; | |
| const img = document.createElement("img"); | |
| img.src = URL.createObjectURL(file); | |
| slot.appendChild(img); | |
| } | |
| setStatus("Idle"); | |
| } catch (err) { | |
| setStatus(String(err)); | |
| } | |
| }; | |
| $("before-stage").addEventListener("dragover", (e) => e.preventDefault()); | |
| $("before-stage").addEventListener("drop", async (e) => { | |
| e.preventDefault(); | |
| const file = e.dataTransfer.files[0]; | |
| if (!file) return; | |
| setStatus("Uploading…", true); | |
| try { | |
| state.sourcePath = await uploadFile(file); | |
| showSource(file); | |
| setStatus("Idle"); | |
| } catch (err) { | |
| setStatus(String(err)); | |
| } | |
| }); | |
| function showSource(file) { | |
| const img = $("source-img"); | |
| img.src = URL.createObjectURL(file); | |
| img.hidden = false; | |
| $("source-hint").style.display = "none"; | |
| img.onload = () => sizeMaskCanvas(); | |
| clearMask(); | |
| $("json-instruction").value = ""; | |
| docState.source = img.src; | |
| const st = $("layer-source-thumb"); | |
| st.innerHTML = ""; | |
| const stImg = document.createElement("img"); | |
| stImg.src = img.src; | |
| st.appendChild(stImg); | |
| } | |
| // ---------- mask brush ---------- | |
| const maskCanvas = $("mask-canvas"); | |
| const mctx = maskCanvas.getContext("2d"); | |
| let strokes = []; | |
| let drawing = false; | |
| function sizeMaskCanvas() { | |
| const stage = $("before-stage"); | |
| const img = $("source-img"); | |
| if (!img.src || img.hidden) return; | |
| const box = img.getBoundingClientRect(); | |
| const stageBox = stage.getBoundingClientRect(); | |
| maskCanvas.width = box.width; | |
| maskCanvas.height = box.height; | |
| maskCanvas.style.left = box.left - stageBox.left + "px"; | |
| maskCanvas.style.top = box.top - stageBox.top + "px"; | |
| maskCanvas.style.width = box.width + "px"; | |
| maskCanvas.style.height = box.height + "px"; | |
| mctx.fillStyle = "#000"; | |
| mctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); | |
| } | |
| function brushPos(e) { | |
| const box = maskCanvas.getBoundingClientRect(); | |
| return { x: e.clientX - box.left, y: e.clientY - box.top }; | |
| } | |
| maskCanvas.addEventListener("pointerdown", (e) => { drawing = true; strokes.push([brushPos(e)]); }); | |
| maskCanvas.addEventListener("pointermove", (e) => { | |
| if (!drawing) return; | |
| strokes[strokes.length - 1].push(brushPos(e)); | |
| redrawMask(); | |
| }); | |
| window.addEventListener("pointerup", () => { | |
| drawing = false; | |
| if (state.brushOn && strokes.length) { | |
| $("layer-mask").hidden = false; | |
| const mt = $("layer-mask-thumb"); | |
| mt.innerHTML = ""; | |
| const t = document.createElement("img"); | |
| t.src = maskCanvas.toDataURL(); | |
| mt.appendChild(t); | |
| } | |
| }); | |
| function redrawMask() { | |
| mctx.fillStyle = "#000"; | |
| mctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); | |
| mctx.strokeStyle = "#fff"; | |
| mctx.lineWidth = Math.max(16, maskCanvas.width / 40); | |
| mctx.lineCap = "round"; | |
| mctx.lineJoin = "round"; | |
| for (const stroke of strokes) { | |
| mctx.beginPath(); | |
| stroke.forEach((p, i) => (i ? mctx.lineTo(p.x, p.y) : mctx.moveTo(p.x, p.y))); | |
| mctx.stroke(); | |
| } | |
| mctx.save(); | |
| mctx.globalCompositeOperation = "source-atop"; | |
| mctx.fillStyle = "rgba(124,92,255,0.35)"; | |
| mctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); | |
| mctx.restore(); | |
| } | |
| function clearMask() { | |
| strokes = []; | |
| maskCanvas.classList.remove("active"); | |
| $("tool-brush").classList.remove("active"); | |
| mctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); | |
| $("layer-mask").hidden = true; | |
| } | |
| state.brushOn = false; | |
| $("tool-brush").onclick = () => { | |
| if (state.sourcePath == null) { setStatus("Upload a source image first."); return; } | |
| state.brushOn = !state.brushOn; | |
| maskCanvas.classList.toggle("active", state.brushOn); | |
| $("tool-brush").classList.toggle("active", state.brushOn); | |
| if (state.brushOn) { sizeMaskCanvas(); redrawMask(); setStatus("Brush the area to repaint."); } | |
| else setStatus("Idle"); | |
| }; | |
| $("tool-clear-mask").onclick = clearMask; | |
| $("layer-mask-del").onclick = (e) => { e.stopPropagation(); clearMask(); }; | |
| $("tool-clear-doc").onclick = () => { | |
| state.sourcePath = null; | |
| docState.source = null; | |
| docState.result = null; | |
| $("source-img").hidden = true; | |
| $("source-hint").style.display = ""; | |
| $("result-img").hidden = true; | |
| $("result-hint").style.display = ""; | |
| $("layer-source-thumb").textContent = "BG"; | |
| clearMask(); | |
| }; | |
| async function maskPath() { | |
| if (!strokes.length) return null; | |
| const img = $("source-img"); | |
| const out = document.createElement("canvas"); | |
| out.width = img.naturalWidth; | |
| out.height = img.naturalHeight; | |
| const octx = out.getContext("2d"); | |
| octx.fillStyle = "#000"; | |
| octx.fillRect(0, 0, out.width, out.height); | |
| const sx = out.width / maskCanvas.width; | |
| const sy = out.height / maskCanvas.height; | |
| octx.strokeStyle = "#fff"; | |
| octx.lineWidth = Math.max(16, maskCanvas.width / 40) * sx; | |
| octx.lineCap = "round"; | |
| octx.lineJoin = "round"; | |
| for (const stroke of strokes) { | |
| octx.beginPath(); | |
| stroke.forEach((p, i) => (i ? octx.lineTo(p.x * sx, p.y * sy) : octx.moveTo(p.x * sx, p.y * sy))); | |
| octx.stroke(); | |
| } | |
| const blob = await new Promise((r) => out.toBlob(r, "image/png")); | |
| return uploadFile(blob); | |
| } | |
| // ---------- history ---------- | |
| function renderHistory() { | |
| const strip = $("history-strip"); | |
| strip.innerHTML = '<span class="hlabel">History</span>'; | |
| docState.history.forEach((h) => { | |
| const chip = document.createElement("div"); | |
| chip.className = "history-chip"; | |
| chip.title = h.instruction || ""; | |
| const img = document.createElement("img"); | |
| img.src = h.uri; | |
| const tag = document.createElement("div"); | |
| tag.className = "hseed"; | |
| tag.textContent = h.seed ?? ""; | |
| chip.appendChild(img); | |
| chip.appendChild(tag); | |
| chip.onclick = () => { | |
| $("instruction").value = h.instruction; | |
| $("seed").value = h.seed ?? 42; | |
| $("randomize").checked = false; | |
| docState.result = h.uri; | |
| $("result-img").src = h.uri; | |
| $("result-img").hidden = false; | |
| $("result-hint").style.display = "none"; | |
| $("split-toggle").hidden = false; | |
| }; | |
| strip.appendChild(chip); | |
| }); | |
| } | |
| function pushHistory(uri, seed, instruction) { | |
| docState.history.unshift({ uri, seed, instruction }); | |
| if (docState.history.length > 8) docState.history.pop(); | |
| renderHistory(); | |
| } | |
| // ---------- before/after split view ---------- | |
| let splitOn = false; | |
| $("split-toggle").onclick = () => { | |
| if ($("result-img").hidden) return; | |
| splitOn = !splitOn; | |
| $("split-toggle").classList.toggle("active", splitOn); | |
| $("split-wrap").hidden = !splitOn; | |
| if (splitOn && !$("source-img").hidden) $("split-source").src = $("source-img").src; | |
| }; | |
| (function () { | |
| const divider = $("split-divider"); | |
| let dragging = false; | |
| divider.addEventListener("pointerdown", (e) => { dragging = true; e.preventDefault(); }); | |
| window.addEventListener("pointermove", (e) => { | |
| if (!dragging) return; | |
| const stage = $("after-stage").getBoundingClientRect(); | |
| const pct = Math.min(97, Math.max(3, ((e.clientX - stage.left) / stage.width) * 100)); | |
| divider.style.left = pct + "%"; | |
| $("split-source").style.clipPath = `inset(0 ${100 - pct}% 0 0)`; | |
| }); | |
| window.addEventListener("pointerup", () => { dragging = false; }); | |
| })(); | |
| // ---------- zoom ---------- | |
| document.querySelectorAll(".doc figcaption").forEach((caption) => { | |
| const btn = document.createElement("button"); | |
| btn.className = "zoom-btn"; | |
| btn.textContent = "FIT"; | |
| btn.onclick = () => { | |
| const img = caption.parentElement.querySelector("img"); | |
| const stage = caption.parentElement.querySelector(".stage"); | |
| const zoomed = img.classList.toggle("zoomed"); | |
| stage.classList.toggle("scrollable", zoomed); | |
| btn.textContent = zoomed ? "1:1" : "FIT"; | |
| }; | |
| caption.appendChild(btn); | |
| }); | |
| // ---------- collapsible panels ---------- | |
| document.querySelectorAll(".panel .panel-title").forEach((title) => { | |
| const body = document.createElement("div"); | |
| body.className = "panel-body"; | |
| [...title.parentElement.children].forEach((child) => { | |
| if (child !== title) body.appendChild(child); | |
| }); | |
| title.parentElement.appendChild(body); | |
| title.onclick = () => title.parentElement.classList.toggle("collapsed"); | |
| }); | |
| // Changing the instruction or the source makes a previously written JSON stale, | |
| // like the classic app: clear it so the next run regenerates it. | |
| $("instruction").addEventListener("input", () => { $("json-instruction").value = ""; }); | |
| // ---------- examples ---------- | |
| document.querySelectorAll(".ex-chip").forEach((chip) => { | |
| chip.onclick = async () => { | |
| setStatus("Loading example…", true); | |
| try { | |
| const blob = await (await fetch(chip.dataset.img)).blob(); | |
| const file = new File([blob], "example.jpg", { type: "image/jpeg" }); | |
| state.sourcePath = await uploadFile(file); | |
| showSource(file); | |
| $("instruction").value = chip.dataset.instruction; | |
| setMode("edit"); | |
| setStatus("Example loaded — hit Generate Edit"); | |
| } catch (err) { | |
| setStatus(String(err)); | |
| } | |
| }; | |
| }); | |
| // ---------- result handling ---------- | |
| function unwrap(result) { | |
| const data = Array.isArray(result?.data) ? result.data[0] : result?.data; | |
| return data ?? {}; | |
| } | |
| function showResult(result) { | |
| const payload = unwrap(result); | |
| const url = typeof payload.image_data === "string" ? payload.image_data : null; | |
| if (!url) { setStatus("Run finished but no image came back (see console)."); return false; } | |
| const img = $("result-img"); | |
| img.src = url; | |
| img.hidden = false; | |
| $("result-hint").style.display = "none"; | |
| docState.result = url; | |
| if (payload.seed != null) $("seed-out").textContent = "seed " + payload.seed; | |
| $("split-toggle").hidden = false; | |
| $("result-meta").textContent = "Edited"; | |
| const instruction = ($("json-instruction").value.trim() || $("instruction").value); | |
| pushHistory(url, payload.seed, instruction); | |
| return true; | |
| } | |
| $("steps").oninput = () => ($("steps-val").textContent = $("steps").value); | |
| $("run-btn").onclick = async () => { | |
| const app = await getApp(); | |
| if (!state.sourcePath) { setStatus("Upload a source image first."); return; } | |
| const seed = parseInt($("seed").value, 10) || 42; | |
| const randomize = $("randomize").checked; | |
| const steps = parseInt($("steps").value, 10); | |
| try { | |
| setStatus("Preparing mask…", true); | |
| const mask = await maskPath(); | |
| setStatus($("json-instruction").value.trim() ? "Editing…" : "Writing the instruction (VGL)…", true); | |
| const result = await app.predict("/edit", { | |
| source: state.sourcePath, | |
| instruction: $("instruction").value, | |
| json_instruction: $("json-instruction").value, | |
| ref2: state.refPaths[2], | |
| ref3: state.refPaths[3], | |
| ref4: state.refPaths[4], | |
| mask, | |
| seed, randomize, steps, | |
| }); | |
| const payload = unwrap(result); | |
| if (payload.json_instruction) $("json-instruction").value = payload.json_instruction; | |
| if (showResult({ data: [payload] })) setStatus("Done"); | |
| } catch (err) { | |
| setStatus(err?.message || String(err)); | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |