Download viewer/index.html from rockeycoss/tmp_1: direct link, hf CLI and curl.
- Browser
- Download file 8.77 kB
-
https://huggingface.co/rockeycoss/tmp_1/resolve/main/viewer/index.html
- Command line
-
hf download hf://rockeycoss/tmp_1/viewer/index.html
-
curl -L -o index.html https://huggingface.co/rockeycoss/tmp_1/resolve/main/viewer/index.html
8.77 kB
| <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> | |