File size: 8,771 Bytes
c474610 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | <!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>
|