Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Studio 01 — Image Edit</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --paper: #FAF8F2; | |
| --paper-2:#FFFFFF; | |
| --ink: #161513; | |
| --ink-2: #524E47; | |
| --ink-3: #8C887F; | |
| --rule: #DAD5C7; | |
| --accent: #C04B2B; | |
| } | |
| * { box-sizing: border-box; } | |
| [hidden] { display: none ; } | |
| html, body { height: 100%; margin: 0; padding: 0; } | |
| body { | |
| display: flex; flex-direction: column; | |
| font-family: 'Inter', system-ui, -apple-system, "Segoe UI", sans-serif; | |
| background: var(--paper); | |
| color: var(--ink); | |
| line-height: 1.45; | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| text-rendering: optimizeLegibility; | |
| } | |
| a { color: inherit; } | |
| /* ---------- nav ---------- */ | |
| .nav { | |
| flex: 0 0 auto; | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 12px 28px; | |
| border-bottom: 1px solid var(--rule); | |
| } | |
| .brand { | |
| font-family: 'Instrument Serif', Georgia, serif; | |
| font-size: 20px; | |
| letter-spacing: 0.005em; | |
| } | |
| .brand sup { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--accent); | |
| margin-left: 4px; | |
| top: -0.7em; | |
| position: relative; | |
| } | |
| .nav .meta { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| letter-spacing: 0.04em; | |
| color: var(--ink-3); | |
| text-transform: uppercase; | |
| } | |
| .nav .meta .dot { | |
| display: inline-block; width: 6px; height: 6px; | |
| background: var(--accent); border-radius: 50%; | |
| margin-right: 8px; vertical-align: 2px; | |
| } | |
| /* ---------- hero ---------- */ | |
| .hero { | |
| flex: 0 0 auto; | |
| padding: 14px 28px 16px; | |
| border-bottom: 1px solid var(--rule); | |
| display: flex; align-items: baseline; gap: 24px; | |
| flex-wrap: wrap; | |
| } | |
| .hero .eyebrow { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| color: var(--ink-3); | |
| } | |
| .hero h1 { | |
| font-family: 'Instrument Serif', Georgia, serif; | |
| font-weight: 400; | |
| font-size: clamp(22px, 3.2vw, 32px); | |
| line-height: 1.05; | |
| letter-spacing: -0.01em; | |
| margin: 0; | |
| } | |
| .hero h1 em { font-style: italic; color: var(--accent); } | |
| .hero p { | |
| color: var(--ink-2); | |
| font-size: 13px; | |
| margin: 0; | |
| margin-left: auto; | |
| max-width: 56ch; | |
| } | |
| /* ---------- workspace grid ---------- */ | |
| main { | |
| flex: 1 1 auto; | |
| min-height: 0; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| border-bottom: 1px solid var(--rule); | |
| } | |
| .panel { | |
| min-width: 0; | |
| min-height: 0; | |
| display: grid; | |
| grid-template-rows: auto 1fr auto; | |
| } | |
| .panel.right { | |
| border-left: 1px solid var(--rule); | |
| } | |
| .cell { padding: 0 28px; min-width: 0; min-height: 0; } | |
| .cell.head { | |
| padding-top: 14px; padding-bottom: 8px; | |
| display: flex; align-items: baseline; justify-content: space-between; gap: 12px; | |
| } | |
| .panel-num { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| color: var(--ink-3); | |
| } | |
| .panel-title { | |
| font-family: 'Instrument Serif', Georgia, serif; | |
| font-size: 18px; | |
| font-style: italic; | |
| color: var(--ink); | |
| } | |
| /* drop / output frame */ | |
| .cell.frame-cell { | |
| padding-top: 0; padding-bottom: 0; | |
| display: flex; | |
| } | |
| .frame { | |
| flex: 1; min-height: 0; min-width: 0; | |
| background: var(--paper-2); | |
| border: 1px solid var(--rule); | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| .frame.dashed { border-style: dashed; cursor: pointer; transition: border-color .15s, background .15s; } | |
| .frame.dashed:hover, .frame.dashed.over { border-color: var(--ink); background: #fff; } | |
| .frame img { width: 100%; height: 100%; object-fit: contain; display: block; background: #fff; } | |
| .placeholder { text-align: center; color: var(--ink-3); padding: 18px; } | |
| .placeholder .glyph { | |
| font-family: 'Instrument Serif', Georgia, serif; | |
| font-size: 36px; font-style: italic; color: var(--ink); | |
| line-height: 1; | |
| } | |
| .placeholder .small { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; letter-spacing: 0.08em; | |
| color: var(--ink-3); margin-top: 10px; | |
| } | |
| .placeholder .small b { color: var(--ink); font-weight: 500; } | |
| /* controls (left bottom row) */ | |
| .cell.controls { | |
| padding-top: 10px; padding-bottom: 14px; | |
| display: flex; flex-direction: column; gap: 8px; | |
| } | |
| textarea { | |
| width: 100%; | |
| padding: 9px 12px; | |
| border: 1px solid var(--rule); | |
| background: var(--paper-2); | |
| font-family: inherit; font-size: 14px; | |
| color: var(--ink); | |
| resize: none; | |
| height: 52px; | |
| border-radius: 0; | |
| } | |
| textarea:focus { outline: none; border-color: var(--ink); } | |
| .controls-row { | |
| display: flex; align-items: center; justify-content: space-between; | |
| gap: 12px; flex-wrap: wrap; | |
| } | |
| .presets { display: flex; flex-wrap: wrap; gap: 5px; } | |
| .preset { | |
| font: 11px/1 'JetBrains Mono', monospace; | |
| color: var(--ink-2); | |
| background: transparent; | |
| border: 1px solid var(--rule); | |
| padding: 6px 9px; | |
| cursor: pointer; | |
| transition: border-color .12s, color .12s; | |
| } | |
| .preset:hover { border-color: var(--ink); color: var(--ink); } | |
| button.go { | |
| padding: 10px 18px; | |
| background: var(--ink); | |
| color: var(--paper); | |
| border: none; | |
| font-family: 'Inter', sans-serif; | |
| font-size: 12px; | |
| font-weight: 500; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| transition: background .15s; | |
| white-space: nowrap; | |
| } | |
| button.go:hover:not(:disabled) { background: var(--accent); } | |
| button.go:disabled { opacity: 0.35; cursor: not-allowed; } | |
| /* result meta (right bottom row) */ | |
| .cell.meta-cell { | |
| padding-top: 10px; padding-bottom: 14px; | |
| display: flex; align-items: center; gap: 14px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3); | |
| } | |
| .cell.meta-cell a { | |
| color: var(--ink); text-decoration: underline; text-underline-offset: 4px; | |
| text-transform: uppercase; letter-spacing: 0.12em; | |
| } | |
| .cell.meta-cell .seed { margin-left: auto; } | |
| .meta-empty { color: var(--ink-3); } | |
| /* loader & empty */ | |
| .frame .empty { | |
| color: var(--ink-3); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; | |
| } | |
| .frame .loader { | |
| position: absolute; inset: 0; | |
| display: flex; align-items: center; justify-content: center; | |
| background: rgba(250, 248, 242, 0.9); | |
| color: var(--ink); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; | |
| } | |
| .frame .loader .bar { | |
| display: inline-block; | |
| width: 12px; height: 1px; background: var(--accent); | |
| margin-right: 10px; | |
| animation: pulse 1.4s ease-in-out infinite; | |
| } | |
| @keyframes pulse { 0%,100% { opacity: 0.2; } 50% { opacity: 1; } } | |
| /* footer */ | |
| footer { | |
| flex: 0 0 auto; | |
| padding: 10px 28px; | |
| display: flex; align-items: center; justify-content: space-between; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; letter-spacing: 0.04em; | |
| color: var(--ink-3); | |
| text-transform: uppercase; | |
| } | |
| footer a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; } | |
| /* mobile / narrow viewports */ | |
| @media (max-width: 880px) { | |
| body { overflow: auto; } | |
| .hero { padding: 12px 18px; gap: 8px; } | |
| .hero p { margin-left: 0; } | |
| main { | |
| grid-template-columns: 1fr; | |
| } | |
| .panel.right { | |
| border-left: 0; | |
| border-top: 1px solid var(--rule); | |
| } | |
| .cell.head.right { | |
| padding-top: 12px; | |
| } | |
| .cell.frame-cell { | |
| min-height: 280px; | |
| } | |
| .nav, .cell, footer { | |
| padding-left: 18px; | |
| padding-right: 18px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="nav"> | |
| <div class="brand">Studio<sup>01</sup></div> | |
| <div class="meta"><span class="dot"></span>FireRed-Image-Edit · gr.Server</div> | |
| </nav> | |
| <header class="hero"> | |
| <div class="eyebrow">Issue 001 · Image Workshop</div> | |
| <h1>Edit any photograph with <em>one sentence</em>.</h1> | |
| <p>Four-step inference on FireRed-Image-Edit 1.1, served on free GPU.</p> | |
| </header> | |
| <main> | |
| <section class="panel left"> | |
| <div class="cell head left"> | |
| <span class="panel-num">01 / Source</span> | |
| <span class="panel-title">your photo</span> | |
| </div> | |
| <div class="cell frame-cell left"> | |
| <div class="frame dashed" id="drop"> | |
| <div class="placeholder" id="placeholder"> | |
| <div class="glyph">+</div> | |
| <div class="small"><b>Click</b> or <b>drop</b> a photo here · JPG · PNG · WEBP</div> | |
| </div> | |
| <img id="preview" hidden alt=""> | |
| </div> | |
| <input type="file" id="file" accept="image/*" hidden> | |
| </div> | |
| <div class="cell controls left"> | |
| <textarea id="prompt" placeholder="convert to black and white with grain, replace background with a misty forest, transform into a 1990s polaroid..."></textarea> | |
| <div class="controls-row"> | |
| <div class="presets" id="presets"> | |
| <button class="preset" type="button" data-p="Convert it to black and white with subtle film grain.">B&W film</button> | |
| <button class="preset" type="button" data-p="Transform the image into a soft watercolor painting.">Watercolor</button> | |
| <button class="preset" type="button" data-p="Cinematic polaroid with soft grain, subtle vignette, gentle lighting, white frame, preserving realistic texture.">Polaroid</button> | |
| <button class="preset" type="button" data-p="Convert into a clean line-art ink drawing on white paper.">Line art</button> | |
| </div> | |
| <button id="go" class="go" disabled>Run edit</button> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="panel right"> | |
| <div class="cell head right"> | |
| <span class="panel-num">02 / Result</span> | |
| <span class="panel-title">the edit</span> | |
| </div> | |
| <div class="cell frame-cell right"> | |
| <div class="frame" id="out"> | |
| <span class="empty" id="empty">Output will appear here</span> | |
| <img id="result" hidden alt=""> | |
| <div class="loader" id="loader" hidden><span class="bar"></span>Editing</div> | |
| </div> | |
| </div> | |
| <div class="cell meta-cell right"> | |
| <a href="#" id="download" hidden>Download PNG</a> | |
| <span class="meta-empty" id="meta-empty">Upload a photo to begin</span> | |
| <span class="seed" id="seed"></span> | |
| </div> | |
| </section> | |
| </main> | |
| <footer> | |
| <span>Backend: <a href="https://www.gradio.app/main/docs/gradio/server">gr.Server</a> · Model: <a href="https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1">FireRed-Image-Edit 1.1</a></span> | |
| <span>Free GPU via <a href="https://huggingface.co/docs/hub/spaces-zerogpu">ZeroGPU</a></span> | |
| </footer> | |
| <script type="module"> | |
| import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| // Connect once. Reused across calls (per the reference). | |
| const clientPromise = Client.connect(window.location.origin); | |
| const drop = document.getElementById('drop'); | |
| const fileInput = document.getElementById('file'); | |
| const placeholder = document.getElementById('placeholder'); | |
| const preview = document.getElementById('preview'); | |
| const promptEl = document.getElementById('prompt'); | |
| const presets = document.getElementById('presets'); | |
| const goBtn = document.getElementById('go'); | |
| const empty = document.getElementById('empty'); | |
| const result = document.getElementById('result'); | |
| const loader = document.getElementById('loader'); | |
| const download = document.getElementById('download'); | |
| const seedEl = document.getElementById('seed'); | |
| const metaEmpty = document.getElementById('meta-empty'); | |
| let currentFile = null; | |
| function setFile(f) { | |
| if (!f || !f.type.startsWith('image/')) return; | |
| currentFile = f; | |
| preview.src = URL.createObjectURL(f); | |
| preview.hidden = false; | |
| placeholder.hidden = true; | |
| goBtn.disabled = false; | |
| if (metaEmpty) metaEmpty.textContent = 'Ready'; | |
| } | |
| drop.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', e => setFile(e.target.files[0])); | |
| drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('over'); }); | |
| drop.addEventListener('dragleave', () => drop.classList.remove('over')); | |
| drop.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| drop.classList.remove('over'); | |
| setFile(e.dataTransfer.files[0]); | |
| }); | |
| presets.addEventListener('click', e => { | |
| const p = e.target.closest('.preset'); | |
| if (!p) return; | |
| promptEl.value = p.dataset.p; | |
| promptEl.focus(); | |
| }); | |
| goBtn.addEventListener('click', async () => { | |
| if (!currentFile) return; | |
| const prompt = promptEl.value.trim(); | |
| if (!prompt) { promptEl.focus(); return; } | |
| goBtn.disabled = true; | |
| loader.hidden = false; | |
| empty.hidden = true; | |
| result.hidden = true; | |
| download.hidden = true; | |
| seedEl.textContent = ''; | |
| if (metaEmpty) metaEmpty.hidden = true; | |
| try { | |
| const client = await clientPromise; | |
| const res = await client.predict("/edit_image", { | |
| image: handle_file(currentFile), | |
| prompt, | |
| }); | |
| console.log('[predict] full res:', res); | |
| console.log('[predict] res.data:', res.data); | |
| const data = res.data[0]; | |
| console.log('[predict] data[0]:', data); | |
| if (data && data.error) { | |
| empty.hidden = false; | |
| empty.textContent = data.error; | |
| } else if (data && data.image) { | |
| const url = data.image.url | |
| || (data.image.path ? `/gradio_api/file=${data.image.path}` : null); | |
| console.log('[predict] resolved image url:', url); | |
| result.src = url; | |
| result.hidden = false; | |
| download.href = url; | |
| download.setAttribute('download', 'edited.png'); | |
| download.hidden = false; | |
| seedEl.textContent = `seed ${data.seed}`; | |
| } else { | |
| empty.hidden = false; | |
| empty.textContent = 'Unexpected response shape (see console)'; | |
| } | |
| } catch (err) { | |
| console.error('[predict] error:', err); | |
| empty.hidden = false; | |
| empty.textContent = 'Error: ' + (err?.message || err); | |
| } finally { | |
| loader.hidden = true; | |
| goBtn.disabled = false; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |