flux2-webgpu / static.html
ryanhlewis's picture
Fix local model base handling
9e3c544 verified
Raw
History Blame Contribute Delete
24.8 kB
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FLUX.2 Static WebGPU</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=Inter:wght@400;500;600;700;800&family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,500,0,0&display=swap" rel="stylesheet">
<style>
:root {
color-scheme: dark;
--bg: #101114;
--panel: #17191f;
--panel-2: #1f232b;
--panel-3: #262b35;
--stage: #0b0c0f;
--text: #f2f4f8;
--muted: #aab2c0;
--line: #333a48;
--line-strong: #465063;
--accent: #18b7a7;
--accent-strong: #42d8c8;
--accent-soft: rgba(24, 183, 167, 0.18);
--danger: #ff766d;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.34);
--radius: 8px;
}
:root[data-theme="light"] {
color-scheme: light;
--bg: #eef1f5;
--panel: #ffffff;
--panel-2: #f7f9fc;
--panel-3: #edf1f6;
--stage: #f8fafc;
--text: #172033;
--muted: #566274;
--line: #d8dee8;
--line-strong: #c4cedc;
--accent: #087f7a;
--accent-strong: #066c67;
--accent-soft: rgba(8, 127, 122, 0.13);
--danger: #b42318;
--shadow: 0 18px 40px rgba(27, 38, 55, 0.12);
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
letter-spacing: 0;
}
main {
display: grid;
grid-template-columns: minmax(340px, 456px) minmax(0, 1fr);
min-height: 100vh;
}
aside {
position: relative;
border-right: 1px solid var(--line);
background: var(--panel);
padding: 18px;
box-shadow: var(--shadow);
z-index: 1;
}
section {
padding: 18px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 18px;
background:
linear-gradient(90deg, color-mix(in srgb, var(--line) 28%, transparent) 1px, transparent 1px),
linear-gradient(0deg, color-mix(in srgb, var(--line) 28%, transparent) 1px, transparent 1px),
var(--bg);
background-size: 28px 28px;
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.settings-popover {
position: absolute;
top: 68px;
left: 18px;
right: 18px;
z-index: 20;
padding: 12px;
border: 1px solid var(--line-strong);
border-radius: var(--radius);
background: var(--panel);
box-shadow: var(--shadow);
}
.settings-popover[hidden] {
display: none;
}
.settings-popover::before {
content: "";
position: absolute;
top: -7px;
right: 58px;
width: 12px;
height: 12px;
border-left: 1px solid var(--line-strong);
border-top: 1px solid var(--line-strong);
background: var(--panel);
transform: rotate(45deg);
}
.settings-popover h2 {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.2;
font-weight: 800;
}
h1 {
margin: 0;
font-size: 19px;
line-height: 1.2;
letter-spacing: 0;
font-weight: 800;
}
label {
display: block;
margin: 10px 0 6px;
color: var(--muted);
font-weight: 650;
font-size: 12px;
}
textarea,
input,
select {
width: 100%;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
color: var(--text);
font: inherit;
outline: none;
transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
}
textarea:hover,
input:hover,
select:hover {
border-color: var(--line-strong);
}
textarea:focus,
input:focus,
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
textarea {
min-height: 124px;
resize: vertical;
padding: 12px;
line-height: 1.4;
}
input,
select {
height: 42px;
padding: 0 12px;
}
select {
appearance: none;
background-image:
linear-gradient(45deg, transparent 50%, var(--muted) 50%),
linear-gradient(135deg, var(--muted) 50%, transparent 50%);
background-position:
calc(100% - 17px) 18px,
calc(100% - 12px) 18px;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 34px;
}
input[type="file"] {
height: auto;
padding: 10px;
}
input[type="range"] {
padding: 0;
accent-color: var(--accent);
}
.range-row {
display: grid;
grid-template-columns: 1fr 96px;
align-items: center;
gap: 10px;
}
.resolution-row {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: end;
gap: 8px;
}
.icon {
font-family: "Material Symbols Rounded";
font-weight: 500;
font-style: normal;
font-size: 21px;
line-height: 1;
display: inline-block;
text-transform: none;
letter-spacing: 0;
white-space: nowrap;
font-feature-settings: "liga";
-webkit-font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
}
button.icon-button {
width: 42px;
min-height: 42px;
padding: 0;
display: grid;
place-items: center;
}
.resolution-advanced {
display: grid;
grid-template-columns: minmax(0, 1fr) 42px minmax(0, 1fr);
gap: 10px;
align-items: end;
}
.aspect-lock {
align-self: end;
}
.aspect-lock[aria-pressed="true"] {
border-color: var(--accent);
color: var(--accent-strong);
background: var(--accent-soft);
}
.aspect-row {
grid-column: 1 / -1;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.img2img-panel {
margin-top: 2px;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: color-mix(in srgb, var(--panel-2) 72%, transparent);
}
.field-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.field-title {
display: block;
font-size: 13px;
font-weight: 800;
color: var(--text);
}
.field-subtitle {
display: block;
margin-top: 2px;
font-size: 12px;
color: var(--muted);
}
.file-picker {
min-height: 42px;
margin: 0;
padding: 0 12px;
border: 1px dashed var(--line-strong);
border-radius: var(--radius);
background: var(--panel-2);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
}
.file-picker:hover {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.source-preview {
display: grid;
grid-template-columns: 64px 1fr;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 8px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
color: var(--muted);
font-size: 12px;
}
.source-preview[hidden] {
display: none;
}
.source-preview img {
width: 64px;
height: 64px;
max-height: none;
border: 1px solid var(--line);
border-radius: var(--radius);
object-fit: cover;
background: var(--panel-3);
}
.img2img-controls {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 10px;
margin-top: 10px;
}
.img2img-controls .range-row {
grid-template-columns: 1fr 88px;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.wide { grid-column: 1 / -1; }
.actions {
display: flex;
gap: 10px;
margin-top: 18px;
align-items: center;
flex-wrap: wrap;
}
.actions[hidden] {
display: none;
}
.download-gate {
margin-top: 18px;
padding: 14px;
border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
border-radius: var(--radius);
background: color-mix(in srgb, var(--accent-soft) 74%, var(--panel-2));
}
.download-gate[hidden] {
display: none;
}
.download-gate-title {
font-size: 13px;
font-weight: 850;
color: var(--text);
}
.download-gate button {
margin-top: 12px;
width: 100%;
}
button {
min-height: 42px;
border: 0;
border-radius: var(--radius);
padding: 0 14px;
background: var(--accent);
color: #061715;
font: inherit;
font-weight: 750;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: transform 120ms ease, filter 120ms ease, background-color 140ms ease, color 140ms ease;
}
button:hover:not(:disabled) {
filter: brightness(1.06);
transform: translateY(-1px);
}
button.secondary {
background: var(--panel-3);
color: var(--text);
border: 1px solid var(--line);
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
transform: none;
}
.elapsed-meter {
min-height: 42px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
color: var(--muted);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 750;
min-width: 96px;
}
.elapsed-meter.active {
border-color: var(--accent);
color: var(--accent-strong);
background: var(--accent-soft);
}
.load-progress {
margin: 14px 0 0;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
}
.load-progress[hidden] {
display: none;
}
.load-progress-head,
.load-progress-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.load-progress-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 800;
color: var(--text);
}
.load-progress-summary {
flex: 0 0 auto;
color: var(--accent-strong);
font-size: 12px;
font-weight: 800;
}
.load-progress-track {
position: relative;
height: 9px;
margin: 10px 0 8px;
overflow: hidden;
border-radius: 999px;
background: color-mix(in srgb, var(--line) 55%, transparent);
}
.load-progress-fill {
width: 0%;
height: 100%;
border-radius: inherit;
background: var(--accent);
transition: width 160ms ease;
}
.load-progress-track.indeterminate .load-progress-fill {
width: 42%;
animation: load-progress-sweep 1.15s ease-in-out infinite;
}
@keyframes load-progress-sweep {
0% { transform: translateX(-110%); }
100% { transform: translateX(245%); }
}
.load-progress-meta {
color: var(--muted);
font: 11px/1.35 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
.load-progress-detail {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.load-progress-rate {
flex: 0 0 auto;
color: var(--muted);
white-space: nowrap;
}
pre {
margin: 16px 0 0;
padding: 12px;
min-height: 132px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
color: var(--muted);
white-space: pre-wrap;
overflow-wrap: anywhere;
font: 12px/1.45 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
.stage {
min-height: calc(100vh - 36px);
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--stage);
box-shadow: var(--shadow);
overflow: hidden;
}
img,
canvas {
display: block;
max-width: 100%;
max-height: calc(100vh - 64px);
object-fit: contain;
}
.placeholder {
color: var(--muted);
font-size: 13px;
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
}
.gallery-panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
box-shadow: var(--shadow);
padding: 14px;
}
.gallery-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.gallery-header h2 {
margin: 0;
font-size: 14px;
line-height: 1.2;
font-weight: 800;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.gallery-card {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-2);
overflow: hidden;
}
.gallery-card img {
width: 100%;
max-height: none;
aspect-ratio: 1 / 1;
object-fit: cover;
border-bottom: 1px solid var(--line);
background: var(--stage);
}
.gallery-body {
padding: 10px;
}
.gallery-meta {
display: grid;
gap: 3px;
margin-bottom: 8px;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.gallery-prompt {
color: var(--text);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.gallery-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.gallery-actions button {
min-height: 32px;
padding: 0 8px;
font-size: 12px;
}
.error { color: var(--danger); }
@media (max-width: 860px) {
main { grid-template-columns: 1fr; }
aside { border-right: 0; border-bottom: 1px solid var(--line); }
.stage { min-height: 55vh; }
}
</style>
</head>
<body>
<main>
<aside>
<div class="app-header">
<h1>FLUX.2 Static WebGPU</h1>
<div class="header-actions">
<button id="settings-toggle" class="secondary icon-button" type="button" aria-label="Open generation settings" aria-expanded="false" aria-controls="settings-popover">
<span class="icon">tune</span>
</button>
<button id="theme-toggle" class="secondary icon-button" type="button" aria-label="Switch to light mode">
<span id="theme-icon" class="icon">light_mode</span>
</button>
</div>
</div>
<div id="settings-popover" class="settings-popover" hidden>
<h2>Generation settings</h2>
<label for="backend">Transformer</label>
<select id="backend">
<option value="custom-lowbit-webgpu" selected>Custom low-bit WebGPU (3.5 GB)</option>
<option value="onnx-webgpu">ONNX WebGPU (5.2 GB)</option>
</select>
<label for="speed-profile">Speed Profile</label>
<select id="speed-profile">
<option value="progressive-fast" selected>Fast default (exact)</option>
<option value="subsecond-final">Subsecond final</option>
<option value="subsecond-ultra">Subsecond ultra preview</option>
<option value="subsecond-instant">Subsecond instant sketch</option>
<option value="progressive">Progressive preview + refine</option>
<option value="auto">Auto coherent fast</option>
<option value="quality">Quality q4</option>
<option value="mlp-dp4a">Experimental MLP DP4A</option>
<option value="all-dp4a">Experimental fastest DP4A</option>
<option value="turbo-cache">Turbo cache 2x approx</option>
<option value="ab-cache">AB cache 2x approx</option>
<option value="tail-cache">Block tail cache approx</option>
<option value="tail-cache-lite">Block tail cache lite approx</option>
<option value="tail-cache-mid">Block tail cache mid approx</option>
<option value="tail-cache-fast">Block tail cache fast approx</option>
<option value="subsecond-cache">Subsecond cache 4x approx</option>
<option value="subsecond-preview">Subsecond preview auto + text trim</option>
</select>
</div>
<label for="prompt">Prompt</label>
<textarea id="prompt">A small ceramic robot on a workbench, product photo lighting</textarea>
<div class="grid">
<div class="wide">
<label for="resolution-preset">Resolution</label>
<div class="resolution-row">
<select id="resolution-preset">
<option value="256" selected>256 x 256</option>
<option value="512">512 x 512</option>
<option value="768">768 x 768</option>
<option value="1024">1024 x 1024</option>
<option value="custom">Custom</option>
</select>
</div>
</div>
<div id="resolution-advanced" class="wide resolution-advanced" hidden>
<div><label for="width">Width</label><input id="width" type="number" min="16" step="16" value="256"></div>
<button id="aspect-lock" class="secondary icon-button aspect-lock" type="button" aria-label="Unlock aspect ratio" aria-pressed="true"><span id="aspect-lock-icon" class="icon">link</span></button>
<div><label for="height">Height</label><input id="height" type="number" min="16" step="16" value="256"></div>
<div class="aspect-row">
<label for="aspect-ratio">Aspect</label>
<select id="aspect-ratio">
<option value="custom">Current aspect</option>
<option value="1:1" selected>1:1 square</option>
<option value="4:3">4:3 landscape</option>
<option value="3:4">3:4 portrait</option>
<option value="16:9">16:9 wide</option>
<option value="9:16">9:16 vertical</option>
<option value="free">Free / unlocked</option>
</select>
</div>
</div>
<div><label for="steps">Steps</label><input id="steps" type="number" min="1" max="4" value="4"></div>
<div><label for="seed">Seed</label><input id="seed" type="number" value="-1"></div>
<div class="wide img2img-panel">
<div class="field-heading">
<div>
<span class="field-title">Image to image</span>
<span class="field-subtitle">Optional source image; leave empty for text to image.</span>
</div>
<button id="clear-init-image" class="secondary icon-button" type="button" aria-label="Remove input image" hidden><span class="icon">close</span></button>
</div>
<label class="file-picker" for="init-image">
<span class="icon">add_photo_alternate</span>
<span id="init-image-label">Choose optional image</span>
</label>
<input id="init-image" class="visually-hidden" type="file" accept="image/*">
<div id="source-preview-wrap" class="source-preview" hidden>
<img id="source-preview" alt="Input image preview">
<div id="source-meta"></div>
</div>
<div id="img2img-controls" class="img2img-controls" hidden>
<div>
<label for="strength">Strength</label>
<div class="range-row">
<input id="strength" type="range" min="0" max="1" step="0.01" value="0.65">
<span id="strength-value">0.65 -> 0.93</span>
</div>
</div>
<div>
<label for="strength-curve">Edit behavior</label>
<select id="strength-curve">
<option value="edit" selected>Prompt edit</option>
<option value="strong">Strong edit boost</option>
<option value="balanced">Balanced</option>
<option value="linear">Preserve source</option>
<option value="rewrite">Prompt dominant</option>
</select>
</div>
<div>
<label for="fit-mode">Source fit</label>
<select id="fit-mode">
<option value="cover" selected>Cover canvas</option>
<option value="contain">Contain image</option>
<option value="stretch">Stretch to canvas</option>
</select>
</div>
</div>
</div>
<input id="decode-max" type="hidden" value="0">
<input id="render-max" type="hidden" value="0">
</div>
<div id="download-gate" class="download-gate">
<div id="download-gate-title" class="download-gate-title">Download model assets (3.5 GB)</div>
<button id="start-download" type="button"><span class="icon">download</span>Start download</button>
</div>
<div id="actions" class="actions" hidden>
<button id="run" type="button" disabled><span class="icon">auto_awesome</span>Generate</button>
<button id="abort" class="secondary" type="button" disabled><span class="icon">stop</span>Stop</button>
<div id="elapsed" class="elapsed-meter" aria-live="polite">0.0s</div>
</div>
<div id="load-progress" class="load-progress" aria-live="polite" hidden>
<div class="load-progress-head">
<div id="load-progress-title" class="load-progress-title">Loading</div>
<div id="load-progress-summary" class="load-progress-summary">0%</div>
</div>
<div id="load-progress-track" class="load-progress-track">
<div id="load-progress-fill" class="load-progress-fill"></div>
</div>
<div class="load-progress-meta">
<div id="load-progress-detail" class="load-progress-detail">Starting</div>
<div id="load-progress-rate" class="load-progress-rate">0 MiB/s</div>
</div>
</div>
<pre id="status">Loading</pre>
</aside>
<section>
<div class="stage">
<img id="image" alt="Generated image" hidden>
<canvas id="image-canvas" hidden></canvas>
<div id="placeholder" class="placeholder">Ready</div>
</div>
<div id="gallery-panel" class="gallery-panel" hidden>
<div class="gallery-header">
<h2>Generation history</h2>
<button id="clear-gallery" class="secondary" type="button"><span class="icon">delete</span>Clear</button>
</div>
<div id="gallery-grid" class="gallery-grid"></div>
</div>
</section>
</main>
<script src="space-config.js"></script>
<script type="module">
const appVersion = new URLSearchParams(window.location.search).get("cacheBust") || "20260606-backend-download-gate";
await import(`./static-app.js?v=${encodeURIComponent(appVersion)}`);
</script>
</body>
</html>