Spaces:
Running on Zero
Running on Zero
Download studio.html from akhaliq/moss-image-edit-workflow: direct link, hf CLI and curl.
- Browser
- Download file 37.3 kB
-
https://huggingface.co/spaces/akhaliq/moss-image-edit-workflow/resolve/main/studio.html
- Command line
-
hf download hf://spaces/akhaliq/moss-image-edit-workflow/studio.html
-
curl -L -o studio.html https://huggingface.co/spaces/akhaliq/moss-image-edit-workflow/resolve/main/studio.html
37.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>MOSS Image Edit Studio</title> | |
| <style> | |
| :root { | |
| --bg: #0e0e10; --panel: #16171a; --panel2: #101114; --raised: #1e2024; | |
| --border: rgba(255,255,255,0.07); --highlight: rgba(255,255,255,0.05); | |
| --text: rgba(255,255,255,0.87); --muted: rgba(255,255,255,0.5); --dim: rgba(255,255,255,0.35); | |
| --accent: #4f9d63; --accent2: #6cc184; --accent-soft: rgba(79,157,99,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; | |
| } | |
| /* ---------- top bar ---------- */ | |
| .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(--accent2); 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 ---------- */ | |
| .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(--accent2); background: var(--accent-soft); } | |
| .viewport { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: auto; position: relative; } | |
| .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: 700; letter-spacing: 0.16em; | |
| color: var(--muted); 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; } | |
| .doc img { max-width: 100%; max-height: 100%; object-fit: contain; 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; } | |
| .workflow-view { position: absolute; inset: 0; padding: 12px; } | |
| .workflow-view iframe { width: 100%; height: 100%; border: none; border-radius: 10px; background: #0c0d10; } | |
| /* Photoshop checkerboard document stage */ | |
| .doc .stage { | |
| background-color: #17171b; | |
| background-image: | |
| linear-gradient(45deg, #1f1f25 25%, transparent 25%), | |
| linear-gradient(-45deg, #1f1f25 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #1f1f25 75%), | |
| linear-gradient(-45deg, transparent 75%, #1f1f25 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0; | |
| } | |
| .doc img.zoomed { max-width: none; max-height: none; } | |
| .doc .stage.scrollable { overflow: auto; justify-content: flex-start; align-items: flex-start; } | |
| .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(--accent2); } | |
| .panel .panel-title { 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; } | |
| /* ---------- panels ---------- */ | |
| .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; } | |
| .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); } | |
| .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); } | |
| .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: #17171c; | |
| } | |
| .ref-slot:hover { border-color: var(--accent2); } | |
| .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; } | |
| #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; } | |
| /* ---------- status bar ---------- */ | |
| .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(--accent2); animation: pulse 1s infinite alternate; } | |
| @keyframes pulse { from { opacity: 0.4; } to { opacity: 1; } } | |
| #file-input { display: none; } | |
| /* ---------- layers, split view, doc bar, history ---------- */ | |
| .layer-row { | |
| display: flex; align-items: center; gap: 10px; padding: 6px 8px; | |
| border: 1px solid var(--border); border-radius: 6px; margin-top: 6px; | |
| background: #17171c; cursor: pointer; | |
| } | |
| .layer-row:hover { border-color: var(--accent2); } | |
| .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); } | |
| .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 .spacer { flex: 1; } | |
| .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(--accent2); } | |
| .doc-bar button.active { background: rgba(108,193,132,0.12); color: var(--accent2); } | |
| .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(--accent2); | |
| cursor: ew-resize; z-index: 5; box-shadow: 0 0 8px rgba(108,193,132,0.6); | |
| } | |
| .split-divider::after { | |
| content: "◂ ▸"; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); | |
| background: var(--accent2); 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: 700; letter-spacing: 0.14em; color: var(--muted); 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(--accent2); } | |
| .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; | |
| } | |
| /* ---------- examples ---------- */ | |
| .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; } | |
| .ex-prompt { | |
| display: block; width: 100%; text-align: left; margin-top: 6px; padding: 8px 10px; | |
| background: var(--raised); border: 1px solid var(--border); border-radius: 7px; | |
| color: var(--text); font-size: 0.75rem; cursor: pointer; transition: border-color 0.15s; | |
| } | |
| .ex-prompt:hover { border-color: var(--accent); } | |
| @media (max-width: 980px) { | |
| .doc-grid { flex-direction: column; } | |
| .panels { width: 260px; } | |
| } | |
| @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; font-size: 1.2rem; } | |
| .viewport { display: block; padding: 10px; overflow: visible; } | |
| .doc-grid { flex-direction: column; align-items: stretch; height: auto; } | |
| .doc { max-width: 100%; width: 100%; } | |
| .doc .stage { min-height: 220px; } | |
| .workflow-view { position: static; height: 78vh; padding: 0 0 10px; } | |
| .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">🎨 MOSS Image Edit Studio</div> | |
| <div class="mode-tabs"> | |
| <button id="mode-edit" class="active">Edit</button> | |
| <button id="mode-generate">Generate</button> | |
| <button id="mode-workflow">Workflow</button> | |
| </div> | |
| <div class="spacer"></div> | |
| <a class="ml-badge" href="https://huggingface.co/chinoll/Moss-Image-Edit" target="_blank" rel="noopener">MOSS Image Edit LoRA</a> | |
| <a class="ml-cta" href="https://hf.co/chat?mode=ml-intern" target="_blank" rel="noopener">✨ Built with ML Intern</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-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 /> | |
| <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"> | |
| <!-- edit mode --> | |
| <div id="edit-panels"> | |
| <section class="panel"> | |
| <div class="panel-title">Examples</div> | |
| <div class="ex-row"> | |
| <button class="ex-chip" data-img="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/007/image.webp" data-instruction="Change the scene to a snowy winter day, keeping the person unchanged."> | |
| <img src="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/007/image.webp" alt="" /><span>Snowy winter day</span> | |
| </button> | |
| <button class="ex-chip" data-img="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/003/image.webp" data-instruction="Change the lighting to warm golden-hour sunlight."> | |
| <img src="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/003/image.webp" alt="" /><span>Golden-hour light</span> | |
| </button> | |
| <button class="ex-chip" data-img="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/011/image.webp" data-instruction="Turn the outfit into a bright red dress, preserving everything else."> | |
| <img src="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/011/image.webp" alt="" /><span>Red outfit</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="Change the background to a sunny beach, preserving the person."></textarea> | |
| <label for="edit-negative">Negative prompt (optional)</label> | |
| <textarea id="edit-negative" rows="2" placeholder="watermark, blurry"></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 1</span></div> | |
| </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">With multiple references, name each image's role — e.g. "Dress the person in image 1 in the outfit from image 2."</div> | |
| </section> | |
| </div> | |
| <!-- generate mode --> | |
| <div id="generate-panels" hidden> | |
| <section class="panel"> | |
| <div class="panel-title">Examples</div> | |
| <button class="ex-prompt" data-prompt="A cinematic close-up portrait of an elderly fisherman with a weathered face, soft window light, shallow depth of field">🎣 Cinematic fisherman portrait</button> | |
| <button class="ex-prompt" data-prompt="A cozy bookstore cafe interior at golden hour, warm lamp light, tall shelves of books, steam rising from a cup">📚 Cozy bookstore cafe</button> | |
| <button class="ex-prompt" data-prompt="A majestic snow leopard resting on a rocky ledge at dawn, cold morning mist, telephoto wildlife photography">🐆 Snow leopard in the mist</button> | |
| <button class="ex-prompt" data-prompt="An isometric diorama of a tiny moss-covered shrine in a bamboo forest, soft volumetric light, miniature photography style">🎋 Moss shrine diorama</button> | |
| </section> | |
| <section class="panel"> | |
| <div class="panel-title">Prompt</div> | |
| <label for="gen-prompt">Describe the image</label> | |
| <textarea id="gen-prompt" rows="4" placeholder="a golden retriever puppy sitting in a lavender field at golden hour"></textarea> | |
| <label for="gen-negative">Negative prompt (optional)</label> | |
| <textarea id="gen-negative" rows="2">watermark, bad anatomy</textarea> | |
| </section> | |
| </div> | |
| <!-- shared advanced --> | |
| <section class="panel"> | |
| <div class="panel-title">Advanced</div> | |
| <div class="row"> | |
| <div><label for="seed">Seed</label><input type="number" id="seed" value="-1" 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">30</span></label><input type="range" id="steps" min="1" max="100" value="30" /></div> | |
| <div><label>CFG <span id="cfg-val">4</span></label><input type="range" id="cfg" min="1" max="10" step="0.5" value="4" /></div> | |
| </div> | |
| <div class="row" style="margin-top:10px"> | |
| <div><label>Shift <span id="shift-val">5</span></label><input type="range" id="shift" min="1" max="10" step="0.5" value="5" /></div> | |
| <div id="num-images-wrap" hidden><label>Images <span id="num-images-val">1</span></label><input type="range" id="num-images" min="1" max="4" step="1" value="1" /></div> | |
| </div> | |
| <div class="row" style="margin-top:10px"> | |
| <div><label>Width <span id="width-val">1024</span></label><input type="range" id="width" min="512" max="1536" step="16" value="1024" /></div> | |
| <div><label>Height <span id="height-val">1024</span></label><input type="range" id="height" min="512" max="1536" step="16" value="1024" /></div> | |
| </div> | |
| <div class="row" id="ref-edge-row" style="margin-top:10px"> | |
| <div><label>Reference max edge <span id="ref-edge-val">512</span></label><input type="range" id="ref-edge" min="256" max="1024" step="16" value="512" /></div> | |
| </div> | |
| </section> | |
| <button id="run-btn">Edit Image</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/dist/index.min.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const state = { | |
| mode: "edit", | |
| sourcePath: null, | |
| refPaths: { 2: null, 3: null, 4: null }, | |
| app: null, | |
| }; | |
| // ---------- client ---------- | |
| 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; | |
| } | |
| // ---------- mode ---------- | |
| function setMode(mode) { | |
| state.mode = mode; | |
| $("mode-edit").classList.toggle("active", mode === "edit"); | |
| $("mode-generate").classList.toggle("active", mode === "generate"); | |
| $("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" : ""; | |
| if (workflow) return; | |
| $("edit-panels").hidden = mode !== "edit"; | |
| $("generate-panels").hidden = mode !== "generate"; | |
| $("doc-before").style.display = mode === "edit" ? "" : "none"; | |
| $("ref-edge-row").style.display = mode === "edit" ? "" : "none"; | |
| $("num-images-wrap").hidden = mode !== "generate"; | |
| $("steps").value = mode === "edit" ? 30 : 50; | |
| $("steps-val").textContent = $("steps").value; | |
| $("cfg").value = mode === "edit" ? 4 : 3; | |
| $("cfg-val").textContent = $("cfg").value; | |
| $("run-btn").textContent = mode === "edit" ? "Edit Image" : "Generate Image"; | |
| renderDocs(mode); | |
| renderHistory(mode); | |
| } | |
| $("mode-edit").onclick = () => setMode("edit"); | |
| $("mode-generate").onclick = () => setMode("generate"); | |
| $("mode-workflow").onclick = () => setMode("workflow"); | |
| // ---------- 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.webp", { type: blob.type || "image/webp" }); | |
| state.sourcePath = await uploadFile(file); | |
| showSource(file); | |
| $("instruction").value = chip.dataset.instruction; | |
| setMode("edit"); | |
| setStatus("Example loaded — hit Edit Image"); | |
| } catch (err) { | |
| setStatus(String(err)); | |
| } | |
| }; | |
| }); | |
| document.querySelectorAll(".ex-prompt").forEach((btn) => { | |
| btn.onclick = () => { | |
| setMode("generate"); | |
| $("gen-prompt").value = btn.dataset.prompt; | |
| setStatus("Example prompt loaded — hit Generate Image"); | |
| }; | |
| }); | |
| // Collapsible panels (Photoshop dock behavior) + document zoom toggle. | |
| 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"); | |
| }); | |
| 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); | |
| }); | |
| // ---------- 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)); | |
| } | |
| }); | |
| // Per-mode document state: Edit and Generate keep separate result panes so | |
| // a run in one tab never bleeds into the other. | |
| const docState = { | |
| edit: { source: null, result: null, history: [] }, | |
| generate: { source: null, result: null, history: [] }, | |
| }; | |
| function renderDocs(mode) { | |
| const d = docState[mode]; | |
| const si = $("source-img"), ri = $("result-img"); | |
| if (d.source) { | |
| si.src = d.source; | |
| si.hidden = false; | |
| $("source-hint").style.display = "none"; | |
| } else { | |
| si.hidden = true; | |
| $("source-hint").style.display = ""; | |
| } | |
| if (d.result) { | |
| ri.src = d.result; | |
| ri.hidden = false; | |
| $("result-hint").style.display = "none"; | |
| $("split-toggle").hidden = mode !== "edit"; | |
| } else { | |
| ri.hidden = true; | |
| $("result-hint").style.display = ""; | |
| $("split-toggle").hidden = true; | |
| } | |
| } | |
| function showSource(file) { | |
| const img = $("source-img"); | |
| img.src = URL.createObjectURL(file); | |
| img.hidden = false; | |
| $("source-hint").style.display = "none"; | |
| docState.edit.source = img.src; | |
| // Layers panel: the source is the Background layer. | |
| const st = $("layer-source-thumb"); | |
| st.innerHTML = ""; | |
| const stImg = document.createElement("img"); | |
| stImg.src = img.src; | |
| st.appendChild(stImg); | |
| } | |
| $("tool-clear-doc").onclick = () => { | |
| state.sourcePath = null; | |
| docState.edit.source = null; | |
| docState.edit.result = null; | |
| $("source-img").hidden = true; | |
| $("source-hint").style.display = ""; | |
| $("result-img").hidden = true; | |
| $("result-hint").style.display = ""; | |
| }; | |
| // ---------- slider readouts ---------- | |
| $("steps").oninput = () => ($("steps-val").textContent = $("steps").value); | |
| $("cfg").oninput = () => ($("cfg-val").textContent = $("cfg").value); | |
| $("shift").oninput = () => ($("shift-val").textContent = $("shift").value); | |
| $("width").oninput = () => ($("width-val").textContent = $("width").value); | |
| $("height").oninput = () => ($("height-val").textContent = $("height").value); | |
| $("ref-edge").oninput = () => ($("ref-edge-val").textContent = $("ref-edge").value); | |
| $("num-images").oninput = () => ($("num-images-val").textContent = $("num-images").value); | |
| function unwrap(result) { | |
| const data = Array.isArray(result?.data) ? result.data[0] : result?.data; | |
| return data ?? {}; | |
| } | |
| // ---------- history (per mode) ---------- | |
| function renderHistory(mode) { | |
| const strip = $("history-strip"); | |
| strip.innerHTML = '<span class="hlabel">History</span>'; | |
| docState[mode].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 = () => { | |
| if (mode === "edit") $("instruction").value = h.instruction; | |
| else $("gen-prompt").value = h.instruction; | |
| $("seed").value = h.seed ?? -1; | |
| $("randomize").checked = false; | |
| docState[mode].result = h.uri; | |
| $("result-img").src = h.uri; | |
| $("result-img").hidden = false; | |
| $("result-hint").style.display = "none"; | |
| $("split-toggle").hidden = mode !== "edit"; | |
| }; | |
| strip.appendChild(chip); | |
| }); | |
| } | |
| function pushHistory(uri, seed, instruction, mode) { | |
| docState[mode].history.unshift({ uri, seed, instruction, mode }); | |
| if (docState[mode].history.length > 8) docState[mode].history.pop(); | |
| renderHistory(mode); | |
| } | |
| // ---------- 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; }); | |
| })(); | |
| 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[state.mode].result = url; | |
| if (payload.seed != null) $("seed-out").textContent = "seed " + payload.seed; | |
| $("split-toggle").hidden = state.mode !== "edit"; | |
| $("result-meta").textContent = payload.status || (state.mode === "edit" ? "Edited" : "Generated"); | |
| const instruction = state.mode === "edit" ? $("instruction").value : $("gen-prompt").value; | |
| const uris = Array.isArray(payload.images_data) && payload.images_data.length | |
| ? payload.images_data : [url]; | |
| uris.forEach((uri) => pushHistory(uri, payload.seed, instruction, state.mode)); | |
| return true; | |
| } | |
| $("run-btn").onclick = async () => { | |
| const app = await getApp(); | |
| const seed = parseInt($("seed").value, 10); | |
| const randomize = $("randomize").checked; | |
| try { | |
| if (state.mode === "edit") { | |
| if (!state.sourcePath) { setStatus("Upload a source image first."); return; } | |
| setStatus("Editing… (first run also loads the weights onto the GPU)", true); | |
| const result = await app.predict("/edit", { | |
| source: state.sourcePath, | |
| instruction: $("instruction").value, | |
| negative_prompt: $("edit-negative").value, | |
| ref2: state.refPaths[2], | |
| ref3: state.refPaths[3], | |
| ref4: state.refPaths[4], | |
| seed: Number.isFinite(seed) ? seed : -1, | |
| randomize, | |
| steps: parseInt($("steps").value, 10), | |
| cfg: parseFloat($("cfg").value), | |
| shift: parseFloat($("shift").value), | |
| width: parseInt($("width").value, 10), | |
| height: parseInt($("height").value, 10), | |
| reference_max_edge: parseInt($("ref-edge").value, 10), | |
| }); | |
| if (showResult(result)) setStatus("Done"); | |
| } else { | |
| setStatus("Generating… (first run also loads the weights onto the GPU)", true); | |
| const result = await app.predict("/generate", { | |
| prompt: $("gen-prompt").value, | |
| negative_prompt: $("gen-negative").value, | |
| seed: Number.isFinite(seed) ? seed : -1, | |
| randomize, | |
| steps: parseInt($("steps").value, 10), | |
| cfg: parseFloat($("cfg").value), | |
| shift: parseFloat($("shift").value), | |
| width: parseInt($("width").value, 10), | |
| height: parseInt($("height").value, 10), | |
| num_images: parseInt($("num-images").value, 10), | |
| }); | |
| if (showResult(result)) setStatus("Done"); | |
| } | |
| } catch (err) { | |
| setStatus(err?.message || String(err)); | |
| } | |
| }; | |
| setMode("edit"); | |
| </script> | |
| </body> | |
| </html> | |