tmp_1 / viewer /index.html
rockeycoss's picture
Add files using upload-large-folder tool
c474610 verified
Raw
History Blame Contribute Delete
8.77 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Model comparison — xj + MJHQ-30K</title>
<style>
:root { --bg:#111; --panel:#1b1b1f; --fg:#eee; --muted:#9aa0aa; --accent:#4c9ffe; --imgw:300px; }
* { box-sizing: border-box; }
body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: var(--bg); color: var(--fg); }
header { position: sticky; top:0; z-index:10; background: var(--panel);
padding: 10px 16px; border-bottom:1px solid #2a2a30; }
.row { display:flex; gap:14px; align-items:center; flex-wrap:wrap; }
input[type="number"], button { background:#23232a; color:var(--fg);
border:1px solid #34343c; border-radius:6px; padding:6px 9px; font-size:14px; }
button { cursor:pointer; } button:hover { border-color: var(--accent); }
button.rand { border-color: var(--accent); color: var(--accent); font-weight:600; }
button.save { background: gold; color:#111; border-color: gold; font-weight:600; }
button.save:disabled { opacity:.5; cursor:default; }
button.filter.active { background: var(--accent); color:#fff; border-color: var(--accent); font-weight:600; }
button.filter:disabled { opacity:.45; cursor:default; }
#status { color: var(--muted); font-size:12px; }
#status.err { color:#ff6b6b; }
#status.ok { color:#5ad17a; }
label { color: var(--muted); font-size:13px; }
input[type="range"] { vertical-align: middle; accent-color: var(--accent); }
#counter { font-variant-numeric: tabular-nums; min-width: 140px; display:inline-block; }
#prompt { padding:10px 16px; color:#dfe3ea; background:#16161a; border-bottom:1px solid #2a2a30;
max-height:120px; overflow:auto; white-space:pre-wrap; line-height:1.4; font-size:14px; }
#prompt .pidx { color: var(--accent); font-weight:600; margin-right:8px; }
#grid { display:grid; grid-template-columns: repeat(4, var(--imgw)); gap:16px;
padding:16px; justify-content:start; overflow-x:auto; }
.cell { text-align:center; }
.cell .name { color:#cfd3da; font-size:14px; font-weight:600; margin-bottom:6px; }
.cell .box { width: var(--imgw); height: var(--imgw); border:1px solid #2a2a30; border-radius:6px;
background:#000; display:flex; align-items:center; justify-content:center; overflow:hidden; }
.cell img { width:100%; height:100%; object-fit:contain; cursor:zoom-in; }
.cell .ph { color:#555; font-size:13px; padding:10px; }
.hint { color: var(--muted); font-size:12px; }
</style>
</head>
<body>
<header>
<div class="row">
<button id="prev">◀ Prev</button>
<button id="next">Next ▶</button>
<button id="random" class="rand">🎲 Random</button>
<button id="save" class="save">★ Save</button>
<button id="filter" class="filter" title="Cycle through indices in selected/">Selected only</button>
<input type="range" id="slider" min="0" value="0">
<span id="counter"></span>
<label>prompt #</label>
<input type="number" id="idxInput" min="0" style="width:90px">
<label>size</label>
<input type="range" id="size" min="160" max="560" step="20" value="300" style="width:120px">
<span class="hint">← / → step · r = random · s = save</span>
<span id="status"></span>
</div>
</header>
<div id="prompt"></div>
<div id="grid"></div>
<script>
let M = null, pos = 0; // pos = index into the active list
let selectedIds = []; // sorted list of prompt indices saved under selected/
let filterActive = false; // when true, navigation cycles selectedIds only
const $ = id => document.getElementById(id);
const pad = (n, w) => String(n).padStart(w, '0');
const active = () => (filterActive && selectedIds.length) ? selectedIds : M.available;
function updateFilterBtn() {
const b = $('filter');
b.textContent = `Selected only (${selectedIds.length})`;
b.disabled = selectedIds.length === 0;
b.classList.toggle('active', filterActive && selectedIds.length > 0);
}
async function refreshSelected() {
try {
const r = await fetch('/api/selected', {cache:'no-store'});
if (r.ok) selectedIds = (await r.json()).ids || [];
} catch (e) { selectedIds = []; }
updateFilterBtn();
}
async function loadManifest() {
// Prefer the live endpoint (viewer.py rescans dirs each load); fall back to static file.
for (const url of ['/api/manifest', 'manifest.json']) {
try {
const r = await fetch(url, {cache: 'no-store'});
if (r.ok) return await r.json();
} catch (e) { /* try next */ }
}
throw new Error('no manifest');
}
async function init() {
try {
M = await loadManifest();
} catch (e) {
$('prompt').textContent = 'Failed to load manifest — run "python test/xj_mjhq30k_inference_outputs/viewer/viewer.py".';
return;
}
if (!M.available.length) { $('prompt').textContent = 'No generated images found yet.'; return; }
await refreshSelected();
syncSlider();
$('prev').onclick = () => go(pos - 1);
$('next').onclick = () => go(pos + 1);
$('random').onclick = () => go(Math.floor(Math.random() * active().length));
$('save').onclick = saveSelection;
$('filter').onclick = toggleFilter;
$('slider').oninput = e => go(+e.target.value);
$('idxInput').onchange = e => goToPromptIdx(+e.target.value);
$('size').oninput = e => document.documentElement.style.setProperty('--imgw', e.target.value + 'px');
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT') return;
if (e.key === 'ArrowRight') { go(pos + 1); e.preventDefault(); }
else if (e.key === 'ArrowLeft') { go(pos - 1); e.preventDefault(); }
else if (e.key === 'r' || e.key === 'R') { go(Math.floor(Math.random() * active().length)); }
else if (e.key === 's' || e.key === 'S') { saveSelection(); }
});
go(0);
}
function syncSlider() { $('slider').max = Math.max(0, active().length - 1); }
function toggleFilter() {
if (!selectedIds.length) return;
// remember the current prompt index, then switch the active list and jump
// to the position of (or nearest to) that index in the new list.
const currentIdx = active()[pos];
filterActive = !filterActive;
updateFilterBtn();
syncSlider();
goToPromptIdx(currentIdx);
}
function setStatus(msg, cls) { const s = $('status'); s.textContent = msg; s.className = cls || ''; }
async function saveSelection() {
const idx = M.available[pos];
$('save').disabled = true;
setStatus('saving…');
try {
const r = await fetch('/api/save', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({idx}),
});
const j = await r.json();
if (j.ok) {
setStatus(`saved #${idx}${j.path} (${j.saved_models.join(', ')})`, 'ok');
await refreshSelected(); // newly saved idx becomes filterable immediately
if (filterActive) syncSlider();
} else setStatus(`save failed: ${j.error}`, 'err');
} catch (e) {
setStatus(`save failed — run viewer.py (not http.server): ${e}`, 'err');
} finally {
$('save').disabled = false;
}
}
// jump to the position at/before a requested prompt index within the active list
function goToPromptIdx(want) {
const list = active();
if (!list.length) return;
let lo = 0, hi = list.length - 1, best = 0;
while (lo <= hi) { const mid = (lo+hi)>>1;
if (list[mid] <= want) { best = mid; lo = mid+1; } else hi = mid-1; }
go(best);
}
function go(p) {
const list = active();
if (!list.length) return;
pos = Math.max(0, Math.min(p, list.length - 1));
const idx = list[pos];
$('slider').value = pos;
$('idxInput').value = idx;
const tag = (filterActive && selectedIds.length) ? 'selected' : 'generated';
$('counter').textContent = `#${idx} (${pos + 1}/${list.length} ${tag})`;
$('prompt').innerHTML = `<span class="pidx">#${idx}</span>` +
(M.prompts[idx] || '<em style="color:#666">(no prompt text)</em>');
const grid = $('grid');
grid.innerHTML = '';
for (const m of M.models) {
const src = `${M.rel_prefix}/${m.dir}/${pad(idx, M.pad)}.png`;
const cell = document.createElement('div');
cell.className = 'cell';
cell.innerHTML = `<div class="name">${m.label || m.name}</div>`;
const box = document.createElement('div');
box.className = 'box';
const img = document.createElement('img');
img.loading = 'lazy';
img.src = src;
img.title = `${m.label || m.name}${pad(idx, M.pad)}.png`;
img.onclick = () => window.open(src, '_blank');
img.onerror = () => { box.innerHTML = '<div class="ph">not generated yet</div>'; };
box.appendChild(img);
cell.appendChild(box);
grid.appendChild(cell);
}
}
init();
</script>
</body>
</html>