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.0, viewport-fit=cover" /> | |
| <meta name="theme-color" content="#76b900" /> | |
| <title>Qwen-Image-Flash · 4-step Text-to-Image</title> | |
| <style> | |
| /* ---------- Design tokens ---------- */ | |
| :root { | |
| --nv-green: #76b900; | |
| --nv-green-2: #8acc2c; | |
| --nv-green-3: #5e8f00; | |
| --nv-green-soft: rgba(118, 185, 0, 0.18); | |
| --nv-green-glow: rgba(118, 185, 0, 0.35); | |
| /* Floating-card palette (dark on the image) */ | |
| --card: rgba(20, 22, 26, 0.78); | |
| --card-strong: rgba(15, 17, 21, 0.92); | |
| --card-border: rgba(255, 255, 255, 0.10); | |
| --card-border-strong: rgba(255, 255, 255, 0.18); | |
| --ink: #ffffff; | |
| --ink-soft: rgba(255, 255, 255, 0.78); | |
| --muted: rgba(255, 255, 255, 0.55); | |
| /* Light fallback for the empty state */ | |
| --bg: #fafbf7; | |
| --bg-2: #f4f5f0; | |
| --ink-dark: #0a0a0a; | |
| --muted-dark: #6a6e63; | |
| --radius: 14px; | |
| --radius-lg: 18px; | |
| --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.35); | |
| --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.20); | |
| } | |
| *, *::before, *::after { box-sizing: border-box; } | |
| html { -webkit-text-size-adjust: 100%; } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| min-height: 100dvh; | |
| color: var(--ink-dark); | |
| background: var(--bg); | |
| font-family: "Inter", "Helvetica Neue", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| line-height: 1.5; | |
| overflow-x: hidden; | |
| } | |
| /* ---------- Stage (full-bleed image / placeholder) ---------- */ | |
| .stage { | |
| position: fixed; | |
| inset: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| height: 100dvh; | |
| overflow: hidden; | |
| background: var(--bg); | |
| } | |
| /* Result image fills the stage. Object-fit:contain so 1024² isn't cropped | |
| on a phone, but max-width/height keep it crisp. */ | |
| .result-img { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| display: none; | |
| animation: rise 0.45s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .result-img.show { display: block; } | |
| @keyframes rise { from { opacity: 0; transform: scale(0.985); } } | |
| /* Placeholder: subtle geometric background until first generation */ | |
| .placeholder { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: | |
| linear-gradient(60deg, transparent 49.5%, rgba(118, 185, 0, 0.05) 49.5%, rgba(118, 185, 0, 0.05) 50.5%, transparent 50.5%) 0 0 / 28px 48px, | |
| linear-gradient(-60deg, transparent 49.5%, rgba(118, 185, 0, 0.05) 49.5%, rgba(118, 185, 0, 0.05) 50.5%, transparent 50.5%) 0 0 / 28px 48px, | |
| radial-gradient(circle at 50% 35%, rgba(118, 185, 0, 0.10), transparent 60%), | |
| var(--bg); | |
| } | |
| .placeholder-inner { | |
| text-align: center; | |
| color: var(--muted-dark); | |
| max-width: 480px; | |
| padding: 24px; | |
| } | |
| .placeholder-inner .emoji { | |
| font-size: 64px; | |
| display: block; | |
| margin-bottom: 12px; | |
| filter: saturate(1.1); | |
| } | |
| .placeholder-inner h2 { | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: var(--ink-dark); | |
| margin: 0 0 6px; | |
| letter-spacing: -0.01em; | |
| } | |
| .placeholder-inner p { | |
| margin: 0; | |
| font-size: 14.5px; | |
| color: var(--muted-dark); | |
| } | |
| .placeholder-inner p b { color: var(--ink-dark); font-weight: 600; } | |
| /* Loading overlay: dimmed stage with a centered pulse + status */ | |
| .loading-overlay { | |
| position: absolute; | |
| inset: 0; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| gap: 18px; | |
| background: | |
| radial-gradient(circle at 50% 50%, rgba(0,0,0,0.25), rgba(0,0,0,0.55) 70%); | |
| color: #fff; | |
| backdrop-filter: blur(2px); | |
| -webkit-backdrop-filter: blur(2px); | |
| padding: 24px; | |
| text-align: center; | |
| } | |
| .loading-overlay.show { display: flex; } | |
| .ring { | |
| width: 56px; height: 56px; | |
| border: 3px solid rgba(255, 255, 255, 0.18); | |
| border-top-color: var(--nv-green); | |
| border-radius: 50%; | |
| animation: spin 0.85s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .load-text { font: 600 15px / 1.5 inherit; max-width: 320px; } | |
| .load-text .sub { | |
| display: block; | |
| font-weight: 400; | |
| color: rgba(255, 255, 255, 0.65); | |
| margin-top: 4px; | |
| font-size: 13px; | |
| } | |
| .progress-line { | |
| width: 220px; max-width: 60vw; height: 4px; | |
| border-radius: 999px; | |
| background: rgba(255, 255, 255, 0.15); | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; width: 0%; | |
| background: linear-gradient(90deg, var(--nv-green-3), var(--nv-green)); | |
| transition: width 0.3s ease; | |
| } | |
| /* ---------- Floating prompt card (bottom-left) ---------- */ | |
| .prompt-card { | |
| position: absolute; | |
| left: 24px; bottom: 24px; | |
| max-width: min(560px, calc(100% - 48px)); | |
| width: calc(100% - 48px); | |
| background: var(--card-strong); | |
| backdrop-filter: blur(20px) saturate(160%); | |
| -webkit-backdrop-filter: blur(20px) saturate(160%); | |
| border: 1px solid var(--card-border-strong); | |
| border-radius: var(--radius-lg); | |
| padding: 14px; | |
| box-shadow: var(--shadow-lg); | |
| color: var(--ink); | |
| } | |
| .prompt-card-head { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| } | |
| .prompt-card-head .label { | |
| font: 700 12px / 1 inherit; | |
| color: var(--ink); | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| } | |
| .prompt-card-head .nvidia-logo { | |
| width: 22px; height: 22px; | |
| border-radius: 5px; | |
| object-fit: cover; | |
| background: #fff; | |
| padding: 2px; | |
| box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08); | |
| } | |
| .prompt-input { | |
| width: 100%; | |
| background: rgba(255, 255, 255, 0.06); | |
| border: 1.5px solid var(--card-border); | |
| border-radius: 10px; | |
| color: var(--ink); | |
| padding: 11px 13px; | |
| font: 400 14.5px / 1.5 inherit; | |
| resize: none; | |
| min-height: 44px; | |
| max-height: 140px; | |
| transition: border-color 0.15s, background 0.15s; | |
| -webkit-appearance: none; | |
| } | |
| .prompt-input:focus { | |
| outline: none; | |
| border-color: var(--nv-green); | |
| background: rgba(255, 255, 255, 0.10); | |
| } | |
| .prompt-input::placeholder { color: var(--muted); } | |
| .prompt-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 10px; | |
| } | |
| .prompt-actions .spacer { flex: 1; } | |
| /* Primary Generate button */ | |
| .gen-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 0 18px; | |
| height: 38px; | |
| border-radius: 10px; | |
| border: none; | |
| background: var(--nv-green); | |
| color: #0a0a0a; | |
| font: 700 14px / 1 inherit; | |
| letter-spacing: 0.01em; | |
| cursor: pointer; | |
| transition: filter 0.15s, transform 0.08s, box-shadow 0.15s; | |
| box-shadow: 0 4px 14px rgba(118, 185, 0, 0.35); | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| .gen-btn:hover:not(:disabled) { filter: brightness(1.08); box-shadow: 0 6px 18px rgba(118, 185, 0, 0.45); } | |
| .gen-btn:active:not(:disabled) { transform: translateY(1px); } | |
| .gen-btn:disabled { cursor: wait; opacity: 0.65; box-shadow: none; } | |
| .gen-btn .spinner { | |
| width: 14px; height: 14px; | |
| border: 2px solid rgba(10, 10, 10, 0.25); | |
| border-top-color: #0a0a0a; | |
| border-radius: 50%; | |
| animation: spin 0.7s linear infinite; | |
| display: none; | |
| } | |
| .gen-btn.loading .spinner { display: inline-block; } | |
| /* ---------- Floating toolbar (bottom-right): examples + download + stop ---------- */ | |
| .fab-stack { | |
| position: absolute; | |
| right: 24px; bottom: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-end; | |
| gap: 10px; | |
| } | |
| /* Subtle model info link (top-right corner) */ | |
| .info-link { | |
| position: absolute; | |
| top: 18px; right: 18px; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 12px 6px 8px; | |
| border-radius: 999px; | |
| background: var(--card); | |
| backdrop-filter: blur(12px) saturate(140%); | |
| -webkit-backdrop-filter: blur(12px) saturate(140%); | |
| border: 1px solid var(--card-border); | |
| color: var(--ink-soft); | |
| font: 600 12px / 1 inherit; | |
| letter-spacing: 0.01em; | |
| text-decoration: none; | |
| transition: border-color 0.15s, background 0.15s, color 0.15s; | |
| max-width: calc(100% - 36px); | |
| } | |
| .info-link:hover { border-color: var(--nv-green); background: var(--card-strong); color: var(--ink); } | |
| .info-mark { | |
| width: 18px; height: 18px; | |
| border-radius: 4px; | |
| object-fit: cover; | |
| background: #fff; | |
| padding: 1px; | |
| flex-shrink: 0; | |
| } | |
| .info-link .info-meta { | |
| color: var(--muted); | |
| font-weight: 500; | |
| margin-left: 2px; | |
| } | |
| .info-link:hover .info-meta { color: var(--ink-soft); } | |
| @media (max-width: 768px) { | |
| .info-link { top: 12px; right: 12px; font-size: 11.5px; padding: 5px 10px 5px 7px; } | |
| } | |
| .fab { | |
| width: 44px; height: 44px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 14px; | |
| background: var(--card-strong); | |
| backdrop-filter: blur(14px) saturate(140%); | |
| -webkit-backdrop-filter: blur(14px) saturate(140%); | |
| border: 1px solid var(--card-border); | |
| color: var(--ink); | |
| cursor: pointer; | |
| text-decoration: none; | |
| box-shadow: var(--shadow-md); | |
| transition: border-color 0.15s, background 0.15s, color 0.15s; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| .fab:hover { border-color: var(--nv-green); } | |
| .fab:active { transform: translateY(1px); } | |
| .fab svg { width: 20px; height: 20px; display: block; } | |
| .fab.primary { | |
| background: var(--nv-green); | |
| border-color: var(--nv-green); | |
| color: #0a0a0a; | |
| box-shadow: 0 6px 20px rgba(118, 185, 0, 0.4); | |
| } | |
| .fab.primary:hover { filter: brightness(1.08); } | |
| .fab.show { display: inline-flex; } | |
| .fab[hidden] { display: none; } | |
| /* ---------- Popover sheets (examples, advanced, status) ---------- */ | |
| .sheet { | |
| position: absolute; | |
| left: 24px; bottom: 24px; | |
| max-width: min(560px, calc(100% - 48px)); | |
| width: calc(100% - 48px); | |
| background: var(--card-strong); | |
| backdrop-filter: blur(20px) saturate(160%); | |
| -webkit-backdrop-filter: blur(20px) saturate(160%); | |
| border: 1px solid var(--card-border-strong); | |
| border-radius: var(--radius-lg); | |
| padding: 16px; | |
| box-shadow: var(--shadow-lg); | |
| color: var(--ink); | |
| display: none; | |
| animation: sheet-in 0.25s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .sheet.show { display: block; } | |
| @keyframes sheet-in { from { opacity: 0; transform: translateY(8px); } } | |
| .sheet-head { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 12px; | |
| } | |
| .sheet-head .title { | |
| font: 700 13px / 1 inherit; | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| color: var(--ink); | |
| } | |
| .sheet-close { | |
| width: 28px; height: 28px; | |
| display: inline-flex; align-items: center; justify-content: center; | |
| border-radius: 8px; | |
| border: 1px solid var(--card-border); | |
| background: rgba(255, 255, 255, 0.04); | |
| color: var(--ink-soft); | |
| cursor: pointer; | |
| } | |
| .sheet-close:hover { color: var(--ink); border-color: var(--card-border-strong); } | |
| /* Example chips in the sheet */ | |
| .examples-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .examples-grid .chip { | |
| text-align: left; | |
| background: rgba(255, 255, 255, 0.04); | |
| border: 1px solid var(--card-border); | |
| color: var(--ink-soft); | |
| border-radius: 10px; | |
| padding: 10px 12px; | |
| font: 400 12.5px / 1.45 inherit; | |
| cursor: pointer; | |
| transition: border-color 0.15s, background 0.15s, color 0.15s; | |
| } | |
| .examples-grid .chip:hover { border-color: var(--nv-green); background: var(--nv-green-soft); color: var(--ink); } | |
| /* Advanced sheet: rows of sliders/inputs */ | |
| .row { | |
| display: flex; | |
| gap: 12px; | |
| align-items: center; | |
| } | |
| .row > .col { flex: 1; min-width: 0; } | |
| .field-label { | |
| font: 500 11px / 1 inherit; | |
| color: var(--muted); | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| margin-bottom: 6px; | |
| } | |
| .slider-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .slider-val { | |
| font-variant-numeric: tabular-nums; | |
| font: 700 13px / 1 inherit; | |
| color: var(--ink); | |
| min-width: 48px; | |
| text-align: right; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| flex: 1; min-width: 0; | |
| height: 6px; | |
| border-radius: 999px; | |
| background: linear-gradient(90deg, var(--nv-green) 0%, var(--nv-green) var(--fill, 50%), rgba(255, 255, 255, 0.10) var(--fill, 50%), rgba(255, 255, 255, 0.10) 100%); | |
| cursor: pointer; | |
| padding: 0; | |
| border: none; | |
| } | |
| input[type="range"]:focus { outline: none; } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 20px; height: 20px; | |
| border-radius: 50%; | |
| background: #fff; | |
| border: 3px solid var(--nv-green); | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); | |
| cursor: grab; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 20px; height: 20px; | |
| border-radius: 50%; | |
| background: #fff; | |
| border: 3px solid var(--nv-green); | |
| cursor: grab; | |
| } | |
| input[type="text"], input[type="number"], textarea { | |
| width: 100%; | |
| background: rgba(255, 255, 255, 0.06); | |
| border: 1.5px solid var(--card-border); | |
| border-radius: 10px; | |
| color: var(--ink); | |
| padding: 10px 12px; | |
| font: 400 14px / 1.5 inherit; | |
| transition: border-color 0.15s, background 0.15s; | |
| -webkit-appearance: none; | |
| } | |
| input[type="text"]:focus, input[type="number"]:focus, textarea:focus { | |
| outline: none; | |
| border-color: var(--nv-green); | |
| background: rgba(255, 255, 255, 0.10); | |
| } | |
| textarea { resize: vertical; min-height: 60px; } | |
| .check-pill { | |
| display: inline-flex; align-items: center; gap: 8px; | |
| padding: 8px 12px; | |
| border-radius: 999px; | |
| border: 1px solid var(--card-border); | |
| background: rgba(255, 255, 255, 0.04); | |
| color: var(--ink-soft); | |
| font: 500 13px / 1 inherit; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .check-pill input { accent-color: var(--nv-green); width: 14px; height: 14px; } | |
| .check-pill:hover { border-color: var(--card-border-strong); color: var(--ink); } | |
| /* ---------- Status / error toast ---------- */ | |
| .status-toast { | |
| position: absolute; | |
| left: 50%; | |
| top: 18px; | |
| transform: translateX(-50%); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 9px 14px; | |
| border-radius: 999px; | |
| background: var(--card-strong); | |
| backdrop-filter: blur(14px) saturate(140%); | |
| -webkit-backdrop-filter: blur(14px) saturate(140%); | |
| border: 1px solid var(--card-border); | |
| color: var(--ink); | |
| font: 600 13px / 1 inherit; | |
| box-shadow: var(--shadow-md); | |
| max-width: calc(100% - 36px); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .status-toast.show { display: inline-flex; } | |
| .status-toast.err { border-color: #c8362a; color: #ff8b8b; } | |
| .status-toast .pip { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--nv-green); | |
| box-shadow: 0 0 8px var(--nv-green-glow); | |
| flex-shrink: 0; | |
| } | |
| .status-toast.busy .pip { animation: pulse 1.1s infinite; } | |
| .status-toast.err .pip { background: #ff5d5d; box-shadow: 0 0 8px rgba(255, 93, 93, 0.5); } | |
| .status-toast.ok .pip { background: var(--nv-green); } | |
| @keyframes pulse { 50% { opacity: 0.45; } } | |
| /* ---------- Mobile ---------- */ | |
| @media (max-width: 768px) { | |
| .prompt-card, .sheet { | |
| left: 12px; right: 12px; bottom: 12px; | |
| max-width: none; | |
| width: auto; | |
| } | |
| .fab-stack { right: 12px; bottom: 12px; } | |
| .examples-grid { grid-template-columns: 1fr; } /* stack chips vertically */ | |
| } | |
| @media (max-width: 420px) { | |
| .prompt-card { padding: 12px; } | |
| .gen-btn { padding: 0 14px; height: 36px; font-size: 13.5px; } | |
| .fab { width: 42px; height: 42px; border-radius: 12px; } | |
| } | |
| /* Reduced motion */ | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { animation-duration: 0.001ms ; transition-duration: 0.001ms ; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="stage" aria-label="Generated image stage"> | |
| <!-- Result image (shown after first generation) --> | |
| <img class="result-img" id="resultImg" alt="Generated image" /> | |
| <!-- Empty-state placeholder --> | |
| <div class="placeholder" id="placeholder"> | |
| <div class="placeholder-inner"> | |
| <span class="emoji" aria-hidden="true">🐢</span> | |
| <h2>Generate at the speed of thought.</h2> | |
| <p>Four-step text-to-image from <b>nvidia/Qwen-Image-Flash</b>. Type a prompt, hit <b>Generate</b>, and the image fills this space.</p> | |
| </div> | |
| </div> | |
| <!-- Loading overlay --> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="ring"></div> | |
| <div class="load-text" id="loadText"> | |
| Queued — waiting for a GPU… | |
| <span class="sub" id="loadSub">First run after build can take a few minutes to warm up CUDA.</span> | |
| </div> | |
| <div class="progress-line"><div class="progress-fill" id="progressFill"></div></div> | |
| </div> | |
| <!-- Status toast (errors + brief status) --> | |
| <div class="status-toast" id="statusToast" role="status" aria-live="polite" hidden> | |
| <span class="pip"></span> | |
| <span class="text" id="statusText">Ready</span> | |
| </div> | |
| <!-- Floating prompt card --> | |
| <section class="prompt-card" id="promptCard" aria-label="Prompt"> | |
| <div class="prompt-card-head"> | |
| <img class="nvidia-logo" | |
| src="https://cdn-avatars.huggingface.co/v1/production/uploads/65df9200dc3292a8983e5017/Vs5FPVCH-VZBipV3qKTuy.png" | |
| alt="NVIDIA" | |
| draggable="false" /> | |
| <span class="label">Qwen-Image-Flash</span> | |
| </div> | |
| <textarea | |
| id="prompt" | |
| class="prompt-input" | |
| rows="2" | |
| autofocus>A red fox in a snowy pine forest at golden hour, photorealistic, sharp focus, soft bokeh</textarea> | |
| <div class="prompt-actions"> | |
| <span class="spacer"></span> | |
| <button class="gen-btn" id="generateBtn" aria-label="Generate"> | |
| <span class="spinner"></span> | |
| <span class="label">Generate</span> | |
| </button> | |
| </div> | |
| </section> | |
| <!-- Examples sheet (auto-opens on first visit; click any chip to load it) --> | |
| <section class="sheet" id="examplesSheet" aria-label="Example prompts" hidden> | |
| <div class="sheet-head"> | |
| <span class="title">Try a prompt</span> | |
| <button class="sheet-close" data-close="examplesSheet" aria-label="Close"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg> | |
| </button> | |
| </div> | |
| <div class="examples-grid"> | |
| <button class="chip">A red fox in a snowy pine forest at golden hour, photorealistic, sharp focus, soft bokeh</button> | |
| <button class="chip">A neon-lit cyberpunk street market at night, rain reflecting on wet pavement, cinematic</button> | |
| <button class="chip">A watercolor painting of a koi fish swimming among lotus flowers, soft pastel palette</button> | |
| <button class="chip">A steaming bowl of ramen, top-down flat lay, chopsticks resting on the side, moody lighting</button> | |
| <button class="chip">A lone astronaut standing on a dune under two moons, vast alien desert, wide angle</button> | |
| <button class="chip">A vintage botanical illustration of exotic orchids, ink and watercolor, parchment background</button> | |
| </div> | |
| </section> | |
| <!-- Floating download button (bottom-right) --> | |
| <div class="fab-stack" aria-label="Actions"> | |
| <a class="fab" id="downloadFab" href="#" download="qwen-image-flash.png" aria-label="Download PNG" hidden> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/> | |
| </svg> | |
| </a> | |
| </div> | |
| <!-- Subtle bottom-right info link (text, not a button) --> | |
| <a class="info-link" href="https://huggingface.co/nvidia/Qwen-Image-Flash" target="_blank" rel="noopener"> | |
| <img class="info-mark" | |
| src="https://cdn-avatars.huggingface.co/v1/production/uploads/65df9200dc3292a8983e5017/Vs5FPVCH-VZBipV3qKTuy.png" | |
| alt="" | |
| aria-hidden="true" | |
| draggable="false" /> | |
| <span><b style="color: var(--ink);">nvidia/Qwen-Image-Flash</b></span> | |
| <span class="info-meta">20B params · 4-step · DMD2 · Apache-2.0</span> | |
| </a> | |
| </main> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const promptEl = $("prompt"), | |
| generateBtn = $("generateBtn"), | |
| examplesSheet = $("examplesSheet"), | |
| placeholder = $("placeholder"), | |
| loadingOverlay = $("loadingOverlay"), | |
| resultImg = $("resultImg"), | |
| downloadFab = $("downloadFab"), | |
| statusToast = $("statusToast"), | |
| statusText = $("statusText"), | |
| loadText = $("loadText"), | |
| loadSub = $("loadSub"), | |
| progressFill = $("progressFill"), | |
| promptCard = $("promptCard"); | |
| // ---- Defaults (advanced controls removed; backend always gets tested settings) ---- | |
| const DEFAULTS = { | |
| negative_prompt: "", | |
| width: 1024, | |
| height: 1024, | |
| num_inference_steps: 4, | |
| randomize_seed: true, | |
| }; | |
| let lastUsedSeed = 42; | |
| // ---- Examples sheet (auto-opens on first visit) ---- | |
| const openExamples = () => { examplesSheet.hidden = false; }; | |
| const closeExamples = () => { examplesSheet.hidden = true; }; | |
| openExamples(); | |
| document.querySelectorAll("#examplesSheet .chip").forEach((chip) => { | |
| chip.addEventListener("click", () => { | |
| promptEl.value = chip.textContent; | |
| closeExamples(); | |
| promptEl.focus(); | |
| }); | |
| }); | |
| document.querySelectorAll("[data-close]").forEach((btn) => { | |
| btn.addEventListener("click", () => { $(btn.dataset.close).hidden = true; }); | |
| }); | |
| // Esc closes the examples sheet | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key === "Escape") closeExamples(); | |
| }); | |
| // ---- Client ---- | |
| let client = null; | |
| async function getClient() { | |
| if (!client) client = await Client.connect(window.location.origin); | |
| return client; | |
| } | |
| function setStatus(state, text) { | |
| statusToast.classList.remove("busy", "ok", "err"); | |
| if (state === "busy") statusToast.classList.add("busy"); | |
| if (state === "ok") statusToast.classList.add("ok"); | |
| if (state === "err") statusToast.classList.add("err"); | |
| statusText.textContent = text; | |
| statusToast.hidden = false; | |
| } | |
| function clearStatus() { statusToast.hidden = true; } | |
| async function generate() { | |
| const p = promptEl.value.trim(); | |
| if (!p) { | |
| setStatus("err", "Prompt cannot be empty."); | |
| promptEl.focus(); | |
| openExamples(); // re-show examples so the user has a starting point | |
| return; | |
| } | |
| closeExamples(); | |
| generateBtn.classList.add("loading"); | |
| generateBtn.disabled = true; | |
| placeholder.style.display = "none"; | |
| resultImg.classList.remove("show"); | |
| downloadFab.hidden = true; | |
| loadingOverlay.classList.add("show"); | |
| progressFill.style.width = "6%"; | |
| setStatus("busy", "Connecting to backend…"); | |
| loadSub.textContent = "First run after build can take a few minutes to warm up CUDA."; | |
| try { | |
| const c = await getClient(); | |
| loadText.firstChild.textContent = "Queued — generating image (4 steps)… "; | |
| progressFill.style.width = "35%"; | |
| const result = await c.predict("/generate_image", { | |
| prompt: p, | |
| negative_prompt: DEFAULTS.negative_prompt, | |
| width: DEFAULTS.width, | |
| height: DEFAULTS.height, | |
| num_inference_steps: DEFAULTS.num_inference_steps, | |
| seed: DEFAULTS.randomize_seed ? Math.floor(Math.random() * 2147483647) : lastUsedSeed, | |
| randomize_seed: DEFAULTS.randomize_seed, | |
| }); | |
| progressFill.style.width = "100%"; | |
| const [fileData, usedSeed, info] = result.data; | |
| if (usedSeed !== undefined && usedSeed !== null) { | |
| lastUsedSeed = usedSeed; | |
| } | |
| const url = (fileData && (fileData.url || (fileData.path ? window.location.origin + "/file=" + fileData.path : null))) || null; | |
| if (!url) throw new Error("No image returned from backend."); | |
| resultImg.src = url; | |
| resultImg.onload = () => { | |
| loadingOverlay.classList.remove("show"); | |
| resultImg.classList.add("show"); | |
| downloadFab.href = url; | |
| downloadFab.hidden = false; | |
| setStatus("ok", info || `Done · seed ${usedSeed ?? ""}`); | |
| setTimeout(() => { if (statusToast.classList.contains("ok")) clearStatus(); }, 4000); | |
| }; | |
| } catch (err) { | |
| console.error(err); | |
| loadingOverlay.classList.remove("show"); | |
| progressFill.style.width = "0%"; | |
| placeholder.style.display = "flex"; | |
| setStatus("err", err?.message?.replace(/^Error: ?/, "") || "Something went wrong."); | |
| } finally { | |
| generateBtn.classList.remove("loading"); | |
| generateBtn.disabled = false; | |
| } | |
| } | |
| generateBtn.addEventListener("click", generate); | |
| promptEl.addEventListener("keydown", (e) => { | |
| if ((e.metaKey || e.ctrlKey) && e.key === "Enter") generate(); | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| // Submit on plain Enter for a quick "tap → image" feel | |
| e.preventDefault(); | |
| generate(); | |
| } | |
| }); | |
| // ---- Auto-grow the prompt textarea as the user types ---- | |
| const autoGrow = () => { | |
| promptEl.style.height = "auto"; | |
| promptEl.style.height = Math.min(promptEl.scrollHeight, 140) + "px"; | |
| }; | |
| promptEl.addEventListener("input", autoGrow); | |
| autoGrow(); | |
| </script> | |
| </body> | |
| </html> |