masterofaudio2077's picture
Change Defaults
3552f92
Raw
History Blame Contribute Delete
12.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Senior LDM ✦ Latent Diffusion</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
:root { --purple:#c9a0ff; --deep:#6a0dad; --panel:#16161f; --line:#3a3a4a; }
* { box-sizing:border-box; }
body {
margin:0; background:#0d0d0d; color:#e0e0e0;
font-family:system-ui, sans-serif;
}
#app { max-width:1000px; margin:0 auto; padding:18px; }
header { text-align:center; padding:14px 0 6px; }
header h1 {
font-family:'Press Start 2P', monospace; font-size:1.25rem;
color:var(--purple); letter-spacing:2px; margin:0 0 10px;
}
.gallery-label {
font-family:'Press Start 2P', monospace; font-size:.55rem;
color:#888; margin:28px 0 8px;
}
.gallery {
display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:10px;
}
.g-card {
margin:0; background:var(--panel); border:1px solid var(--line);
border-radius:6px; overflow:hidden; aspect-ratio:1/1;
}
.g-card img { width:100%; height:100%; object-fit:cover; image-rendering:auto; display:block; }
.ideas-label {
font-family:'Press Start 2P', monospace; font-size:.55rem;
color:#888; margin:16px 0 6px;
}
.chips { line-height:2; }
.chip {
display:inline-block; background:#1e1e2e; border:1px solid var(--line);
border-radius:4px; padding:4px 8px; margin:3px; font-size:.72rem;
cursor:pointer; transition:background .15s, border-color .15s, color .15s;
}
.chip:hover { background:#2e2e4e; border-color:var(--purple); color:var(--purple); }
.cols { display:flex; gap:18px; margin-top:16px; flex-wrap:wrap; }
.col { flex:1 1 360px; }
label.field { display:block; font-size:.8rem; color:#aaa; margin:12px 0 4px; }
textarea, input[type=range] { width:100%; }
textarea {
background:var(--panel); color:#eee; border:1px solid var(--line);
border-radius:6px; padding:10px; font-size:.95rem; resize:vertical; min-height:90px;
}
.seed-row { display:flex; align-items:center; gap:10px; }
.seed-row output { font-family:'Press Start 2P', monospace; font-size:.6rem; color:var(--purple); min-width:48px; text-align:right; }
.select {
width:100%; background:var(--panel); color:#eee; border:1px solid var(--line);
border-radius:6px; padding:8px; font-size:.85rem;
}
button.go {
margin-top:16px; width:100%; padding:12px;
font-family:'Press Start 2P', monospace; font-size:.7rem;
color:#fff; background:linear-gradient(90deg,var(--deep),var(--purple));
border:none; border-radius:6px; cursor:pointer; letter-spacing:1px;
}
button.go:disabled { opacity:.5; cursor:not-allowed; }
.viewer {
aspect-ratio:1/1; width:100%;
background:#0a0a0a; border:1px solid var(--line); border-radius:8px;
display:flex; align-items:center; justify-content:center; overflow:hidden;
}
.viewer img { width:100%; height:100%; object-fit:contain; image-rendering:auto; }
.viewer.empty span { color:#444; font-size:.9rem; }
/* progress zone */
#progress-zone { position:relative; height:84px; margin-top:26px; }
#rem-sprite {
position:absolute; top:0; left:0%; width:56px;
image-rendering:pixelated; z-index:10; transition:none;
}
#progress-track {
position:absolute; bottom:0; left:0; width:100%; height:22px;
background:#1a1a2e; border:2px solid var(--line); border-radius:2px; overflow:hidden;
}
#progress-fill {
height:100%; width:0%;
background:linear-gradient(90deg,var(--deep),var(--purple));
transition:width .35s linear;
}
#progress-label {
position:absolute; bottom:4px; right:8px;
font-family:'Press Start 2P', monospace; font-size:.5rem; color:#fff;
pointer-events:none; z-index:11;
}
</style>
</head>
<body>
<div id="app">
<header>
<h1>Senior LDM &nbsp;✦&nbsp; Latent Diffusion</h1>
</header>
<div class="ideas-label">Prompt ideas β€” click to use</div>
<div class="chips">
<span class="chip" v-for="p in cfg.prompts" :key="p" @click="prompt = p">{{ p }}</span>
</div>
<div class="cols">
<div class="col">
<label class="field">Prompt</label>
<textarea v-model="prompt" placeholder="describe an image..." :disabled="busy"></textarea>
<label class="field">Seed</label>
<div class="seed-row">
<input type="range" min="0" max="9999" step="1" v-model.number="seed" :disabled="busy">
<output>{{ seed }}</output>
</div>
<label class="field">Denoising steps</label>
<div class="seed-row">
<input type="range" min="1" :max="cfg.max_steps" step="1" v-model.number="steps" :disabled="busy">
<output>{{ steps }}</output>
</div>
<label class="field">Guidance scale</label>
<div class="seed-row">
<input type="range" min="0" max="20" step="0.5" v-model.number="guidance" :disabled="busy">
<output>{{ guidance.toFixed(1) }}</output>
</div>
<label class="field">CFG rescale</label>
<div class="seed-row">
<input type="range" min="0" max="1" step="0.05" v-model.number="guidanceRescale" :disabled="busy">
<output>{{ guidanceRescale.toFixed(2) }}</output>
</div>
<label class="field">Timestep spacing</label>
<select class="select" v-model="spacing" :disabled="busy">
<option v-for="s in cfg.spacings" :key="s" :value="s">{{ s }}</option>
</select>
<button class="go" @click="run" :disabled="busy || !prompt.trim()">
{{ busy ? 'Generating…' : 'Generate' }}
</button>
</div>
<div class="col">
<label class="field">Generated image</label>
<div class="viewer" :class="{empty:!image}">
<img v-if="image" :src="image" alt="">
<span v-else>β€”</span>
</div>
</div>
</div>
<div id="progress-zone">
<img id="rem-sprite" :src="remSrc" alt="rem">
<div id="progress-track"><div id="progress-fill" :style="{width: fillPct + '%'}"></div></div>
<div id="progress-label">{{ label }}</div>
</div>
<div class="gallery-label" v-if="cfg.gallery.length">Gallery β€” sample generations</div>
<div class="gallery">
<figure class="g-card" v-for="g in cfg.gallery" :key="g.url">
<img :src="g.url" :alt="g.label" :title="g.label" loading="lazy">
</figure>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, ref, reactive, computed, onMounted } = Vue;
createApp({
setup() {
const cfg = reactive({ sprites:null, prompts:[], gallery:[], num_steps:10,
max_steps:200, guidance:7.5, guidance_rescale:0.5,
spacing:'trailing', spacings:['trailing'] });
const prompt = ref('');
const seed = ref(42);
const steps = ref(10);
const guidance = ref(7.5);
const guidanceRescale = ref(0.5);
const spacing = ref('trailing');
const busy = ref(false);
const image = ref(null); // single generated image
const fillPct = ref(0);
const label = ref('Step 0 / 0');
const remSrc = ref('');
// ── animation controller ────────────────────────────────────────────
let frame = 0, frameTimer = null, state = 'idle';
let pos = 0, dir = 1, dashing = false, completed = false;
function setFrame() {
const arr = cfg.sprites[state];
remSrc.value = arr[frame % arr.length];
}
function spriteEl() { return document.getElementById('rem-sprite'); }
function applyTransform() {
const el = spriteEl();
if (el) el.style.transform = dir < 0 ? 'scaleX(-1)' : 'scaleX(1)';
}
function setTransition(css) {
const el = spriteEl();
if (el) el.style.transition = css;
}
function startAnim(newState, ms) {
if (frameTimer) clearInterval(frameTimer);
state = newState; frame = 0; setFrame(); applyTransform();
frameTimer = setInterval(() => { frame++; setFrame(); }, ms);
}
function placeSprite() {
const el = spriteEl();
if (el) el.style.left = pos + '%';
}
function idle() {
dashing = false; dir = 1; pos = 0;
setTransition('none'); placeSprite();
startAnim('idle', 700);
}
// Each new generation: a quick fast run to the far left, then face right
// and run rightward chasing the progress bar (no backward walk / glitch).
function dashLeftThenRun() {
dashing = true;
dir = -1; startAnim('run', 70); // face left, fast frames
setTransition('left .3s linear');
spriteEl() && spriteEl().offsetWidth; // force reflow
pos = 0; placeSprite();
setTimeout(() => {
setTransition('none');
dir = 1; applyTransform();
startAnim('run', 120); // now running rightward
dashing = false;
}, 320);
}
function onStep(step, total) {
if (dashing) return; // let the dash finish first
const pct = step / total;
const ms = Math.round(140 - 85 * pct); // speed ramps up
if (state !== 'run') startAnim('run', ms);
else { clearInterval(frameTimer); frameTimer = setInterval(() => { frame++; setFrame(); }, ms); }
if (dir !== 1) { dir = 1; applyTransform(); } // always rightward
const target = pct * 96 + 2; // chase the fill bar
pos += (target - pos) * 0.5;
if (pos > 97) pos = 97;
if (pos < 2) pos = 2;
placeSprite();
}
function onDone() {
completed = true;
dashing = false;
if (frameTimer) clearInterval(frameTimer);
state = 'done'; frame = 0; setFrame();
dir = 1; applyTransform();
setTransition('none');
pos = 94; placeSprite();
}
// ── streaming generation ────────────────────────────────────────────
async function run() {
if (busy.value || !prompt.value.trim()) return;
busy.value = true;
completed = false;
fillPct.value = 0;
label.value = 'Step 0 / ' + steps.value;
dashLeftThenRun();
try {
const resp = await fetch('/api/generate', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
prompt: prompt.value.trim(), seed: seed.value,
steps: steps.value, guidance: guidance.value,
guidance_rescale: guidanceRescale.value, spacing: spacing.value,
}),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream:true });
let nl;
while ((nl = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, nl).trim();
buf = buf.slice(nl + 1);
if (!line) continue;
handle(JSON.parse(line));
}
}
} catch (e) {
label.value = 'Error: ' + e.message;
} finally {
busy.value = false;
// Stream ended without a 'done' (error/disconnect): stop the run loop so
// the sprite animation can't keep cycling (and re-requesting) forever.
if (!completed) idle();
}
}
function handle(msg) {
if (msg.type === 'error') { label.value = 'Error: ' + msg.message; return; }
const mins = Math.floor(msg.elapsed / 60);
const secs = Math.floor(msg.elapsed % 60).toString().padStart(2,'0');
label.value = `Step ${msg.step} / ${msg.total} | ${mins}:${secs}`;
fillPct.value = (msg.step / msg.total) * 100;
if (msg.type === 'step') {
onStep(msg.step, msg.total);
} else if (msg.type === 'done') {
fillPct.value = 100;
onDone();
image.value = msg.image;
}
}
onMounted(async () => {
const r = await fetch('/api/config');
const c = await r.json();
Object.assign(cfg, c);
// seed the controls from server defaults
steps.value = cfg.num_steps;
guidance.value = cfg.guidance;
guidanceRescale.value = cfg.guidance_rescale;
spacing.value = cfg.spacing;
// Warm the browser cache so animation frame swaps don't hit the network.
Object.values(cfg.sprites || {}).flat().forEach(src => { const im = new Image(); im.src = src; });
idle();
});
return { cfg, prompt, seed, steps, guidance, guidanceRescale, spacing,
busy, image, fillPct, label, remSrc, run };
}
}).mount('#app');
</script>
</body>
</html>