| <!doctype html> |
| <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 !important; } |
| 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 { |
| 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 { |
| 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; |
| } |
| |
| |
| main { |
| flex: 1 1 auto; |
| min-height: 0; |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| grid-template-rows: auto 1fr auto; |
| border-bottom: 1px solid var(--rule); |
| } |
| .cell { padding: 0 28px; min-width: 0; min-height: 0; } |
| .cell.right { border-left: 1px solid var(--rule); } |
| |
| .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); |
| } |
| |
| |
| .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; } |
| |
| |
| .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; } |
| |
| |
| .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); } |
| |
| |
| .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 { |
| 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; } |
| |
| |
| @media (max-width: 880px) { |
| body { overflow: auto; } |
| .hero { padding: 12px 18px; gap: 8px; } |
| .hero p { margin-left: 0; } |
| main { |
| grid-template-columns: 1fr; |
| grid-template-rows: auto auto auto auto auto auto; |
| } |
| .cell.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> |
| |
| <div class="cell head left"> |
| <span class="panel-num">01 / Source</span> |
| <span class="panel-title">your photo</span> |
| </div> |
| <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 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 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 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> |
| <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> |
| </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"; |
| |
| |
| 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> |
|
|