Download cms/page.html from silvanus0930/vidaio-subnet: direct link, hf CLI and curl.
- Browser
- Download file 30.8 kB
-
https://huggingface.co/silvanus0930/vidaio-subnet/resolve/main/cms/page.html
- Command line
-
hf download hf://silvanus0930/vidaio-subnet/cms/page.html
-
curl -L -o page.html https://huggingface.co/silvanus0930/vidaio-subnet/resolve/main/cms/page.html
30.8 kB
| <html><head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Vidaio Miner CMS</title> | |
| <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%236366f1'/%3E%3Cpath d='M9 11.5A2.5 2.5 0 0 1 11.5 9h6A2.5 2.5 0 0 1 20 11.5v9a2.5 2.5 0 0 1-2.5 2.5h-6A2.5 2.5 0 0 1 9 20.5v-9Z' fill='none' stroke='white' stroke-width='1.8'/%3E%3Cpath d='M20 13.8l4.2-2.4a.8.8 0 0 1 1.2.7v7.8a.8.8 0 0 1-1.2.7L20 18.2' fill='none' stroke='white' stroke-width='1.8' stroke-linejoin='round'/%3E%3C/svg%3E"> | |
| <style> | |
| :root { | |
| color-scheme: dark; | |
| --bg: #0b0d12; --surface: #12151c; --surface-2: #171b24; --border: #232836; | |
| --text: #e8eaf0; --text-dim: #8b93a7; --text-faint: #565f74; | |
| --accent: #6366f1; --accent-soft: rgba(99, 102, 241, 0.14); | |
| --good: #34d399; --good-soft: rgba(52, 211, 153, 0.14); | |
| --warn: #fbbf24; --warn-soft: rgba(251, 191, 36, 0.14); | |
| --crit: #f87171; --crit-soft: rgba(248, 113, 113, 0.14); | |
| --radius: 12px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif; | |
| background: var(--bg); color: var(--text); margin: 0; | |
| padding: 28px clamp(16px, 4vw, 48px) 60px; line-height: 1.5; | |
| } | |
| header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; } | |
| .logo { | |
| width: 36px; height: 36px; border-radius: 9px; | |
| background: linear-gradient(135deg, var(--accent), #818cf8); | |
| display: flex; align-items: center; justify-content: center; flex-shrink: 0; | |
| } | |
| .logo svg { width: 20px; height: 20px; } | |
| h1 { font-size: 17px; font-weight: 650; margin: 0; letter-spacing: -0.01em; } | |
| .subtitle { font-size: 12.5px; color: var(--text-dim); margin-top: 2px; } | |
| nav { display: flex; gap: 4px; margin: 20px 0 24px; border-bottom: 1px solid var(--border); } | |
| nav button { | |
| background: none; border: none; color: var(--text-dim); font-size: 13px; font-weight: 600; | |
| padding: 9px 14px; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; | |
| font-family: inherit; | |
| } | |
| nav button.active { color: var(--text); border-bottom-color: var(--accent); } | |
| nav button:hover { color: var(--text); } | |
| .view { display: none; } | |
| .view.active { display: block; } | |
| #alert-banner { margin-bottom: 20px; display: none; } | |
| .alert-row { | |
| display: flex; align-items: center; gap: 10px; padding: 9px 14px; border-radius: 8px; | |
| font-size: 12.5px; margin-bottom: 6px; border: 1px solid var(--border); | |
| } | |
| .alert-row.critical { background: var(--crit-soft); border-color: rgba(248,113,113,0.3); } | |
| .alert-row.warning { background: var(--warn-soft); border-color: rgba(251,191,36,0.3); } | |
| .alert-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; } | |
| .alert-row.critical .alert-dot { background: var(--crit); } | |
| .alert-row.warning .alert-dot { background: var(--warn); } | |
| .alert-time { color: var(--text-faint); margin-left: auto; flex-shrink: 0; } | |
| .stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 10px; margin-bottom: 24px; } | |
| .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; font-size: 12.5px; color: var(--text-dim); } | |
| .stat-card b { display: block; font-size: 22px; font-weight: 650; color: var(--text); letter-spacing: -0.02em; margin-bottom: 2px; } | |
| .stat-card .cat { color: var(--accent); font-weight: 600; } | |
| .stat-card.ok b { color: var(--good); } | |
| .stat-card.warn b { color: var(--warn); } | |
| .stat-card.crit b { color: var(--crit); } | |
| .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; } | |
| @media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } } | |
| .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 20px; } | |
| .panel-head { padding: 14px 18px; border-bottom: 1px solid var(--border); font-size: 12.5px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; display: flex; justify-content: space-between; align-items: center; } | |
| .panel-head .hint { text-transform: none; letter-spacing: 0; font-weight: 400; color: var(--text-faint); } | |
| .panel-body { padding: 16px 18px; } | |
| select, input, button.btn { | |
| background: var(--surface-2); border: 1px solid var(--border); color: var(--text); | |
| border-radius: 7px; padding: 6px 10px; font-size: 12.5px; font-family: inherit; | |
| } | |
| button.btn { cursor: pointer; font-weight: 600; } | |
| button.btn.primary { background: var(--accent); border-color: var(--accent); color: white; } | |
| button.btn:hover { border-color: #3a4155; } | |
| .controls { display: flex; gap: 8px; padding: 12px 18px; border-bottom: 1px solid var(--border); flex-wrap: wrap; align-items: center; } | |
| table { width: 100%; border-collapse: collapse; font-size: 13px; } | |
| th, td { text-align: left; padding: 10px 18px; } | |
| th { color: var(--text-faint); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border); cursor: pointer; user-select: none; } | |
| th:hover { color: var(--text-dim); } | |
| th.sorted { color: var(--accent); } | |
| tbody tr { border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.1s; } | |
| tbody tr:last-child { border-bottom: none; } | |
| tbody tr:hover { background: var(--surface-2); } | |
| td.task { color: var(--text-dim); font-family: ui-monospace, monospace; font-size: 12px; } | |
| .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: var(--accent-soft); color: #a5a8ff; font-size: 11px; font-weight: 600; } | |
| .score-pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 700; } | |
| .score-pill.hi { background: var(--good-soft); color: var(--good); } | |
| .score-pill.mid { background: var(--warn-soft); color: var(--warn); } | |
| .score-pill.lo { background: var(--crit-soft); color: var(--crit); } | |
| .empty { padding: 48px 18px; text-align: center; color: var(--text-faint); font-size: 13px; } | |
| #detail { display: none; } | |
| #detail .panel-head .task-id { font-family: ui-monospace, monospace; color: var(--text); font-size: 13px; font-weight: 600; text-transform: none; } | |
| .detail-meta { font-size: 12.5px; color: var(--text-dim); display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; } | |
| .detail-meta span b { color: var(--text); font-weight: 600; } | |
| .compare { | |
| background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); | |
| padding: 16px; overflow: hidden; | |
| } | |
| .compare-videos { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } | |
| @media (max-width: 720px) { .compare-videos { grid-template-columns: 1fr; } } | |
| .compare-cell { position: relative; border-radius: 9px; overflow: hidden; background: #000; border: 1px solid var(--border); aspect-ratio: 16/9; } | |
| .compare-cell video { width: 100%; height: 100%; object-fit: contain; display: block; background: #000; } | |
| .compare-chip { | |
| position: absolute; top: 10px; left: 10px; z-index: 2; | |
| padding: 3px 9px; border-radius: 999px; font-size: 10.5px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: 0.05em; backdrop-filter: blur(6px); | |
| } | |
| .compare-chip.reference { background: rgba(99,102,241,0.28); color: #c7c8ff; border: 1px solid rgba(99,102,241,0.4); } | |
| .compare-chip.compressed { background: rgba(52,211,153,0.24); color: #a7f3d3; border: 1px solid rgba(52,211,153,0.4); } | |
| .compare-size { | |
| position: absolute; bottom: 8px; right: 10px; z-index: 2; font-size: 10.5px; color: rgba(255,255,255,0.75); | |
| background: rgba(0,0,0,0.5); padding: 2px 7px; border-radius: 6px; font-variant-numeric: tabular-nums; | |
| } | |
| .compare-loading { | |
| position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; | |
| font-size: 11px; color: var(--text-faint); background: #000; z-index: 1; | |
| } | |
| .compare-controls { display: flex; align-items: center; gap: 12px; margin-top: 14px; } | |
| .compare-playbtn { | |
| width: 34px; height: 34px; border-radius: 50%; background: var(--accent); border: none; color: white; | |
| display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; font-size: 13px; | |
| } | |
| .compare-playbtn:hover { background: #7678f5; } | |
| .compare-seek { | |
| flex: 1; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 999px; | |
| background: var(--border); outline: none; cursor: pointer; | |
| } | |
| .compare-seek::-webkit-slider-thumb { | |
| -webkit-appearance: none; width: 13px; height: 13px; border-radius: 50%; background: var(--accent); | |
| border: 2px solid #1a1f26; cursor: pointer; | |
| } | |
| .compare-time { font-size: 11.5px; color: var(--text-dim); font-variant-numeric: tabular-nums; flex-shrink: 0; min-width: 80px; text-align: right; } | |
| .missing { color: var(--text-faint); font-style: italic; padding: 24px; text-align: center; font-size: 13px; } | |
| .health-row td { padding: 9px 18px; } | |
| .health-badge { padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 700; } | |
| .health-badge.match { background: var(--good-soft); color: var(--good); } | |
| .health-badge.gap { background: var(--warn-soft); color: var(--warn); } | |
| .health-badge.none { background: var(--crit-soft); color: var(--crit); } | |
| .thumb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; } | |
| .thumb-card { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--surface-2); } | |
| .thumb-card img { width: 100%; aspect-ratio: 16/9; object-fit: cover; background: #000; display: block; } | |
| .thumb-meta { padding: 6px 8px; font-size: 10.5px; color: var(--text-dim); } | |
| .coverage-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; margin-bottom: 16px; } | |
| .coverage-cell { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; font-size: 12px; } | |
| .coverage-cell.thin { border-color: rgba(251,191,36,0.4); background: var(--warn-soft); } | |
| .coverage-cell b { display: block; font-size: 18px; } | |
| form.cal-form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; } | |
| .run-row { padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 12.5px; } | |
| .run-row:last-child { border-bottom: none; } | |
| .run-status { font-weight: 700; } | |
| .run-status.running { color: var(--warn); } | |
| .run-status.done { color: var(--good); } | |
| pre.log-tail { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; font-size: 11px; color: var(--text-dim); max-height: 160px; overflow: auto; margin-top: 6px; } | |
| svg.chart { width: 100%; height: 220px; display: block; } | |
| .chart-tooltip-hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.8"> | |
| <rect x="2" y="4.5" width="14" height="15" rx="3"/> | |
| <path d="M16 9.8l4.6-2.7a.8.8 0 0 1 1.2.7v9.2a.8.8 0 0 1-1.2.7L16 15" stroke-linejoin="round"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h1>Vidaio Miner CMS</h1> | |
| <div class="subtitle">Compression outcomes · live from real validator traffic</div> | |
| </div> | |
| </header> | |
| <div id="alert-banner"></div> | |
| <nav> | |
| <button class="tab active" data-view="overview">Overview</button> | |
| <button class="tab" data-view="items">Items</button> | |
| <button class="tab" data-view="health">CQ Health</button> | |
| <button class="tab" data-view="calibration">Calibration</button> | |
| <button class="tab" data-view="library">Library</button> | |
| </nav> | |
| <!-- OVERVIEW --> | |
| <div class="view active" id="view-overview"> | |
| <div class="stats" id="overview-stats"></div> | |
| <div class="grid-2"> | |
| <div class="panel"> | |
| <div class="panel-head">Score trend <span class="hint">daily average, last 14 days</span></div> | |
| <div class="panel-body"><svg class="chart" id="score-trend-chart"></svg></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-head">Rate-distortion <span class="hint">VMAF vs. compression ratio</span></div> | |
| <div class="panel-body"> | |
| <svg class="chart" id="rd-scatter-chart"></svg> | |
| <div class="chart-tooltip-hint">Each point is one item. Lower-right = more compression at higher quality.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ITEMS --> | |
| <div class="view" id="view-items"> | |
| <div class="panel"> | |
| <div class="controls"> | |
| <select id="filter-codec"><option value="">All codecs</option></select> | |
| <select id="filter-threshold"><option value="">All thresholds</option></select> | |
| <button class="btn" onclick="loadItems()">Refresh</button> | |
| </div> | |
| <table> | |
| <thead><tr> | |
| <th data-sort="ts">Time</th><th>Task</th><th data-sort="cq">CQ</th> | |
| <th data-sort="vmaf">VMAF</th><th data-sort="ratio">Ratio</th><th data-sort="score">Score</th> | |
| <th data-sort="elapsed_seconds">Elapsed</th> | |
| </tr></thead> | |
| <tbody id="rows"></tbody> | |
| </table> | |
| </div> | |
| <div class="panel" id="detail"> | |
| <div class="panel-head"><span>Item detail</span><span class="task-id" id="detail-task"></span></div> | |
| <div class="panel-body" id="detail-body"></div> | |
| </div> | |
| </div> | |
| <!-- CQ HEALTH --> | |
| <div class="view" id="view-health"> | |
| <div class="panel"> | |
| <div class="panel-head">CQ table health <span class="hint">current default vs. best real-scored value seen in calibration</span></div> | |
| <table> | |
| <thead><tr><th>Table</th><th>Tier</th><th>Threshold</th><th>Current CQ</th><th>Best known</th><th>Status</th></tr></thead> | |
| <tbody id="health-rows"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- CALIBRATION --> | |
| <div class="view" id="view-calibration"> | |
| <div class="panel"> | |
| <div class="panel-head">Run calibration</div> | |
| <div class="panel-body"> | |
| <form class="cal-form" onsubmit="startCalibration(event)"> | |
| <select id="cal-codec"><option value="hevc">hevc</option><option value="av1">av1</option></select> | |
| <input id="cal-threshold" type="number" step="0.1" value="85.0" style="width:80px" placeholder="threshold"> | |
| <input id="cal-grid" type="text" value="30,33,35,38,40" style="width:160px" placeholder="cq grid"> | |
| <input id="cal-limit" type="number" value="6" style="width:60px" placeholder="clips"> | |
| <button class="btn primary" type="submit">Start run</button> | |
| </form> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-head">Recent runs</div> | |
| <div class="panel-body" id="runs-body"></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-head">Calibration log</div> | |
| <table> | |
| <thead><tr><th>Task</th><th>Codec</th><th>Threshold</th><th>CQ</th><th>VMAF</th><th>Ratio</th></tr></thead> | |
| <tbody id="cal-log-rows"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- LIBRARY --> | |
| <div class="view" id="view-library"> | |
| <div class="panel"> | |
| <div class="panel-head">Coverage <span class="hint">reference clips by category, cap 80 total, reserve 3 per category</span></div> | |
| <div class="panel-body"><div class="coverage-grid" id="coverage-grid"></div></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-head">Recent clips</div> | |
| <div class="panel-body"><div class="thumb-grid" id="thumb-grid"></div></div> | |
| </div> | |
| </div> | |
| <script> | |
| function switchView(view) { | |
| document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.view === view)); | |
| document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view)); | |
| if (view === 'health') loadHealth(); | |
| if (view === 'calibration') { loadRuns(); loadCalLog(); } | |
| if (view === 'library') loadLibrary(); | |
| } | |
| document.querySelectorAll('.tab').forEach(b => b.onclick = () => switchView(b.dataset.view)); | |
| function scoreClass(s) { | |
| if (s === null || s === undefined) return ''; | |
| if (s >= 0.5) return 'hi'; | |
| if (s >= 0.15) return 'mid'; | |
| return 'lo'; | |
| } | |
| async function loadAlerts() { | |
| const alerts = await (await fetch('/api/alerts?limit=8')).json(); | |
| const el = document.getElementById('alert-banner'); | |
| if (!alerts.length) { el.style.display = 'none'; return; } | |
| el.style.display = 'block'; | |
| el.innerHTML = alerts.map(a => ` | |
| <div class="alert-row ${a.severity}"> | |
| <span class="alert-dot"></span> | |
| <span>${a.message}</span> | |
| <span class="alert-time">${(a.ts||'').replace('T',' ').slice(0,19)}</span> | |
| </div>`).join(''); | |
| } | |
| async function loadOverview() { | |
| const [stats, health, alerts] = await Promise.all([ | |
| fetch('/api/stats').then(r => r.json()), | |
| fetch('/api/cq-health').then(r => r.json()), | |
| fetch('/api/alerts?limit=100').then(r => r.json()), | |
| ]); | |
| const staleTiers = health.filter(h => !h.calibrated).length; | |
| const critAlerts = alerts.filter(a => a.severity === 'critical').length; | |
| const avgScore = stats.by_category.length | |
| ? stats.by_category.reduce((s, c) => s + (c.avg_score||0)*c.n, 0) / Math.max(1, stats.total_items) | |
| : null; | |
| document.getElementById('overview-stats').innerHTML = ` | |
| <div class="stat-card"><b>${stats.total_items}</b>total items logged</div> | |
| <div class="stat-card ${avgScore >= 0.4 ? 'ok' : avgScore >= 0.15 ? 'warn' : ''}"><b>${avgScore !== null ? avgScore.toFixed(3) : '-'}</b>overall avg score</div> | |
| <div class="stat-card ${staleTiers === 0 ? 'ok' : 'warn'}"><b>${staleTiers}</b>/${health.length} CQ tiers uncalibrated</div> | |
| <div class="stat-card ${critAlerts === 0 ? 'ok' : 'crit'}"><b>${critAlerts}</b>critical alerts</div> | |
| `; | |
| drawScoreTrend(); | |
| drawRateDistortion(); | |
| } | |
| function svgLine(points, w, h, pad, color) { | |
| if (!points.length) return ''; | |
| const xs = points.map(p => p.x), ys = points.map(p => p.y); | |
| const xMin = Math.min(...xs), xMax = Math.max(...xs) || 1; | |
| const yMin = 0, yMax = Math.max(...ys, 0.01); | |
| const sx = x => pad + (x - xMin) / (xMax - xMin || 1) * (w - 2*pad); | |
| const sy = y => h - pad - (y - yMin) / (yMax - yMin || 1) * (h - 2*pad); | |
| const path = points.map((p,i) => `${i===0?'M':'L'}${sx(p.x).toFixed(1)},${sy(p.y).toFixed(1)}`).join(' '); | |
| const dots = points.map(p => `<circle cx="${sx(p.x).toFixed(1)}" cy="${sy(p.y).toFixed(1)}" r="2.5" fill="${color}"/>`).join(''); | |
| return `<path d="${path}" fill="none" stroke="${color}" stroke-width="2"/>${dots}`; | |
| } | |
| async function drawScoreTrend() { | |
| const rows = await (await fetch('/api/score-trend?days=14')).json(); | |
| const svg = document.getElementById('score-trend-chart'); | |
| const w = svg.clientWidth || 500, h = 220, pad = 28; | |
| if (!rows.length) { svg.innerHTML = `<text x="${w/2}" y="${h/2}" fill="#565f74" font-size="12" text-anchor="middle">No score data yet</text>`; return; } | |
| const points = rows.map((r,i) => ({ x: i, y: r.avg_score || 0 })); | |
| svg.setAttribute('viewBox', `0 0 ${w} ${h}`); | |
| let grid = ''; | |
| for (let g = 0; g <= 1; g += 0.25) { | |
| const y = h - pad - g * (h - 2*pad); | |
| grid += `<line x1="${pad}" y1="${y}" x2="${w-pad}" y2="${y}" stroke="#232836" stroke-width="1"/>`; | |
| grid += `<text x="4" y="${y+3}" fill="#565f74" font-size="10">${g.toFixed(2)}</text>`; | |
| } | |
| svg.innerHTML = grid + svgLine(points, w, h, pad, '#6366f1') + | |
| rows.map((r,i) => `<title>${r.day}: avg ${r.avg_score?.toFixed(3)} (n=${r.n})</title>`).join(''); | |
| } | |
| async function drawRateDistortion() { | |
| const rows = await (await fetch('/api/rate-distortion?limit=500')).json(); | |
| const svg = document.getElementById('rd-scatter-chart'); | |
| const w = svg.clientWidth || 500, h = 220, pad = 30; | |
| if (!rows.length) { svg.innerHTML = `<text x="${w/2}" y="${h/2}" fill="#565f74" font-size="12" text-anchor="middle">No data yet</text>`; return; } | |
| svg.setAttribute('viewBox', `0 0 ${w} ${h}`); | |
| const sx = ratio => pad + Math.min(ratio, 1) * (w - 2*pad); | |
| const sy = vmaf => h - pad - Math.max(0, Math.min(vmaf, 100)) / 100 * (h - 2*pad); | |
| const colors = { hevc: '#6366f1', av1: '#34d399' }; | |
| let grid = ''; | |
| [0, 50, 100].forEach(v => { | |
| const y = sy(v); | |
| grid += `<line x1="${pad}" y1="${y}" x2="${w-pad}" y2="${y}" stroke="#232836" stroke-width="1"/><text x="4" y="${y+3}" fill="#565f74" font-size="10">${v}</text>`; | |
| }); | |
| const dots = rows.map(r => { | |
| const color = colors[(r.codec||'').toLowerCase()] || '#8b93a7'; | |
| return `<circle cx="${sx(r.ratio).toFixed(1)}" cy="${sy(r.vmaf).toFixed(1)}" r="3" fill="${color}" fill-opacity="0.7"><title>${r.task_id} vmaf=${r.vmaf?.toFixed(1)} ratio=${r.ratio?.toFixed(3)}</title></circle>`; | |
| }).join(''); | |
| svg.innerHTML = grid + dots; | |
| } | |
| let itemSort = { col: 'ts', dir: 'desc' }; | |
| async function loadFilters() { | |
| const stats = await (await fetch('/api/stats')).json(); | |
| const codecs = [...new Set(stats.by_category.map(c => c.codec))]; | |
| const thresholds = [...new Set(stats.by_category.map(c => c.vmaf_threshold))]; | |
| const codecSel = document.getElementById('filter-codec'); | |
| const thrSel = document.getElementById('filter-threshold'); | |
| codecs.forEach(c => codecSel.innerHTML += `<option value="${c}">${c}</option>`); | |
| thresholds.forEach(t => thrSel.innerHTML += `<option value="${t}">${t}</option>`); | |
| } | |
| document.querySelectorAll('th[data-sort]').forEach(th => th.onclick = () => { | |
| const col = th.dataset.sort; | |
| itemSort.dir = (itemSort.col === col && itemSort.dir === 'desc') ? 'asc' : 'desc'; | |
| itemSort.col = col; | |
| loadItems(); | |
| }); | |
| async function loadItems() { | |
| const codec = document.getElementById('filter-codec').value; | |
| const threshold = document.getElementById('filter-threshold').value; | |
| const params = new URLSearchParams({ limit: 200, sort: itemSort.col, order: itemSort.dir }); | |
| if (codec) params.set('codec', codec); | |
| if (threshold) params.set('vmaf_threshold', threshold); | |
| const items = await (await fetch(`/api/items?${params}`)).json(); | |
| document.querySelectorAll('th[data-sort]').forEach(th => th.classList.toggle('sorted', th.dataset.sort === itemSort.col)); | |
| const rowsEl = document.getElementById('rows'); | |
| if (!items.length) { | |
| rowsEl.innerHTML = `<tr><td colspan="7"><div class="empty">No items match this filter yet.</div></td></tr>`; | |
| return; | |
| } | |
| rowsEl.innerHTML = items.map(it => ` | |
| <tr onclick="showDetail('${it.task_id}')"> | |
| <td>${(it.ts||'').replace('T',' ').slice(0,19)}</td> | |
| <td class="task">${it.task_id} <span class="pill">${it.codec||''} ${it.vmaf_threshold??''}</span></td> | |
| <td>${it.cq ?? ''}</td> | |
| <td>${it.vmaf?.toFixed(2) ?? ''}</td> | |
| <td>${it.ratio?.toFixed(3) ?? ''}</td> | |
| <td>${it.score !== null && it.score !== undefined ? `<span class="score-pill ${scoreClass(it.score)}">${it.score.toFixed(3)}</span>` : ''}</td> | |
| <td>${it.elapsed_seconds?.toFixed(1) ?? ''}s</td> | |
| </tr>`).join(''); | |
| } | |
| function fmtTime(s) { | |
| if (!isFinite(s)) return '0:00'; | |
| const m = Math.floor(s / 60), sec = Math.floor(s % 60); | |
| return `${m}:${sec.toString().padStart(2, '0')}`; | |
| } | |
| function fmtBytes(n) { | |
| if (n === null || n === undefined) return '-'; | |
| if (n > 1e6) return (n / 1e6).toFixed(1) + ' MB'; | |
| if (n > 1e3) return (n / 1e3).toFixed(0) + ' KB'; | |
| return n + ' B'; | |
| } | |
| let compareState = null; // { videos: [el, el], playing: bool } | |
| function bindCompareViewer(taskId) { | |
| const videos = [ | |
| document.getElementById('cmp-video-reference'), | |
| document.getElementById('cmp-video-compressed'), | |
| ].filter(Boolean); | |
| if (!videos.length) return; | |
| const master = videos[0]; | |
| const playBtn = document.getElementById('cmp-play'); | |
| const seek = document.getElementById('cmp-seek'); | |
| const timeEl = document.getElementById('cmp-time'); | |
| const loadingEls = document.querySelectorAll(`#cmp-container-${taskId} .compare-loading`); | |
| let ready = 0; | |
| videos.forEach((v, i) => { | |
| v.addEventListener('loadeddata', () => { | |
| ready++; | |
| const el = v.closest('.compare-cell')?.querySelector('.compare-loading'); | |
| if (el) el.style.display = 'none'; | |
| if (ready === videos.length && master.duration) { | |
| seek.max = master.duration; | |
| timeEl.textContent = `0:00 / ${fmtTime(master.duration)}`; | |
| } | |
| }); | |
| }); | |
| function playAll() { videos.forEach(v => v.play()); playBtn.textContent = '⏸'; } | |
| function pauseAll() { videos.forEach(v => v.pause()); playBtn.textContent = '▶'; } | |
| playBtn.onclick = () => { | |
| if (master.paused) playAll(); else pauseAll(); | |
| }; | |
| master.addEventListener('timeupdate', () => { | |
| if (!seek.matches(':active')) seek.value = master.currentTime; | |
| timeEl.textContent = `${fmtTime(master.currentTime)} / ${fmtTime(master.duration || 0)}`; | |
| // Nudge any follower video back in sync if it's drifted noticeably -- | |
| // independent <video> elements decoding separately-encoded files can | |
| // drift a little over time even started together. | |
| for (let i = 1; i < videos.length; i++) { | |
| if (Math.abs(videos[i].currentTime - master.currentTime) > 0.2) { | |
| videos[i].currentTime = master.currentTime; | |
| } | |
| } | |
| }); | |
| master.addEventListener('ended', () => { playBtn.textContent = '▶'; }); | |
| seek.addEventListener('input', () => { | |
| videos.forEach(v => { v.currentTime = seek.valueAsNumber; }); | |
| }); | |
| } | |
| async function showDetail(taskId) { | |
| const it = await (await fetch(`/api/items/${taskId}`)).json(); | |
| const det = document.getElementById('detail'); | |
| det.style.display = 'block'; | |
| document.getElementById('detail-task').textContent = taskId; | |
| const cells = []; | |
| if (it.has_reference_sample) cells.push({ kind: 'reference', label: 'Reference', bytes: it.original_size }); | |
| if (it.has_compressed_sample) cells.push({ kind: 'compressed', label: 'Compressed', bytes: it.compressed_size }); | |
| const videoHtml = cells.length ? ` | |
| <div class="compare" id="cmp-container-${taskId}"> | |
| <div class="compare-videos"> | |
| ${cells.map(c => ` | |
| <div class="compare-cell"> | |
| <div class="compare-loading">loading…</div> | |
| <span class="compare-chip ${c.kind}">${c.label}</span> | |
| <span class="compare-size">${fmtBytes(c.bytes)}</span> | |
| <video id="cmp-video-${c.kind}" src="/video/${taskId}/${c.kind}" muted playsinline></video> | |
| </div>`).join('')} | |
| </div> | |
| ${cells.length > 1 ? ` | |
| <div class="compare-controls"> | |
| <button class="compare-playbtn" id="cmp-play">▶</button> | |
| <input type="range" class="compare-seek" id="cmp-seek" value="0" step="0.05"> | |
| <span class="compare-time" id="cmp-time">0:00 / 0:00</span> | |
| </div>` : ''} | |
| </div>` : '<div class="missing">No video sample retained for this item.</div>'; | |
| document.getElementById('detail-body').innerHTML = ` | |
| <div class="detail-meta"> | |
| <span><b>${it.codec} ${it.codec_mode}</b></span> | |
| <span>threshold <b>${it.vmaf_threshold}</b></span> | |
| <span>cq <b>${it.cq}</b></span> | |
| <span>vmaf <b>${it.vmaf?.toFixed(2) ?? '-'}</b></span> | |
| <span>ratio <b>${it.ratio?.toFixed(4) ?? '-'}</b></span> | |
| <span>score <b>${it.score?.toFixed(4) ?? '-'}</b></span> | |
| <span><b>${it.original_size}</b> → <b>${it.compressed_size}</b> bytes</span> | |
| <span>elapsed <b>${it.elapsed_seconds?.toFixed(1)}s</b></span> | |
| <span>${it.searched ? 'search-driven' : 'static table'}</span> | |
| </div> | |
| ${videoHtml}`; | |
| if (cells.length > 1) bindCompareViewer(taskId); | |
| det.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| async function loadHealth() { | |
| const rows = await (await fetch('/api/cq-health')).json(); | |
| document.getElementById('health-rows').innerHTML = rows.map(r => { | |
| let badge, label; | |
| if (!r.calibrated) { badge = 'none'; label = 'no evidence'; } | |
| else if (r.matches_best) { badge = 'match'; label = 'matches best known'; } | |
| else { badge = 'gap'; label = `best known: ${r.best_known_cq} (score ${r.best_known_score})`; } | |
| return `<tr class="health-row"> | |
| <td>${r.table}</td><td>${r.tier}</td><td>${r.threshold}</td><td>${r.current_cq}</td> | |
| <td>${r.best_known_cq ?? '-'}</td> | |
| <td><span class="health-badge ${badge}">${label}</span></td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| async function startCalibration(ev) { | |
| ev.preventDefault(); | |
| const codec = document.getElementById('cal-codec').value; | |
| const threshold = document.getElementById('cal-threshold').value; | |
| const grid = document.getElementById('cal-grid').value; | |
| const limit = document.getElementById('cal-limit').value; | |
| const params = new URLSearchParams({ codec, vmaf_threshold: threshold, cq_grid: grid, limit_clips: limit }); | |
| await fetch(`/api/calibration/start?${params}`, { method: 'POST' }); | |
| loadRuns(); | |
| } | |
| async function loadRuns() { | |
| const runs = await (await fetch('/api/calibration/runs')).json(); | |
| document.getElementById('runs-body').innerHTML = runs.length ? runs.map(r => ` | |
| <div class="run-row"> | |
| <span class="run-status ${r.status}">${r.status.toUpperCase()}</span> | |
| — ${r.codec} thr${r.vmaf_threshold}, grid [${r.cq_grid}], ${r.limit_clips} clips | |
| · started ${(r.started_at||'').replace('T',' ').slice(0,19)} | |
| <pre class="log-tail">${(r.log_tail || 'waiting for output...').slice(-2000)}</pre> | |
| </div>`).join('') : '<div class="empty">No calibration runs yet.</div>'; | |
| } | |
| async function loadCalLog() { | |
| const rows = await (await fetch('/api/calibration/log?limit=100')).json(); | |
| document.getElementById('cal-log-rows').innerHTML = rows.length ? rows.map(r => ` | |
| <tr><td class="task">${r.task_id||''}</td><td>${r.codec||''}</td><td>${r.vmaf_threshold??''}</td> | |
| <td>${r.cq??''}</td><td>${r.vmaf?.toFixed(2)??''}</td><td>${r.ratio?.toFixed(3)??''}</td></tr> | |
| `).join('') : `<tr><td colspan="6"><div class="empty">No calibration data yet.</div></td></tr>`; | |
| } | |
| async function loadLibrary() { | |
| const [coverage, clips] = await Promise.all([ | |
| fetch('/api/library/coverage').then(r => r.json()), | |
| fetch('/api/library/clips').then(r => r.json()), | |
| ]); | |
| document.getElementById('coverage-grid').innerHTML = coverage.map(c => ` | |
| <div class="coverage-cell ${c.thin ? 'thin' : ''}"><b>${c.count}</b>${c.codec} / thr${c.vmaf_threshold}${c.thin ? ' · thin' : ''}</div> | |
| `).join('') || '<div class="empty">No clips captured yet.</div>'; | |
| document.getElementById('thumb-grid').innerHTML = clips.slice(0, 40).map(c => ` | |
| <div class="thumb-card"> | |
| <img src="/thumbnail/${c.task_id}" loading="lazy" onerror="this.style.display='none'"> | |
| <div class="thumb-meta">${c.codec||''} thr${c.vmaf_threshold||''}</div> | |
| </div>`).join('') || '<div class="empty">No clips captured yet.</div>'; | |
| } | |
| loadAlerts(); loadOverview(); loadFilters(); loadItems(); | |
| setInterval(() => { loadAlerts(); loadOverview(); if (document.getElementById('view-items').classList.contains('active')) loadItems(); }, 20000); | |
| </script> | |
| </body></html> | |