Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
CarolinePascal commited on
feat(gradio): update to gradio app
Browse files- README.md +28 -2
- app.jsx +0 -1241
- app.py +0 -0
- assets-embedded.json +0 -0
- assets/colors_and_type.css +0 -169
- assets/hf-logo.png +0 -3
- assets/huggy-hello.png +0 -3
- assets/huggy-lab.png +0 -3
- assets/huggy-rocket.png +0 -3
- assets/huggy-vision.png +0 -3
- colors_and_type.css +0 -169
- data.js +0 -153
- index.html +0 -0
- requirements.txt +6 -0
- src/__init__.py +13 -0
- src/compute.py +352 -0
- src/schema.py +320 -0
- styles.css +1497 -734
README.md
CHANGED
|
@@ -3,8 +3,9 @@ title: Video Benchmark
|
|
| 3 |
emoji: 🎬
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: indigo
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
short_description: Benchmark video encoding and decoding performance
|
|
@@ -38,4 +39,29 @@ The benchmark uses LeRobot image datasets from the HuggingFace Hub:
|
|
| 38 |
| `lerobot/paris_street` | 720 x 1280 | Real-world outdoor, moving camera |
|
| 39 |
| `lerobot/kitchen` | 1080 x 1920 | Real-world indoor, fixed camera |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 3 |
emoji: 🎬
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.12.0
|
| 8 |
+
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
short_description: Benchmark video encoding and decoding performance
|
|
|
|
| 39 |
| `lerobot/paris_street` | 720 x 1280 | Real-world outdoor, moving camera |
|
| 40 |
| `lerobot/kitchen` | 1080 x 1920 | Real-world indoor, fixed camera |
|
| 41 |
|
| 42 |
+
## Architecture
|
| 43 |
+
|
| 44 |
+
Single-process Gradio Space. One language (Python), one entry point (`app.py`), no build step.
|
| 45 |
+
|
| 46 |
+
- [`app.py`](app.py) builds the `gr.Blocks` layout: hero banner (as `gr.HTML` so the HF branding stays pixel-close), then six tabs — Results, Leaderboards, Compare, Submit, Parameters, About. Event handlers call into `src.compute` directly; there is no HTTP hop between frontend and data layer.
|
| 47 |
+
- [`src/schema.py`](src/schema.py) is the single source of truth for UI vocabulary: column metadata, filter / submit option lists, leaderboard categories, parameter-reference prose, About-page narration. A column or option only ever changes here.
|
| 48 |
+
- [`src/compute.py`](src/compute.py) holds pure-Python row math: chip filtering, composite ranking, per-axis normalization, leaderboard weighted scoring, and the three Compare-tab aggregations. All functions are `list[dict] → list[dict]` or `dict`, trivially testable.
|
| 49 |
+
- Results rows come from [`lerobot/video-benchmark-results`](https://huggingface.co/datasets/lerobot/video-benchmark-results) via `datasets.load_dataset`, cached in-process with a single-flight lock and a stale-but-usable fallback.
|
| 50 |
+
- Submissions are committed to [`lerobot/video-benchmark-submissions`](https://huggingface.co/datasets/lerobot/video-benchmark-submissions) as one JSON file per submission. The schema matches what existing LeRobot benchmark workers already consume, so they keep picking up jobs unchanged.
|
| 51 |
+
- [`styles.css`](styles.css) layers HF brand colors, hero typography, and the About / Parameters card styling on top of the Gradio theme defined in `_hf_theme()`.
|
| 52 |
+
|
| 53 |
+
### Required Space secrets
|
| 54 |
+
|
| 55 |
+
- `HF_TOKEN` — a bot token with **write** access to `lerobot/video-benchmark-submissions`. Without it, the Submit button raises an error.
|
| 56 |
+
|
| 57 |
+
### Local development
|
| 58 |
+
|
| 59 |
+
```
|
| 60 |
+
pip install -r requirements.txt
|
| 61 |
+
export HF_TOKEN=hf_... # optional; needed for the Submit tab
|
| 62 |
+
python app.py
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
Then open http://localhost:7860.
|
| 66 |
+
|
| 67 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.jsx
DELETED
|
@@ -1,1241 +0,0 @@
|
|
| 1 |
-
// Main React app for the Video Benchmark UI
|
| 2 |
-
const { useState, useMemo, useEffect, useRef } = React;
|
| 3 |
-
|
| 4 |
-
// Data (ALL_ROWS, COLUMNS, etc) comes from data.js. Hoist window.VB_DATA
|
| 5 |
-
// keys onto window so bare identifiers resolve — no-op in the bundled build
|
| 6 |
-
// where data.js is concatenated into this same block.
|
| 7 |
-
(function hoistData(){
|
| 8 |
-
if (typeof window === 'undefined' || !window.VB_DATA) return;
|
| 9 |
-
for (const k of Object.keys(window.VB_DATA)) {
|
| 10 |
-
if (typeof window[k] === 'undefined') window[k] = window.VB_DATA[k];
|
| 11 |
-
}
|
| 12 |
-
})();
|
| 13 |
-
|
| 14 |
-
// ---------- helpers ----------
|
| 15 |
-
const METRIC_COLS = COLUMNS.filter(c => c.metric);
|
| 16 |
-
|
| 17 |
-
function normalize(rows, col) {
|
| 18 |
-
const vals = rows.map(r => r[col.key]).filter(v => v != null && !isNaN(v));
|
| 19 |
-
if (!vals.length) return () => 0;
|
| 20 |
-
const min = Math.min(...vals), max = Math.max(...vals);
|
| 21 |
-
if (max === min) return () => 0.5;
|
| 22 |
-
return (v) => {
|
| 23 |
-
if (v == null || isNaN(v)) return 0;
|
| 24 |
-
const t = (v - min) / (max - min);
|
| 25 |
-
return col.lower ? 1 - t : t;
|
| 26 |
-
};
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
function rdylgn(t) {
|
| 30 |
-
t = Math.max(0, Math.min(1, t));
|
| 31 |
-
const lerp = (a,b,s) => a + (b-a)*s;
|
| 32 |
-
if (t < 0.5) { const s = t/0.5; return [lerp(215,255,s), lerp(48,255,s), lerp(39,75,s)]; }
|
| 33 |
-
const s = (t-0.5)/0.5; return [lerp(255,26,s), lerp(255,152,s), lerp(75,80,s)];
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function compositeRank(rows, cols) {
|
| 37 |
-
const fns = cols.map(c => [c, normalize(rows, c)]);
|
| 38 |
-
return rows.map(r => {
|
| 39 |
-
let sum = 0, n = 0;
|
| 40 |
-
for (const [c, fn] of fns) {
|
| 41 |
-
const v = fn(r[c.key]);
|
| 42 |
-
sum += (1 - v); // lower normalized => better rank
|
| 43 |
-
n++;
|
| 44 |
-
}
|
| 45 |
-
return { row: r, score: n ? sum/n : 0 };
|
| 46 |
-
}).sort((a,b) => a.score - b.score);
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
const GROUP_COLOR = {
|
| 50 |
-
Config: '#6A768F', Compression: '#FF9D00', Speed: '#496EF0', Quality: '#009B3A',
|
| 51 |
-
};
|
| 52 |
-
|
| 53 |
-
// ---------- Filter Bar ----------
|
| 54 |
-
function FilterBar({ filters, setFilters, counts, visible, setVisible }) {
|
| 55 |
-
const fields = [
|
| 56 |
-
{ key: 'backend', opts: BACKENDS },
|
| 57 |
-
{ key: 'vcodec', opts: VCODECS },
|
| 58 |
-
{ key: 'pix_fmt', opts: PIX_FMTS },
|
| 59 |
-
{ key: 'g', opts: G_VALUES.map(String) },
|
| 60 |
-
{ key: 'crf', opts: CRF_VALUES.map(String) },
|
| 61 |
-
].map(f => ({ ...f, label: COLUMNS.find(c => c.key === f.key).label, desc: COLUMNS.find(c => c.key === f.key).desc }));
|
| 62 |
-
const toggle = (field, v) => {
|
| 63 |
-
const cur = new Set(filters[field] || []);
|
| 64 |
-
cur.has(v) ? cur.delete(v) : cur.add(v);
|
| 65 |
-
setFilters({ ...filters, [field]: [...cur] });
|
| 66 |
-
};
|
| 67 |
-
const clearAll = () => setFilters({});
|
| 68 |
-
return (
|
| 69 |
-
<div className="filters-bar">
|
| 70 |
-
{fields.map(f => (
|
| 71 |
-
<div className="filter-field" key={f.key}>
|
| 72 |
-
<label data-tt={f.desc}>{f.label}</label>
|
| 73 |
-
<div className="chips">
|
| 74 |
-
{f.opts.map(o => {
|
| 75 |
-
const active = (filters[f.key] || []).includes(String(o));
|
| 76 |
-
return <span key={o} className={`filter-chip ${active?'active':''}`} onClick={()=>toggle(f.key, String(o))}>{o}</span>;
|
| 77 |
-
})}
|
| 78 |
-
</div>
|
| 79 |
-
</div>
|
| 80 |
-
))}
|
| 81 |
-
<div className="filter-actions">
|
| 82 |
-
<ColumnPicker visible={visible} setVisible={setVisible} />
|
| 83 |
-
<button className="btn ghost" onClick={clearAll}>Reset</button>
|
| 84 |
-
</div>
|
| 85 |
-
</div>
|
| 86 |
-
);
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
// ---------- Column Picker ----------
|
| 90 |
-
function ColumnPicker({ visible, setVisible }) {
|
| 91 |
-
const [open, setOpen] = useState(false);
|
| 92 |
-
const ref = useRef(null);
|
| 93 |
-
useEffect(() => {
|
| 94 |
-
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
| 95 |
-
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
| 96 |
-
document.addEventListener('mousedown', onDoc);
|
| 97 |
-
document.addEventListener('keydown', onKey);
|
| 98 |
-
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
|
| 99 |
-
}, []);
|
| 100 |
-
const toggle = (k) => {
|
| 101 |
-
const s = new Set(visible);
|
| 102 |
-
s.has(k) ? s.delete(k) : s.add(k);
|
| 103 |
-
setVisible(s);
|
| 104 |
-
};
|
| 105 |
-
const toggleGroup = (g) => {
|
| 106 |
-
const cols = COLUMNS.filter(c => c.group === g).map(c => c.key);
|
| 107 |
-
const allOn = cols.every(k => visible.has(k));
|
| 108 |
-
const s = new Set(visible);
|
| 109 |
-
cols.forEach(k => allOn ? s.delete(k) : s.add(k));
|
| 110 |
-
setVisible(s);
|
| 111 |
-
};
|
| 112 |
-
const resetAll = () => setVisible(new Set(COLUMNS.map(c=>c.key)));
|
| 113 |
-
const hideAll = () => setVisible(new Set(COLUMNS.filter(c=>c.group==='Config').map(c=>c.key)));
|
| 114 |
-
return (
|
| 115 |
-
<div className="columns-dd-wrap" ref={ref}>
|
| 116 |
-
<button className={`columns-dd-trigger ${open?'open':''}`} onClick={()=>setOpen(o=>!o)}>
|
| 117 |
-
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{flexShrink:0}}>
|
| 118 |
-
<rect x="1" y="2" width="12" height="2" rx="0.5" fill="currentColor" opacity="0.9"/>
|
| 119 |
-
<rect x="1" y="6" width="12" height="2" rx="0.5" fill="currentColor" opacity="0.6"/>
|
| 120 |
-
<rect x="1" y="10" width="12" height="2" rx="0.5" fill="currentColor" opacity="0.3"/>
|
| 121 |
-
</svg>
|
| 122 |
-
<span>Columns</span>
|
| 123 |
-
<span className="dd-count">{visible.size} <span style={{opacity:0.5}}>/ {COLUMNS.length}</span></span>
|
| 124 |
-
<svg width="10" height="10" viewBox="0 0 10 10" style={{marginLeft:4, transition:'transform .15s', transform: open?'rotate(180deg)':'none'}}>
|
| 125 |
-
<path d="M2 4L5 7L8 4" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
| 126 |
-
</svg>
|
| 127 |
-
</button>
|
| 128 |
-
{open && (
|
| 129 |
-
<div className="columns-dd-panel">
|
| 130 |
-
<div className="dd-panel-head">
|
| 131 |
-
<span>Visible columns</span>
|
| 132 |
-
<div style={{marginLeft:'auto', display:'flex', gap:4}}>
|
| 133 |
-
<button className="dd-mini" onClick={resetAll}>Show all</button>
|
| 134 |
-
<button className="dd-mini" onClick={hideAll}>Reset</button>
|
| 135 |
-
</div>
|
| 136 |
-
</div>
|
| 137 |
-
<div className="dd-groups">
|
| 138 |
-
{COL_GROUPS.map(g => {
|
| 139 |
-
const groupCols = COLUMNS.filter(c => c.group === g);
|
| 140 |
-
const visCount = groupCols.filter(c=>visible.has(c.key)).length;
|
| 141 |
-
const allOn = visCount === groupCols.length;
|
| 142 |
-
const some = visCount > 0 && visCount < groupCols.length;
|
| 143 |
-
return (
|
| 144 |
-
<div className="dd-group" key={g}>
|
| 145 |
-
<button className="dd-group-head" onClick={()=>toggleGroup(g)}>
|
| 146 |
-
<span className="sw" style={{background: GROUP_COLOR[g]}}/>
|
| 147 |
-
<span className={`dd-cbx ${allOn?'on':some?'some':''}`}/>
|
| 148 |
-
<span className="dd-group-name">{g}</span>
|
| 149 |
-
<span className="dd-group-count">{visCount}/{groupCols.length}</span>
|
| 150 |
-
</button>
|
| 151 |
-
<div className="dd-group-items">
|
| 152 |
-
{groupCols.map(c => {
|
| 153 |
-
const on = visible.has(c.key);
|
| 154 |
-
return (
|
| 155 |
-
<button key={c.key} className={`dd-item ${on?'on':''}`} onClick={()=>toggle(c.key)} title={c.desc}>
|
| 156 |
-
<span className={`dd-cbx ${on?'on':''}`}/>
|
| 157 |
-
<span className="dd-item-label">{c.label.replace('\n', ' ')}</span>
|
| 158 |
-
</button>
|
| 159 |
-
);
|
| 160 |
-
})}
|
| 161 |
-
</div>
|
| 162 |
-
</div>
|
| 163 |
-
);
|
| 164 |
-
})}
|
| 165 |
-
</div>
|
| 166 |
-
</div>
|
| 167 |
-
)}
|
| 168 |
-
</div>
|
| 169 |
-
);
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
// ---------- Table ----------
|
| 173 |
-
function BenchTable({ rows, visible }) {
|
| 174 |
-
const visibleCols = COLUMNS.filter(c => visible.has(c.key));
|
| 175 |
-
const metricColorFns = useMemo(() => {
|
| 176 |
-
const m = {};
|
| 177 |
-
visibleCols.filter(c => c.metric).forEach(c => { m[c.key] = normalize(rows, c); });
|
| 178 |
-
return m;
|
| 179 |
-
}, [rows, visibleCols.map(c=>c.key).join(',')]);
|
| 180 |
-
|
| 181 |
-
const sorted = useMemo(() => {
|
| 182 |
-
if (!rows.length) return [];
|
| 183 |
-
const ranked = compositeRank(rows, visibleCols.filter(c => c.metric));
|
| 184 |
-
return ranked.map(r => r.row);
|
| 185 |
-
}, [rows, visibleCols]);
|
| 186 |
-
|
| 187 |
-
const renderCell = (r, col) => {
|
| 188 |
-
const v = r[col.key];
|
| 189 |
-
if (col.key === 'repo_id') return <span className="mono" style={{color:'var(--fg-link)'}}>{v.replace('lerobot/','')}</span>;
|
| 190 |
-
if (col.key === 'vcodec') return <span className={`codec-pill codec-${v}`}>{v}</span>;
|
| 191 |
-
if (col.key === 'pix_fmt') return <span className="mono">{v}</span>;
|
| 192 |
-
if (['g','crf'].includes(col.key)) return <span className="mono" style={{color:'var(--fg-1)'}}>{v}</span>;
|
| 193 |
-
if (['timestamps_mode','backend'].includes(col.key)) return <span className="mono">{v}</span>;
|
| 194 |
-
if (col.metric) {
|
| 195 |
-
const fn = metricColorFns[col.key];
|
| 196 |
-
const t = fn ? fn(v) : 0.5;
|
| 197 |
-
const [rr,gg,bb] = rdylgn(t);
|
| 198 |
-
const bg = `rgba(${(rr|0)},${(gg|0)},${(bb|0)},0.35)`;
|
| 199 |
-
const fmt = col.fmt ? col.fmt(v) : String(v);
|
| 200 |
-
const std = col.stdKey ? r[col.stdKey] : null;
|
| 201 |
-
return (
|
| 202 |
-
<div style={{display:'inline-block', background: bg, padding:'2px 6px', borderRadius:4}}>
|
| 203 |
-
{fmt}{std != null && <span className="std"> ±{col.fmt(std)}</span>}
|
| 204 |
-
</div>
|
| 205 |
-
);
|
| 206 |
-
}
|
| 207 |
-
return String(v);
|
| 208 |
-
};
|
| 209 |
-
|
| 210 |
-
return (
|
| 211 |
-
<div className="table-wrap">
|
| 212 |
-
<div className="table-top">
|
| 213 |
-
<span className="row-count"><b>{sorted.length}</b> configurations • ranked by composite score</span>
|
| 214 |
-
<span style={{fontSize:11, color:'var(--fg-3)', fontFamily:'var(--font-mono)'}}>🟢 best · 🔴 worst</span>
|
| 215 |
-
</div>
|
| 216 |
-
<div className="table-scroll">
|
| 217 |
-
<table className="bench">
|
| 218 |
-
<thead>
|
| 219 |
-
<tr>
|
| 220 |
-
<th style={{width:40}}>#</th>
|
| 221 |
-
{visibleCols.map((c,i) => {
|
| 222 |
-
const prevGroup = i > 0 ? visibleCols[i-1].group : null;
|
| 223 |
-
const groupStart = c.group !== prevGroup && i > 0;
|
| 224 |
-
return (
|
| 225 |
-
<th key={c.key} className={groupStart?'group-start':''} data-tt={c.desc}>
|
| 226 |
-
{c.label}
|
| 227 |
-
</th>
|
| 228 |
-
);
|
| 229 |
-
})}
|
| 230 |
-
</tr>
|
| 231 |
-
</thead>
|
| 232 |
-
<tbody>
|
| 233 |
-
{sorted.slice(0, 250).map((r, i) => (
|
| 234 |
-
<tr key={i}>
|
| 235 |
-
<td className="rank">{String(i+1).padStart(2,'0')}</td>
|
| 236 |
-
{visibleCols.map((c, j) => {
|
| 237 |
-
const prevGroup = j > 0 ? visibleCols[j-1].group : null;
|
| 238 |
-
const groupStart = c.group !== prevGroup && j > 0;
|
| 239 |
-
return <td key={c.key} className={`${c.metric?'metric':''} ${groupStart?'group-start':''}`}>{renderCell(r, c)}</td>;
|
| 240 |
-
})}
|
| 241 |
-
</tr>
|
| 242 |
-
))}
|
| 243 |
-
</tbody>
|
| 244 |
-
</table>
|
| 245 |
-
</div>
|
| 246 |
-
</div>
|
| 247 |
-
);
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
// ---------- Radar Chart ----------
|
| 251 |
-
function RadarChart({ data, axes, colors }) {
|
| 252 |
-
const size = 380, cx = size/2, cy = size/2, r = 140;
|
| 253 |
-
const n = axes.length;
|
| 254 |
-
const angleFor = (i) => -Math.PI/2 + (i / n) * Math.PI * 2;
|
| 255 |
-
const gridLevels = [0.25, 0.5, 0.75, 1.0];
|
| 256 |
-
|
| 257 |
-
const axisPoint = (i, t) => {
|
| 258 |
-
const a = angleFor(i);
|
| 259 |
-
return [cx + Math.cos(a) * r * t, cy + Math.sin(a) * r * t];
|
| 260 |
-
};
|
| 261 |
-
|
| 262 |
-
return (
|
| 263 |
-
<svg viewBox={`0 0 ${size} ${size}`} xmlns="http://www.w3.org/2000/svg">
|
| 264 |
-
{/* Grid */}
|
| 265 |
-
{gridLevels.map(lvl => (
|
| 266 |
-
<polygon key={lvl}
|
| 267 |
-
points={axes.map((_, i) => axisPoint(i, lvl).join(',')).join(' ')}
|
| 268 |
-
fill="none" stroke="var(--border-1)" strokeWidth="1" />
|
| 269 |
-
))}
|
| 270 |
-
{/* Axes */}
|
| 271 |
-
{axes.map((ax, i) => {
|
| 272 |
-
const [x,y] = axisPoint(i, 1);
|
| 273 |
-
const [lx,ly] = axisPoint(i, 1.18);
|
| 274 |
-
return (
|
| 275 |
-
<g key={ax}>
|
| 276 |
-
<line x1={cx} y1={cy} x2={x} y2={y} stroke="var(--border-1)" strokeWidth="1" />
|
| 277 |
-
<text x={lx} y={ly} textAnchor="middle" dominantBaseline="central"
|
| 278 |
-
style={{font:"600 11px var(--font-sans)", fill:"var(--fg-2)"}}>{ax}</text>
|
| 279 |
-
</g>
|
| 280 |
-
);
|
| 281 |
-
})}
|
| 282 |
-
{/* Data polygons */}
|
| 283 |
-
{data.map((d, idx) => {
|
| 284 |
-
const pts = d.values.map((v, i) => axisPoint(i, Math.max(0, Math.min(1, v)))).map(p => p.join(',')).join(' ');
|
| 285 |
-
const color = colors[idx];
|
| 286 |
-
return (
|
| 287 |
-
<g key={idx}>
|
| 288 |
-
<polygon points={pts} fill={color} fillOpacity="0.18" stroke={color} strokeWidth="2" />
|
| 289 |
-
{d.values.map((v, i) => {
|
| 290 |
-
const [x,y] = axisPoint(i, Math.max(0, Math.min(1, v)));
|
| 291 |
-
return <circle key={i} cx={x} cy={y} r="3.5" fill={color} stroke="white" strokeWidth="1.5"/>;
|
| 292 |
-
})}
|
| 293 |
-
</g>
|
| 294 |
-
);
|
| 295 |
-
})}
|
| 296 |
-
</svg>
|
| 297 |
-
);
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
// ---------- Leaderboards ----------
|
| 301 |
-
// All categories share the same 6 axes — what changes between tabs is the
|
| 302 |
-
// WEIGHTING used to rank configurations.
|
| 303 |
-
const LEADERBOARD_AXES = ['encoding_time_ms', 'median_load_time_video_ms', 'video_images_size_ratio', 'median_mse', 'median_psnr', 'median_ssim'];
|
| 304 |
-
const LEADERBOARD_CATS = {
|
| 305 |
-
Overall: { desc: 'Balanced across encoding, decoding, size, and quality.',
|
| 306 |
-
weights: { encoding_time_ms: 1, median_load_time_video_ms: 1, video_images_size_ratio: 1, median_mse: 1, median_psnr: 1, median_ssim: 1 } },
|
| 307 |
-
Quality: { desc: 'Weighted toward reconstruction fidelity (MSE, PSNR, SSIM).',
|
| 308 |
-
weights: { encoding_time_ms: 0.15, median_load_time_video_ms: 0.15, video_images_size_ratio: 0.3, median_mse: 1.5, median_psnr: 1.5, median_ssim: 1.5 } },
|
| 309 |
-
Encoding: { desc: 'Weighted toward encoding speed and output size.',
|
| 310 |
-
weights: { encoding_time_ms: 2, median_load_time_video_ms: 0.2, video_images_size_ratio: 1.5, median_mse: 0.3, median_psnr: 0.3, median_ssim: 0.3 } },
|
| 311 |
-
Decoding: { desc: 'Weighted toward decoding throughput and size-on-disk impact.',
|
| 312 |
-
weights: { encoding_time_ms: 0.2, median_load_time_video_ms: 2, video_images_size_ratio: 1.2, median_mse: 0.3, median_psnr: 0.3, median_ssim: 0.3 } },
|
| 313 |
-
};
|
| 314 |
-
// Axis labels (short form) pulled from COLUMNS so everything stays in sync.
|
| 315 |
-
const AXIS_LABEL = Object.fromEntries(COLUMNS.map(c => [c.key, c.short || c.label]));
|
| 316 |
-
const PODIUM_COLORS = ['#FFD21E', '#496EF0', '#FF9D00', '#9747FF', '#009B3A', '#DB3328', '#353E60', '#FFBD59'];
|
| 317 |
-
|
| 318 |
-
function Leaderboards() {
|
| 319 |
-
const [cat, setCat] = useState('Overall');
|
| 320 |
-
const [ts, setTs] = useState('1_frame');
|
| 321 |
-
const [topN, setTopN] = useState(4);
|
| 322 |
-
|
| 323 |
-
const scoped = useMemo(() => ALL_ROWS.filter(r => r.timestamps_mode === ts), [ts]);
|
| 324 |
-
|
| 325 |
-
const { top, axes, values } = useMemo(() => {
|
| 326 |
-
const axisKeys = LEADERBOARD_AXES;
|
| 327 |
-
const axisCols = axisKeys.map(k => COLUMNS.find(c => c.key === k));
|
| 328 |
-
const axes = axisKeys.map(k => AXIS_LABEL[k]);
|
| 329 |
-
|
| 330 |
-
// Aggregate over per-dataset repeats: group by (vcodec, pix_fmt, g, crf, backend)
|
| 331 |
-
const groups = new Map();
|
| 332 |
-
for (const r of scoped) {
|
| 333 |
-
const key = `${r.vcodec}|${r.pix_fmt}|${r.g}|${r.crf}|${r.backend}`;
|
| 334 |
-
if (!groups.has(key)) groups.set(key, []);
|
| 335 |
-
groups.get(key).push(r);
|
| 336 |
-
}
|
| 337 |
-
const agg = [...groups.entries()].map(([k, rs]) => {
|
| 338 |
-
const ag = { ...rs[0] };
|
| 339 |
-
for (const c of axisCols) {
|
| 340 |
-
const vals = rs.map(r => r[c.key]).filter(v => v!=null);
|
| 341 |
-
ag[c.key] = vals.length ? vals.reduce((a,b)=>a+b,0)/vals.length : null;
|
| 342 |
-
}
|
| 343 |
-
return ag;
|
| 344 |
-
});
|
| 345 |
-
|
| 346 |
-
// Rank by category weighting
|
| 347 |
-
const weights = LEADERBOARD_CATS[cat].weights;
|
| 348 |
-
const normFns = axisCols.map(c => normalize(agg, c));
|
| 349 |
-
const weighted = agg.map(row => {
|
| 350 |
-
let sum = 0, wsum = 0;
|
| 351 |
-
axisCols.forEach((c, i) => {
|
| 352 |
-
const w = weights[c.key] || 1;
|
| 353 |
-
sum += w * (1 - normFns[i](row[c.key]));
|
| 354 |
-
wsum += w;
|
| 355 |
-
});
|
| 356 |
-
return { row, score: wsum ? sum/wsum : 0 };
|
| 357 |
-
}).sort((a,b) => a.score - b.score).slice(0, topN);
|
| 358 |
-
const top = weighted.map(x => x.row);
|
| 359 |
-
|
| 360 |
-
const values = top.map(row => axisCols.map((c, i) => normFns[i](row[c.key])));
|
| 361 |
-
return { top, axes, values };
|
| 362 |
-
}, [cat, ts, topN, scoped]);
|
| 363 |
-
|
| 364 |
-
return (
|
| 365 |
-
<div>
|
| 366 |
-
<div className="lb-controls">
|
| 367 |
-
<div className="lb-field">
|
| 368 |
-
<label>Access pattern</label>
|
| 369 |
-
<select className="select" value={ts} onChange={e=>setTs(e.target.value)}>
|
| 370 |
-
{TS_MODES.map(m => <option key={m} value={m}>{m}</option>)}
|
| 371 |
-
</select>
|
| 372 |
-
</div>
|
| 373 |
-
<div className="lb-field">
|
| 374 |
-
<label>Top N configs</label>
|
| 375 |
-
<div className="slider-wrap">
|
| 376 |
-
<input type="range" min="1" max="12" value={topN} onChange={e=>setTopN(+e.target.value)} />
|
| 377 |
-
<span className="v">{topN}</span>
|
| 378 |
-
</div>
|
| 379 |
-
</div>
|
| 380 |
-
<div className="lb-field" style={{marginLeft:'auto'}}>
|
| 381 |
-
<label>Ranking</label>
|
| 382 |
-
<div className="lb-subtabs">
|
| 383 |
-
{Object.keys(LEADERBOARD_CATS).map(c => (
|
| 384 |
-
<span key={c} className={`lb-subtab ${cat===c?'active':''}`} onClick={()=>setCat(c)}>{c}</span>
|
| 385 |
-
))}
|
| 386 |
-
</div>
|
| 387 |
-
</div>
|
| 388 |
-
</div>
|
| 389 |
-
|
| 390 |
-
<div className="lb-grid">
|
| 391 |
-
<div className="card radar-card">
|
| 392 |
-
<RadarChart data={top.map((r,i)=>({ values: values[i] }))} axes={axes} colors={PODIUM_COLORS} />
|
| 393 |
-
</div>
|
| 394 |
-
<div className="card lb-list-card">
|
| 395 |
-
<h3>Top configurations — <span style={{color:'var(--fg-3)', fontWeight:400}}>{cat.toLowerCase()}</span></h3>
|
| 396 |
-
<p style={{fontSize:'var(--fs-xs)', color:'var(--fg-3)', margin:'0 0 12px'}}>
|
| 397 |
-
{LEADERBOARD_CATS[cat].desc} Axes normalized so <b>1.0 = best-in-class</b>.
|
| 398 |
-
</p>
|
| 399 |
-
<div className="legend-list lb-legend-scroll">
|
| 400 |
-
{top.map((r, i) => (
|
| 401 |
-
<div className="legend-item" key={i}>
|
| 402 |
-
<div className="rank" style={{background: PODIUM_COLORS[i % PODIUM_COLORS.length]+'33', color: PODIUM_COLORS[i % PODIUM_COLORS.length]}}>{i+1}</div>
|
| 403 |
-
<div>
|
| 404 |
-
<div className="name"><span className={`codec-pill codec-${r.vcodec}`}>{r.vcodec}</span></div>
|
| 405 |
-
<div className="name" style={{color:'var(--fg-3)', marginTop:4}}>
|
| 406 |
-
{r.pix_fmt} · g={r.g} · crf={r.crf} · {r.backend}
|
| 407 |
-
</div>
|
| 408 |
-
</div>
|
| 409 |
-
<div className="score">{(values[i].reduce((a,b)=>a+b,0) / values[i].length).toFixed(3)}</div>
|
| 410 |
-
</div>
|
| 411 |
-
))}
|
| 412 |
-
</div>
|
| 413 |
-
</div>
|
| 414 |
-
</div>
|
| 415 |
-
|
| 416 |
-
<div className="card">
|
| 417 |
-
<h3>Detailed metrics</h3>
|
| 418 |
-
<div style={{overflowX:'auto'}}>
|
| 419 |
-
<table className="bench" style={{fontSize:'var(--fs-xs)'}}>
|
| 420 |
-
<thead>
|
| 421 |
-
<tr>
|
| 422 |
-
<th>#</th>
|
| 423 |
-
<th data-tt={COLUMNS.find(c=>c.key==='vcodec').desc}>Codec</th>
|
| 424 |
-
<th data-tt={COLUMNS.find(c=>c.key==='pix_fmt').desc}>Pixel format</th>
|
| 425 |
-
<th data-tt={COLUMNS.find(c=>c.key==='g').desc}>GOP</th>
|
| 426 |
-
<th data-tt={COLUMNS.find(c=>c.key==='crf').desc}>CRF</th>
|
| 427 |
-
<th data-tt={COLUMNS.find(c=>c.key==='backend').desc}>Backend</th>
|
| 428 |
-
{LEADERBOARD_AXES.map(k => {
|
| 429 |
-
const c = COLUMNS.find(x=>x.key===k);
|
| 430 |
-
return <th key={k} className="group-start">{c.label}</th>;
|
| 431 |
-
})}
|
| 432 |
-
</tr>
|
| 433 |
-
</thead>
|
| 434 |
-
<tbody>
|
| 435 |
-
{top.map((r, i) => (
|
| 436 |
-
<tr key={i}>
|
| 437 |
-
<td className="rank">{String(i+1).padStart(2,'0')}</td>
|
| 438 |
-
<td><span className={`codec-pill codec-${r.vcodec}`}>{r.vcodec}</span></td>
|
| 439 |
-
<td className="mono">{r.pix_fmt}</td>
|
| 440 |
-
<td className="mono">{r.g}</td>
|
| 441 |
-
<td className="mono">{r.crf}</td>
|
| 442 |
-
<td className="mono">{r.backend}</td>
|
| 443 |
-
{LEADERBOARD_AXES.map(k => {
|
| 444 |
-
const c = COLUMNS.find(x=>x.key===k);
|
| 445 |
-
return <td key={k} className="metric group-start">{c.fmt(r[k])}</td>;
|
| 446 |
-
})}
|
| 447 |
-
</tr>
|
| 448 |
-
))}
|
| 449 |
-
</tbody>
|
| 450 |
-
</table>
|
| 451 |
-
</div>
|
| 452 |
-
</div>
|
| 453 |
-
</div>
|
| 454 |
-
);
|
| 455 |
-
}
|
| 456 |
-
|
| 457 |
-
// ---------- Compare charts ----------
|
| 458 |
-
function BarChart({ data, xLabel, yLabel, xDesc, yDesc, color='#496EF0' }) {
|
| 459 |
-
const width = 780, height = 360, mL = 60, mR = 20, mT = 20, mB = 90;
|
| 460 |
-
const iw = width - mL - mR, ih = height - mT - mB;
|
| 461 |
-
if (!data.length) return <svg viewBox={`0 0 ${width} ${height}`} className="chart-svg" />;
|
| 462 |
-
const max = Math.max(...data.map(d => d.v));
|
| 463 |
-
const barW = iw / data.length * 0.7;
|
| 464 |
-
const step = iw / data.length;
|
| 465 |
-
const rotate = step < 72;
|
| 466 |
-
|
| 467 |
-
const ticks = 5;
|
| 468 |
-
const tickVals = Array.from({length: ticks+1}, (_, i) => (max * i / ticks));
|
| 469 |
-
|
| 470 |
-
return (
|
| 471 |
-
<svg viewBox={`0 0 ${width} ${height}`} className="chart-svg">
|
| 472 |
-
{/* grid */}
|
| 473 |
-
{tickVals.map((v, i) => {
|
| 474 |
-
const y = mT + ih - (v/max)*ih;
|
| 475 |
-
return (
|
| 476 |
-
<g key={i}>
|
| 477 |
-
<line x1={mL} x2={mL+iw} y1={y} y2={y} stroke="var(--border-1)" strokeDasharray="2,3"/>
|
| 478 |
-
<text x={mL-8} y={y} textAnchor="end" dominantBaseline="central" style={{font:"11px var(--font-mono)", fill:"var(--fg-3)"}}>{v.toFixed(2)}</text>
|
| 479 |
-
</g>
|
| 480 |
-
);
|
| 481 |
-
})}
|
| 482 |
-
{/* bars */}
|
| 483 |
-
{data.map((d, i) => {
|
| 484 |
-
const x = mL + i*step + (step-barW)/2;
|
| 485 |
-
const h = (d.v / max) * ih;
|
| 486 |
-
const y = mT + ih - h;
|
| 487 |
-
const tx = x + barW/2;
|
| 488 |
-
const ty = mT + ih + 14;
|
| 489 |
-
return (
|
| 490 |
-
<g key={d.k}>
|
| 491 |
-
<rect x={x} y={y} width={barW} height={h} fill={color} rx="4">
|
| 492 |
-
<title>{yDesc ? `${yLabel}: ${d.v.toFixed(3)}\n\n${yDesc}` : `${d.k}: ${d.v.toFixed(3)}`}</title>
|
| 493 |
-
</rect>
|
| 494 |
-
{rotate ? (
|
| 495 |
-
<text x={tx} y={ty} textAnchor="end"
|
| 496 |
-
transform={`rotate(-32 ${tx} ${ty})`}
|
| 497 |
-
style={{font:"11px var(--font-mono)", fill:"var(--fg-2)"}}>{d.k}</text>
|
| 498 |
-
) : (
|
| 499 |
-
<text x={tx} y={mT+ih+18} textAnchor="middle" style={{font:"11px var(--font-mono)", fill:"var(--fg-2)"}}>{d.k}</text>
|
| 500 |
-
)}
|
| 501 |
-
<text x={tx} y={y-6} textAnchor="middle" style={{font:"600 10px var(--font-mono)", fill:"var(--fg-1)"}}>{d.v.toFixed(2)}</text>
|
| 502 |
-
</g>
|
| 503 |
-
);
|
| 504 |
-
})}
|
| 505 |
-
{/* axis labels */}
|
| 506 |
-
<text x={mL+iw/2} y={height-8} textAnchor="middle" style={{font:"600 11px var(--font-sans)", fill:"var(--fg-2)"}}>{xLabel}</text>
|
| 507 |
-
<text x={18} y={mT+ih/2} textAnchor="middle" transform={`rotate(-90 18 ${mT+ih/2})`} style={{font:"600 11px var(--font-sans)", fill:"var(--fg-2)"}}>{yLabel}</text>
|
| 508 |
-
</svg>
|
| 509 |
-
);
|
| 510 |
-
}
|
| 511 |
-
|
| 512 |
-
function ScatterChart({ points, xLabel, yLabel, xDesc, yDesc, colorBy }) {
|
| 513 |
-
const width = 780, height = 360, mL = 60, mR = 140, mT = 20, mB = 60;
|
| 514 |
-
const iw = width - mL - mR, ih = height - mT - mB;
|
| 515 |
-
if (!points.length) return <svg viewBox={`0 0 ${width} ${height}`} className="chart-svg" />;
|
| 516 |
-
const xMin = Math.min(...points.map(p=>p.x));
|
| 517 |
-
const xMax = Math.max(...points.map(p=>p.x));
|
| 518 |
-
const yMin = Math.min(...points.map(p=>p.y));
|
| 519 |
-
const yMax = Math.max(...points.map(p=>p.y));
|
| 520 |
-
const xS = v => mL + ((v - xMin)/(xMax-xMin || 1)) * iw;
|
| 521 |
-
const yS = v => mT + ih - ((v - yMin)/(yMax-yMin || 1)) * ih;
|
| 522 |
-
|
| 523 |
-
const colors = { h264: '#1D4ED8', hevc: '#7E22CE', libsvtav1: '#047857' };
|
| 524 |
-
const cats = [...new Set(points.map(p => p.c))];
|
| 525 |
-
|
| 526 |
-
return (
|
| 527 |
-
<svg viewBox={`0 0 ${width} ${height}`} className="chart-svg">
|
| 528 |
-
{/* grid */}
|
| 529 |
-
{[0,0.25,0.5,0.75,1].map((t,i) => {
|
| 530 |
-
const y = mT + ih - t*ih;
|
| 531 |
-
const v = yMin + (yMax-yMin)*t;
|
| 532 |
-
return <g key={i}>
|
| 533 |
-
<line x1={mL} x2={mL+iw} y1={y} y2={y} stroke="var(--border-1)" strokeDasharray="2,3"/>
|
| 534 |
-
<text x={mL-8} y={y} textAnchor="end" dominantBaseline="central" style={{font:"11px var(--font-mono)", fill:"var(--fg-3)"}}>{v.toFixed(1)}</text>
|
| 535 |
-
</g>;
|
| 536 |
-
})}
|
| 537 |
-
{[0,0.25,0.5,0.75,1].map((t,i) => {
|
| 538 |
-
const x = mL + t*iw;
|
| 539 |
-
const v = xMin + (xMax-xMin)*t;
|
| 540 |
-
return <g key={i}>
|
| 541 |
-
<line x1={x} x2={x} y1={mT} y2={mT+ih} stroke="var(--border-1)" strokeDasharray="2,3"/>
|
| 542 |
-
<text x={x} y={mT+ih+16} textAnchor="middle" style={{font:"11px var(--font-mono)", fill:"var(--fg-3)"}}>{v.toFixed(2)}</text>
|
| 543 |
-
</g>;
|
| 544 |
-
})}
|
| 545 |
-
{/* points */}
|
| 546 |
-
{points.map((p, i) => (
|
| 547 |
-
<circle key={i} cx={xS(p.x)} cy={yS(p.y)} r="4" fill={colors[p.c]||'#496EF0'} fillOpacity="0.55" stroke={colors[p.c]||'#496EF0'} strokeWidth="1">
|
| 548 |
-
<title>{`${p.c} · ${p.label}\n${xLabel}: ${p.x.toFixed(4)}\n${yLabel}: ${p.y.toFixed(2)}`}</title>
|
| 549 |
-
</circle>
|
| 550 |
-
))}
|
| 551 |
-
{/* legend */}
|
| 552 |
-
{cats.map((c, i) => (
|
| 553 |
-
<g key={c} transform={`translate(${mL+iw+20} ${mT+i*22})`}>
|
| 554 |
-
<circle cx="6" cy="6" r="5" fill={colors[c]||'#496EF0'}/>
|
| 555 |
-
<text x="18" y="6" dominantBaseline="central" style={{font:"11px var(--font-mono)", fill:"var(--fg-1)"}}>{c}</text>
|
| 556 |
-
</g>
|
| 557 |
-
))}
|
| 558 |
-
<text x={mL+iw/2} y={height-10} textAnchor="middle" style={{font:"600 11px var(--font-sans)", fill:"var(--fg-2)"}}>{xLabel}</text>
|
| 559 |
-
<text x={18} y={mT+ih/2} textAnchor="middle" transform={`rotate(-90 18 ${mT+ih/2})`} style={{font:"600 11px var(--font-sans)", fill:"var(--fg-2)"}}>{yLabel}</text>
|
| 560 |
-
</svg>
|
| 561 |
-
);
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
const STACK_COLORS = ['#496EF0', '#FF9D00', '#009B3A', '#9747FF', '#FFD21E', '#DB3328'];
|
| 565 |
-
|
| 566 |
-
function StackedBarChart({ data, keys, xLabel, yLabel }) {
|
| 567 |
-
const width = 780, rowH = 46, mL = 90, mR = 40, mT = 30, mB = 70;
|
| 568 |
-
const n = data.length;
|
| 569 |
-
const ih = rowH * n;
|
| 570 |
-
const height = mT + ih + mB;
|
| 571 |
-
const iw = width - mL - mR;
|
| 572 |
-
if (!n) return <svg viewBox={`0 0 ${width} ${height}`} className="chart-svg" />;
|
| 573 |
-
const max = Math.max(...data.map(d => d.total));
|
| 574 |
-
const ticks = 5;
|
| 575 |
-
const tickVals = Array.from({length: ticks+1}, (_, i) => (max * i / ticks));
|
| 576 |
-
|
| 577 |
-
return (
|
| 578 |
-
<svg viewBox={`0 0 ${width} ${height}`} className="chart-svg">
|
| 579 |
-
{/* grid */}
|
| 580 |
-
{tickVals.map((v, i) => {
|
| 581 |
-
const x = mL + (v/max)*iw;
|
| 582 |
-
return (
|
| 583 |
-
<g key={i}>
|
| 584 |
-
<line x1={x} x2={x} y1={mT} y2={mT+ih} stroke="var(--border-1)" strokeDasharray="2,3"/>
|
| 585 |
-
<text x={x} y={mT+ih+16} textAnchor="middle" style={{font:"11px var(--font-mono)", fill:"var(--fg-3)"}}>{Math.round(v)}</text>
|
| 586 |
-
</g>
|
| 587 |
-
);
|
| 588 |
-
})}
|
| 589 |
-
{/* bars */}
|
| 590 |
-
{data.map((d, i) => {
|
| 591 |
-
const y = mT + i*rowH + 6;
|
| 592 |
-
const h = rowH - 14;
|
| 593 |
-
let cx = mL;
|
| 594 |
-
return (
|
| 595 |
-
<g key={d.k}>
|
| 596 |
-
<text x={mL-10} y={y+h/2} textAnchor="end" dominantBaseline="central" style={{font:"600 12px var(--font-sans)", fill:"var(--fg-1)"}}>{d.k}</text>
|
| 597 |
-
{d.segments.map((s, j) => {
|
| 598 |
-
const w = (s.v/max)*iw;
|
| 599 |
-
const fill = STACK_COLORS[j % STACK_COLORS.length];
|
| 600 |
-
const rect = <rect key={j} x={cx} y={y} width={w} height={h} fill={fill}>
|
| 601 |
-
<title>{d.k} · {s.key}: {s.v.toFixed(1)} ms</title>
|
| 602 |
-
</rect>;
|
| 603 |
-
const label = w > 36 ? <text key={'t'+j} x={cx+w/2} y={y+h/2} textAnchor="middle" dominantBaseline="central" style={{font:"600 10px var(--font-mono)", fill:"white"}}>{s.v.toFixed(0)}</text> : null;
|
| 604 |
-
cx += w;
|
| 605 |
-
return <g key={j}>{rect}{label}</g>;
|
| 606 |
-
})}
|
| 607 |
-
<text x={cx+8} y={y+h/2} dominantBaseline="central" style={{font:"600 11px var(--font-mono)", fill:"var(--fg-1)"}}>{d.total.toFixed(0)} ms</text>
|
| 608 |
-
</g>
|
| 609 |
-
);
|
| 610 |
-
})}
|
| 611 |
-
{/* legend */}
|
| 612 |
-
<g transform={`translate(${mL}, ${mT+ih+44})`}>
|
| 613 |
-
{keys.map((k, i) => (
|
| 614 |
-
<g key={k} transform={`translate(${i*130}, 0)`}>
|
| 615 |
-
<rect x={0} y={-9} width={12} height={12} fill={STACK_COLORS[i % STACK_COLORS.length]} rx="2"/>
|
| 616 |
-
<text x={18} y={0} dominantBaseline="central" style={{font:"11px var(--font-mono)", fill:"var(--fg-2)"}}>{k}</text>
|
| 617 |
-
</g>
|
| 618 |
-
))}
|
| 619 |
-
</g>
|
| 620 |
-
<text x={mL+iw/2} y={height-8} textAnchor="middle" style={{font:"600 11px var(--font-sans)", fill:"var(--fg-2)"}}>{xLabel}</text>
|
| 621 |
-
</svg>
|
| 622 |
-
);
|
| 623 |
-
}
|
| 624 |
-
|
| 625 |
-
function Compare() {
|
| 626 |
-
const metricKeys = METRIC_COLS.map(c => c.key);
|
| 627 |
-
const groupKeys = ['vcodec', 'pix_fmt', 'timestamps_mode', 'backend', 'repo_id', 'g', 'crf'];
|
| 628 |
-
const [metric, setMetric] = useState('median_psnr');
|
| 629 |
-
const [groupBy, setGroupBy] = useState('vcodec');
|
| 630 |
-
|
| 631 |
-
const barData = useMemo(() => {
|
| 632 |
-
const g = new Map();
|
| 633 |
-
for (const r of ALL_ROWS) {
|
| 634 |
-
const v = r[metric];
|
| 635 |
-
if (v == null || isNaN(v)) continue;
|
| 636 |
-
const k = String(r[groupBy]);
|
| 637 |
-
if (!g.has(k)) g.set(k, []);
|
| 638 |
-
g.get(k).push(v);
|
| 639 |
-
}
|
| 640 |
-
return [...g.entries()].map(([k, arr]) => ({
|
| 641 |
-
k: k.replace('lerobot/',''),
|
| 642 |
-
v: arr.reduce((a,b)=>a+b,0)/arr.length
|
| 643 |
-
})).sort((a,b) => b.v - a.v);
|
| 644 |
-
}, [metric, groupBy]);
|
| 645 |
-
|
| 646 |
-
const scatterData = useMemo(() => {
|
| 647 |
-
return ALL_ROWS.slice(0, 600).map(r => ({
|
| 648 |
-
x: r.video_images_size_ratio,
|
| 649 |
-
y: r.median_psnr,
|
| 650 |
-
c: r.vcodec,
|
| 651 |
-
label: `g=${r.g} crf=${r.crf}`,
|
| 652 |
-
}));
|
| 653 |
-
}, []);
|
| 654 |
-
|
| 655 |
-
// Decoding speed: stacked bar — one bar per codec, segments = access patterns.
|
| 656 |
-
const stackedSpeed = useMemo(() => {
|
| 657 |
-
const codecs = [...new Set(ALL_ROWS.map(r => r.vcodec))];
|
| 658 |
-
const modes = [...new Set(ALL_ROWS.map(r => r.timestamps_mode))];
|
| 659 |
-
const data = codecs.map(codec => {
|
| 660 |
-
const seg = modes.map(mode => {
|
| 661 |
-
const vals = ALL_ROWS.filter(r => r.vcodec === codec && r.timestamps_mode === mode).map(r => r.median_load_time_video_ms);
|
| 662 |
-
return { key: mode, v: vals.length ? vals.reduce((a,b)=>a+b,0)/vals.length : 0 };
|
| 663 |
-
});
|
| 664 |
-
return { k: codec, segments: seg, total: seg.reduce((a,b)=>a+b.v,0) };
|
| 665 |
-
}).sort((a,b) => a.total - b.total);
|
| 666 |
-
return { codecs, modes, data };
|
| 667 |
-
}, []);
|
| 668 |
-
|
| 669 |
-
const metricLabel = COLUMNS.find(c=>c.key===metric).label.replace('\n', ' ');
|
| 670 |
-
const groupLabel = COLUMNS.find(c=>c.key===groupBy).label.replace('\n', ' ');
|
| 671 |
-
|
| 672 |
-
return (
|
| 673 |
-
<div className="compare-grid">
|
| 674 |
-
<div className="card chart-card">
|
| 675 |
-
<div className="chart-head">
|
| 676 |
-
<div>
|
| 677 |
-
<h3 style={{margin:0}}>Compare parameters</h3>
|
| 678 |
-
<p style={{margin:'4px 0 0', fontSize:'var(--fs-xs)', color:'var(--fg-3)'}}>Mean of <b>{metricLabel}</b> grouped by <b>{groupLabel}</b></p>
|
| 679 |
-
</div>
|
| 680 |
-
<div className="controls">
|
| 681 |
-
<div className="lb-field"><label>Metric</label>
|
| 682 |
-
<select className="select" value={metric} onChange={e=>setMetric(e.target.value)}>
|
| 683 |
-
{metricKeys.map(k => <option key={k} value={k}>{COLUMNS.find(c=>c.key===k).label.replace('\n', ' ')}</option>)}
|
| 684 |
-
</select>
|
| 685 |
-
</div>
|
| 686 |
-
<div className="lb-field"><label>Group by</label>
|
| 687 |
-
<select className="select" value={groupBy} onChange={e=>setGroupBy(e.target.value)}>
|
| 688 |
-
{groupKeys.map(k => <option key={k} value={k}>{COLUMNS.find(c=>c.key===k).label.replace('\n', ' ')}</option>)}
|
| 689 |
-
</select>
|
| 690 |
-
</div>
|
| 691 |
-
</div>
|
| 692 |
-
</div>
|
| 693 |
-
<BarChart data={barData} xLabel={groupLabel} yLabel={metricLabel} yDesc={COLUMNS.find(c=>c.key===metric).desc} xDesc={COLUMNS.find(c=>c.key===groupBy).desc} />
|
| 694 |
-
</div>
|
| 695 |
-
|
| 696 |
-
<div className="card chart-card">
|
| 697 |
-
<div className="chart-head">
|
| 698 |
-
<div>
|
| 699 |
-
<h3 style={{margin:0}}>Compression vs quality</h3>
|
| 700 |
-
<p style={{margin:'4px 0 0', fontSize:'var(--fs-xs)', color:'var(--fg-3)'}}>Every encoded config as a dot. Up and to the left = smaller files at higher quality.</p>
|
| 701 |
-
</div>
|
| 702 |
-
</div>
|
| 703 |
-
<ScatterChart points={scatterData} xLabel="Size ratio (lower is better)" yLabel="PSNR (higher is better)" xDesc={COLUMNS.find(c=>c.key==='video_images_size_ratio').desc} yDesc={COLUMNS.find(c=>c.key==='median_psnr').desc} />
|
| 704 |
-
</div>
|
| 705 |
-
|
| 706 |
-
<div className="card chart-card">
|
| 707 |
-
<div className="chart-head">
|
| 708 |
-
<div>
|
| 709 |
-
<h3 style={{margin:0}}>Decoding speed by codec × access pattern</h3>
|
| 710 |
-
<p style={{margin:'4px 0 0', fontSize:'var(--fs-xs)', color:'var(--fg-3)'}}>Cumulative median decode time per codec, broken down by access pattern. Shorter bar = faster overall.</p>
|
| 711 |
-
</div>
|
| 712 |
-
</div>
|
| 713 |
-
<StackedBarChart data={stackedSpeed.data} keys={stackedSpeed.modes} xLabel="Decode time across access patterns (ms)" yLabel="Codec" />
|
| 714 |
-
</div>
|
| 715 |
-
</div>
|
| 716 |
-
);
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
-
// ---------- Submit ----------
|
| 720 |
-
function Submit() {
|
| 721 |
-
const [repos, setRepos] = useState(new Set(REPOS.slice(0,2)));
|
| 722 |
-
const [vcodecs, setVcodecs] = useState(new Set(['h264']));
|
| 723 |
-
const [pixs, setPixs] = useState(new Set(['yuv420p']));
|
| 724 |
-
const [gs, setGs] = useState(new Set(['2','10']));
|
| 725 |
-
const [crfs, setCrfs] = useState(new Set(['10','30']));
|
| 726 |
-
const [tsModes, setTsModes] = useState(new Set(['1_frame','2_frames']));
|
| 727 |
-
const [backends, setBackends] = useState(new Set(['pyav']));
|
| 728 |
-
const [samples, setSamples] = useState(50);
|
| 729 |
-
const [workers, setWorkers] = useState(4);
|
| 730 |
-
const [submitted, setSubmitted] = useState(false);
|
| 731 |
-
|
| 732 |
-
const chip = (set, setSet, v) => {
|
| 733 |
-
const s = new Set(set);
|
| 734 |
-
s.has(v) ? s.delete(v) : s.add(v);
|
| 735 |
-
setSet(s);
|
| 736 |
-
};
|
| 737 |
-
|
| 738 |
-
const totalConfigs = repos.size * vcodecs.size * pixs.size * gs.size * crfs.size * tsModes.size * backends.size;
|
| 739 |
-
const estMinutes = Math.round(totalConfigs * samples * 0.08 / 60);
|
| 740 |
-
|
| 741 |
-
const submit = () => { setSubmitted(true); setTimeout(()=>setSubmitted(false), 3500); };
|
| 742 |
-
|
| 743 |
-
const queue = [
|
| 744 |
-
{ id: 'sub_3a9f', status: 'done', repo: 'lerobot/kitchen', when: '2 min ago' },
|
| 745 |
-
{ id: 'sub_3a9e', status: 'running', repo: 'lerobot/paris_street', when: '14 min ago', progress: '42/96' },
|
| 746 |
-
{ id: 'sub_3a9d', status: 'running', repo: 'aliberts/aloha', when: '28 min ago', progress: '8/24' },
|
| 747 |
-
{ id: 'sub_3a9c', status: 'queued', repo: 'lerobot/pusht_image', when: '1 h ago' },
|
| 748 |
-
{ id: 'sub_3a9b', status: 'done', repo: 'lerobot/kitchen', when: '3 h ago' },
|
| 749 |
-
];
|
| 750 |
-
|
| 751 |
-
return (
|
| 752 |
-
<div className="submit-grid">
|
| 753 |
-
<div className="card">
|
| 754 |
-
<h2>Submit a benchmark run</h2>
|
| 755 |
-
<p style={{fontSize:'var(--fs-sm)', color:'var(--fg-2)', margin:'0 0 var(--space-5)'}}>
|
| 756 |
-
Queue a benchmark to a pool of evaluation workers. Results land in the Results tab automatically.
|
| 757 |
-
</p>
|
| 758 |
-
|
| 759 |
-
<div className="param-section">
|
| 760 |
-
<h4>Datasets</h4>
|
| 761 |
-
<p className="hint">Hugging Face Hub dataset repo IDs. The benchmark runs on every (dataset × config) combination.</p>
|
| 762 |
-
<div className="checkbox-group">
|
| 763 |
-
{REPOS.map(r => (
|
| 764 |
-
<span key={r} className={`checkbox-chip ${repos.has(r)?'checked':''}`} onClick={()=>chip(repos, setRepos, r)}>{r}</span>
|
| 765 |
-
))}
|
| 766 |
-
</div>
|
| 767 |
-
</div>
|
| 768 |
-
|
| 769 |
-
<div className="param-section">
|
| 770 |
-
<h4>Encoding — codec & pixel format</h4>
|
| 771 |
-
<p className="hint">Codec defines the compression algorithm; pixel format controls chroma subsampling.</p>
|
| 772 |
-
<div className="checkbox-group" style={{marginBottom: 8}}>
|
| 773 |
-
{VCODECS.map(v => (
|
| 774 |
-
<span key={v} className={`checkbox-chip ${vcodecs.has(v)?'checked':''}`} onClick={()=>chip(vcodecs, setVcodecs, v)}>{v}</span>
|
| 775 |
-
))}
|
| 776 |
-
</div>
|
| 777 |
-
<div className="checkbox-group">
|
| 778 |
-
{PIX_FMTS.map(v => (
|
| 779 |
-
<span key={v} className={`checkbox-chip ${pixs.has(v)?'checked':''}`} onClick={()=>chip(pixs, setPixs, v)}>{v}</span>
|
| 780 |
-
))}
|
| 781 |
-
</div>
|
| 782 |
-
</div>
|
| 783 |
-
|
| 784 |
-
<div className="param-section">
|
| 785 |
-
<h4>Encoding — quality knobs <span style={{color:'var(--fg-3)', fontWeight:400}}>GOP · CRF</span></h4>
|
| 786 |
-
<p className="hint">GOP = distance between keyframes. CRF = quality (0 lossless → 50 very compressed).</p>
|
| 787 |
-
<div style={{display:'flex', gap:'var(--space-2)', alignItems:'center', marginBottom: 8}}>
|
| 788 |
-
<span style={{fontSize:11, color:'var(--fg-3)', width:32, fontFamily:'var(--font-mono)'}}>GOP</span>
|
| 789 |
-
<div className="checkbox-group">
|
| 790 |
-
{['1','2','3','4','5','6','10','15','20','40','100'].map(v => (
|
| 791 |
-
<span key={v} className={`checkbox-chip ${gs.has(v)?'checked':''}`} onClick={()=>chip(gs, setGs, v)}>{v}</span>
|
| 792 |
-
))}
|
| 793 |
-
</div>
|
| 794 |
-
</div>
|
| 795 |
-
<div style={{display:'flex', gap:'var(--space-2)', alignItems:'center'}}>
|
| 796 |
-
<span style={{fontSize:11, color:'var(--fg-3)', width:32, fontFamily:'var(--font-mono)'}}>CRF</span>
|
| 797 |
-
<div className="checkbox-group">
|
| 798 |
-
{['0','5','10','15','20','25','30','40','50'].map(v => (
|
| 799 |
-
<span key={v} className={`checkbox-chip ${crfs.has(v)?'checked':''}`} onClick={()=>chip(crfs, setCrfs, v)}>{v}</span>
|
| 800 |
-
))}
|
| 801 |
-
</div>
|
| 802 |
-
</div>
|
| 803 |
-
</div>
|
| 804 |
-
|
| 805 |
-
<div className="param-section">
|
| 806 |
-
<h4>Decoding — access pattern & backend</h4>
|
| 807 |
-
<p className="hint">How frames are requested and which library decodes them.</p>
|
| 808 |
-
<div className="checkbox-group" style={{marginBottom:8}}>
|
| 809 |
-
{TS_MODES.map(v => (
|
| 810 |
-
<span key={v} className={`checkbox-chip ${tsModes.has(v)?'checked':''}`} onClick={()=>chip(tsModes, setTsModes, v)}>{v}</span>
|
| 811 |
-
))}
|
| 812 |
-
</div>
|
| 813 |
-
<div className="checkbox-group">
|
| 814 |
-
{['pyav','torchcodec','video_reader'].map(v => (
|
| 815 |
-
<span key={v} className={`checkbox-chip ${backends.has(v)?'checked':''}`} onClick={()=>chip(backends, setBackends, v)}>{v}</span>
|
| 816 |
-
))}
|
| 817 |
-
</div>
|
| 818 |
-
</div>
|
| 819 |
-
|
| 820 |
-
<div className="param-section">
|
| 821 |
-
<h4>Sampling</h4>
|
| 822 |
-
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:'var(--space-4)'}}>
|
| 823 |
-
<div>
|
| 824 |
-
<div style={{fontSize:11, color:'var(--fg-3)', fontWeight:600, textTransform:'uppercase', letterSpacing:'0.06em'}}>Samples per config <span style={{float:'right', fontFamily:'var(--font-mono)', color:'var(--fg-1)'}}>{samples}</span></div>
|
| 825 |
-
<input type="range" min="1" max="200" value={samples} onChange={e=>setSamples(+e.target.value)} style={{width:'100%', accentColor:'var(--hf-blue)'}} />
|
| 826 |
-
</div>
|
| 827 |
-
<div>
|
| 828 |
-
<div style={{fontSize:11, color:'var(--fg-3)', fontWeight:600, textTransform:'uppercase', letterSpacing:'0.06em'}}>Worker threads <span style={{float:'right', fontFamily:'var(--font-mono)', color:'var(--fg-1)'}}>{workers}</span></div>
|
| 829 |
-
<input type="range" min="1" max="32" value={workers} onChange={e=>setWorkers(+e.target.value)} style={{width:'100%', accentColor:'var(--hf-blue)'}} />
|
| 830 |
-
</div>
|
| 831 |
-
</div>
|
| 832 |
-
</div>
|
| 833 |
-
|
| 834 |
-
<button className="btn primary" style={{marginTop:'var(--space-4)'}} onClick={submit} disabled={!totalConfigs}>
|
| 835 |
-
{submitted ? '✓ Submitted to queue' : `Submit ${totalConfigs} configuration${totalConfigs===1?'':'s'}`}
|
| 836 |
-
</button>
|
| 837 |
-
</div>
|
| 838 |
-
|
| 839 |
-
<div>
|
| 840 |
-
<div className="card summary-card">
|
| 841 |
-
<div className="count-box">
|
| 842 |
-
<div>
|
| 843 |
-
<div className="n">{totalConfigs.toLocaleString()}</div>
|
| 844 |
-
<div className="l">configurations<br/>in this run</div>
|
| 845 |
-
</div>
|
| 846 |
-
<img src={(typeof window !== 'undefined' && window.__ROCKET) || "assets/huggy-rocket.png"} alt=""/>
|
| 847 |
-
</div>
|
| 848 |
-
<div className="summary-list">
|
| 849 |
-
<div className="row"><span>Datasets</span><b>{repos.size}</b></div>
|
| 850 |
-
<div className="row"><span>Codecs</span><b>{vcodecs.size}</b></div>
|
| 851 |
-
<div className="row"><span>Pixel formats</span><b>{pixs.size}</b></div>
|
| 852 |
-
<div className="row"><span>GOP values</span><b>{gs.size}</b></div>
|
| 853 |
-
<div className="row"><span>CRF values</span><b>{crfs.size}</b></div>
|
| 854 |
-
<div className="row"><span>Access modes</span><b>{tsModes.size}</b></div>
|
| 855 |
-
<div className="row"><span>Backends</span><b>{backends.size}</b></div>
|
| 856 |
-
<div className="row"><span>Samples / config</span><b>{samples}</b></div>
|
| 857 |
-
<div className="row" style={{marginTop:8, paddingTop:12, borderTop:'1px solid var(--border-1)', borderBottom:'none'}}>
|
| 858 |
-
<span style={{color:'var(--fg-1)', fontWeight:600}}>Est. wall time</span>
|
| 859 |
-
<b style={{color: estMinutes>60 ? 'var(--hf-red)' : 'var(--hf-green)'}}>~{estMinutes} min</b>
|
| 860 |
-
</div>
|
| 861 |
-
</div>
|
| 862 |
-
</div>
|
| 863 |
-
|
| 864 |
-
<div className="card" style={{marginTop:'var(--space-4)'}}>
|
| 865 |
-
<h3 style={{display:'flex', alignItems:'center', gap:6}}>
|
| 866 |
-
Queue
|
| 867 |
-
<span style={{marginLeft:'auto', fontSize:11, color:'var(--fg-3)', fontWeight:400}}>5 recent</span>
|
| 868 |
-
</h3>
|
| 869 |
-
<table className="queue-table">
|
| 870 |
-
<thead>
|
| 871 |
-
<tr><th>ID</th><th>Status</th><th>Submitted</th></tr>
|
| 872 |
-
</thead>
|
| 873 |
-
<tbody>
|
| 874 |
-
{queue.map(q => (
|
| 875 |
-
<tr key={q.id}>
|
| 876 |
-
<td>{q.id}</td>
|
| 877 |
-
<td>
|
| 878 |
-
<span className={`queue-status ${q.status}`}>
|
| 879 |
-
<span className="dot"/>
|
| 880 |
-
{q.status}{q.progress?` (${q.progress})`:''}
|
| 881 |
-
</span>
|
| 882 |
-
</td>
|
| 883 |
-
<td style={{color:'var(--fg-3)'}}>{q.when}</td>
|
| 884 |
-
</tr>
|
| 885 |
-
))}
|
| 886 |
-
</tbody>
|
| 887 |
-
</table>
|
| 888 |
-
</div>
|
| 889 |
-
</div>
|
| 890 |
-
</div>
|
| 891 |
-
);
|
| 892 |
-
}
|
| 893 |
-
|
| 894 |
-
// ---------- Parameters ----------
|
| 895 |
-
// Descriptions live in COLUMNS (data.js). Group structure stays here so we can
|
| 896 |
-
// narrate the concept groupings.
|
| 897 |
-
const PARAM_GROUPS = [
|
| 898 |
-
{ t: 'Inputs — what you benchmark',
|
| 899 |
-
desc: 'The corpus. Each dataset is a LeRobot episode recording with a few minutes of RGB observations.',
|
| 900 |
-
keys: ['repo_id'] },
|
| 901 |
-
{ t: 'Encoding — how the video is compressed',
|
| 902 |
-
desc: 'Passed to the FFmpeg/PyAV encoder. These are the knobs operators actually tune.',
|
| 903 |
-
keys: ['vcodec', 'pix_fmt', 'g', 'crf'] },
|
| 904 |
-
{ t: 'Decoding — how you read it back',
|
| 905 |
-
desc: 'The other half of the equation. The same MP4 can decode very differently depending on library and access pattern.',
|
| 906 |
-
keys: ['backend', 'timestamps_mode'] },
|
| 907 |
-
{ t: 'Fidelity metrics — how faithful is decoded frame vs. source',
|
| 908 |
-
desc: 'We decode the compressed video, re-read the original PNG frames, and compare pixel-for-pixel.',
|
| 909 |
-
keys: ['median_mse', 'median_psnr', 'median_ssim'] },
|
| 910 |
-
{ t: 'Performance metrics — how fast, how small',
|
| 911 |
-
desc: 'The cost side. Run on a single CPU thread unless noted.',
|
| 912 |
-
keys: ['encoding_time_ms', 'video_images_size_ratio', 'median_load_time_video_ms', 'video_images_load_time_ratio'] },
|
| 913 |
-
];
|
| 914 |
-
|
| 915 |
-
function Parameters() {
|
| 916 |
-
return (
|
| 917 |
-
<div className="params-page">
|
| 918 |
-
{PARAM_GROUPS.map((g, gi) => (
|
| 919 |
-
<section className="param-group" key={gi}>
|
| 920 |
-
<div className="param-group-head">
|
| 921 |
-
<h3>{g.t}</h3>
|
| 922 |
-
<p>{g.desc}</p>
|
| 923 |
-
</div>
|
| 924 |
-
<div className="param-cards">
|
| 925 |
-
{g.keys.map(k => {
|
| 926 |
-
const col = COLUMNS.find(c => c.key === k);
|
| 927 |
-
if (!col) return null;
|
| 928 |
-
return (
|
| 929 |
-
<div className="param-card" key={k}>
|
| 930 |
-
<div className="param-card-head">
|
| 931 |
-
<code className="param-key">{k}</code>
|
| 932 |
-
<span className="param-label">{col.label.replace('\n', ' ')}</span>
|
| 933 |
-
</div>
|
| 934 |
-
<p>{col.desc}</p>
|
| 935 |
-
</div>
|
| 936 |
-
);
|
| 937 |
-
})}
|
| 938 |
-
</div>
|
| 939 |
-
</section>
|
| 940 |
-
))}
|
| 941 |
-
</div>
|
| 942 |
-
);
|
| 943 |
-
}
|
| 944 |
-
|
| 945 |
-
// ---------- About ----------
|
| 946 |
-
function About() {
|
| 947 |
-
return (
|
| 948 |
-
<div className="about-grid">
|
| 949 |
-
<div className="prose">
|
| 950 |
-
<h3 id="what">What this is</h3>
|
| 951 |
-
<p>
|
| 952 |
-
A benchmark for video encoding and decoding in the context of robotics datasets. LeRobot stores episode observations as MP4 rather than PNG sequences — this page quantifies <i>how much</i> we gain in size and what we pay in decode latency and pixel fidelity.
|
| 953 |
-
</p>
|
| 954 |
-
<p>
|
| 955 |
-
We sweep over four axes — <code>vcodec</code>, <code>pix_fmt</code>, <code>g</code> (GOP size), and <code>crf</code> (quality) — across four representative datasets, and decode each clip through <code>pyav</code>, <code>torchcodec</code>, and <code>video_reader</code>.
|
| 956 |
-
</p>
|
| 957 |
-
|
| 958 |
-
<h3 id="metrics">Metrics</h3>
|
| 959 |
-
<ul>
|
| 960 |
-
<li><b>Size ratio</b> — encoded video ÷ sum of PNG frames. Lower is smaller on disk.</li>
|
| 961 |
-
<li><b>Decode time</b> — median wall-clock to fetch N frames at a given timestamp.</li>
|
| 962 |
-
<li><b>PSNR / SSIM / MSE</b> — fidelity of decoded frames against the uncompressed source.</li>
|
| 963 |
-
<li><b>Encoding time</b> — wall-clock to encode the whole clip, end-to-end.</li>
|
| 964 |
-
</ul>
|
| 965 |
-
|
| 966 |
-
<h3 id="access">Access patterns</h3>
|
| 967 |
-
<p>
|
| 968 |
-
Decode cost depends heavily on <i>how</i> frames are requested. <code>1_frame</code> pays the full seek+IDR cost per sample; <code>6_frames</code> amortizes it across contiguous frames; <code>2_frames_4_space</code> probes the worst case where the decoder must step across two distant windows.
|
| 969 |
-
</p>
|
| 970 |
-
|
| 971 |
-
<h3 id="repro">Reproduce locally</h3>
|
| 972 |
-
<p>The full sweep is open source. From the <code>lerobot</code> repo:</p>
|
| 973 |
-
<pre style={{background:'var(--hf-gray-900)', color:'var(--hf-gray-100)', padding:'var(--space-4)', borderRadius:'var(--radius-md)', fontSize:'var(--fs-xs)', overflow:'auto'}}>
|
| 974 |
-
{`python benchmark/video/run_video_benchmark.py \\
|
| 975 |
-
--output-dir outputs/video_benchmark \\
|
| 976 |
-
--repo-ids lerobot/pusht_image lerobot/kitchen \\
|
| 977 |
-
--vcodec h264 hevc libsvtav1 \\
|
| 978 |
-
--pix-fmt yuv420p yuv444p \\
|
| 979 |
-
--g 2 10 40 \\
|
| 980 |
-
--crf 10 20 30 \\
|
| 981 |
-
--timestamps-modes 1_frame 2_frames 6_frames \\
|
| 982 |
-
--backends pyav torchcodec \\
|
| 983 |
-
--num-samples 50`}
|
| 984 |
-
</pre>
|
| 985 |
-
|
| 986 |
-
<h3 id="contrib">Contribute</h3>
|
| 987 |
-
<p>
|
| 988 |
-
Submit your own configurations through the <b>Submit</b> tab. A background worker picks them up and pushes results to the Hub so the whole community benefits from the same measurements.
|
| 989 |
-
</p>
|
| 990 |
-
</div>
|
| 991 |
-
|
| 992 |
-
<aside className="about-toc">
|
| 993 |
-
<div className="t">On this page</div>
|
| 994 |
-
<a href="#what">What this is</a>
|
| 995 |
-
<a href="#metrics">Metrics</a>
|
| 996 |
-
<a href="#access">Access patterns</a>
|
| 997 |
-
<a href="#repro">Reproduce locally</a>
|
| 998 |
-
<a href="#contrib">Contribute</a>
|
| 999 |
-
</aside>
|
| 1000 |
-
</div>
|
| 1001 |
-
);
|
| 1002 |
-
}
|
| 1003 |
-
|
| 1004 |
-
// ---------- Tweaks ----------
|
| 1005 |
-
function Tweaks({ state, setState }) {
|
| 1006 |
-
const [open, setOpen] = useState(false);
|
| 1007 |
-
useEffect(() => {
|
| 1008 |
-
const onMsg = (e) => {
|
| 1009 |
-
if (e.data?.type === '__activate_edit_mode') setOpen(true);
|
| 1010 |
-
if (e.data?.type === '__deactivate_edit_mode') setOpen(false);
|
| 1011 |
-
};
|
| 1012 |
-
window.addEventListener('message', onMsg);
|
| 1013 |
-
window.parent.postMessage({ type: '__edit_mode_available' }, '*');
|
| 1014 |
-
return () => window.removeEventListener('message', onMsg);
|
| 1015 |
-
}, []);
|
| 1016 |
-
const set = (k, v) => {
|
| 1017 |
-
const next = { ...state, [k]: v };
|
| 1018 |
-
setState(next);
|
| 1019 |
-
window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [k]: v } }, '*');
|
| 1020 |
-
};
|
| 1021 |
-
return (
|
| 1022 |
-
<div className={`tweaks-panel ${open?'open':''}`}>
|
| 1023 |
-
<h5>⚡ Tweaks</h5>
|
| 1024 |
-
<div className="tweak-row">
|
| 1025 |
-
<label>Density</label>
|
| 1026 |
-
<select value={state.density} onChange={e=>set('density', e.target.value)}>
|
| 1027 |
-
<option value="comfortable">Comfortable</option>
|
| 1028 |
-
<option value="compact">Compact</option>
|
| 1029 |
-
</select>
|
| 1030 |
-
</div>
|
| 1031 |
-
<div className="tweak-row">
|
| 1032 |
-
<label>Theme</label>
|
| 1033 |
-
<select value={state.theme} onChange={e=>set('theme', e.target.value)}>
|
| 1034 |
-
<option value="light">Light</option>
|
| 1035 |
-
<option value="dark">Dark</option>
|
| 1036 |
-
</select>
|
| 1037 |
-
</div>
|
| 1038 |
-
<div className="tweak-row">
|
| 1039 |
-
<label>Hero mascot</label>
|
| 1040 |
-
<select value={state.mascot} onChange={e=>set('mascot', e.target.value)}>
|
| 1041 |
-
<option value="vision">Vision</option>
|
| 1042 |
-
<option value="rocket">Rocket</option>
|
| 1043 |
-
<option value="coding">Coding</option>
|
| 1044 |
-
<option value="lab">Lab</option>
|
| 1045 |
-
<option value="hello">Hello</option>
|
| 1046 |
-
<option value="none">None</option>
|
| 1047 |
-
</select>
|
| 1048 |
-
</div>
|
| 1049 |
-
<div className="tweak-row">
|
| 1050 |
-
<label>Hero style</label>
|
| 1051 |
-
<select value={state.hero} onChange={e=>set('hero', e.target.value)}>
|
| 1052 |
-
<option value="cream">Cream</option>
|
| 1053 |
-
<option value="yellow">Yellow</option>
|
| 1054 |
-
<option value="white">Minimal white</option>
|
| 1055 |
-
</select>
|
| 1056 |
-
</div>
|
| 1057 |
-
</div>
|
| 1058 |
-
);
|
| 1059 |
-
}
|
| 1060 |
-
|
| 1061 |
-
// ---------- App ----------
|
| 1062 |
-
function App() {
|
| 1063 |
-
const TWEAKS_DEFAULTS = /*EDITMODE-BEGIN*/{
|
| 1064 |
-
"density": "comfortable",
|
| 1065 |
-
"theme": "light",
|
| 1066 |
-
"mascot": "vision",
|
| 1067 |
-
"hero": "cream"
|
| 1068 |
-
}/*EDITMODE-END*/;
|
| 1069 |
-
const [tweaks, setTweaks] = useState(TWEAKS_DEFAULTS);
|
| 1070 |
-
|
| 1071 |
-
useEffect(() => {
|
| 1072 |
-
document.body.classList.toggle('density-compact', tweaks.density === 'compact');
|
| 1073 |
-
document.body.classList.toggle('dark', tweaks.theme === 'dark');
|
| 1074 |
-
}, [tweaks.density, tweaks.theme]);
|
| 1075 |
-
|
| 1076 |
-
const [tab, setTab] = useState(() => {
|
| 1077 |
-
const t = localStorage.getItem('vb_tab');
|
| 1078 |
-
return (t === 'params' ? 'about' : t) || 'results';
|
| 1079 |
-
});
|
| 1080 |
-
useEffect(() => { localStorage.setItem('vb_tab', tab); }, [tab]);
|
| 1081 |
-
|
| 1082 |
-
const [filters, setFilters] = useState({});
|
| 1083 |
-
const [visible, setVisible] = useState(new Set(COLUMNS.map(c => c.key)));
|
| 1084 |
-
|
| 1085 |
-
const filteredRows = useMemo(() => {
|
| 1086 |
-
return ALL_ROWS.filter(r => {
|
| 1087 |
-
for (const [k, vs] of Object.entries(filters)) {
|
| 1088 |
-
if (vs && vs.length && !vs.includes(String(r[k]))) return false;
|
| 1089 |
-
}
|
| 1090 |
-
return true;
|
| 1091 |
-
});
|
| 1092 |
-
}, [filters]);
|
| 1093 |
-
|
| 1094 |
-
// Tooltip handler
|
| 1095 |
-
useEffect(() => {
|
| 1096 |
-
const tt = document.createElement('div'); tt.className='tt'; document.body.appendChild(tt);
|
| 1097 |
-
const onOver = (e) => {
|
| 1098 |
-
const el = e.target.closest('[data-tt]'); if (!el) return;
|
| 1099 |
-
tt.textContent = el.getAttribute('data-tt');
|
| 1100 |
-
const rect = el.getBoundingClientRect();
|
| 1101 |
-
tt.style.left = (rect.left + rect.width/2 - 140) + 'px';
|
| 1102 |
-
tt.style.top = (rect.top - 8 - 40 + window.scrollY) + 'px';
|
| 1103 |
-
tt.classList.add('show');
|
| 1104 |
-
};
|
| 1105 |
-
const onOut = (e) => { if (e.target.closest('[data-tt]')) tt.classList.remove('show'); };
|
| 1106 |
-
document.addEventListener('mouseover', onOver);
|
| 1107 |
-
document.addEventListener('mouseout', onOut);
|
| 1108 |
-
return () => { document.removeEventListener('mouseover', onOver); document.removeEventListener('mouseout', onOut); tt.remove(); };
|
| 1109 |
-
}, []);
|
| 1110 |
-
|
| 1111 |
-
const heroClass = tweaks.hero === 'yellow' ? { background: 'var(--hf-yellow)' } :
|
| 1112 |
-
tweaks.hero === 'white' ? { background: 'var(--hf-white)', border: '1px solid var(--border-1)' } :
|
| 1113 |
-
{};
|
| 1114 |
-
|
| 1115 |
-
const mascotSrc = tweaks.mascot === 'none' ? null : (window.__MASCOTS && window.__MASCOTS[tweaks.mascot]) || `assets/huggy-${tweaks.mascot}.png`;
|
| 1116 |
-
|
| 1117 |
-
const tabs = [
|
| 1118 |
-
{ k: 'results', label: 'Results', count: filteredRows.length },
|
| 1119 |
-
{ k: 'leaderboards', label: 'Leaderboards' },
|
| 1120 |
-
{ k: 'compare', label: 'Compare' },
|
| 1121 |
-
{ k: 'submit', label: 'Submit' },
|
| 1122 |
-
{ k: 'about', label: 'About & parameters' },
|
| 1123 |
-
];
|
| 1124 |
-
|
| 1125 |
-
return (
|
| 1126 |
-
<div>
|
| 1127 |
-
{/* Main */}
|
| 1128 |
-
<div className="page">
|
| 1129 |
-
{/* Hero */}
|
| 1130 |
-
<section className="hero" style={heroClass}>
|
| 1131 |
-
<div>
|
| 1132 |
-
<span className="hero-tag"><span className="dot"/> Open benchmark</span>
|
| 1133 |
-
<h1>Which video codec belongs in your robotics dataset?</h1>
|
| 1134 |
-
<p className="subtitle">
|
| 1135 |
-
Encode with h264, HEVC, or AV1. Decode with PyAV, torchcodec, or video_reader. Across four LeRobot datasets, {ALL_ROWS.length.toLocaleString()} configurations tell you exactly what you trade for what.
|
| 1136 |
-
</p>
|
| 1137 |
-
<div style={{display:'flex', gap:'var(--space-3)'}}>
|
| 1138 |
-
<button className="btn primary" onClick={()=>setTab('results')}>Explore results →</button>
|
| 1139 |
-
<button className="btn" onClick={()=>setTab('submit')}>Submit a run</button>
|
| 1140 |
-
</div>
|
| 1141 |
-
<div className="hero-stats">
|
| 1142 |
-
<div className="hero-stat"><div className="v">{ALL_ROWS.length.toLocaleString()}</div><div className="l">Configs</div></div>
|
| 1143 |
-
<div className="hero-stat"><div className="v">3</div><div className="l">Codecs</div></div>
|
| 1144 |
-
<div className="hero-stat"><div className="v">{REPOS.length}</div><div className="l">Datasets</div></div>
|
| 1145 |
-
<div className="hero-stat"><div className="v">4</div><div className="l">Access modes</div></div>
|
| 1146 |
-
</div>
|
| 1147 |
-
</div>
|
| 1148 |
-
{mascotSrc && <div className="hero-mascot"><img src={mascotSrc} alt=""/></div>}
|
| 1149 |
-
</section>
|
| 1150 |
-
|
| 1151 |
-
{/* Tabs */}
|
| 1152 |
-
<div className="tabs-bar">
|
| 1153 |
-
{tabs.map(t => (
|
| 1154 |
-
<button key={t.k} className={`tab-btn ${tab===t.k?'active':''}`} onClick={()=>setTab(t.k)}>
|
| 1155 |
-
{t.label}
|
| 1156 |
-
{t.count != null && <span className="tab-count">{t.count}</span>}
|
| 1157 |
-
</button>
|
| 1158 |
-
))}
|
| 1159 |
-
</div>
|
| 1160 |
-
|
| 1161 |
-
{/* Panels */}
|
| 1162 |
-
<div className={`tab-panel ${tab==='results'?'active':''}`}>
|
| 1163 |
-
<FilterBar filters={filters} setFilters={setFilters} visible={visible} setVisible={setVisible} />
|
| 1164 |
-
<BenchTable rows={filteredRows} visible={visible} />
|
| 1165 |
-
</div>
|
| 1166 |
-
|
| 1167 |
-
<div className={`tab-panel ${tab==='leaderboards'?'active':''}`}>
|
| 1168 |
-
<div className="section-title">
|
| 1169 |
-
<div>
|
| 1170 |
-
<h2>Leaderboards</h2>
|
| 1171 |
-
<p style={{color:'var(--fg-2)', fontSize:'var(--fs-sm)', margin:'4px 0 0', maxWidth:'780px'}}>
|
| 1172 |
-
The top configurations on six normalized axes — encoding time, decode time, size, MSE, PSNR, SSIM. Bigger polygon means a better all-rounder; each axis is rescaled so <b>1.0 = best-in-class</b>.
|
| 1173 |
-
</p>
|
| 1174 |
-
</div>
|
| 1175 |
-
</div>
|
| 1176 |
-
<Leaderboards/>
|
| 1177 |
-
</div>
|
| 1178 |
-
|
| 1179 |
-
<div className={`tab-panel ${tab==='compare'?'active':''}`}>
|
| 1180 |
-
<div className="section-title">
|
| 1181 |
-
<div>
|
| 1182 |
-
<h2>Compare parameters</h2>
|
| 1183 |
-
<p style={{color:'var(--fg-2)', fontSize:'var(--fs-sm)', margin:'4px 0 0'}}>Pick any metric and any dimension to see how the field behaves.</p>
|
| 1184 |
-
</div>
|
| 1185 |
-
</div>
|
| 1186 |
-
<Compare/>
|
| 1187 |
-
</div>
|
| 1188 |
-
|
| 1189 |
-
<div className={`tab-panel ${tab==='submit'?'active':''}`}>
|
| 1190 |
-
<div className="section-title">
|
| 1191 |
-
<div>
|
| 1192 |
-
<h2>Submit a benchmark</h2>
|
| 1193 |
-
<p style={{color:'var(--fg-2)', fontSize:'var(--fs-sm)', margin:'4px 0 0'}}>Queue a configuration sweep; results are pushed to the Hub and appear here automatically.</p>
|
| 1194 |
-
</div>
|
| 1195 |
-
</div>
|
| 1196 |
-
<Submit/>
|
| 1197 |
-
</div>
|
| 1198 |
-
|
| 1199 |
-
<div className={`tab-panel ${tab==='about'?'active':''}`}>
|
| 1200 |
-
<div className="section-title">
|
| 1201 |
-
<div>
|
| 1202 |
-
<h2>About this benchmark</h2>
|
| 1203 |
-
<p style={{color:'var(--fg-2)', fontSize:'var(--fs-sm)', margin:'4px 0 0'}}>Methodology, metrics, and how to reproduce — followed by a full parameter reference.</p>
|
| 1204 |
-
</div>
|
| 1205 |
-
</div>
|
| 1206 |
-
<About/>
|
| 1207 |
-
<div className="section-title" style={{marginTop:'var(--space-8)'}}>
|
| 1208 |
-
<div>
|
| 1209 |
-
<h2>Parameters reference</h2>
|
| 1210 |
-
<p style={{color:'var(--fg-2)', fontSize:'var(--fs-sm)', margin:'4px 0 0', maxWidth:'780px'}}>A plain-English reference for every knob you can turn and every number we report.</p>
|
| 1211 |
-
</div>
|
| 1212 |
-
</div>
|
| 1213 |
-
<Parameters/>
|
| 1214 |
-
</div>
|
| 1215 |
-
|
| 1216 |
-
{/* Citation */}
|
| 1217 |
-
<div style={{marginTop:'var(--space-8)'}}>
|
| 1218 |
-
<h3 style={{fontSize:'var(--fs-md)', fontWeight:600, marginBottom:'var(--space-3)'}}>Citation</h3>
|
| 1219 |
-
<div className="citation">
|
| 1220 |
-
<button className="copy-btn" onClick={(e)=>{navigator.clipboard?.writeText(e.target.parentNode.innerText.replace('Copy',''));}}>Copy</button>
|
| 1221 |
-
{`@misc{cadene2024lerobot,
|
| 1222 |
-
author = {Cadene, Remi and Alibert, Simon and Pascal, Caroline and others},
|
| 1223 |
-
title = {LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch},
|
| 1224 |
-
url = {https://github.com/huggingface/lerobot},
|
| 1225 |
-
year = {2024}
|
| 1226 |
-
}`}
|
| 1227 |
-
</div>
|
| 1228 |
-
</div>
|
| 1229 |
-
</div>
|
| 1230 |
-
|
| 1231 |
-
<Tweaks state={tweaks} setState={setTweaks} />
|
| 1232 |
-
</div>
|
| 1233 |
-
);
|
| 1234 |
-
}
|
| 1235 |
-
|
| 1236 |
-
class __EB extends React.Component {
|
| 1237 |
-
constructor(p){super(p);this.state={err:null};}
|
| 1238 |
-
componentDidCatch(err, info){ console.error('App error:', err, info.componentStack); this.setState({err: err.message + '\n' + (info.componentStack||'')}); }
|
| 1239 |
-
render(){ if (this.state.err) return <pre style={{padding:20,color:'red',fontSize:11,whiteSpace:'pre-wrap'}}>{this.state.err}</pre>; return this.props.children; }
|
| 1240 |
-
}
|
| 1241 |
-
ReactDOM.createRoot(document.getElementById('root')).render(<__EB><App/></__EB>);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets-embedded.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/colors_and_type.css
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
/* ==========================================================================
|
| 2 |
-
Hugging Face — Colors & Type
|
| 3 |
-
Sourced from the official HF brand (chunte-hfba) and huggingface.co.
|
| 4 |
-
========================================================================== */
|
| 5 |
-
|
| 6 |
-
:root {
|
| 7 |
-
/* ---------- Brand core ---------- */
|
| 8 |
-
--hf-yellow: #FFD21E; /* The mascot yellow; primary surface accent */
|
| 9 |
-
--hf-yellow-deep: #FFBD59; /* Shadow side of the face */
|
| 10 |
-
--hf-orange: #FF9D00; /* Secondary warm accent, crescent shadow */
|
| 11 |
-
--hf-orange-soft: #FFAD05;
|
| 12 |
-
--hf-blue: #496EF0; /* Electric blue — wordmark, outlines */
|
| 13 |
-
--hf-blue-deep: #353E60; /* Dark blue for body text on warm bgs */
|
| 14 |
-
--hf-red: #DB3328; /* Mascot mouth / accent */
|
| 15 |
-
--hf-green: #009B3A;
|
| 16 |
-
--hf-purple: #9747FF; /* Secondary accent (used on wordmark frames) */
|
| 17 |
-
|
| 18 |
-
/* Rainbow logo gradient stops (ordered) */
|
| 19 |
-
--hf-rainbow-1: #FFD21E;
|
| 20 |
-
--hf-rainbow-2: #FF9D00;
|
| 21 |
-
--hf-rainbow-3: #DB3328;
|
| 22 |
-
--hf-rainbow-4: #9747FF;
|
| 23 |
-
--hf-rainbow-5: #496EF0;
|
| 24 |
-
--hf-rainbow-6: #009B3A;
|
| 25 |
-
|
| 26 |
-
/* ---------- Neutrals (from huggingface.co) ---------- */
|
| 27 |
-
--hf-black: #000000;
|
| 28 |
-
--hf-gray-900: #1B1B1D;
|
| 29 |
-
--hf-gray-800: #32343D;
|
| 30 |
-
--hf-gray-700: #475169; /* Sidebar text */
|
| 31 |
-
--hf-gray-600: #6A768F; /* Secondary text */
|
| 32 |
-
--hf-gray-500: #9BA3AF;
|
| 33 |
-
--hf-gray-400: #D1D5DB;
|
| 34 |
-
--hf-gray-300: #E5E7EB;
|
| 35 |
-
--hf-gray-200: #EEEFF2; /* Subtle border */
|
| 36 |
-
--hf-gray-100: #F5F6F8; /* Page background */
|
| 37 |
-
--hf-gray-50: #FAFAFA;
|
| 38 |
-
--hf-white: #FFFFFF;
|
| 39 |
-
|
| 40 |
-
/* ---------- Warm brand backgrounds ---------- */
|
| 41 |
-
--hf-cream: #FFF8DE; /* Soft yellow card bg */
|
| 42 |
-
--hf-cream-deep: #FFF2C5;
|
| 43 |
-
--hf-paper: #FFF0B4;
|
| 44 |
-
|
| 45 |
-
/* ---------- Semantic (UI surfaces on huggingface.co) ---------- */
|
| 46 |
-
--fg-1: var(--hf-gray-900); /* Primary body copy */
|
| 47 |
-
--fg-2: var(--hf-gray-700); /* Secondary body copy */
|
| 48 |
-
--fg-3: var(--hf-gray-600); /* Muted / meta */
|
| 49 |
-
--fg-inverse: var(--hf-white);
|
| 50 |
-
--fg-link: var(--hf-blue);
|
| 51 |
-
--fg-accent: var(--hf-blue);
|
| 52 |
-
|
| 53 |
-
--bg-1: var(--hf-white); /* Card surface */
|
| 54 |
-
--bg-2: var(--hf-gray-100); /* Page background */
|
| 55 |
-
--bg-3: var(--hf-gray-200); /* Inset surface */
|
| 56 |
-
--bg-brand: var(--hf-yellow);
|
| 57 |
-
--bg-brand-soft: var(--hf-cream);
|
| 58 |
-
--bg-invert: var(--hf-gray-900);
|
| 59 |
-
|
| 60 |
-
--border-1: var(--hf-gray-200);
|
| 61 |
-
--border-2: var(--hf-gray-300);
|
| 62 |
-
--border-strong: var(--hf-gray-800);
|
| 63 |
-
|
| 64 |
-
/* Pill / product chips seen throughout hf.co */
|
| 65 |
-
--tag-models-fg: #B45309; /* amber-700 */
|
| 66 |
-
--tag-models-bg: #FEF3C7;
|
| 67 |
-
--tag-datasets-fg: #1D4ED8; /* blue-700 */
|
| 68 |
-
--tag-datasets-bg: #DBEAFE;
|
| 69 |
-
--tag-spaces-fg: #7E22CE; /* purple-700 */
|
| 70 |
-
--tag-spaces-bg: #F3E8FF;
|
| 71 |
-
--tag-papers-fg: #047857; /* emerald-700 */
|
| 72 |
-
--tag-papers-bg: #D1FAE5;
|
| 73 |
-
--tag-posts-fg: #BE185D; /* pink-700 */
|
| 74 |
-
--tag-posts-bg: #FCE7F3;
|
| 75 |
-
|
| 76 |
-
/* ---------- Radius ---------- */
|
| 77 |
-
--radius-xs: 4px;
|
| 78 |
-
--radius-sm: 6px;
|
| 79 |
-
--radius-md: 8px; /* hf.co default control radius */
|
| 80 |
-
--radius-lg: 12px; /* Cards */
|
| 81 |
-
--radius-xl: 16px;
|
| 82 |
-
--radius-2xl: 24px; /* Brand poster frames */
|
| 83 |
-
--radius-pill: 999px;
|
| 84 |
-
|
| 85 |
-
/* ---------- Elevation ---------- */
|
| 86 |
-
--shadow-xs: 0 1px 1px rgba(27, 27, 29, 0.04);
|
| 87 |
-
--shadow-sm: 0 1px 2px rgba(27, 27, 29, 0.06), 0 1px 3px rgba(27, 27, 29, 0.08);
|
| 88 |
-
--shadow-md: 0 4px 10px rgba(27, 27, 29, 0.06), 0 2px 4px rgba(27, 27, 29, 0.06);
|
| 89 |
-
--shadow-lg: 0 12px 24px rgba(27, 27, 29, 0.08), 0 4px 8px rgba(27, 27, 29, 0.04);
|
| 90 |
-
--shadow-brand: 0 10px 30px rgba(73, 110, 240, 0.20);
|
| 91 |
-
|
| 92 |
-
/* ---------- Spacing (4-pt base, consistent with tailwind hf uses) ---------- */
|
| 93 |
-
--space-1: 4px;
|
| 94 |
-
--space-2: 8px;
|
| 95 |
-
--space-3: 12px;
|
| 96 |
-
--space-4: 16px;
|
| 97 |
-
--space-5: 20px;
|
| 98 |
-
--space-6: 24px;
|
| 99 |
-
--space-8: 32px;
|
| 100 |
-
--space-10: 40px;
|
| 101 |
-
--space-12: 48px;
|
| 102 |
-
--space-16: 64px;
|
| 103 |
-
--space-24: 96px;
|
| 104 |
-
|
| 105 |
-
/* ---------- Typography ----------
|
| 106 |
-
hf.co uses "Source Sans 3" (successor to Source Sans Pro) for UI,
|
| 107 |
-
IBM Plex Mono for code/tags, and a heavy slanted display face
|
| 108 |
-
(Hugging Face wordmark proper) for marketing. Substitution: we use
|
| 109 |
-
Source Sans 3 + IBM Plex Mono from Google Fonts; the display slanted
|
| 110 |
-
wordmark is used only via the supplied SVG.
|
| 111 |
-
*/
|
| 112 |
-
--font-sans: "Source Sans 3", "Source Sans Pro", ui-sans-serif, system-ui,
|
| 113 |
-
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
| 114 |
-
"Apple Color Emoji", "Segoe UI Emoji", sans-serif;
|
| 115 |
-
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
| 116 |
-
Consolas, "Liberation Mono", "Courier New", monospace;
|
| 117 |
-
--font-display: "Source Sans 3", var(--font-sans); /* For big headings */
|
| 118 |
-
|
| 119 |
-
/* Type scale — matches hf.co docs + marketing pages */
|
| 120 |
-
--fs-xs: 12px; --lh-xs: 16px;
|
| 121 |
-
--fs-sm: 14px; --lh-sm: 20px;
|
| 122 |
-
--fs-base: 16px; --lh-base: 24px;
|
| 123 |
-
--fs-md: 18px; --lh-md: 28px;
|
| 124 |
-
--fs-lg: 20px; --lh-lg: 28px;
|
| 125 |
-
--fs-xl: 24px; --lh-xl: 32px;
|
| 126 |
-
--fs-2xl: 30px; --lh-2xl: 36px;
|
| 127 |
-
--fs-3xl: 36px; --lh-3xl: 44px;
|
| 128 |
-
--fs-4xl: 48px; --lh-4xl: 56px;
|
| 129 |
-
--fs-5xl: 60px; --lh-5xl: 68px;
|
| 130 |
-
--fs-6xl: 80px; --lh-6xl: 88px;
|
| 131 |
-
|
| 132 |
-
--fw-regular: 400;
|
| 133 |
-
--fw-medium: 500;
|
| 134 |
-
--fw-semibold: 600;
|
| 135 |
-
--fw-bold: 700;
|
| 136 |
-
--fw-black: 900;
|
| 137 |
-
|
| 138 |
-
/* ---------- Motion ---------- */
|
| 139 |
-
--ease-out: cubic-bezier(.16,1,.3,1);
|
| 140 |
-
--ease-spring: cubic-bezier(.34,1.56,.64,1);
|
| 141 |
-
--t-quick: 120ms;
|
| 142 |
-
--t-base: 200ms;
|
| 143 |
-
--t-slow: 360ms;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
/* ==========================================================================
|
| 147 |
-
Semantic element styles. Include with: <link rel="stylesheet" href="colors_and_type.css">
|
| 148 |
-
========================================================================== */
|
| 149 |
-
html, body { font-family: var(--font-sans); color: var(--fg-1); background: var(--bg-2); -webkit-font-smoothing: antialiased; }
|
| 150 |
-
|
| 151 |
-
h1, .h1 { font-family: var(--font-display); font-size: var(--fs-5xl); line-height: var(--lh-5xl); font-weight: var(--fw-bold); letter-spacing: -0.02em; color: var(--fg-1); }
|
| 152 |
-
h2, .h2 { font-family: var(--font-display); font-size: var(--fs-3xl); line-height: var(--lh-3xl); font-weight: var(--fw-bold); letter-spacing: -0.01em; color: var(--fg-1); }
|
| 153 |
-
h3, .h3 { font-family: var(--font-sans); font-size: var(--fs-2xl); line-height: var(--lh-2xl); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 154 |
-
h4, .h4 { font-family: var(--font-sans); font-size: var(--fs-xl); line-height: var(--lh-xl); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 155 |
-
h5, .h5 { font-family: var(--font-sans); font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 156 |
-
h6, .h6 { font-family: var(--font-sans); font-size: var(--fs-md); line-height: var(--lh-md); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 157 |
-
|
| 158 |
-
p, .p { font-size: var(--fs-base); line-height: var(--lh-base); color: var(--fg-1); }
|
| 159 |
-
.small { font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--fg-2); }
|
| 160 |
-
.meta { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--fg-3); }
|
| 161 |
-
|
| 162 |
-
code, kbd, pre, .mono { font-family: var(--font-mono); font-size: .92em; }
|
| 163 |
-
code { background: var(--bg-3); border-radius: var(--radius-xs); padding: 2px 6px; }
|
| 164 |
-
pre { background: var(--hf-gray-900); color: var(--hf-gray-100); border-radius: var(--radius-md); padding: var(--space-4); overflow: auto; }
|
| 165 |
-
|
| 166 |
-
a { color: var(--fg-link); text-decoration: underline; text-underline-offset: 3px; }
|
| 167 |
-
a:hover { opacity: .8; }
|
| 168 |
-
|
| 169 |
-
hr { border: 0; border-top: 1px solid var(--border-1); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/hf-logo.png
DELETED
Git LFS Details
|
assets/huggy-hello.png
DELETED
Git LFS Details
|
assets/huggy-lab.png
DELETED
Git LFS Details
|
assets/huggy-rocket.png
DELETED
Git LFS Details
|
assets/huggy-vision.png
DELETED
Git LFS Details
|
colors_and_type.css
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
/* ==========================================================================
|
| 2 |
-
Hugging Face — Colors & Type
|
| 3 |
-
Sourced from the official HF brand (chunte-hfba) and huggingface.co.
|
| 4 |
-
========================================================================== */
|
| 5 |
-
|
| 6 |
-
:root {
|
| 7 |
-
/* ---------- Brand core ---------- */
|
| 8 |
-
--hf-yellow: #FFD21E; /* The mascot yellow; primary surface accent */
|
| 9 |
-
--hf-yellow-deep: #FFBD59; /* Shadow side of the face */
|
| 10 |
-
--hf-orange: #FF9D00; /* Secondary warm accent, crescent shadow */
|
| 11 |
-
--hf-orange-soft: #FFAD05;
|
| 12 |
-
--hf-blue: #496EF0; /* Electric blue — wordmark, outlines */
|
| 13 |
-
--hf-blue-deep: #353E60; /* Dark blue for body text on warm bgs */
|
| 14 |
-
--hf-red: #DB3328; /* Mascot mouth / accent */
|
| 15 |
-
--hf-green: #009B3A;
|
| 16 |
-
--hf-purple: #9747FF; /* Secondary accent (used on wordmark frames) */
|
| 17 |
-
|
| 18 |
-
/* Rainbow logo gradient stops (ordered) */
|
| 19 |
-
--hf-rainbow-1: #FFD21E;
|
| 20 |
-
--hf-rainbow-2: #FF9D00;
|
| 21 |
-
--hf-rainbow-3: #DB3328;
|
| 22 |
-
--hf-rainbow-4: #9747FF;
|
| 23 |
-
--hf-rainbow-5: #496EF0;
|
| 24 |
-
--hf-rainbow-6: #009B3A;
|
| 25 |
-
|
| 26 |
-
/* ---------- Neutrals (from huggingface.co) ---------- */
|
| 27 |
-
--hf-black: #000000;
|
| 28 |
-
--hf-gray-900: #1B1B1D;
|
| 29 |
-
--hf-gray-800: #32343D;
|
| 30 |
-
--hf-gray-700: #475169; /* Sidebar text */
|
| 31 |
-
--hf-gray-600: #6A768F; /* Secondary text */
|
| 32 |
-
--hf-gray-500: #9BA3AF;
|
| 33 |
-
--hf-gray-400: #D1D5DB;
|
| 34 |
-
--hf-gray-300: #E5E7EB;
|
| 35 |
-
--hf-gray-200: #EEEFF2; /* Subtle border */
|
| 36 |
-
--hf-gray-100: #F5F6F8; /* Page background */
|
| 37 |
-
--hf-gray-50: #FAFAFA;
|
| 38 |
-
--hf-white: #FFFFFF;
|
| 39 |
-
|
| 40 |
-
/* ---------- Warm brand backgrounds ---------- */
|
| 41 |
-
--hf-cream: #FFF8DE; /* Soft yellow card bg */
|
| 42 |
-
--hf-cream-deep: #FFF2C5;
|
| 43 |
-
--hf-paper: #FFF0B4;
|
| 44 |
-
|
| 45 |
-
/* ---------- Semantic (UI surfaces on huggingface.co) ---------- */
|
| 46 |
-
--fg-1: var(--hf-gray-900); /* Primary body copy */
|
| 47 |
-
--fg-2: var(--hf-gray-700); /* Secondary body copy */
|
| 48 |
-
--fg-3: var(--hf-gray-600); /* Muted / meta */
|
| 49 |
-
--fg-inverse: var(--hf-white);
|
| 50 |
-
--fg-link: var(--hf-blue);
|
| 51 |
-
--fg-accent: var(--hf-blue);
|
| 52 |
-
|
| 53 |
-
--bg-1: var(--hf-white); /* Card surface */
|
| 54 |
-
--bg-2: var(--hf-gray-100); /* Page background */
|
| 55 |
-
--bg-3: var(--hf-gray-200); /* Inset surface */
|
| 56 |
-
--bg-brand: var(--hf-yellow);
|
| 57 |
-
--bg-brand-soft: var(--hf-cream);
|
| 58 |
-
--bg-invert: var(--hf-gray-900);
|
| 59 |
-
|
| 60 |
-
--border-1: var(--hf-gray-200);
|
| 61 |
-
--border-2: var(--hf-gray-300);
|
| 62 |
-
--border-strong: var(--hf-gray-800);
|
| 63 |
-
|
| 64 |
-
/* Pill / product chips seen throughout hf.co */
|
| 65 |
-
--tag-models-fg: #B45309; /* amber-700 */
|
| 66 |
-
--tag-models-bg: #FEF3C7;
|
| 67 |
-
--tag-datasets-fg: #1D4ED8; /* blue-700 */
|
| 68 |
-
--tag-datasets-bg: #DBEAFE;
|
| 69 |
-
--tag-spaces-fg: #7E22CE; /* purple-700 */
|
| 70 |
-
--tag-spaces-bg: #F3E8FF;
|
| 71 |
-
--tag-papers-fg: #047857; /* emerald-700 */
|
| 72 |
-
--tag-papers-bg: #D1FAE5;
|
| 73 |
-
--tag-posts-fg: #BE185D; /* pink-700 */
|
| 74 |
-
--tag-posts-bg: #FCE7F3;
|
| 75 |
-
|
| 76 |
-
/* ---------- Radius ---------- */
|
| 77 |
-
--radius-xs: 4px;
|
| 78 |
-
--radius-sm: 6px;
|
| 79 |
-
--radius-md: 8px; /* hf.co default control radius */
|
| 80 |
-
--radius-lg: 12px; /* Cards */
|
| 81 |
-
--radius-xl: 16px;
|
| 82 |
-
--radius-2xl: 24px; /* Brand poster frames */
|
| 83 |
-
--radius-pill: 999px;
|
| 84 |
-
|
| 85 |
-
/* ---------- Elevation ---------- */
|
| 86 |
-
--shadow-xs: 0 1px 1px rgba(27, 27, 29, 0.04);
|
| 87 |
-
--shadow-sm: 0 1px 2px rgba(27, 27, 29, 0.06), 0 1px 3px rgba(27, 27, 29, 0.08);
|
| 88 |
-
--shadow-md: 0 4px 10px rgba(27, 27, 29, 0.06), 0 2px 4px rgba(27, 27, 29, 0.06);
|
| 89 |
-
--shadow-lg: 0 12px 24px rgba(27, 27, 29, 0.08), 0 4px 8px rgba(27, 27, 29, 0.04);
|
| 90 |
-
--shadow-brand: 0 10px 30px rgba(73, 110, 240, 0.20);
|
| 91 |
-
|
| 92 |
-
/* ---------- Spacing (4-pt base, consistent with tailwind hf uses) ---------- */
|
| 93 |
-
--space-1: 4px;
|
| 94 |
-
--space-2: 8px;
|
| 95 |
-
--space-3: 12px;
|
| 96 |
-
--space-4: 16px;
|
| 97 |
-
--space-5: 20px;
|
| 98 |
-
--space-6: 24px;
|
| 99 |
-
--space-8: 32px;
|
| 100 |
-
--space-10: 40px;
|
| 101 |
-
--space-12: 48px;
|
| 102 |
-
--space-16: 64px;
|
| 103 |
-
--space-24: 96px;
|
| 104 |
-
|
| 105 |
-
/* ---------- Typography ----------
|
| 106 |
-
hf.co uses "Source Sans 3" (successor to Source Sans Pro) for UI,
|
| 107 |
-
IBM Plex Mono for code/tags, and a heavy slanted display face
|
| 108 |
-
(Hugging Face wordmark proper) for marketing. Substitution: we use
|
| 109 |
-
Source Sans 3 + IBM Plex Mono from Google Fonts; the display slanted
|
| 110 |
-
wordmark is used only via the supplied SVG.
|
| 111 |
-
*/
|
| 112 |
-
--font-sans: "Source Sans 3", "Source Sans Pro", ui-sans-serif, system-ui,
|
| 113 |
-
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
| 114 |
-
"Apple Color Emoji", "Segoe UI Emoji", sans-serif;
|
| 115 |
-
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
| 116 |
-
Consolas, "Liberation Mono", "Courier New", monospace;
|
| 117 |
-
--font-display: "Source Sans 3", var(--font-sans); /* For big headings */
|
| 118 |
-
|
| 119 |
-
/* Type scale — matches hf.co docs + marketing pages */
|
| 120 |
-
--fs-xs: 12px; --lh-xs: 16px;
|
| 121 |
-
--fs-sm: 14px; --lh-sm: 20px;
|
| 122 |
-
--fs-base: 16px; --lh-base: 24px;
|
| 123 |
-
--fs-md: 18px; --lh-md: 28px;
|
| 124 |
-
--fs-lg: 20px; --lh-lg: 28px;
|
| 125 |
-
--fs-xl: 24px; --lh-xl: 32px;
|
| 126 |
-
--fs-2xl: 30px; --lh-2xl: 36px;
|
| 127 |
-
--fs-3xl: 36px; --lh-3xl: 44px;
|
| 128 |
-
--fs-4xl: 48px; --lh-4xl: 56px;
|
| 129 |
-
--fs-5xl: 60px; --lh-5xl: 68px;
|
| 130 |
-
--fs-6xl: 80px; --lh-6xl: 88px;
|
| 131 |
-
|
| 132 |
-
--fw-regular: 400;
|
| 133 |
-
--fw-medium: 500;
|
| 134 |
-
--fw-semibold: 600;
|
| 135 |
-
--fw-bold: 700;
|
| 136 |
-
--fw-black: 900;
|
| 137 |
-
|
| 138 |
-
/* ---------- Motion ---------- */
|
| 139 |
-
--ease-out: cubic-bezier(.16,1,.3,1);
|
| 140 |
-
--ease-spring: cubic-bezier(.34,1.56,.64,1);
|
| 141 |
-
--t-quick: 120ms;
|
| 142 |
-
--t-base: 200ms;
|
| 143 |
-
--t-slow: 360ms;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
/* ==========================================================================
|
| 147 |
-
Semantic element styles. Include with: <link rel="stylesheet" href="colors_and_type.css">
|
| 148 |
-
========================================================================== */
|
| 149 |
-
html, body { font-family: var(--font-sans); color: var(--fg-1); background: var(--bg-2); -webkit-font-smoothing: antialiased; }
|
| 150 |
-
|
| 151 |
-
h1, .h1 { font-family: var(--font-display); font-size: var(--fs-5xl); line-height: var(--lh-5xl); font-weight: var(--fw-bold); letter-spacing: -0.02em; color: var(--fg-1); }
|
| 152 |
-
h2, .h2 { font-family: var(--font-display); font-size: var(--fs-3xl); line-height: var(--lh-3xl); font-weight: var(--fw-bold); letter-spacing: -0.01em; color: var(--fg-1); }
|
| 153 |
-
h3, .h3 { font-family: var(--font-sans); font-size: var(--fs-2xl); line-height: var(--lh-2xl); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 154 |
-
h4, .h4 { font-family: var(--font-sans); font-size: var(--fs-xl); line-height: var(--lh-xl); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 155 |
-
h5, .h5 { font-family: var(--font-sans); font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 156 |
-
h6, .h6 { font-family: var(--font-sans); font-size: var(--fs-md); line-height: var(--lh-md); font-weight: var(--fw-semibold); color: var(--fg-1); }
|
| 157 |
-
|
| 158 |
-
p, .p { font-size: var(--fs-base); line-height: var(--lh-base); color: var(--fg-1); }
|
| 159 |
-
.small { font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--fg-2); }
|
| 160 |
-
.meta { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--fg-3); }
|
| 161 |
-
|
| 162 |
-
code, kbd, pre, .mono { font-family: var(--font-mono); font-size: .92em; }
|
| 163 |
-
code { background: var(--bg-3); border-radius: var(--radius-xs); padding: 2px 6px; }
|
| 164 |
-
pre { background: var(--hf-gray-900); color: var(--hf-gray-100); border-radius: var(--radius-md); padding: var(--space-4); overflow: auto; }
|
| 165 |
-
|
| 166 |
-
a { color: var(--fg-link); text-decoration: underline; text-underline-offset: 3px; }
|
| 167 |
-
a:hover { opacity: .8; }
|
| 168 |
-
|
| 169 |
-
hr { border: 0; border-top: 1px solid var(--border-1); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
// Mock benchmark data that mirrors the real schema from the Gradio app.
|
| 2 |
-
// Columns match COLUMN_HEADER_LABELS in the source.
|
| 3 |
-
// Fields: repo_id, vcodec, pix_fmt, g, crf, timestamps_mode, backend,
|
| 4 |
-
// video_images_size_ratio, video_images_load_time_ratio,
|
| 5 |
-
// median_load_time_video_ms, std_load_time_video_ms,
|
| 6 |
-
// median_psnr, std_psnr, median_ssim, std_ssim,
|
| 7 |
-
// median_mse, std_mse, encoding_fps, encoding_time_ms
|
| 8 |
-
|
| 9 |
-
const REPOS = [
|
| 10 |
-
"lerobot/pusht_image",
|
| 11 |
-
"lerobot/aloha_mobile_shrimp_image",
|
| 12 |
-
"lerobot/paris_street",
|
| 13 |
-
"lerobot/kitchen",
|
| 14 |
-
];
|
| 15 |
-
const VCODECS = ["h264", "hevc", "libsvtav1"];
|
| 16 |
-
const PIX_FMTS = ["yuv444p", "yuv420p"];
|
| 17 |
-
const G_VALUES = [1, 2, 6, 15, 40, 100];
|
| 18 |
-
const CRF_VALUES = [10, 20, 30, 40, 50];
|
| 19 |
-
const TS_MODES = ["1_frame", "2_frames", "2_frames_4_space", "6_frames"];
|
| 20 |
-
const BACKENDS = ["pyav", "torchcodec"];
|
| 21 |
-
|
| 22 |
-
// Deterministic pseudo-random so the table is stable across reloads
|
| 23 |
-
function mulberry32(seed) {
|
| 24 |
-
return function () {
|
| 25 |
-
let t = (seed += 0x6d2b79f5);
|
| 26 |
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
| 27 |
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
| 28 |
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
| 29 |
-
};
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function codecBias(v) {
|
| 33 |
-
if (v === "libsvtav1") return { size: 0.35, psnr: 1.8, speed: 1.15, enc: 0.6 };
|
| 34 |
-
if (v === "hevc") return { size: 0.55, psnr: 1.1, speed: 1.02, enc: 0.85 };
|
| 35 |
-
return { size: 0.85, psnr: 0.0, speed: 0.92, enc: 1.15 }; // h264
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
function genRows() {
|
| 39 |
-
const rand = mulberry32(42);
|
| 40 |
-
const rows = [];
|
| 41 |
-
for (const repo of REPOS) {
|
| 42 |
-
const repoScale = repo.includes("pusht") ? 0.6 : repo.includes("kitchen") ? 1.25 : 1.0;
|
| 43 |
-
for (const vcodec of VCODECS) {
|
| 44 |
-
for (const pix of PIX_FMTS) {
|
| 45 |
-
// libsvtav1 only with yuv420p
|
| 46 |
-
if (vcodec === "libsvtav1" && pix !== "yuv420p") continue;
|
| 47 |
-
for (const g of G_VALUES) {
|
| 48 |
-
for (const crf of CRF_VALUES) {
|
| 49 |
-
for (const ts of TS_MODES) {
|
| 50 |
-
for (const be of BACKENDS) {
|
| 51 |
-
// torchcodec doesn't exist for av1 on all
|
| 52 |
-
if (vcodec === "libsvtav1" && be === "torchcodec" && rand() < 0.3) continue;
|
| 53 |
-
const b = codecBias(vcodec);
|
| 54 |
-
const r = 0.5 + rand() * 0.5;
|
| 55 |
-
|
| 56 |
-
// Compression ratio (smaller is better). crf high + g high → small file
|
| 57 |
-
const sizeRatio = (b.size * (1 - crf * 0.008) * (1 / (1 + g * 0.03)) * (pix === "yuv420p" ? 0.85 : 1)) * (0.8 + rand() * 0.4);
|
| 58 |
-
|
| 59 |
-
// Decode time ms (lower is better). Multi-frame access ~ faster per-frame.
|
| 60 |
-
const tsMult = ts === "1_frame" ? 1.0 : ts === "2_frames" ? 0.68 : ts === "2_frames_4_space" ? 0.72 : 0.35;
|
| 61 |
-
const beMult = be === "torchcodec" ? 0.62 : 1.0;
|
| 62 |
-
const medDec = 7.2 * b.speed * repoScale * tsMult * beMult * (0.9 + rand() * 0.2);
|
| 63 |
-
const stdDec = medDec * (0.08 + rand() * 0.12);
|
| 64 |
-
|
| 65 |
-
// PSNR (higher is better) — av1 best, h264 worst. crf low → high psnr.
|
| 66 |
-
const medPsnr = 40 + b.psnr - crf * 0.22 - g * 0.02 + (pix === "yuv444p" ? 0.6 : 0) + (rand() * 0.8 - 0.4);
|
| 67 |
-
const stdPsnr = 0.3 + rand() * 0.5;
|
| 68 |
-
|
| 69 |
-
// SSIM
|
| 70 |
-
const medSsim = Math.min(0.999, 0.92 + b.psnr * 0.01 - crf * 0.0015 + rand() * 0.01);
|
| 71 |
-
const stdSsim = 0.001 + rand() * 0.004;
|
| 72 |
-
|
| 73 |
-
const medMse = Math.max(0.1, 4 + crf * 0.6 - b.psnr * 0.8 + rand() * 1.2);
|
| 74 |
-
const stdMse = medMse * (0.1 + rand() * 0.2);
|
| 75 |
-
|
| 76 |
-
const loadRatio = 0.3 + rand() * 0.6 + (ts === "1_frame" ? 0.2 : 0);
|
| 77 |
-
|
| 78 |
-
const encFps = 120 / (b.enc * repoScale * (1 + crf * 0.004));
|
| 79 |
-
const encMs = 1000 / encFps;
|
| 80 |
-
|
| 81 |
-
rows.push({
|
| 82 |
-
repo_id: repo,
|
| 83 |
-
vcodec, pix_fmt: pix, g, crf,
|
| 84 |
-
timestamps_mode: ts, backend: be,
|
| 85 |
-
video_images_size_ratio: sizeRatio,
|
| 86 |
-
video_images_load_time_ratio: loadRatio,
|
| 87 |
-
median_load_time_video_ms: medDec,
|
| 88 |
-
std_load_time_video_ms: stdDec,
|
| 89 |
-
median_load_time_images_ms: medDec * (1.6 + rand() * 0.4),
|
| 90 |
-
std_load_time_images_ms: stdDec * 1.3,
|
| 91 |
-
median_psnr: medPsnr,
|
| 92 |
-
std_psnr: stdPsnr,
|
| 93 |
-
median_ssim: medSsim,
|
| 94 |
-
std_ssim: stdSsim,
|
| 95 |
-
median_mse: medMse,
|
| 96 |
-
std_mse: stdMse,
|
| 97 |
-
encoding_fps: encFps,
|
| 98 |
-
encoding_time_ms: encMs,
|
| 99 |
-
});
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
-
}
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
return rows;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
// Limit to a representative subset so the UI stays responsive
|
| 111 |
-
const ALL_ROWS = genRows();
|
| 112 |
-
|
| 113 |
-
// Column metadata — mirrors the Gradio app labels + descriptions
|
| 114 |
-
// Canonical label + description for every parameter and metric.
|
| 115 |
-
// Rendered in: table headers, About & parameters, chart hover bubbles,
|
| 116 |
-
// column picker, Submit form. Edit HERE to change everywhere.
|
| 117 |
-
const COLUMNS = [
|
| 118 |
-
{ key: "repo_id", label: "Dataset", short: "Dataset", group: "Config",
|
| 119 |
-
desc: "Hugging Face Hub dataset repo ID. We pick one representative episode per dataset." },
|
| 120 |
-
{ key: "vcodec", label: "Codec", short: "Codec", group: "Config",
|
| 121 |
-
desc: "Video codec. h264 (widest compatibility), hevc (≈30% smaller at same quality, slower), libsvtav1 (AV1, smallest, newest)." },
|
| 122 |
-
{ key: "pix_fmt", label: "Pixel format", short: "Pixel format", group: "Config",
|
| 123 |
-
desc: "Pixel format. yuv420p = 8-bit 4:2:0 (standard, smallest). yuv444p = full chroma (bigger, better for fine edges)." },
|
| 124 |
-
{ key: "g", label: "GOP", short: "GOP", group: "Config",
|
| 125 |
-
desc: "GOP size (group-of-pictures). Keyframe interval. Small g = cheap seeks, bigger files. Large g = great compression, expensive random access." },
|
| 126 |
-
{ key: "crf", label: "CRF", short: "CRF", group: "Config",
|
| 127 |
-
desc: "Constant rate factor. Quality knob: 0 is lossless, ~18 is visually lossless, 23 is default, 51 is worst. Lower CRF = bigger file, higher PSNR/SSIM." },
|
| 128 |
-
{ key: "timestamps_mode", label: "Access pattern", short: "Access", group: "Config",
|
| 129 |
-
desc: "How frames are requested. 1_frame: seek, decode one frame, done. 2_frames: two adjacent frames. 6_frames: contiguous window. 2_frames_4_space: two samples four frames apart — worst-case for GOP-heavy settings." },
|
| 130 |
-
{ key: "backend", label: "Backend", short: "Backend", group: "Config",
|
| 131 |
-
desc: "Decoder library. pyav (FFmpeg Python bindings), torchcodec (Meta, GPU-accel), video_reader (torchvision fallback)." },
|
| 132 |
-
|
| 133 |
-
{ key: "video_images_size_ratio", label: "Video/Image\nsize ratio ↓", short: "Video/Image size", group: "Compression", metric: true, lower: true, fmt: (v) => v.toFixed(4),
|
| 134 |
-
desc: "Encoded video size ÷ sum of original PNG image sizes. 0.02 means the video is 50× smaller than the frame dump. Lower is better." },
|
| 135 |
-
{ key: "video_images_load_time_ratio", label: "Video/Image\nload ratio ↓", short: "Video/Image load", group: "Compression", metric: true, lower: true, fmt: (v) => v.toFixed(4),
|
| 136 |
-
desc: "Video decode time ÷ PNG image load time for the same frames. <1 means the video is faster to read than the PNGs; >1 means you pay for the compression at read time." },
|
| 137 |
-
|
| 138 |
-
{ key: "median_load_time_video_ms", label: "Decoding (ms) ↓", short: "Decoding", group: "Speed", metric: true, lower: true, fmt: (v) => v.toFixed(2), stdKey: "std_load_time_video_ms",
|
| 139 |
-
desc: "Median wall-clock time to decode N frames (per access pattern) from the compressed video." },
|
| 140 |
-
{ key: "encoding_time_ms", label: "Encoding (ms) ↓", short: "Encoding", group: "Speed", metric: true, lower: true, fmt: (v) => v.toFixed(2),
|
| 141 |
-
desc: "Wall time to encode the whole episode once. Includes container muxing. Lower is faster." },
|
| 142 |
-
|
| 143 |
-
{ key: "median_psnr", label: "PSNR ↑", short: "PSNR", group: "Quality", metric: true, higher: true, fmt: (v) => v.toFixed(2), stdKey: "std_psnr",
|
| 144 |
-
desc: "Peak signal-to-noise ratio in dB. Logarithmic, so +3 dB ≈ half the error. 40+ is excellent, 30 is acceptable, 20 is visible artefacts." },
|
| 145 |
-
{ key: "median_ssim", label: "SSIM ↑", short: "SSIM", group: "Quality", metric: true, higher: true, fmt: (v) => v.toFixed(4), stdKey: "std_ssim",
|
| 146 |
-
desc: "Structural similarity index in [0,1]. Perceptual — weights luminance, contrast, structure. 0.95+ is good, 0.80 is degraded." },
|
| 147 |
-
{ key: "median_mse", label: "MSE ↓", short: "MSE", group: "Quality", metric: true, lower: true, fmt: (v) => v.toFixed(2), stdKey: "std_mse",
|
| 148 |
-
desc: "Median mean-squared error between decoded and original frames. Pixel-space error. Lower is better. 0 = identical." },
|
| 149 |
-
];
|
| 150 |
-
|
| 151 |
-
const COL_GROUPS = ["Config", "Compression", "Speed", "Quality"];
|
| 152 |
-
|
| 153 |
-
window.VB_DATA = { ALL_ROWS, COLUMNS, COL_GROUPS, REPOS, VCODECS, PIX_FMTS, G_VALUES, CRF_VALUES, TS_MODES, BACKENDS };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index.html
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.12
|
| 2 |
+
plotly
|
| 3 |
+
pandas
|
| 4 |
+
huggingface_hub
|
| 5 |
+
datasets
|
| 6 |
+
pydantic
|
src/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Server-side data layer for the Video Benchmark Space.
|
| 2 |
+
|
| 3 |
+
``schema`` holds the canonical UI vocabulary (column metadata, option
|
| 4 |
+
lists, leaderboard categories, parameter group narration, About-page
|
| 5 |
+
prose) that the Gradio app in ``app.py`` reads directly to build its
|
| 6 |
+
controls and tabs. ``compute`` holds the pure-Python row math the app
|
| 7 |
+
calls from its event handlers: filtering, composite ranking, leaderboard
|
| 8 |
+
aggregation, and the three Compare-tab chart aggregations.
|
| 9 |
+
|
| 10 |
+
Both modules are framework-agnostic — they only know about ``list[dict]``
|
| 11 |
+
rows and the ``COLUMNS`` metadata, so they're trivial to import from a
|
| 12 |
+
notebook or a test harness.
|
| 13 |
+
"""
|
src/compute.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Row math for the Video Benchmark Space.
|
| 2 |
+
|
| 3 |
+
Pure functions that take ``list[dict]`` rows (a subset of the
|
| 4 |
+
``lerobot/video-benchmark-results`` schema — see ``_RESULTS_KEEP_KEYS``
|
| 5 |
+
in ``app.py``) and return JSON-serializable values. The Gradio handlers
|
| 6 |
+
in ``app.py`` call these to build the Results table heatmap, the
|
| 7 |
+
Leaderboards bar chart, and the three Compare-tab plots.
|
| 8 |
+
|
| 9 |
+
Nothing here imports Gradio, pandas, or plotly: the module is purely
|
| 10 |
+
arithmetic so it stays trivially testable and reusable from notebooks.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from typing import Any, Iterable
|
| 15 |
+
|
| 16 |
+
from . import schema
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
Row = dict[str, Any]
|
| 20 |
+
Scale = dict[str, Any] # {"min": float, "max": float, "lower": bool}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# --------------------------------------------------------------------------- #
|
| 24 |
+
# Helpers
|
| 25 |
+
# --------------------------------------------------------------------------- #
|
| 26 |
+
def _is_num(v: Any) -> bool:
|
| 27 |
+
"""True for finite ints/floats; False for ``None``, NaN, and bools.
|
| 28 |
+
|
| 29 |
+
Bools are explicitly rejected so accidental flag columns can't sneak
|
| 30 |
+
into numeric aggregations.
|
| 31 |
+
"""
|
| 32 |
+
if v is None:
|
| 33 |
+
return False
|
| 34 |
+
if isinstance(v, bool):
|
| 35 |
+
return False
|
| 36 |
+
if isinstance(v, (int, float)):
|
| 37 |
+
# NaN is the only float that's not equal to itself.
|
| 38 |
+
return v == v
|
| 39 |
+
return False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _column(key: str) -> dict[str, Any] | None:
|
| 43 |
+
for c in schema.COLUMNS:
|
| 44 |
+
if c["key"] == key:
|
| 45 |
+
return c
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# --------------------------------------------------------------------------- #
|
| 50 |
+
# Per-metric normalization (used by the Results table color ramp and the
|
| 51 |
+
# Leaderboards weighted score).
|
| 52 |
+
# --------------------------------------------------------------------------- #
|
| 53 |
+
def normalize_scale(rows: list[Row], col: dict[str, Any]) -> Scale:
|
| 54 |
+
"""Return ``{min, max, lower}`` for a metric column.
|
| 55 |
+
|
| 56 |
+
Used to map raw values into [0, 1] before feeding the rdylgn color
|
| 57 |
+
ramp: ``t = (v - min) / (max - min); if lower: t = 1 - t``. When all
|
| 58 |
+
values are missing, returns a degenerate scale tagged with
|
| 59 |
+
``empty=True`` so callers can fall back to mid-gray.
|
| 60 |
+
"""
|
| 61 |
+
vals = [r.get(col["key"]) for r in rows]
|
| 62 |
+
nums = [v for v in vals if _is_num(v)]
|
| 63 |
+
lower = bool(col.get("lower"))
|
| 64 |
+
if not nums:
|
| 65 |
+
return {"min": 0.0, "max": 0.0, "lower": lower, "empty": True}
|
| 66 |
+
lo = min(nums)
|
| 67 |
+
hi = max(nums)
|
| 68 |
+
return {"min": lo, "max": hi, "lower": lower}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _normalize_value(v: Any, scale: Scale) -> float:
|
| 72 |
+
"""Map ``v`` into [0, 1] using a scale from :func:`normalize_scale`.
|
| 73 |
+
|
| 74 |
+
Returns ``0.5`` for empty scales, ``0.0`` for non-numeric values, and
|
| 75 |
+
inverts the result when the column is polarity-``lower`` so that
|
| 76 |
+
``1.0`` always means "best on this axis".
|
| 77 |
+
"""
|
| 78 |
+
if scale.get("empty"):
|
| 79 |
+
return 0.5
|
| 80 |
+
if not _is_num(v):
|
| 81 |
+
return 0.0
|
| 82 |
+
lo = scale["min"]
|
| 83 |
+
hi = scale["max"]
|
| 84 |
+
if hi == lo:
|
| 85 |
+
return 0.5
|
| 86 |
+
t = (v - lo) / (hi - lo)
|
| 87 |
+
return (1 - t) if scale["lower"] else t
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def composite_rank(rows: list[Row], metric_cols: list[dict[str, Any]]) -> list[Row]:
|
| 91 |
+
"""Sort rows by their composite score across the visible metric columns.
|
| 92 |
+
|
| 93 |
+
Score is the unweighted average of ``(1 - normalized_value)`` across
|
| 94 |
+
``metric_cols``, so *lower* = better. Sort is stable: ties fall back
|
| 95 |
+
to original input order so repeated calls on the same input produce
|
| 96 |
+
the same ordering.
|
| 97 |
+
"""
|
| 98 |
+
if not rows:
|
| 99 |
+
return []
|
| 100 |
+
if not metric_cols:
|
| 101 |
+
return list(rows)
|
| 102 |
+
|
| 103 |
+
scales = [(c, normalize_scale(rows, c)) for c in metric_cols]
|
| 104 |
+
|
| 105 |
+
def score(r: Row) -> float:
|
| 106 |
+
s = 0.0
|
| 107 |
+
n = 0
|
| 108 |
+
for c, sc in scales:
|
| 109 |
+
v = r.get(c["key"])
|
| 110 |
+
s += (1 - _normalize_value(v, sc))
|
| 111 |
+
n += 1
|
| 112 |
+
return s / n if n else 0.0
|
| 113 |
+
|
| 114 |
+
indexed = list(enumerate(rows))
|
| 115 |
+
indexed.sort(key=lambda pair: (score(pair[1]), pair[0]))
|
| 116 |
+
return [r for _, r in indexed]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# --------------------------------------------------------------------------- #
|
| 120 |
+
# Filtering
|
| 121 |
+
# --------------------------------------------------------------------------- #
|
| 122 |
+
def filter_rows(rows: list[Row], filters: dict[str, Iterable[str]] | None) -> list[Row]:
|
| 123 |
+
"""Keep rows whose column values match the Results-tab chip selections.
|
| 124 |
+
|
| 125 |
+
``filters`` maps a column key to the set of allowed values. Values
|
| 126 |
+
are always compared as strings (the CheckboxGroups emit strings,
|
| 127 |
+
even for numeric columns like ``g`` / ``crf``), so row values are
|
| 128 |
+
coerced via ``str()`` before lookup. An empty / missing entry for a
|
| 129 |
+
key disables that filter.
|
| 130 |
+
"""
|
| 131 |
+
if not filters:
|
| 132 |
+
return list(rows)
|
| 133 |
+
cleaned: dict[str, set[str]] = {}
|
| 134 |
+
for k, vs in filters.items():
|
| 135 |
+
if not vs:
|
| 136 |
+
continue
|
| 137 |
+
s = {str(v) for v in vs}
|
| 138 |
+
if s:
|
| 139 |
+
cleaned[k] = s
|
| 140 |
+
if not cleaned:
|
| 141 |
+
return list(rows)
|
| 142 |
+
out: list[Row] = []
|
| 143 |
+
for r in rows:
|
| 144 |
+
keep = True
|
| 145 |
+
for k, allowed in cleaned.items():
|
| 146 |
+
if str(r.get(k)) not in allowed:
|
| 147 |
+
keep = False
|
| 148 |
+
break
|
| 149 |
+
if keep:
|
| 150 |
+
out.append(r)
|
| 151 |
+
return out
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def metric_scales(rows: list[Row], cols: list[dict[str, Any]]) -> dict[str, Scale]:
|
| 155 |
+
"""Return ``{column_key: scale}`` for every metric column in ``cols``.
|
| 156 |
+
|
| 157 |
+
Pre-computing scales once per render lets the heatmap colorize each
|
| 158 |
+
cell without re-walking the rows for each column.
|
| 159 |
+
"""
|
| 160 |
+
return {c["key"]: normalize_scale(rows, c) for c in cols if c.get("metric")}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# --------------------------------------------------------------------------- #
|
| 164 |
+
# Leaderboards
|
| 165 |
+
# --------------------------------------------------------------------------- #
|
| 166 |
+
def leaderboard(rows: list[Row], ts: str, cat: str) -> dict[str, Any]:
|
| 167 |
+
"""Build the ranked leaderboard for one category and access pattern.
|
| 168 |
+
|
| 169 |
+
Steps:
|
| 170 |
+
1. Keep rows whose ``timestamps_mode`` matches ``ts``.
|
| 171 |
+
2. Group by ``(vcodec, pix_fmt, g, crf, backend)`` and average each
|
| 172 |
+
axis across the per-dataset repeats.
|
| 173 |
+
3. Normalize each axis across the aggregated set.
|
| 174 |
+
4. Score each aggregated row as the weighted sum of
|
| 175 |
+
``(1 - normalized)`` using ``schema.LEADERBOARD_CATS[cat]['weights']``
|
| 176 |
+
and sort ascending (lower = better).
|
| 177 |
+
|
| 178 |
+
Returns ``{axes, axis_keys, items}`` where each ``items[i].values`` is the
|
| 179 |
+
list of *normalized* per-axis values oriented so 1.0 = best-in-class.
|
| 180 |
+
Every aggregated configuration is returned (no top-N truncation).
|
| 181 |
+
Raises ``ValueError`` for an unknown category and ``RuntimeError`` if
|
| 182 |
+
any leaderboard axis is missing from ``schema.COLUMNS``.
|
| 183 |
+
"""
|
| 184 |
+
if cat not in schema.LEADERBOARD_CATS:
|
| 185 |
+
raise ValueError(f"unknown leaderboard category: {cat!r}")
|
| 186 |
+
weights = schema.LEADERBOARD_CATS[cat]["weights"]
|
| 187 |
+
axis_keys = schema.LEADERBOARD_AXES
|
| 188 |
+
axis_cols = [_column(k) for k in axis_keys]
|
| 189 |
+
if any(c is None for c in axis_cols):
|
| 190 |
+
missing = [k for k, c in zip(axis_keys, axis_cols) if c is None]
|
| 191 |
+
raise RuntimeError(f"missing column metadata for axes: {missing}")
|
| 192 |
+
|
| 193 |
+
scoped = [r for r in rows if r.get("timestamps_mode") == ts]
|
| 194 |
+
|
| 195 |
+
# Group by the five-tuple, preserving first-seen order so two equally
|
| 196 |
+
# ranked configs always come out in the same order across requests.
|
| 197 |
+
groups: dict[tuple, list[Row]] = {}
|
| 198 |
+
order: list[tuple] = []
|
| 199 |
+
for r in scoped:
|
| 200 |
+
key = (r.get("vcodec"), r.get("pix_fmt"), r.get("g"), r.get("crf"), r.get("backend"))
|
| 201 |
+
if key not in groups:
|
| 202 |
+
groups[key] = []
|
| 203 |
+
order.append(key)
|
| 204 |
+
groups[key].append(r)
|
| 205 |
+
|
| 206 |
+
aggregated: list[Row] = []
|
| 207 |
+
for key in order:
|
| 208 |
+
rs = groups[key]
|
| 209 |
+
ag: Row = dict(rs[0])
|
| 210 |
+
for c in axis_cols:
|
| 211 |
+
vals = [r.get(c["key"]) for r in rs]
|
| 212 |
+
nums = [v for v in vals if _is_num(v)]
|
| 213 |
+
ag[c["key"]] = sum(nums) / len(nums) if nums else None
|
| 214 |
+
aggregated.append(ag)
|
| 215 |
+
|
| 216 |
+
if not aggregated:
|
| 217 |
+
return {"axes": [c["short"] or c["label"] for c in axis_cols], "items": []}
|
| 218 |
+
|
| 219 |
+
scales = [normalize_scale(aggregated, c) for c in axis_cols]
|
| 220 |
+
|
| 221 |
+
scored: list[tuple[float, int, Row, list[float]]] = []
|
| 222 |
+
for idx, row in enumerate(aggregated):
|
| 223 |
+
sum_w = 0.0
|
| 224 |
+
wsum = 0.0
|
| 225 |
+
values: list[float] = []
|
| 226 |
+
for i, c in enumerate(axis_cols):
|
| 227 |
+
w = weights.get(c["key"], 1)
|
| 228 |
+
n = _normalize_value(row.get(c["key"]), scales[i])
|
| 229 |
+
values.append(n)
|
| 230 |
+
sum_w += w * (1 - n)
|
| 231 |
+
wsum += w
|
| 232 |
+
score = sum_w / wsum if wsum else 0.0
|
| 233 |
+
scored.append((score, idx, row, values))
|
| 234 |
+
|
| 235 |
+
scored.sort(key=lambda t: (t[0], t[1]))
|
| 236 |
+
|
| 237 |
+
items = [
|
| 238 |
+
{
|
| 239 |
+
"row": _slim_row(r),
|
| 240 |
+
"values": vals,
|
| 241 |
+
"score": s,
|
| 242 |
+
}
|
| 243 |
+
for s, _, r, vals in scored
|
| 244 |
+
]
|
| 245 |
+
return {
|
| 246 |
+
"axes": [c["short"] or c["label"] for c in axis_cols],
|
| 247 |
+
"axis_keys": axis_keys,
|
| 248 |
+
"items": items,
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _slim_row(r: Row) -> Row:
|
| 253 |
+
"""Project a row down to the keys the leaderboard payload actually reads."""
|
| 254 |
+
keep = (
|
| 255 |
+
"vcodec", "pix_fmt", "g", "crf", "backend", "timestamps_mode", "repo_id",
|
| 256 |
+
*schema.LEADERBOARD_AXES,
|
| 257 |
+
)
|
| 258 |
+
return {k: r.get(k) for k in keep if k in r}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# --------------------------------------------------------------------------- #
|
| 262 |
+
# Compare-tab aggregations
|
| 263 |
+
# --------------------------------------------------------------------------- #
|
| 264 |
+
def compare_bar(rows: list[Row], metric: str, group_by: str) -> list[dict[str, Any]]:
|
| 265 |
+
"""Mean of ``metric`` grouped by ``group_by``, sorted descending by value.
|
| 266 |
+
|
| 267 |
+
Returns ``[{"k": label, "v": mean}, ...]``. The ``lerobot/`` prefix is
|
| 268 |
+
stripped from dataset labels so x-axis ticks stay readable when
|
| 269 |
+
``group_by="repo_id"``.
|
| 270 |
+
"""
|
| 271 |
+
g: dict[str, list[float]] = {}
|
| 272 |
+
order: list[str] = []
|
| 273 |
+
for r in rows:
|
| 274 |
+
v = r.get(metric)
|
| 275 |
+
if not _is_num(v):
|
| 276 |
+
continue
|
| 277 |
+
k = str(r.get(group_by))
|
| 278 |
+
if k not in g:
|
| 279 |
+
g[k] = []
|
| 280 |
+
order.append(k)
|
| 281 |
+
g[k].append(v)
|
| 282 |
+
out = [
|
| 283 |
+
{"k": k.replace("lerobot/", ""), "v": sum(g[k]) / len(g[k])}
|
| 284 |
+
for k in order
|
| 285 |
+
if g[k]
|
| 286 |
+
]
|
| 287 |
+
out.sort(key=lambda d: d["v"], reverse=True)
|
| 288 |
+
return out
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def compare_scatter(rows: list[Row]) -> list[dict[str, Any]]:
|
| 292 |
+
"""Return one point per row for the quality-vs-compression scatter.
|
| 293 |
+
|
| 294 |
+
Each point is ``{x: size_ratio, y: psnr, c: vcodec, label}``. Rows
|
| 295 |
+
that lack a numeric size ratio or PSNR are skipped; everything else
|
| 296 |
+
is kept so the chart reflects the full Hub dataset.
|
| 297 |
+
"""
|
| 298 |
+
pts: list[dict[str, Any]] = []
|
| 299 |
+
for r in rows:
|
| 300 |
+
x = r.get("video_images_size_ratio")
|
| 301 |
+
y = r.get("median_psnr")
|
| 302 |
+
if not _is_num(x) or not _is_num(y):
|
| 303 |
+
continue
|
| 304 |
+
pts.append({
|
| 305 |
+
"x": x,
|
| 306 |
+
"y": y,
|
| 307 |
+
"c": r.get("vcodec"),
|
| 308 |
+
"label": f"g={r.get('g')} crf={r.get('crf')}",
|
| 309 |
+
})
|
| 310 |
+
return pts
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def compare_stacked(rows: list[Row]) -> dict[str, Any]:
|
| 314 |
+
"""Aggregate decoding latency per codec, broken down by access pattern.
|
| 315 |
+
|
| 316 |
+
Returns ``{codecs, modes, data}`` where ``data[i].segments`` carries
|
| 317 |
+
the per-mean-decode-time mean for each access pattern. The codec list
|
| 318 |
+
is sorted by total stacked height ascending so the fastest codec sits
|
| 319 |
+
at the left of the chart.
|
| 320 |
+
"""
|
| 321 |
+
codecs: list[str] = []
|
| 322 |
+
seen_c: set[str] = set()
|
| 323 |
+
modes: list[str] = []
|
| 324 |
+
seen_m: set[str] = set()
|
| 325 |
+
for r in rows:
|
| 326 |
+
c = r.get("vcodec")
|
| 327 |
+
if c is not None and c not in seen_c:
|
| 328 |
+
seen_c.add(c)
|
| 329 |
+
codecs.append(c)
|
| 330 |
+
m = r.get("timestamps_mode")
|
| 331 |
+
if m is not None and m not in seen_m:
|
| 332 |
+
seen_m.add(m)
|
| 333 |
+
modes.append(m)
|
| 334 |
+
|
| 335 |
+
data: list[dict[str, Any]] = []
|
| 336 |
+
for codec in codecs:
|
| 337 |
+
segs: list[dict[str, Any]] = []
|
| 338 |
+
for mode in modes:
|
| 339 |
+
vals = [
|
| 340 |
+
r["median_load_time_video_ms"]
|
| 341 |
+
for r in rows
|
| 342 |
+
if r.get("vcodec") == codec
|
| 343 |
+
and r.get("timestamps_mode") == mode
|
| 344 |
+
and _is_num(r.get("median_load_time_video_ms"))
|
| 345 |
+
]
|
| 346 |
+
v = sum(vals) / len(vals) if vals else 0.0
|
| 347 |
+
segs.append({"key": mode, "v": v})
|
| 348 |
+
total = sum(s["v"] for s in segs)
|
| 349 |
+
data.append({"k": codec, "segments": segs, "total": total})
|
| 350 |
+
|
| 351 |
+
data.sort(key=lambda d: d["total"])
|
| 352 |
+
return {"codecs": codecs, "modes": modes, "data": data}
|
src/schema.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical UI vocabulary for the Video Benchmark.
|
| 2 |
+
|
| 3 |
+
Single source of truth for everything the Gradio app needs to *describe*
|
| 4 |
+
a column, option, parameter group, leaderboard ranking, or About-page
|
| 5 |
+
narration. ``app.py`` imports these constants directly to wire up its
|
| 6 |
+
filter chips, dropdowns, table headers, and prose blocks; adding or
|
| 7 |
+
renaming a knob means editing exactly one file.
|
| 8 |
+
|
| 9 |
+
Each ``COLUMNS`` entry carries display metadata plus, for numeric
|
| 10 |
+
columns, a printf-style ``fmt_spec`` (e.g. ``"%.2f"``) so values render
|
| 11 |
+
consistently across the table and tooltips.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# --------------------------------------------------------------------------- #
|
| 19 |
+
# Datasets the UI talks to. Mirrored as constants so the frontend can render
|
| 20 |
+
# friendly links without duplicating strings.
|
| 21 |
+
# --------------------------------------------------------------------------- #
|
| 22 |
+
RESULTS_DATASET = "lerobot/video-benchmark-results"
|
| 23 |
+
SUBMISSIONS_DATASET = "lerobot/video-benchmark-submissions"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# --------------------------------------------------------------------------- #
|
| 27 |
+
# Column metadata
|
| 28 |
+
#
|
| 29 |
+
# Order matters: it drives table column order, the Column-picker grouping,
|
| 30 |
+
# Parameters-page ordering, and Compare-tab dropdowns. ``metric=True`` flags
|
| 31 |
+
# numeric columns that participate in composite ranking and color ramps;
|
| 32 |
+
# ``lower=True`` / ``higher=True`` set the polarity.
|
| 33 |
+
# --------------------------------------------------------------------------- #
|
| 34 |
+
COLUMNS: list[dict[str, Any]] = [
|
| 35 |
+
# Config
|
| 36 |
+
{"key": "repo_id", "label": "Dataset", "short": "Dataset", "group": "Config",
|
| 37 |
+
"desc": "Hugging Face Hub dataset repo ID. We pick one representative episode per dataset."},
|
| 38 |
+
{"key": "vcodec", "label": "Codec", "short": "Codec", "group": "Config",
|
| 39 |
+
"desc": "Video codec. h264 (widest compatibility), hevc (≈30% smaller at same quality, slower), libsvtav1 (AV1, smallest, newest)."},
|
| 40 |
+
{"key": "pix_fmt", "label": "Pixel format", "short": "Pixel format", "group": "Config",
|
| 41 |
+
"desc": "yuv420p = 8-bit 4:2:0 (standard, smallest). yuv444p = full chroma (bigger, better for fine edges)."},
|
| 42 |
+
{"key": "g", "label": "GOP", "short": "GOP", "group": "Config",
|
| 43 |
+
"desc": "Group Of Pictures. Keyframe interval. Small g = cheap seeks, bigger files. Large g = great compression, expensive random access."},
|
| 44 |
+
{"key": "crf", "label": "CRF", "short": "CRF", "group": "Config",
|
| 45 |
+
"desc": "Constant Rate Factor. Quality knob: 0 is lossless, ~18 is visually lossless, 23 is default, 51 is worst. Lower CRF = bigger file, higher PSNR/SSIM."},
|
| 46 |
+
{"key": "timestamps_mode", "label": "Access pattern", "short": "Access", "group": "Config",
|
| 47 |
+
"desc": "How frames are requested. 1_frame: seek, decode one frame, done. 2_frames: two adjacent frames. 6_frames: contiguous window. 2_frames_4_space: two samples four frames apart — worst-case for GOP-heavy settings."},
|
| 48 |
+
{"key": "backend", "label": "Backend", "short": "Backend", "group": "Config",
|
| 49 |
+
"desc": "Video decoding library. pyav (torchvision default), torchcodec"},
|
| 50 |
+
|
| 51 |
+
# Compression
|
| 52 |
+
{"key": "video_images_size_ratio", "label": "Video/Image\nsize ratio ↓", "short": "Video/Image size ratio",
|
| 53 |
+
"group": "Compression", "metric": True, "lower": True, "fmt_spec": "%.4f",
|
| 54 |
+
"desc": "Encoded video size ÷ sum of original PNG image sizes. 0.02 means the video is 50× smaller than the frame dump. Lower is better."},
|
| 55 |
+
{"key": "video_images_load_time_ratio", "label": "Video/Image\nload ratio ↓", "short": "Video/Image load ratio",
|
| 56 |
+
"group": "Compression", "metric": True, "lower": True, "fmt_spec": "%.4f",
|
| 57 |
+
"desc": "Video decoding time ÷ PNG image load time for the same frames. <1 means the video is faster to read than the PNGs; >1 means you pay for the compression at read time."},
|
| 58 |
+
|
| 59 |
+
# Speed
|
| 60 |
+
{"key": "median_load_time_video_ms", "label": "Decoding (ms) ↓", "short": "Decoding",
|
| 61 |
+
"group": "Speed", "metric": True, "lower": True, "fmt_spec": "%.2f", "std_key": "std_load_time_video_ms",
|
| 62 |
+
"desc": "Median wall-clock time to decode N frames (per access pattern) from the compressed video."},
|
| 63 |
+
{"key": "encoding_time_ms", "label": "Encoding (ms) ↓", "short": "Encoding",
|
| 64 |
+
"group": "Speed", "metric": True, "lower": True, "fmt_spec": "%.2f",
|
| 65 |
+
"desc": "Wall time to encode the whole episode once. Includes container muxing. Lower is faster."},
|
| 66 |
+
{"key": "encoding_fps", "label": "Encoding (fps) ↑", "short": "Encoding fps",
|
| 67 |
+
"group": "Speed", "metric": True, "higher": True, "fmt_spec": "%.1f",
|
| 68 |
+
"desc": "Encoding throughput — frames encoded per second across the whole episode. Higher is faster."},
|
| 69 |
+
|
| 70 |
+
# Quality
|
| 71 |
+
{"key": "median_psnr", "label": "PSNR (dB) ↑", "short": "PSNR",
|
| 72 |
+
"group": "Quality", "metric": True, "higher": True, "fmt_spec": "%.2f", "std_key": "std_psnr",
|
| 73 |
+
"desc": "Peak signal-to-noise ratio in dB. Logarithmic, so +3 dB ≈ half the error. 40+ is excellent, 30 is acceptable, 20 is visible artefacts."},
|
| 74 |
+
{"key": "median_ssim", "label": "SSIM (0–1) ↑", "short": "SSIM",
|
| 75 |
+
"group": "Quality", "metric": True, "higher": True, "fmt_spec": "%.4f", "std_key": "std_ssim",
|
| 76 |
+
"desc": "Structural similarity index in [0,1], dimensionless. Perceptual — weights luminance, contrast, structure. 0.95+ is good, 0.80 is degraded."},
|
| 77 |
+
{"key": "median_mse", "label": "MSE (px²) ↓", "short": "MSE",
|
| 78 |
+
"group": "Quality", "metric": True, "lower": True, "fmt_spec": "%.2f", "std_key": "std_mse",
|
| 79 |
+
"desc": "Median mean-squared error between decoded and original frames, in squared 8-bit pixel intensities (px², 0–65025). Lower is better. 0 = identical."},
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
# Qualitative palette for leaderboard rank badges + radar polygons.
|
| 83 |
+
# Muted on purpose — the cards overlay four to twelve colors on the
|
| 84 |
+
# same card surface (and the radar stacks the same count on one axis
|
| 85 |
+
# system), so a saturated HF-brand palette reads as visual noise.
|
| 86 |
+
# These are chromatic neighbours of the brand hues, pulled 20–30%
|
| 87 |
+
# toward gray, so they stay recognizable next to the brand palette
|
| 88 |
+
# without fighting it for attention.
|
| 89 |
+
PODIUM_COLORS: list[str] = [
|
| 90 |
+
"#6B8EBF", # dusty blue
|
| 91 |
+
"#D99B5A", # warm sand
|
| 92 |
+
"#7FA97E", # sage
|
| 93 |
+
"#A98FBF", # muted lavender
|
| 94 |
+
"#C98595", # dusty rose
|
| 95 |
+
"#6FA3A3", # dusty teal
|
| 96 |
+
"#B8A773", # soft olive
|
| 97 |
+
"#8A95A8", # slate
|
| 98 |
+
"#D1A663", # honey
|
| 99 |
+
"#7FA3B8", # powder blue
|
| 100 |
+
"#B88A73", # terracotta
|
| 101 |
+
"#8F9F8A", # moss
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
# Codec → brand-aligned but desaturated accent color. Shared by the
|
| 105 |
+
# leaderboard cards (codec pill), the leaderboard dataframe (codec
|
| 106 |
+
# column chip), and anywhere else codec identity is rendered. Keys
|
| 107 |
+
# are lowercased for case-insensitive lookup.
|
| 108 |
+
CODEC_COLORS: dict[str, str] = {
|
| 109 |
+
"h264": "#6B8EBF", # dusty blue (mirrors HF blue)
|
| 110 |
+
"hevc": "#D99B5A", # warm sand (mirrors HF orange)
|
| 111 |
+
"libsvtav1": "#7FA97E", # sage (mirrors HF green)
|
| 112 |
+
}
|
| 113 |
+
CODEC_FALLBACK_COLOR: str = "#8A95A8"
|
| 114 |
+
|
| 115 |
+
STACK_COLORS: list[str] = [
|
| 116 |
+
"#6B8EBF", "#D99B5A", "#7FA97E", "#A98FBF", "#C98595", "#6FA3A3",
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# --------------------------------------------------------------------------- #
|
| 121 |
+
# Filter-bar option lists (used by the Results tab's chip filters).
|
| 122 |
+
# These define the *UI vocabulary*; actual row values come from the Hub
|
| 123 |
+
# dataset and may be a subset.
|
| 124 |
+
# --------------------------------------------------------------------------- #
|
| 125 |
+
REPOS: list[str] = [
|
| 126 |
+
"lerobot/pusht_image",
|
| 127 |
+
"lerobot/aloha_mobile_shrimp_image",
|
| 128 |
+
"lerobot/paris_street",
|
| 129 |
+
"lerobot/kitchen",
|
| 130 |
+
]
|
| 131 |
+
VCODECS: list[str] = ["h264", "hevc", "libsvtav1"]
|
| 132 |
+
PIX_FMTS: list[str] = ["yuv444p", "yuv420p"]
|
| 133 |
+
G_VALUES: list[int] = [ 2, 3, 4, 5, 6, 10, 15, 20, 40]
|
| 134 |
+
CRF_VALUES: list[int] = [0, 5, 10, 15, 20, 25, 30, 40, 50]
|
| 135 |
+
TS_MODES: list[str] = ["1_frame", "2_frames", "2_frames_4_space", "6_frames"]
|
| 136 |
+
BACKENDS: list[str] = ["pyav", "torchcodec"]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# --------------------------------------------------------------------------- #
|
| 140 |
+
# Submit-form option lists. Wider than the filter-bar lists because the
|
| 141 |
+
# Submit form lets users *queue* sweeps over knobs that aren't represented
|
| 142 |
+
# in current results.
|
| 143 |
+
# --------------------------------------------------------------------------- #
|
| 144 |
+
SUBMIT_OPTIONS: dict[str, list[str]] = {
|
| 145 |
+
"repos": REPOS,
|
| 146 |
+
"vcodecs": VCODECS,
|
| 147 |
+
"pix_fmts": PIX_FMTS,
|
| 148 |
+
"g": ["1", "2", "3", "4", "5", "6", "10", "15", "20", "40", "100"],
|
| 149 |
+
"crf": ["0", "5", "10", "15", "20", "25", "30", "40", "50"],
|
| 150 |
+
"timestamps_modes": TS_MODES,
|
| 151 |
+
"backends": BACKENDS,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
SUBMIT_DEFAULTS: dict[str, Any] = {
|
| 155 |
+
"repos": REPOS[:2],
|
| 156 |
+
"vcodecs": ["h264"],
|
| 157 |
+
"pix_fmts": ["yuv420p"],
|
| 158 |
+
"g": ["2", "10"],
|
| 159 |
+
"crf": ["10", "30"],
|
| 160 |
+
"timestamps_modes": ["1_frame", "2_frames"],
|
| 161 |
+
"backends": ["pyav"],
|
| 162 |
+
"samples_per_config": 50,
|
| 163 |
+
"workers": 4,
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# --------------------------------------------------------------------------- #
|
| 168 |
+
# Maintainer "full sweep" — every curated knob, full Cartesian product. The
|
| 169 |
+
# Submit tab exposes this behind a collapsed accordion (no public link). The
|
| 170 |
+
# resulting submission size (~tens of thousands of configs) intentionally
|
| 171 |
+
# blows past ``MAX_CONFIGS_PER_SUBMISSION``; the corresponding handler in
|
| 172 |
+
# ``app.py`` bypasses that cap because this path is gated behind a confirm
|
| 173 |
+
# checkbox and is meant to be triggered only by maintainers re-baselining
|
| 174 |
+
# the leaderboard.
|
| 175 |
+
# --------------------------------------------------------------------------- #
|
| 176 |
+
FULL_SWEEP: dict[str, Any] = {
|
| 177 |
+
"repos": list(REPOS),
|
| 178 |
+
"vcodecs": list(VCODECS),
|
| 179 |
+
"pix_fmts": list(PIX_FMTS),
|
| 180 |
+
"g": [str(v) for v in G_VALUES],
|
| 181 |
+
"crf": [str(v) for v in CRF_VALUES],
|
| 182 |
+
"timestamps_modes": list(TS_MODES),
|
| 183 |
+
"backends": list(BACKENDS),
|
| 184 |
+
"samples_per_config": 50,
|
| 185 |
+
"workers": 4,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# --------------------------------------------------------------------------- #
|
| 190 |
+
# Leaderboards
|
| 191 |
+
# --------------------------------------------------------------------------- #
|
| 192 |
+
# All categories share the same six axes — what changes between tabs is
|
| 193 |
+
# the *weighting* used to rank configurations.
|
| 194 |
+
LEADERBOARD_AXES: list[str] = [
|
| 195 |
+
"encoding_time_ms",
|
| 196 |
+
"median_load_time_video_ms",
|
| 197 |
+
"video_images_size_ratio",
|
| 198 |
+
"median_mse",
|
| 199 |
+
"median_psnr",
|
| 200 |
+
"median_ssim",
|
| 201 |
+
]
|
| 202 |
+
|
| 203 |
+
LEADERBOARD_CATS: dict[str, dict[str, Any]] = {
|
| 204 |
+
"Overall": {
|
| 205 |
+
"desc": "Balanced across encoding, decoding, size, and quality.",
|
| 206 |
+
"weights": {
|
| 207 |
+
"encoding_time_ms": 1, "median_load_time_video_ms": 1,
|
| 208 |
+
"video_images_size_ratio": 1, "median_mse": 1,
|
| 209 |
+
"median_psnr": 1, "median_ssim": 1,
|
| 210 |
+
},
|
| 211 |
+
},
|
| 212 |
+
"Quality": {
|
| 213 |
+
"desc": "Weighted toward reconstruction fidelity (MSE, PSNR, SSIM).",
|
| 214 |
+
"weights": {
|
| 215 |
+
"encoding_time_ms": 0.15, "median_load_time_video_ms": 0.15,
|
| 216 |
+
"video_images_size_ratio": 0.3, "median_mse": 1.5,
|
| 217 |
+
"median_psnr": 1.5, "median_ssim": 1.5,
|
| 218 |
+
},
|
| 219 |
+
},
|
| 220 |
+
"Encoding": {
|
| 221 |
+
"desc": "Weighted toward encoding speed and output size.",
|
| 222 |
+
"weights": {
|
| 223 |
+
"encoding_time_ms": 2, "median_load_time_video_ms": 0.2,
|
| 224 |
+
"video_images_size_ratio": 1.5, "median_mse": 0.3,
|
| 225 |
+
"median_psnr": 0.3, "median_ssim": 0.3,
|
| 226 |
+
},
|
| 227 |
+
},
|
| 228 |
+
"Decoding": {
|
| 229 |
+
"desc": "Weighted toward decoding throughput and size-on-disk impact.",
|
| 230 |
+
"weights": {
|
| 231 |
+
"encoding_time_ms": 0.2, "median_load_time_video_ms": 2,
|
| 232 |
+
"video_images_size_ratio": 1.2, "median_mse": 0.3,
|
| 233 |
+
"median_psnr": 0.3, "median_ssim": 0.3,
|
| 234 |
+
},
|
| 235 |
+
},
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# --------------------------------------------------------------------------- #
|
| 240 |
+
# Parameters reference (drives the About tab's "Parameters reference" pane)
|
| 241 |
+
# --------------------------------------------------------------------------- #
|
| 242 |
+
PARAM_GROUPS: list[dict[str, Any]] = [
|
| 243 |
+
{"t": "Inputs — what you benchmark",
|
| 244 |
+
"desc": "The corpus. Each dataset is a LeRobot episode recording with a few minutes of RGB observations.",
|
| 245 |
+
"keys": ["repo_id"]},
|
| 246 |
+
{"t": "Encoding — how the video is compressed",
|
| 247 |
+
"desc": "Passed to the FFmpeg/PyAV encoder. These are the knobs operators actually tune.",
|
| 248 |
+
"keys": ["vcodec", "pix_fmt", "g", "crf"]},
|
| 249 |
+
{"t": "Decoding — how you read it back",
|
| 250 |
+
"desc": "The other half of the equation. The same MP4 can decode very differently depending on library and access pattern.",
|
| 251 |
+
"keys": ["backend", "timestamps_mode"]},
|
| 252 |
+
{"t": "Fidelity metrics — how faithful is decoded frame vs. source",
|
| 253 |
+
"desc": "We decode the compressed video, re-read the original PNG frames, and compare pixel-for-pixel.",
|
| 254 |
+
"keys": ["median_mse", "median_psnr", "median_ssim"]},
|
| 255 |
+
{"t": "Performance metrics — how fast, how small",
|
| 256 |
+
"desc": "The cost side. Run on a single CPU thread unless noted.",
|
| 257 |
+
"keys": ["encoding_time_ms", "encoding_fps", "video_images_size_ratio", "median_load_time_video_ms", "video_images_load_time_ratio"]},
|
| 258 |
+
]
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# --------------------------------------------------------------------------- #
|
| 262 |
+
# About-page prose. Authored here so the React layer just renders it. The
|
| 263 |
+
# code snippet uses literal newlines; React renders it inside a <pre>.
|
| 264 |
+
# --------------------------------------------------------------------------- #
|
| 265 |
+
ABOUT_HTML: str = """\
|
| 266 |
+
<h3 id="what">What this is</h3>
|
| 267 |
+
<p>
|
| 268 |
+
A benchmark for video encoding and decoding in the context of robotics datasets. LeRobot stores episode observations as MP4 rather than PNG sequences — this page quantifies <i>how much</i> we gain in size and what we pay in decoding latency and pixel fidelity.
|
| 269 |
+
</p>
|
| 270 |
+
|
| 271 |
+
<p>
|
| 272 |
+
<h3 id="parameters">Parameters</h3>
|
| 273 |
+
We sweep across five axes and benchmark every combination end-to-end.
|
| 274 |
+
<ul>
|
| 275 |
+
<li><code>vcodec</code> — <code>h264</code>, <code>hevc</code>, or <code>libsvtav1</code>. Trades encode speed against compression.</li>
|
| 276 |
+
<li><code>pix_fmt</code> — chroma subsampling. <code>yuv420p</code> is smaller; <code>yuv444p</code> preserves full color.</li>
|
| 277 |
+
<li><code>g</code> — GOP size. Small = fast seeks, bigger files; large = smaller files, slower single-frame access.</li>
|
| 278 |
+
<li><code>crf</code> — quality knob. Lower = higher quality, bigger file.</li>
|
| 279 |
+
<li><code>backend</code> — decoder library. <code>pyav</code> (FFmpeg Python bindings, also <code>torchvision</code> backend) or <code>torchcodec</code>.</li>
|
| 280 |
+
</ul>
|
| 281 |
+
</p>
|
| 282 |
+
|
| 283 |
+
<p>
|
| 284 |
+
<h3 id="metrics">Metrics</h3>
|
| 285 |
+
<ul>
|
| 286 |
+
<li><b>Video/image size ratio</b> — encoded video ÷ sum of PNG frames. Lower is smaller on disk.</li>
|
| 287 |
+
<li><b>Video/image load ratio</b> — video decoding time ÷ PNG image load time for the same frames. <1 means the video is faster to read than the PNGs; >1 means you pay for the compression at read time.</li>
|
| 288 |
+
<li><b>Decoding time</b> — median wall-clock to decode N frames at a given timestamp.</li>
|
| 289 |
+
<li><b>PSNR / SSIM / MSE</b> — fidelity of decoded frames against the uncompressed source.</li>
|
| 290 |
+
<li><b>Encoding time</b> — wall-clock to encode the whole clip, end-to-end.</li>
|
| 291 |
+
<li><b>Encoding fps</b> — encode throughput in frames per second, derived from the encoding time and the episode length. Higher is faster; the handy complement to the raw time column.</li>
|
| 292 |
+
</ul>
|
| 293 |
+
</p>
|
| 294 |
+
|
| 295 |
+
<p>
|
| 296 |
+
<h3 id="access">Access patterns</h3>
|
| 297 |
+
<p>
|
| 298 |
+
Decode cost depends heavily on <i>how</i> frames are requested. <code>1_frame</code> pays the full seek+IDR cost per sample; <code>6_frames</code> amortizes it across contiguous frames; <code>2_frames_4_space</code> probes the worst case where the decoder must step across two distant windows.
|
| 299 |
+
</p>
|
| 300 |
+
|
| 301 |
+
<h3 id="repro">Reproduce locally</h3>
|
| 302 |
+
<p>The full sweep is open source. First, set <code>HF_RESULTS_REPO_ID</code> to the Hugging Face Hub dataset where you want results pushed:</p>
|
| 303 |
+
<pre style="background:var(--hf-gray-900);color:var(--hf-gray-100);padding:var(--space-4);border-radius:var(--radius-md);font-size:var(--fs-xs);overflow:auto">export HF_RESULTS_REPO_ID=lerobot/video-benchmark-results</pre>
|
| 304 |
+
<p>Then run the benchmark:</p>
|
| 305 |
+
<pre style="background:var(--hf-gray-900);color:var(--hf-gray-100);padding:var(--space-4);border-radius:var(--radius-md);font-size:var(--fs-xs);overflow:auto">python benchmark/video/run_video_benchmark.py \\
|
| 306 |
+
--output-dir outputs/video_benchmark \\
|
| 307 |
+
--repo-ids lerobot/pusht_image lerobot/kitchen \\
|
| 308 |
+
--vcodec h264 hevc libsvtav1 \\
|
| 309 |
+
--pix-fmt yuv420p yuv444p \\
|
| 310 |
+
--g 2 10 40 \\
|
| 311 |
+
--crf 10 20 30 \\
|
| 312 |
+
--timestamps-modes 1_frame 2_frames 6_frames \\
|
| 313 |
+
--backends pyav torchcodec \\
|
| 314 |
+
--num-samples 50</pre>
|
| 315 |
+
|
| 316 |
+
<h3 id="contrib">Contribute</h3>
|
| 317 |
+
<p>
|
| 318 |
+
Submit your own configurations through the <b>Submit</b> tab. A background worker picks them up and pushes results to the Hub so the whole community benefits from the same measurements.
|
| 319 |
+
</p>
|
| 320 |
+
"""
|
styles.css
CHANGED
|
@@ -1,831 +1,1594 @@
|
|
| 1 |
-
/* Video Benchmark —
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
position: relative;
|
| 43 |
}
|
| 44 |
-
.nav-right { display: flex; align-items: center; gap: var(--space-3); }
|
| 45 |
-
.nav-right .avatar { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(135deg, var(--hf-yellow), var(--hf-orange)); }
|
| 46 |
-
.nav-btn { padding: 6px 12px; border-radius: var(--radius-md); border: 1px solid var(--border-1); font-size: var(--fs-sm); background: var(--hf-white); color: var(--fg-1); font-weight: var(--fw-medium); }
|
| 47 |
-
.nav-btn:hover { background: var(--bg-2); }
|
| 48 |
-
.nav-btn.primary { background: var(--hf-gray-900); color: var(--hf-white); border-color: var(--hf-gray-900); }
|
| 49 |
-
.nav-btn.primary:hover { background: var(--hf-gray-800); }
|
| 50 |
-
|
| 51 |
-
/* --- Space header (mimics hf.co Space page) --- */
|
| 52 |
-
.space-header {
|
| 53 |
-
background: var(--hf-white);
|
| 54 |
-
border-bottom: 1px solid var(--border-1);
|
| 55 |
-
padding: var(--space-6) var(--space-8);
|
| 56 |
-
}
|
| 57 |
-
.space-header-inner { max-width: 1440px; margin: 0 auto; display: flex; align-items: flex-start; gap: var(--space-6); }
|
| 58 |
-
.space-breadcrumb { font-size: var(--fs-sm); color: var(--fg-2); display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
| 59 |
-
.space-breadcrumb a { color: var(--fg-link); text-decoration: none; font-weight: var(--fw-medium); }
|
| 60 |
-
.space-breadcrumb .sep { color: var(--fg-3); }
|
| 61 |
-
.space-title { font-size: var(--fs-xl); font-weight: var(--fw-bold); letter-spacing: -0.01em; display: flex; align-items: center; gap: var(--space-2); }
|
| 62 |
-
.space-meta { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-2); font-size: var(--fs-xs); color: var(--fg-3); }
|
| 63 |
-
.space-meta .pill { background: var(--tag-spaces-bg); color: var(--tag-spaces-fg); padding: 2px 8px; border-radius: var(--radius-pill); font-weight: var(--fw-semibold); font-size: 11px; text-transform: none; }
|
| 64 |
-
.space-meta .chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius-sm); color: var(--fg-2); }
|
| 65 |
-
.space-actions { margin-left: auto; display: flex; gap: var(--space-2); align-items: flex-start; }
|
| 66 |
-
.like-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--hf-white); border: 1px solid var(--border-1); border-radius: var(--radius-md); font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-1); }
|
| 67 |
-
.like-btn .count { color: var(--fg-2); border-left: 1px solid var(--border-1); padding-left: 8px; margin-left: 4px; }
|
| 68 |
-
.like-btn:hover { background: var(--bg-2); }
|
| 69 |
-
|
| 70 |
-
/* --- Main layout --- */
|
| 71 |
-
.page { max-width: 1440px; margin: 0 auto; padding: var(--space-6) var(--space-8); }
|
| 72 |
|
| 73 |
-
/* --- Hero ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
.hero {
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
border-radius: var(--radius-2xl);
|
| 77 |
-
padding: var(--space-
|
| 78 |
-
|
| 79 |
-
grid-template-columns: 1fr auto;
|
| 80 |
-
gap: var(--space-6);
|
| 81 |
align-items: center;
|
| 82 |
-
margin-bottom: var(--space-8);
|
| 83 |
-
position: relative;
|
| 84 |
-
overflow: hidden;
|
| 85 |
}
|
| 86 |
-
.hero h1 {
|
| 87 |
font-size: var(--fs-4xl);
|
| 88 |
line-height: var(--lh-4xl);
|
| 89 |
font-weight: var(--fw-black);
|
| 90 |
letter-spacing: -0.025em;
|
| 91 |
-
margin:
|
| 92 |
-
color: var(--
|
| 93 |
}
|
| 94 |
-
.hero .
|
| 95 |
font-size: var(--fs-md);
|
| 96 |
-
|
| 97 |
-
color: var(--hf-gray-800);
|
| 98 |
max-width: 620px;
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
.hero-stats { display: flex; gap: var(--space-8); margin-top: var(--space-4); }
|
| 102 |
-
.hero-stat .v { font-family: var(--font-mono); font-size: var(--fs-2xl); font-weight: var(--fw-semibold); color: var(--hf-blue-deep); letter-spacing: -0.01em; }
|
| 103 |
-
.hero-stat .l { font-size: var(--fs-xs); color: var(--hf-gray-800); text-transform: uppercase; letter-spacing: 0.08em; font-weight: var(--fw-semibold); margin-top: 2px; }
|
| 104 |
-
.hero-mascot { width: 200px; justify-self: end; position: relative; z-index: 2; }
|
| 105 |
-
.hero-mascot img { width: 100%; display: block; filter: drop-shadow(0 8px 24px rgba(53,62,96,0.18)); }
|
| 106 |
-
.hero-tag {
|
| 107 |
-
display: inline-flex; align-items: center; gap: 6px;
|
| 108 |
-
background: var(--hf-white); border: 1px solid rgba(53,62,96,0.12);
|
| 109 |
-
padding: 4px 10px; border-radius: var(--radius-pill);
|
| 110 |
-
font-size: var(--fs-xs); font-weight: var(--fw-semibold);
|
| 111 |
-
color: var(--hf-blue-deep); margin-bottom: var(--space-3);
|
| 112 |
-
}
|
| 113 |
-
.hero-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--hf-green); }
|
| 114 |
-
|
| 115 |
-
/* --- Tabs --- */
|
| 116 |
-
.tabs-bar {
|
| 117 |
-
display: flex;
|
| 118 |
-
gap: 2px;
|
| 119 |
-
border-bottom: 1px solid var(--border-1);
|
| 120 |
-
margin-bottom: var(--space-6);
|
| 121 |
-
overflow-x: auto;
|
| 122 |
}
|
| 123 |
-
.
|
| 124 |
-
|
| 125 |
-
font-size: var(--fs-
|
| 126 |
-
font-weight: var(--fw-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
white-space: nowrap;
|
| 130 |
-
display: inline-flex; align-items: center; gap: 6px;
|
| 131 |
}
|
| 132 |
-
.
|
| 133 |
-
.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
-
.card-flush { padding: 0; overflow: hidden; }
|
| 149 |
-
.card h2 { font-size: var(--fs-lg); font-weight: var(--fw-semibold); margin: 0 0 var(--space-4); letter-spacing: -0.01em; }
|
| 150 |
-
.card h3 { font-size: var(--fs-md); font-weight: var(--fw-semibold); margin: 0 0 var(--space-3); }
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
/* ---
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
display: grid;
|
| 159 |
-
grid-template-columns: repeat(
|
| 160 |
-
grid-template-rows: auto 1fr;
|
| 161 |
gap: var(--space-3);
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
border: 1px solid var(--border-1);
|
| 165 |
border-radius: var(--radius-lg);
|
| 166 |
-
|
| 167 |
-
|
| 168 |
}
|
| 169 |
-
.
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
padding: 3px 8px;
|
| 186 |
border-radius: var(--radius-sm);
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
color: var(--fg-2);
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
user-select: none;
|
| 193 |
}
|
| 194 |
-
.filter-chip:hover { background: var(--bg-3); }
|
| 195 |
-
.filter-chip.active { background: var(--hf-blue-deep); color: var(--hf-white); border-color: var(--hf-blue-deep); }
|
| 196 |
-
.filter-chip.active:hover { background: var(--hf-gray-800); }
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
.
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
-
/* ---
|
| 215 |
-
.
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
font-size: var(--fs-sm);
|
| 224 |
font-weight: var(--fw-medium);
|
| 225 |
-
|
| 226 |
cursor: pointer;
|
| 227 |
-
transition:
|
| 228 |
-
|
| 229 |
-
.columns-dd-trigger:hover { border-color: var(--fg-3); }
|
| 230 |
-
.columns-dd-trigger.open { border-color: var(--hf-blue-deep); box-shadow: 0 0 0 3px color-mix(in oklab, var(--hf-blue-deep) 18%, transparent); }
|
| 231 |
-
.columns-dd-trigger .dd-count {
|
| 232 |
-
font-family: var(--font-mono);
|
| 233 |
-
font-size: 11px;
|
| 234 |
-
padding: 2px 6px;
|
| 235 |
-
background: var(--bg-2);
|
| 236 |
-
border-radius: 10px;
|
| 237 |
-
color: var(--fg-2);
|
| 238 |
-
margin-left: 2px;
|
| 239 |
}
|
| 240 |
-
.
|
| 241 |
-
|
| 242 |
-
top: calc(100% + 6px);
|
| 243 |
-
right: 0;
|
| 244 |
-
z-index: 50;
|
| 245 |
-
width: 520px;
|
| 246 |
-
max-height: 520px;
|
| 247 |
-
background: var(--hf-white);
|
| 248 |
-
border: 1px solid var(--border-1);
|
| 249 |
-
border-radius: var(--radius-lg);
|
| 250 |
-
box-shadow: 0 12px 32px rgba(17,19,24,.14), 0 2px 8px rgba(17,19,24,.06);
|
| 251 |
-
display: flex;
|
| 252 |
-
flex-direction: column;
|
| 253 |
-
overflow: hidden;
|
| 254 |
-
animation: ddIn .14s var(--ease-out);
|
| 255 |
}
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
font-size: var(--fs-xs);
|
| 262 |
font-weight: var(--fw-semibold);
|
| 263 |
-
text-transform: uppercase;
|
| 264 |
letter-spacing: 0.06em;
|
| 265 |
-
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
}
|
| 280 |
-
.
|
| 281 |
-
.
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
-
.
|
| 290 |
-
|
| 291 |
-
flex-direction: column;
|
| 292 |
-
padding: 4px 0 8px;
|
| 293 |
}
|
| 294 |
-
.
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
background: transparent;
|
| 298 |
-
border: none;
|
| 299 |
-
cursor: pointer;
|
| 300 |
-
border-radius: var(--radius-sm);
|
| 301 |
-
width: 100%;
|
| 302 |
-
text-align: left;
|
| 303 |
}
|
| 304 |
-
.
|
| 305 |
-
.
|
| 306 |
-
.
|
| 307 |
-
|
| 308 |
-
font-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
font-weight: var(--fw-semibold);
|
|
|
|
|
|
|
| 310 |
text-transform: uppercase;
|
| 311 |
-
letter-spacing: 0.06em;
|
| 312 |
-
color: var(--fg-1);
|
| 313 |
}
|
| 314 |
-
.
|
| 315 |
-
|
| 316 |
-
font-family: var(--font-mono);
|
| 317 |
-
font-size: 10px;
|
| 318 |
-
color: var(--fg-3);
|
| 319 |
-
padding: 1px 6px;
|
| 320 |
-
background: var(--bg-2);
|
| 321 |
-
border-radius: 8px;
|
| 322 |
}
|
| 323 |
-
.
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
padding: 2px 0 0 10px;
|
| 327 |
}
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
border-radius: var(--radius-sm);
|
| 334 |
-
|
| 335 |
-
font-
|
| 336 |
-
|
| 337 |
-
cursor: pointer;
|
| 338 |
-
width: 100%;
|
| 339 |
-
text-align: left;
|
| 340 |
-
transition: background var(--t-quick) var(--ease-out), color var(--t-quick) var(--ease-out);
|
| 341 |
}
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
.
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
}
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
background-position: center;
|
| 361 |
-
}
|
| 362 |
-
.dd-cbx.some {
|
| 363 |
-
background: var(--hf-blue-deep);
|
| 364 |
-
border-color: var(--hf-blue-deep);
|
| 365 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 6L9 6' stroke='white' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
| 366 |
-
background-size: 12px 12px;
|
| 367 |
-
background-repeat: no-repeat;
|
| 368 |
-
background-position: center;
|
| 369 |
}
|
| 370 |
|
| 371 |
-
/*
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
grid-template-columns: repeat(4, 1fr) auto;
|
| 393 |
-
gap: var(--space-3);
|
| 394 |
-
padding: var(--space-4);
|
| 395 |
-
background: var(--hf-white);
|
| 396 |
border: 1px solid var(--border-1);
|
| 397 |
border-radius: var(--radius-lg);
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
}
|
| 401 |
-
.
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
}
|
| 407 |
-
.
|
| 408 |
-
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
font-size: var(--fs-xs);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
font-weight: var(--fw-medium);
|
| 412 |
-
padding: 3px 0;
|
| 413 |
-
background: transparent;
|
| 414 |
-
border: none;
|
| 415 |
-
color: var(--fg-1);
|
| 416 |
cursor: pointer;
|
|
|
|
| 417 |
user-select: none;
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
width: 100%;
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
}
|
| 423 |
-
.columns-bar .col-chip::before {
|
| 424 |
-
content: "";
|
| 425 |
-
width: 12px; height: 12px; border-radius: 3px;
|
| 426 |
-
border: 1.5px solid var(--border-2);
|
| 427 |
-
background: var(--hf-white);
|
| 428 |
-
flex-shrink: 0;
|
| 429 |
-
transition: all var(--t-quick) var(--ease-out);
|
| 430 |
-
}
|
| 431 |
-
.columns-bar .col-chip.active { color: var(--fg-1); }
|
| 432 |
-
.columns-bar .col-chip.active::before {
|
| 433 |
-
background: var(--hf-blue-deep);
|
| 434 |
-
border-color: var(--hf-blue-deep);
|
| 435 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M2 5L4 7L8 3' stroke='white' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
| 436 |
-
background-size: 10px 10px;
|
| 437 |
-
background-repeat: no-repeat;
|
| 438 |
-
background-position: center;
|
| 439 |
-
}
|
| 440 |
-
.columns-bar .col-chip:not(.active) { color: var(--fg-3); }
|
| 441 |
-
.columns-bar .col-chip:hover { color: var(--hf-blue); }
|
| 442 |
-
.columns-bar .col-chip:hover::before { border-color: var(--hf-blue); }
|
| 443 |
-
body.dark .columns-bar .col-chip { color: #F5F6F8; }
|
| 444 |
-
body.dark .columns-bar .col-chip:not(.active) { color: #6A768F; }
|
| 445 |
-
body.dark .columns-bar .col-chip::before { background: #0F1115; border-color: #475064; }
|
| 446 |
-
body.dark .columns-bar .col-chip.active::before { background: var(--hf-yellow); border-color: var(--hf-yellow); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M2 5L4 7L8 3' stroke='%231B1B1D' stroke-width='1.8' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
| 447 |
-
body.dark .columns-bar .col-chip:hover { color: var(--hf-yellow); }
|
| 448 |
-
body.dark .columns-bar .col-chip:hover::before { border-color: var(--hf-yellow); }
|
| 449 |
|
| 450 |
-
/*
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
|
|
|
|
|
|
| 455 |
margin-bottom: var(--space-4);
|
| 456 |
-
overflow: hidden;
|
| 457 |
}
|
| 458 |
-
.
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
}
|
| 481 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
border: 1px solid var(--border-1);
|
| 487 |
border-radius: var(--radius-lg);
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
}
|
| 490 |
-
.
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
font-size: var(--fs-xs);
|
| 497 |
-
font-
|
|
|
|
| 498 |
}
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
table.bench tbody tr:hover td { background: rgba(73,110,240,0.03); }
|
| 524 |
-
table.bench tbody tr.highlight td { background: rgba(255,210,30,0.15); }
|
| 525 |
-
|
| 526 |
-
table.bench tbody td.mono { font-family: var(--font-mono); font-size: 11px; color: var(--fg-2); }
|
| 527 |
-
table.bench tbody td .std { color: var(--fg-3); font-size: 10px; margin-left: 2px; }
|
| 528 |
-
table.bench tbody td.metric { font-family: var(--font-mono); }
|
| 529 |
-
|
| 530 |
-
table.bench .rank { font-family: var(--font-mono); color: var(--fg-3); font-size: 10px; }
|
| 531 |
-
|
| 532 |
-
.codec-pill { display: inline-flex; align-items: center; padding: 2px 7px; border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 11px; font-weight: var(--fw-semibold); }
|
| 533 |
-
.codec-h264 { background: var(--tag-datasets-bg); color: var(--tag-datasets-fg); }
|
| 534 |
-
.codec-hevc { background: var(--tag-spaces-bg); color: var(--tag-spaces-fg); }
|
| 535 |
-
.codec-libsvtav1 { background: var(--tag-papers-bg); color: var(--tag-papers-fg); }
|
| 536 |
-
|
| 537 |
-
/* Tooltip for info icons */
|
| 538 |
-
.tt {
|
| 539 |
-
position: absolute; z-index: 50; pointer-events: none;
|
| 540 |
-
background: var(--hf-gray-900); color: var(--hf-white);
|
| 541 |
-
font-size: var(--fs-xs); line-height: 1.4; padding: 8px 10px;
|
| 542 |
-
border-radius: var(--radius-md); max-width: 280px;
|
| 543 |
-
box-shadow: var(--shadow-md); opacity: 0; transition: opacity 120ms;
|
| 544 |
-
font-weight: var(--fw-regular); letter-spacing: 0;
|
| 545 |
-
text-transform: none;
|
| 546 |
-
}
|
| 547 |
-
.tt.show { opacity: 1; }
|
| 548 |
-
|
| 549 |
-
/* --- Leaderboards --- */
|
| 550 |
-
.lb-controls { display: flex; gap: var(--space-4); align-items: flex-end; margin-bottom: var(--space-4); padding: var(--space-4); background: var(--hf-white); border: 1px solid var(--border-1); border-radius: var(--radius-lg); }
|
| 551 |
-
.lb-field { display: flex; flex-direction: column; gap: 6px; }
|
| 552 |
-
.lb-field label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-3); font-weight: var(--fw-semibold); }
|
| 553 |
-
.select {
|
| 554 |
-
padding: 6px 28px 6px 10px; border: 1px solid var(--border-1);
|
| 555 |
-
border-radius: var(--radius-md); background: var(--hf-white);
|
| 556 |
-
font-family: var(--font-mono); font-size: var(--fs-xs);
|
| 557 |
-
color: var(--fg-1); appearance: none; min-width: 140px;
|
| 558 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%236A768F' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
| 559 |
-
background-repeat: no-repeat;
|
| 560 |
-
background-position: right 8px center;
|
| 561 |
-
}
|
| 562 |
-
.slider-wrap { display: flex; align-items: center; gap: 8px; }
|
| 563 |
-
.slider-wrap input[type="range"] { accent-color: var(--hf-blue); width: 160px; }
|
| 564 |
-
.slider-wrap .v { font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: var(--fw-semibold); width: 24px; text-align: center; }
|
| 565 |
-
|
| 566 |
-
.lb-subtabs { display: flex; gap: 4px; margin-bottom: var(--space-4); }
|
| 567 |
-
.lb-subtab {
|
| 568 |
-
padding: 6px 14px; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
|
| 569 |
-
border-radius: var(--radius-pill); background: var(--bg-3);
|
| 570 |
-
color: var(--fg-2); cursor: pointer;
|
| 571 |
-
transition: all var(--t-quick);
|
| 572 |
-
}
|
| 573 |
-
.lb-subtab:hover { background: var(--border-2); }
|
| 574 |
-
.lb-subtab.active { background: var(--hf-blue-deep); color: var(--hf-white); }
|
| 575 |
-
|
| 576 |
-
.lb-grid { display: grid; grid-template-columns: 1.15fr 1fr; gap: var(--space-4); margin-bottom: var(--space-4); }
|
| 577 |
-
.radar-card { padding: var(--space-5); display: flex; align-items: center; justify-content: center; min-height: 420px; height: 420px; }
|
| 578 |
-
.radar-card svg { width: 420px; max-width: 100%; height: 420px; }
|
| 579 |
-
.lb-list-card { display: flex; flex-direction: column; max-height: 420px; overflow: hidden; }
|
| 580 |
-
.lb-legend-scroll { overflow-y: auto; padding-right: 4px; flex: 1; scrollbar-width: thin; }
|
| 581 |
-
.lb-legend-scroll::-webkit-scrollbar { width: 6px; }
|
| 582 |
-
.lb-legend-scroll::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
|
| 583 |
-
body.dark .lb-legend-scroll::-webkit-scrollbar-thumb { background: #2C3240; }
|
| 584 |
-
|
| 585 |
-
.legend-list { display: flex; flex-direction: column; gap: var(--space-2); }
|
| 586 |
-
.legend-item {
|
| 587 |
-
display: flex; align-items: center; gap: var(--space-3);
|
| 588 |
-
padding: var(--space-3); background: var(--bg-2); border-radius: var(--radius-md);
|
| 589 |
border: 1px solid var(--border-1);
|
| 590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
}
|
| 592 |
-
.
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
font-
|
| 597 |
-
flex-shrink: 0;
|
| 598 |
}
|
| 599 |
-
.legend-item .name { font-family: var(--font-mono); font-size: 11px; color: var(--fg-1); line-height: 1.4; }
|
| 600 |
-
.legend-item .score { margin-left: auto; font-family: var(--font-mono); font-weight: var(--fw-semibold); font-size: var(--fs-sm); color: var(--fg-1); }
|
| 601 |
|
| 602 |
-
/* -
|
| 603 |
-
|
| 604 |
-
.
|
| 605 |
-
.
|
| 606 |
-
|
| 607 |
-
.chart-svg { width: 100%; height: 360px; }
|
| 608 |
-
|
| 609 |
-
/* --- Submit form --- */
|
| 610 |
-
.submit-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: var(--space-4); align-items: start; }
|
| 611 |
-
.param-section { margin-bottom: var(--space-5); padding-bottom: var(--space-5); border-bottom: 1px solid var(--border-1); }
|
| 612 |
-
.param-section:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
|
| 613 |
-
.param-section h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); margin: 0 0 var(--space-1); display: flex; align-items: center; gap: 6px; }
|
| 614 |
-
.param-section .hint { font-size: var(--fs-xs); color: var(--fg-3); margin-bottom: var(--space-3); }
|
| 615 |
-
.checkbox-group { display: flex; flex-wrap: wrap; gap: 6px; }
|
| 616 |
-
.checkbox-chip {
|
| 617 |
-
display: inline-flex; align-items: center; gap: 6px;
|
| 618 |
-
padding: 6px 10px; border: 1px solid var(--border-1);
|
| 619 |
-
border-radius: var(--radius-md); background: var(--hf-white);
|
| 620 |
-
font-family: var(--font-mono); font-size: 11px; color: var(--fg-2);
|
| 621 |
-
cursor: pointer; user-select: none;
|
| 622 |
-
transition: all var(--t-quick);
|
| 623 |
-
}
|
| 624 |
-
.checkbox-chip:hover { background: var(--bg-2); }
|
| 625 |
-
.checkbox-chip.checked { background: var(--hf-blue); color: var(--hf-white); border-color: var(--hf-blue); }
|
| 626 |
-
|
| 627 |
-
.textarea {
|
| 628 |
-
width: 100%; padding: 8px 10px; border: 1px solid var(--border-1);
|
| 629 |
-
border-radius: var(--radius-md); font-family: var(--font-mono);
|
| 630 |
-
font-size: 11px; resize: vertical; min-height: 36px; background: var(--hf-white);
|
| 631 |
}
|
| 632 |
-
.
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
}
|
| 640 |
-
.summary-card .count-box .n { font-family: var(--font-mono); font-size: var(--fs-xl); font-weight: var(--fw-bold); color: var(--hf-blue-deep); letter-spacing: -0.01em; line-height: 1; }
|
| 641 |
-
.summary-card .count-box .l { font-size: 11px; color: var(--hf-blue-deep); font-weight: var(--fw-semibold); line-height: 1.35; text-transform: uppercase; letter-spacing: 0.04em; margin-top: 4px; }
|
| 642 |
-
.summary-card .count-box img { width: 44px; margin-left: auto; flex-shrink: 0; }
|
| 643 |
-
|
| 644 |
-
.summary-list { display: flex; flex-direction: column; gap: 6px; font-size: var(--fs-xs); }
|
| 645 |
-
.summary-list .row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed var(--border-1); color: var(--fg-2); }
|
| 646 |
-
.summary-list .row:last-child { border-bottom: none; }
|
| 647 |
-
.summary-list .row b { color: var(--fg-1); font-family: var(--font-mono); }
|
| 648 |
|
| 649 |
-
|
| 650 |
-
.
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
.
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
.queue-status.queued { background: var(--bg-3); color: var(--fg-2); }
|
| 659 |
-
.queue-status.queued .dot { background: var(--fg-3); }
|
| 660 |
-
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
| 661 |
|
| 662 |
-
/*
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
.
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
padding-bottom: 6px;
|
| 670 |
border-bottom: 2px solid var(--hf-yellow);
|
| 671 |
display: inline-block;
|
|
|
|
| 672 |
}
|
| 673 |
-
.
|
| 674 |
-
.
|
| 675 |
-
|
| 676 |
-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 677 |
-
gap: var(--space-3);
|
| 678 |
}
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
}
|
| 686 |
-
.
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
}
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
}
|
| 697 |
-
|
| 698 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
}
|
| 700 |
-
.param-card p { margin: 0; color: var(--fg-2); font-size: var(--fs-sm); line-height: 1.55; }
|
| 701 |
|
| 702 |
-
/* ---
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
.
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
| 716 |
|
| 717 |
-
/*
|
| 718 |
-
.
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
|
| 722 |
-
padding: var(--space-4); z-index: 1000;
|
| 723 |
-
display: none;
|
| 724 |
-
}
|
| 725 |
-
.tweaks-panel.open { display: block; }
|
| 726 |
-
.tweaks-panel h5 { margin: 0 0 var(--space-3); font-size: var(--fs-sm); font-weight: var(--fw-semibold); display: flex; align-items: center; gap: 6px; }
|
| 727 |
-
.tweak-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: var(--fs-xs); }
|
| 728 |
-
.tweak-row label { color: var(--fg-2); }
|
| 729 |
-
.tweak-row select { padding: 4px 8px; border: 1px solid var(--border-1); border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 11px; background: var(--hf-white); }
|
| 730 |
|
| 731 |
-
/*
|
| 732 |
-
.
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
color: var(--hf-white); border-radius: var(--radius-sm); border: 1px solid rgba(255,255,255,0.15);
|
| 742 |
-
}
|
| 743 |
-
.citation .copy-btn:hover { background: rgba(255,255,255,0.2); }
|
| 744 |
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
-
/*
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
body.dark .btn.primary { background: var(--hf-yellow); color: #1B1B1D; border-color: var(--hf-yellow); }
|
| 770 |
-
body.dark table.bench thead th { background: #161A20; color: #C7CDD8; border-color: #2E3442; }
|
| 771 |
-
body.dark table.bench tbody td { border-color: #262B36; color: #E5E7EB; }
|
| 772 |
-
body.dark table.bench tbody td.mono { color: #B5BCC8; }
|
| 773 |
-
body.dark table.bench tbody tr:hover td { background: rgba(73,110,240,0.08); }
|
| 774 |
-
body.dark .table-top { background: #1A1F27; border-color: #2E3442; }
|
| 775 |
-
body.dark .tab-btn { color: #9BA3AF; }
|
| 776 |
-
body.dark .tab-btn:hover { color: #F5F6F8; }
|
| 777 |
-
body.dark .tab-btn.active { color: var(--hf-yellow); }
|
| 778 |
-
body.dark .tab-count { background: #262B36; color: #C7CDD8; }
|
| 779 |
-
body.dark .tab-btn.active .tab-count { background: var(--hf-yellow); color: #1B1B1D; }
|
| 780 |
-
body.dark .select,
|
| 781 |
-
body.dark .textarea,
|
| 782 |
-
body.dark .checkbox-chip,
|
| 783 |
-
body.dark .filter-chip { background: #1E232C; border-color: #3A4152; color: #E5E7EB; }
|
| 784 |
-
body.dark .filter-chip:hover { background: #262B36; border-color: #475064; }
|
| 785 |
-
body.dark .filter-chip.active { background: var(--hf-yellow); color: #1B1B1D; border-color: var(--hf-yellow); }
|
| 786 |
-
body.dark .filter-chip.active:hover { background: var(--hf-yellow-deep); }
|
| 787 |
-
body.dark .checkbox-chip.checked { background: var(--hf-yellow); color: #1B1B1D; border-color: var(--hf-yellow); }
|
| 788 |
-
body.dark .filter-field label,
|
| 789 |
-
body.dark .filter-field label span,
|
| 790 |
-
body.dark .lb-field label,
|
| 791 |
-
body.dark .param-section h4 { color: #C7CDD8; }
|
| 792 |
-
body.dark .columns-bar .filter-field label span { color: #F5F6F8; }
|
| 793 |
-
body.dark .columns-bar .filter-field label span[style*="line-through"] { color: #6A768F !important; }
|
| 794 |
-
body.dark .codec-h264 { background: rgba(73,110,240,0.25); color: #A9BDFF; }
|
| 795 |
-
body.dark .codec-hevc { background: rgba(151,71,255,0.25); color: #CFB0FF; }
|
| 796 |
-
body.dark .codec-libsvtav1 { background: rgba(0,155,58,0.22); color: #7CD896; }
|
| 797 |
-
body.dark .nav-brand, body.dark .space-title { color: #F5F6F8; }
|
| 798 |
-
body.dark .nav-links a { color: #C7CDD8; }
|
| 799 |
-
body.dark .nav-links a:hover { background: #262B36; color: #F5F6F8; }
|
| 800 |
-
body.dark .nav-btn { background: #1E232C; color: #E5E7EB; border-color: #2E3442; }
|
| 801 |
-
body.dark .nav-btn.primary { background: var(--hf-yellow); color: #1B1B1D; border-color: var(--hf-yellow); }
|
| 802 |
-
body.dark .legend-item { background: #1E232C; border-color: #2E3442; }
|
| 803 |
-
body.dark .legend-item .name,
|
| 804 |
-
body.dark .legend-item .score { color: #E5E7EB; }
|
| 805 |
-
body.dark .queue-table th { color: #C7CDD8; border-color: #2E3442; }
|
| 806 |
-
body.dark .queue-table td { color: #E5E7EB; border-color: #262B36; }
|
| 807 |
-
body.dark .summary-list .row { color: #C7CDD8; border-color: #2E3442; }
|
| 808 |
-
body.dark .summary-list .row b { color: #F5F6F8; }
|
| 809 |
-
body.dark .summary-card .count-box { background: #2A2410; border-color: var(--hf-yellow-deep); }
|
| 810 |
-
body.dark .summary-card .count-box .n,
|
| 811 |
-
body.dark .summary-card .count-box .l { color: var(--hf-yellow); }
|
| 812 |
-
body.dark .param-card-head .param-label { color: #F5F6F8; }
|
| 813 |
-
body.dark .param-card-head .param-key { background: #2A2410; color: var(--hf-yellow); }
|
| 814 |
-
body.dark .param-card p { color: #C7CDD8; }
|
| 815 |
-
body.dark .param-group-head p { color: #C7CDD8; }
|
| 816 |
-
body.dark .prose { color: #E5E7EB; }
|
| 817 |
-
body.dark .prose code { background: #262B36; color: #E5E7EB; }
|
| 818 |
-
body.dark .about-toc { background: #2A2410; }
|
| 819 |
-
body.dark .about-toc .t,
|
| 820 |
-
body.dark .about-toc a { color: var(--hf-yellow); }
|
| 821 |
-
body.dark .chart-svg text { fill: #C7CDD8 !important; }
|
| 822 |
-
body.dark .chart-svg line[stroke="var(--border-1)"] { stroke: #2E3442 !important; }
|
| 823 |
-
body.dark .space-breadcrumb { color: #C7CDD8; }
|
| 824 |
-
body.dark .space-breadcrumb a { color: #A9BDFF; }
|
| 825 |
-
body.dark .space-breadcrumb a[style*="fg-1"] { color: #F5F6F8 !important; }
|
| 826 |
-
body.dark .space-meta { color: #9BA3AF; }
|
| 827 |
-
body.dark .space-meta .chip { background: #1E232C; border-color: #2E3442; color: #C7CDD8; }
|
| 828 |
-
body.dark .like-btn { background: #1E232C; border-color: #2E3442; color: #E5E7EB; }
|
| 829 |
-
body.dark .nav-search { background: #1E232C; border-color: #2E3442; color: #C7CDD8; }
|
| 830 |
-
body.dark .citation { background: #0B0D11; }
|
| 831 |
-
body.dark .bg-2 { background: #0F1115; }
|
|
|
|
| 1 |
+
/* Video Benchmark — custom styling layered on top of the Gradio theme.
|
| 2 |
+
*
|
| 3 |
+
* Strategy: Gradio's theme handles buttons, inputs, tabs, dataframes,
|
| 4 |
+
* plots. This file owns only the non-Gradio pieces — the HF-branded hero,
|
| 5 |
+
* About-page prose, parameter cards — plus a few overrides so Gradio
|
| 6 |
+
* widgets inherit HF spacing / typography.
|
| 7 |
+
*
|
| 8 |
+
* All HF brand tokens live at the top as CSS variables so anything below
|
| 9 |
+
* can reference them without pulling an extra stylesheet. */
|
| 10 |
|
| 11 |
+
:root {
|
| 12 |
+
/* Brand core */
|
| 13 |
+
--hf-yellow: #FFD21E;
|
| 14 |
+
--hf-yellow-deep: #FFBD59;
|
| 15 |
+
--hf-orange: #FF9D00;
|
| 16 |
+
--hf-blue: #496EF0;
|
| 17 |
+
--hf-blue-deep: #353E60;
|
| 18 |
+
--hf-red: #DB3328;
|
| 19 |
+
--hf-green: #009B3A;
|
| 20 |
+
--hf-purple: #9747FF;
|
| 21 |
+
/* Neutrals */
|
| 22 |
+
--hf-gray-900: #1B1B1D;
|
| 23 |
+
--hf-gray-800: #32343D;
|
| 24 |
+
--hf-gray-700: #475169;
|
| 25 |
+
--hf-gray-600: #6A768F;
|
| 26 |
+
--hf-gray-500: #9BA3AF;
|
| 27 |
+
--hf-gray-300: #E5E7EB;
|
| 28 |
+
--hf-gray-200: #EEEFF2;
|
| 29 |
+
--hf-gray-100: #F5F6F8;
|
| 30 |
+
--hf-white: #FFFFFF;
|
| 31 |
+
/* Warm backgrounds */
|
| 32 |
+
--hf-cream: #FFF8DE;
|
| 33 |
+
/* Semantic */
|
| 34 |
+
--fg-1: var(--hf-gray-900);
|
| 35 |
+
--fg-2: var(--hf-gray-700);
|
| 36 |
+
--fg-3: var(--hf-gray-600);
|
| 37 |
+
--bg-1: var(--hf-white);
|
| 38 |
+
--bg-2: var(--hf-gray-100);
|
| 39 |
+
--bg-3: var(--hf-gray-200);
|
| 40 |
+
--border-1: var(--hf-gray-200);
|
| 41 |
+
--border-2: var(--hf-gray-300);
|
| 42 |
+
/* Spacing + radii */
|
| 43 |
+
--space-2: 8px;
|
| 44 |
+
--space-3: 12px;
|
| 45 |
+
--space-4: 16px;
|
| 46 |
+
--space-5: 20px;
|
| 47 |
+
--space-6: 24px;
|
| 48 |
+
--space-8: 32px;
|
| 49 |
+
--space-10: 40px;
|
| 50 |
+
--radius-sm: 6px;
|
| 51 |
+
--radius-md: 8px;
|
| 52 |
+
--radius-lg: 12px;
|
| 53 |
+
--radius-2xl: 24px;
|
| 54 |
+
--radius-pill: 999px;
|
| 55 |
+
/* Type scale */
|
| 56 |
+
--fs-xs: 12px;
|
| 57 |
+
--fs-sm: 14px;
|
| 58 |
+
--fs-md: 18px;
|
| 59 |
+
--fs-lg: 20px;
|
| 60 |
+
--fs-xl: 24px;
|
| 61 |
+
--fs-2xl: 30px;
|
| 62 |
+
--fs-4xl: 48px;
|
| 63 |
+
--lh-4xl: 56px;
|
| 64 |
+
--fw-medium: 500;
|
| 65 |
+
--fw-semibold: 600;
|
| 66 |
+
--fw-bold: 700;
|
| 67 |
+
--fw-black: 900;
|
| 68 |
}
|
| 69 |
|
| 70 |
+
/* Dark-mode semantic tokens. Flipping the `--fg-*` / `--bg-*` / `--border-*`
|
| 71 |
+
* scale in one place keeps typography consistent across every tab — any rule
|
| 72 |
+
* that reads these tokens (prose, param cards, chips, tables, leaderboard
|
| 73 |
+
* cards, tab strip) automatically picks up the dark palette without needing
|
| 74 |
+
* its own `.dark` override. Covers both Gradio's `.dark` root class and the
|
| 75 |
+
* OS-level `prefers-color-scheme` fallback. */
|
| 76 |
+
.dark,
|
| 77 |
+
body.dark {
|
| 78 |
+
--fg-1: var(--hf-gray-100);
|
| 79 |
+
--fg-2: var(--hf-gray-300);
|
| 80 |
+
--fg-3: var(--hf-gray-500);
|
| 81 |
+
--bg-1: #1E2233;
|
| 82 |
+
--bg-2: rgba(255, 255, 255, 0.04);
|
| 83 |
+
--bg-3: rgba(255, 255, 255, 0.07);
|
| 84 |
+
--border-1: rgba(255, 255, 255, 0.08);
|
| 85 |
+
--border-2: rgba(255, 255, 255, 0.18);
|
| 86 |
+
}
|
| 87 |
+
@media (prefers-color-scheme: dark) {
|
| 88 |
+
:root:not(.light) {
|
| 89 |
+
--fg-1: var(--hf-gray-100);
|
| 90 |
+
--fg-2: var(--hf-gray-300);
|
| 91 |
+
--fg-3: var(--hf-gray-500);
|
| 92 |
+
--bg-1: #1E2233;
|
| 93 |
+
--bg-2: rgba(255, 255, 255, 0.04);
|
| 94 |
+
--bg-3: rgba(255, 255, 255, 0.07);
|
| 95 |
+
--border-1: rgba(255, 255, 255, 0.08);
|
| 96 |
+
--border-2: rgba(255, 255, 255, 0.18);
|
| 97 |
+
}
|
|
|
|
| 98 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
/* --- Hero ---
|
| 101 |
+
* The hero is now a plain `gr.Row` of two `gr.Column`s (copy + mascot). Since
|
| 102 |
+
* Gradio owns layout and widths, CSS here only paints — no escape-hatch
|
| 103 |
+
* positioning, no viewport-unit math.
|
| 104 |
+
*
|
| 105 |
+
* Colors are driven by local CSS variables so dark mode can flip the whole
|
| 106 |
+
* hero in one place (see the `.dark .hero` block at the bottom of this
|
| 107 |
+
* section). Gradio applies `.dark` to the root when the dark theme is on,
|
| 108 |
+
* and the `prefers-color-scheme` fallback covers cases where Gradio's theme
|
| 109 |
+
* isn't overriding the OS preference. */
|
| 110 |
.hero {
|
| 111 |
+
--hero-bg: var(--hf-cream);
|
| 112 |
+
--hero-title: var(--hf-blue-deep);
|
| 113 |
+
--hero-text: var(--hf-gray-800);
|
| 114 |
+
--hero-tag-bg: var(--hf-white);
|
| 115 |
+
--hero-tag-fg: var(--hf-blue-deep);
|
| 116 |
+
--hero-tag-border: rgba(53, 62, 96, 0.12);
|
| 117 |
+
--hero-stat-v: var(--hf-blue-deep);
|
| 118 |
+
background: var(--hero-bg);
|
| 119 |
border-radius: var(--radius-2xl);
|
| 120 |
+
padding: var(--space-8) var(--space-10);
|
| 121 |
+
margin-bottom: var(--space-6);
|
|
|
|
|
|
|
| 122 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
+
.hero .hero-copy h1 {
|
| 125 |
font-size: var(--fs-4xl);
|
| 126 |
line-height: var(--lh-4xl);
|
| 127 |
font-weight: var(--fw-black);
|
| 128 |
letter-spacing: -0.025em;
|
| 129 |
+
margin: var(--space-2) 0 var(--space-3);
|
| 130 |
+
color: var(--hero-title);
|
| 131 |
}
|
| 132 |
+
.hero .hero-copy p {
|
| 133 |
font-size: var(--fs-md);
|
| 134 |
+
color: var(--hero-text);
|
|
|
|
| 135 |
max-width: 620px;
|
| 136 |
+
line-height: 1.5;
|
| 137 |
+
margin: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
+
.hero .hero-copy strong:first-child {
|
| 140 |
+
display: inline-block;
|
| 141 |
+
font-size: var(--fs-xs);
|
| 142 |
+
font-weight: var(--fw-semibold);
|
| 143 |
+
letter-spacing: 0.08em;
|
| 144 |
+
text-transform: uppercase;
|
| 145 |
+
color: var(--hero-tag-fg);
|
| 146 |
+
background: var(--hero-tag-bg);
|
| 147 |
+
border: 1px solid var(--hero-tag-border);
|
| 148 |
+
border-radius: var(--radius-pill);
|
| 149 |
+
padding: 4px 10px;
|
| 150 |
+
}
|
| 151 |
+
/* Dark-mode palette: keep the HF feel (yellow accent pill on deep navy)
|
| 152 |
+
* while staying legible on a dark theme. Cover both Gradio's `.dark` root
|
| 153 |
+
* class and the OS-level `prefers-color-scheme` media query. */
|
| 154 |
+
.dark .hero,
|
| 155 |
+
body.dark .hero {
|
| 156 |
+
--hero-bg: #1E2233;
|
| 157 |
+
--hero-title: #FFFFFF;
|
| 158 |
+
--hero-text: #C7CCD6;
|
| 159 |
+
--hero-tag-bg: rgba(255, 209, 30, 0.16);
|
| 160 |
+
--hero-tag-fg: var(--hf-yellow);
|
| 161 |
+
--hero-tag-border: rgba(255, 209, 30, 0.3);
|
| 162 |
+
--hero-stat-v: var(--hf-yellow);
|
| 163 |
+
}
|
| 164 |
+
@media (prefers-color-scheme: dark) {
|
| 165 |
+
.hero:not(.light *) {
|
| 166 |
+
--hero-bg: #1E2233;
|
| 167 |
+
--hero-title: #FFFFFF;
|
| 168 |
+
--hero-text: #C7CCD6;
|
| 169 |
+
--hero-tag-bg: rgba(255, 209, 30, 0.16);
|
| 170 |
+
--hero-tag-fg: var(--hf-yellow);
|
| 171 |
+
--hero-tag-border: rgba(255, 209, 30, 0.3);
|
| 172 |
+
--hero-stat-v: var(--hf-yellow);
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
/* Stats are rendered as a 4-column markdown table — header row = numbers
|
| 176 |
+
* (big, blue), body row = labels (small caps). The negative top margin pulls
|
| 177 |
+
* the table up tight against the subtitle above it (Gradio adds its own
|
| 178 |
+
* sibling-block gap which we don't want here). */
|
| 179 |
+
.hero .hero-stats {
|
| 180 |
+
margin-top: var(--space-2) !important;
|
| 181 |
+
padding-top: 0 !important;
|
| 182 |
+
}
|
| 183 |
+
.hero .hero-stats table,
|
| 184 |
+
.hero .hero-stats thead,
|
| 185 |
+
.hero .hero-stats tbody,
|
| 186 |
+
.hero .hero-stats tr,
|
| 187 |
+
.hero .hero-stats th,
|
| 188 |
+
.hero .hero-stats td {
|
| 189 |
+
border: 0 !important;
|
| 190 |
+
background: transparent !important;
|
| 191 |
+
box-shadow: none !important;
|
| 192 |
+
}
|
| 193 |
+
.hero .hero-stats table {
|
| 194 |
+
border-collapse: collapse;
|
| 195 |
+
width: auto;
|
| 196 |
+
}
|
| 197 |
+
.hero .hero-stats th,
|
| 198 |
+
.hero .hero-stats td {
|
| 199 |
+
padding: 0 var(--space-6) 0 0;
|
| 200 |
+
text-align: left;
|
| 201 |
+
vertical-align: top;
|
| 202 |
white-space: nowrap;
|
|
|
|
| 203 |
}
|
| 204 |
+
.hero .hero-stats th:last-child,
|
| 205 |
+
.hero .hero-stats td:last-child {
|
| 206 |
+
padding-right: 0;
|
| 207 |
+
}
|
| 208 |
+
.hero .hero-stats th {
|
| 209 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 210 |
+
font-size: var(--fs-2xl);
|
| 211 |
+
font-weight: var(--fw-semibold);
|
| 212 |
+
color: var(--hero-stat-v);
|
| 213 |
+
letter-spacing: -0.01em;
|
| 214 |
+
padding-bottom: 2px;
|
| 215 |
+
}
|
| 216 |
+
.hero .hero-stats td {
|
| 217 |
+
font-size: var(--fs-xs);
|
| 218 |
+
color: var(--hero-text);
|
| 219 |
+
text-transform: uppercase;
|
| 220 |
+
letter-spacing: 0.08em;
|
| 221 |
+
font-weight: var(--fw-semibold);
|
| 222 |
+
}
|
| 223 |
+
.hero .hero-mascot,
|
| 224 |
+
.hero .hero-mascot img {
|
| 225 |
+
max-width: 220px;
|
| 226 |
+
margin-left: auto;
|
| 227 |
+
}
|
| 228 |
+
.hero .hero-mascot img {
|
| 229 |
+
filter: drop-shadow(0 8px 24px rgba(53, 62, 96, 0.18));
|
| 230 |
}
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
+
/* Gradio's `<gradio-html>` custom element defaults to `display: inline`,
|
| 233 |
+
* which makes any flex/grid child collapse to its intrinsic width. Force
|
| 234 |
+
* the About-tab wrappers (prose + parameters grid) to behave as blocks
|
| 235 |
+
* so they each use the full tab width instead of shrinking to content. */
|
| 236 |
+
gradio-html:has(.params-page),
|
| 237 |
+
gradio-html:has(.prose) {
|
| 238 |
+
display: block;
|
| 239 |
+
width: 100%;
|
| 240 |
+
}
|
| 241 |
|
| 242 |
+
/* --- Parameters page ---
|
| 243 |
+
*
|
| 244 |
+
* Rendered at the bottom of the About tab after ``schema.ABOUT_HTML``,
|
| 245 |
+
* so the top margin separates the parameter reference from the prose
|
| 246 |
+
* and draws a visible yellow rule — the same ``.tab-intro`` treatment
|
| 247 |
+
* used elsewhere — to introduce the new section. */
|
| 248 |
+
.params-page {
|
| 249 |
+
display: flex;
|
| 250 |
+
flex-direction: column;
|
| 251 |
+
gap: var(--space-6);
|
| 252 |
+
/* Gradio renders `gr.HTML` inside a `<gradio-html>` custom element
|
| 253 |
+
* that defaults to `display: inline`, so the flex/grid children
|
| 254 |
+
* collapse to their intrinsic width and the cards wrap long before
|
| 255 |
+
* the window edge. Force the tree to span the full tab width. */
|
| 256 |
+
width: 100%;
|
| 257 |
+
margin-top: var(--space-6);
|
| 258 |
+
}
|
| 259 |
+
.param-group {
|
| 260 |
+
width: 100%;
|
| 261 |
+
}
|
| 262 |
+
.param-group-head {
|
| 263 |
+
margin-bottom: var(--space-4);
|
| 264 |
+
}
|
| 265 |
+
.param-group-head h3 {
|
| 266 |
+
font-size: var(--fs-md);
|
| 267 |
+
font-weight: var(--fw-semibold);
|
| 268 |
+
margin: 0 0 4px;
|
| 269 |
+
padding-bottom: 6px;
|
| 270 |
+
border-bottom: 2px solid var(--hf-yellow);
|
| 271 |
+
display: inline-block;
|
| 272 |
+
}
|
| 273 |
+
.param-group-head p {
|
| 274 |
+
margin: 8px 0 0;
|
| 275 |
+
color: var(--fg-2);
|
| 276 |
+
font-size: var(--fs-sm);
|
| 277 |
+
max-width: 760px;
|
| 278 |
+
}
|
| 279 |
+
.param-cards {
|
| 280 |
+
/* Fixed 4-column grid: parameters read as a comparison table at a
|
| 281 |
+
* glance, so equal track widths are more useful than ``auto-fit``
|
| 282 |
+
* here. ``minmax(0, 1fr)`` is what stops long descriptions from
|
| 283 |
+
* blowing past their column (grid items default to ``min-width:
|
| 284 |
+
* auto``, which uses the *content* min as the lower bound). The
|
| 285 |
+
* narrower-viewport fallbacks below collapse the grid gracefully
|
| 286 |
+
* before the cards become unreadable. */
|
| 287 |
display: grid;
|
| 288 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
| 289 |
gap: var(--space-3);
|
| 290 |
+
/* Explicit full width so the grid fills the Gradio tab content area
|
| 291 |
+
* instead of shrinking to its intrinsic width inside the inline
|
| 292 |
+
* `<gradio-html>` wrapper, which would otherwise make the cards wrap
|
| 293 |
+
* mid-window. */
|
| 294 |
+
width: 100%;
|
| 295 |
+
}
|
| 296 |
+
@media (max-width: 1100px) {
|
| 297 |
+
.param-cards {
|
| 298 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
@media (max-width: 820px) {
|
| 302 |
+
.param-cards {
|
| 303 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
@media (max-width: 540px) {
|
| 307 |
+
.param-cards {
|
| 308 |
+
grid-template-columns: 1fr;
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
.param-card {
|
| 312 |
+
--param-card-bg: var(--hf-white);
|
| 313 |
+
--param-key-bg: var(--hf-cream);
|
| 314 |
+
--param-key-fg: var(--hf-blue-deep);
|
| 315 |
+
min-width: 0;
|
| 316 |
+
background: var(--param-card-bg);
|
| 317 |
border: 1px solid var(--border-1);
|
| 318 |
border-radius: var(--radius-lg);
|
| 319 |
+
padding: var(--space-4);
|
| 320 |
+
transition: border-color 120ms cubic-bezier(0.16, 1, 0.3, 1);
|
| 321 |
}
|
| 322 |
+
.param-card:hover {
|
| 323 |
+
border-color: var(--hf-yellow-deep);
|
| 324 |
+
}
|
| 325 |
+
.param-card-head {
|
| 326 |
+
display: flex;
|
| 327 |
+
align-items: baseline;
|
| 328 |
+
gap: 10px;
|
| 329 |
+
flex-wrap: wrap;
|
| 330 |
+
margin-bottom: 8px;
|
| 331 |
+
}
|
| 332 |
+
.param-card-head .param-key {
|
| 333 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 334 |
+
font-size: var(--fs-xs);
|
| 335 |
+
background: var(--param-key-bg);
|
| 336 |
+
color: var(--param-key-fg);
|
| 337 |
+
padding: 2px 8px;
|
|
|
|
| 338 |
border-radius: var(--radius-sm);
|
| 339 |
+
font-weight: var(--fw-semibold);
|
| 340 |
+
}
|
| 341 |
+
/* Dark-mode palette for param cards — drop the HF cream/blue chip
|
| 342 |
+
* (which is a light-mode-only brand accent) and borrow the generic
|
| 343 |
+
* `--bg-2` / `--bg-3` / `--fg-1` tokens so the pill matches `.prose
|
| 344 |
+
* code` on the About tab exactly. Cover both Gradio's `.dark` root
|
| 345 |
+
* class and the OS-level `prefers-color-scheme` media query. */
|
| 346 |
+
.dark .param-card,
|
| 347 |
+
body.dark .param-card {
|
| 348 |
+
--param-card-bg: var(--bg-1);
|
| 349 |
+
--param-key-bg: var(--bg-3);
|
| 350 |
+
--param-key-fg: var(--fg-1);
|
| 351 |
+
}
|
| 352 |
+
@media (prefers-color-scheme: dark) {
|
| 353 |
+
.param-card:not(.light *) {
|
| 354 |
+
--param-card-bg: var(--bg-1);
|
| 355 |
+
--param-key-bg: var(--bg-3);
|
| 356 |
+
--param-key-fg: var(--fg-1);
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
.param-card-head .param-label {
|
| 360 |
+
font-size: var(--fs-sm);
|
| 361 |
+
font-weight: var(--fw-semibold);
|
| 362 |
+
color: var(--fg-1);
|
| 363 |
+
}
|
| 364 |
+
.param-card p {
|
| 365 |
+
margin: 0;
|
| 366 |
color: var(--fg-2);
|
| 367 |
+
font-size: var(--fs-sm);
|
| 368 |
+
line-height: 1.55;
|
|
|
|
| 369 |
}
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
+
/* --- About ---
|
| 372 |
+
* No ``max-width`` on the prose wrapper: the About tab intentionally
|
| 373 |
+
* spans the full tab width so it lines up with the Parameters card grid
|
| 374 |
+
* below it. Per-element line length is kept readable by the smaller
|
| 375 |
+
* font-size + generous line-height instead. */
|
| 376 |
+
.prose {
|
| 377 |
+
font-size: 15px;
|
| 378 |
+
line-height: 1.6;
|
| 379 |
+
color: var(--fg-1);
|
| 380 |
+
width: 100%;
|
| 381 |
+
}
|
| 382 |
+
.prose h3 {
|
| 383 |
+
font-size: var(--fs-lg);
|
| 384 |
+
font-weight: var(--fw-semibold);
|
| 385 |
+
margin: var(--space-6) 0 var(--space-2);
|
| 386 |
+
}
|
| 387 |
+
.prose h3:first-child {
|
| 388 |
+
margin-top: 0;
|
| 389 |
+
}
|
| 390 |
+
.prose p {
|
| 391 |
+
margin: 0 0 var(--space-3);
|
| 392 |
+
}
|
| 393 |
+
.prose ul {
|
| 394 |
+
margin: 0 0 var(--space-3);
|
| 395 |
+
padding-left: 20px;
|
| 396 |
+
}
|
| 397 |
+
.prose li {
|
| 398 |
+
margin-bottom: 4px;
|
| 399 |
+
}
|
| 400 |
+
.prose code {
|
| 401 |
+
/* Both `--bg-3` and `--fg-1` flip at the root in dark mode, so the
|
| 402 |
+
* inline-code chip picks up the right palette automatically — no
|
| 403 |
+
* per-element dark override needed. */
|
| 404 |
+
background: var(--bg-3);
|
| 405 |
+
color: var(--fg-1);
|
| 406 |
+
padding: 1px 6px;
|
| 407 |
+
border-radius: 4px;
|
| 408 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 409 |
+
font-size: 0.88em;
|
| 410 |
+
}
|
| 411 |
+
/* --- Results-tab accordions (Filters / Visible columns) ---
|
| 412 |
+
* Both panels are `gr.Accordion(elem_classes="results-accordion")` so
|
| 413 |
+
* the header chrome is identical — a small uppercase title with Gradio's
|
| 414 |
+
* chevron on the right. Filters starts open, Visible columns starts
|
| 415 |
+
* closed, but the card border / typography / padding all match. */
|
| 416 |
+
.results-accordion {
|
| 417 |
+
margin-bottom: var(--space-3);
|
| 418 |
+
}
|
| 419 |
+
.results-accordion > .label-wrap {
|
| 420 |
+
padding: 4px var(--space-2) !important;
|
| 421 |
+
gap: var(--space-2);
|
| 422 |
+
}
|
| 423 |
+
.results-accordion > .label-wrap > span,
|
| 424 |
+
.results-accordion > .label-wrap span.label {
|
| 425 |
+
padding-left: 0 !important;
|
| 426 |
+
margin-left: 0 !important;
|
| 427 |
+
font-size: var(--fs-sm);
|
| 428 |
+
font-weight: var(--fw-semibold);
|
| 429 |
+
letter-spacing: 0.06em;
|
| 430 |
+
text-transform: uppercase;
|
| 431 |
+
color: var(--fg-3);
|
| 432 |
+
}
|
| 433 |
+
.results-accordion[open] > .label-wrap {
|
| 434 |
+
border-bottom: 1px solid var(--border-1);
|
| 435 |
+
}
|
| 436 |
+
/* Inset the accordion body so its contents aren't flush against the card
|
| 437 |
+
* border. `.label-wrap ~ *` targets every sibling after the header,
|
| 438 |
+
* which in Gradio's accordion is the single content div. */
|
| 439 |
+
.gradio-container .results-accordion > .label-wrap ~ * {
|
| 440 |
+
padding: 2px var(--space-2) 4px !important;
|
| 441 |
+
box-sizing: border-box;
|
| 442 |
+
}
|
| 443 |
+
.gradio-container .results-accordion:not(.filters-accordion) > .label-wrap ~ * {
|
| 444 |
+
padding-bottom: 0 !important;
|
| 445 |
+
}
|
| 446 |
+
.gradio-container .results-accordion:not(.filters-accordion) > .label-wrap ~ * *:last-child {
|
| 447 |
+
margin-bottom: 0 !important;
|
| 448 |
+
padding-bottom: 0 !important;
|
| 449 |
+
}
|
| 450 |
+
/* Kill any bottom margin on the last content element in either accordion
|
| 451 |
+
* body, so the visible gap between last row and the card bottom border
|
| 452 |
+
* is identical in Filters and Visible columns (driven purely by the
|
| 453 |
+
* body's own 12px bottom padding above). */
|
| 454 |
+
.gradio-container .results-accordion > .label-wrap ~ * > *:last-child,
|
| 455 |
+
.gradio-container .results-accordion > .label-wrap ~ * > * > *:last-child {
|
| 456 |
+
margin-bottom: 0 !important;
|
| 457 |
+
}
|
| 458 |
+
/* Strip every inner wrapper background + border inside both results
|
| 459 |
+
* accordions, so filter chips and column checkboxes sit flat on the
|
| 460 |
+
* accordion's own card — no nested "inner window" compartments.
|
| 461 |
+
* Per-category color still applies because it's driven by checkbox /
|
| 462 |
+
* label styling, not by wrapper backgrounds. */
|
| 463 |
+
.gradio-container .results-accordion .form,
|
| 464 |
+
.gradio-container .results-accordion .block,
|
| 465 |
+
.gradio-container .results-accordion .wrap,
|
| 466 |
+
.gradio-container .results-accordion .gr-group,
|
| 467 |
+
.gradio-container .results-accordion .gr-form {
|
| 468 |
+
background: transparent !important;
|
| 469 |
+
border: 0 !important;
|
| 470 |
+
box-shadow: none !important;
|
| 471 |
+
}
|
| 472 |
|
| 473 |
+
/* --- Filter chips (toggle-button CheckboxGroup) ---
|
| 474 |
+
* Each value in a `.filter-chips` CheckboxGroup renders as a pill button.
|
| 475 |
+
* The underlying native checkbox is hidden; the whole label is the click
|
| 476 |
+
* target. Checked state is detected with `:has(input:checked)` so Gradio's
|
| 477 |
+
* own state management still works — we just restyle the presentation.
|
| 478 |
+
*
|
| 479 |
+
* All filters share the HF deep-navy selected color; we strip the
|
| 480 |
+
* CheckboxGroup's own background + border so the chips sit directly on
|
| 481 |
+
* the accordion's card instead of inside a nested "inner window". */
|
| 482 |
+
.filter-chips {
|
| 483 |
+
--chip-color: var(--hf-yellow);
|
| 484 |
+
--chip-color-text: var(--hf-gray-900);
|
| 485 |
+
background: transparent !important;
|
| 486 |
+
border: 0 !important;
|
| 487 |
+
box-shadow: none !important;
|
| 488 |
+
padding: 0 !important;
|
| 489 |
+
min-width: 0 !important;
|
| 490 |
+
overflow: visible;
|
| 491 |
+
}
|
| 492 |
+
/* Filters row: flex-wrap row so the five filter groups sit on one line
|
| 493 |
+
* when there's room, and gracefully wrap to a second line when there
|
| 494 |
+
* isn't. Each group asks for 20% of the row but is allowed to shrink
|
| 495 |
+
* (min-width: 0) so its chips wrap inside it, and is allowed to grow
|
| 496 |
+
* into the full width on narrow viewports. */
|
| 497 |
+
.filters-accordion,
|
| 498 |
+
.filters-accordion * {
|
| 499 |
+
box-sizing: border-box;
|
| 500 |
+
}
|
| 501 |
+
.filters-row {
|
| 502 |
+
display: flex !important;
|
| 503 |
+
flex-direction: row !important;
|
| 504 |
+
flex-wrap: wrap !important;
|
| 505 |
+
align-items: flex-start !important;
|
| 506 |
+
gap: var(--space-5) 0 !important;
|
| 507 |
+
width: 100% !important;
|
| 508 |
+
}
|
| 509 |
+
/* All 5 groups share the row equally (flex: 1 1 0). Inter-group
|
| 510 |
+
* spacing is added inside each group via a transparent spacer on the
|
| 511 |
+
* label+option wrapper, because Gradio reliably overrides outer `gap`
|
| 512 |
+
* / `margin` / `padding` on our CheckboxGroup root. */
|
| 513 |
+
.filter-chips {
|
| 514 |
+
box-sizing: border-box !important;
|
| 515 |
+
}
|
| 516 |
+
.filters-accordion .filter-chips {
|
| 517 |
+
flex: 1 1 0 !important;
|
| 518 |
+
min-width: 0 !important;
|
| 519 |
+
max-width: 100% !important;
|
| 520 |
+
}
|
| 521 |
+
.filters-accordion .filter-chips ~ .filter-chips > * {
|
| 522 |
+
padding-left: 32px !important;
|
| 523 |
+
}
|
| 524 |
+
/* Target whichever container Gradio wraps its CheckboxGroup options in
|
| 525 |
+
* (class name varies between versions) by matching any direct child of
|
| 526 |
+
* `.filter-chips` that itself contains at least one checkbox label. */
|
| 527 |
+
.filter-chips > *:has(> label input[type="checkbox"]) {
|
| 528 |
+
display: flex !important;
|
| 529 |
+
flex-direction: row !important;
|
| 530 |
+
flex-wrap: wrap !important;
|
| 531 |
+
gap: 6px !important;
|
| 532 |
+
align-items: flex-start;
|
| 533 |
+
width: 100% !important;
|
| 534 |
+
min-width: 0 !important;
|
| 535 |
+
max-width: 100% !important;
|
| 536 |
+
}
|
| 537 |
+
/* Labels must be allowed to shrink below their content width so the
|
| 538 |
+
* container can wrap them aggressively instead of letting a long pill
|
| 539 |
+
* push the row wider than its column. */
|
| 540 |
+
.filter-chips label {
|
| 541 |
+
flex: 0 0 auto;
|
| 542 |
+
max-width: 100%;
|
| 543 |
+
}
|
| 544 |
+
.filter-chips label {
|
| 545 |
+
display: inline-flex;
|
| 546 |
+
align-items: center;
|
| 547 |
+
justify-content: center;
|
| 548 |
+
text-align: center;
|
| 549 |
+
gap: 0 !important;
|
| 550 |
+
min-width: 56px;
|
| 551 |
+
padding: 6px 12px;
|
| 552 |
+
border: 1px solid var(--border-2);
|
| 553 |
+
border-radius: var(--radius-pill);
|
| 554 |
+
background: var(--bg-1);
|
| 555 |
+
color: var(--fg-2);
|
| 556 |
font-size: var(--fs-sm);
|
| 557 |
font-weight: var(--fw-medium);
|
| 558 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 559 |
cursor: pointer;
|
| 560 |
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
| 561 |
+
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
}
|
| 563 |
+
.filter-chips label > * {
|
| 564 |
+
margin: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
}
|
| 566 |
+
.filter-chips label:hover {
|
| 567 |
+
border-color: var(--fg-3);
|
| 568 |
+
color: var(--fg-1);
|
| 569 |
+
}
|
| 570 |
+
.filter-chips label input[type="checkbox"] {
|
| 571 |
+
position: absolute !important;
|
| 572 |
+
opacity: 0 !important;
|
| 573 |
+
width: 0 !important;
|
| 574 |
+
height: 0 !important;
|
| 575 |
+
margin: 0 !important;
|
| 576 |
+
pointer-events: none !important;
|
| 577 |
+
}
|
| 578 |
+
.filter-chips label:has(input:checked),
|
| 579 |
+
.filter-chips label.selected,
|
| 580 |
+
.filter-chips label[aria-checked="true"] {
|
| 581 |
+
background: var(--chip-color) !important;
|
| 582 |
+
border-color: var(--chip-color) !important;
|
| 583 |
+
color: var(--chip-color-text) !important;
|
| 584 |
+
}
|
| 585 |
+
.filter-chips label:has(input:checked):hover,
|
| 586 |
+
.filter-chips label.selected:hover,
|
| 587 |
+
.filter-chips label[aria-checked="true"]:hover {
|
| 588 |
+
filter: brightness(0.94);
|
| 589 |
+
}
|
| 590 |
+
/* Click feedback — flash the pill yellow on press for tactile feedback,
|
| 591 |
+
* independent of its checked state. */
|
| 592 |
+
.filter-chips label:active {
|
| 593 |
+
background: var(--chip-color) !important;
|
| 594 |
+
border-color: var(--chip-color) !important;
|
| 595 |
+
color: var(--chip-color-text) !important;
|
| 596 |
+
}
|
| 597 |
+
.filter-chips > .block-label,
|
| 598 |
+
.filter-chips > label > span:first-child {
|
| 599 |
font-size: var(--fs-xs);
|
| 600 |
font-weight: var(--fw-semibold);
|
|
|
|
| 601 |
letter-spacing: 0.06em;
|
| 602 |
+
text-transform: uppercase;
|
| 603 |
+
color: var(--fg-3);
|
| 604 |
+
margin-bottom: 2px;
|
| 605 |
}
|
| 606 |
+
|
| 607 |
+
/* Category-tinted checkboxes — each `CheckboxGroup` inside the Visible-
|
| 608 |
+
* columns accordion gets a brand color applied through Gradio's own
|
| 609 |
+
* checkbox CSS variables. Overriding the variables (not the `input`
|
| 610 |
+
* itself) means the hover / focus / disabled states still track
|
| 611 |
+
* Gradio's theme; we just recolor the selected chip.
|
| 612 |
+
*
|
| 613 |
+
* Colors are pulled from the leaderboard ``PODIUM_COLORS`` palette in
|
| 614 |
+
* ``src/schema.py`` — chromatic neighbours of the brand hues, pulled
|
| 615 |
+
* 20–30% toward gray. Using the same desaturated set across the column
|
| 616 |
+
* picker and the leaderboard radar / cards keeps the page reading as
|
| 617 |
+
* one visual system instead of two competing palettes (the saturated
|
| 618 |
+
* HF brand colors fight the muted heatmap chips in the table below).
|
| 619 |
+
*
|
| 620 |
+
* Config · dusty blue (#6B8EBF)
|
| 621 |
+
* Compression · warm sand (#D99B5A)
|
| 622 |
+
* Speed · sage (#7FA97E)
|
| 623 |
+
* Quality · muted lavender (#A98FBF)
|
| 624 |
+
*/
|
| 625 |
+
.col-cat {
|
| 626 |
+
--cat-color: #6B8EBF;
|
| 627 |
}
|
| 628 |
+
.col-cat-config { --cat-color: #6B8EBF; }
|
| 629 |
+
.col-cat-compression { --cat-color: #D99B5A; }
|
| 630 |
+
.col-cat-speed { --cat-color: #7FA97E; }
|
| 631 |
+
.col-cat-quality { --cat-color: #A98FBF; }
|
| 632 |
+
|
| 633 |
+
.col-cat {
|
| 634 |
+
--checkbox-background-color-selected: var(--cat-color);
|
| 635 |
+
--checkbox-border-color-selected: var(--cat-color);
|
| 636 |
+
--checkbox-background-color-focus: var(--cat-color);
|
| 637 |
+
--checkbox-border-color-focus: var(--cat-color);
|
| 638 |
}
|
| 639 |
+
.col-cat input[type="checkbox"] {
|
| 640 |
+
accent-color: var(--cat-color);
|
|
|
|
|
|
|
| 641 |
}
|
| 642 |
+
.col-cat input[type="checkbox"]:checked {
|
| 643 |
+
background-color: var(--cat-color) !important;
|
| 644 |
+
border-color: var(--cat-color) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
}
|
| 646 |
+
.col-cat > .block-label,
|
| 647 |
+
.col-cat > label > span:first-child,
|
| 648 |
+
.col-cat span[data-testid="block-info"] {
|
| 649 |
+
color: var(--cat-color) !important;
|
| 650 |
+
font-weight: var(--fw-semibold);
|
| 651 |
+
}
|
| 652 |
+
/* Strip the default pill / border Gradio renders around each
|
| 653 |
+
* CheckboxGroup option so the category picker reads as a flat list of
|
| 654 |
+
* checkbox + label rows, not buttons. Forcing zero left padding on
|
| 655 |
+
* every wrapper ensures the first sub-checkbox of each row aligns
|
| 656 |
+
* vertically with the master checkbox above it. */
|
| 657 |
+
.col-cat,
|
| 658 |
+
.col-cat > *,
|
| 659 |
+
.col-cat .wrap {
|
| 660 |
+
padding-left: 0 !important;
|
| 661 |
+
margin-left: 0 !important;
|
| 662 |
+
}
|
| 663 |
+
.col-cat .wrap {
|
| 664 |
+
gap: 6px 28px !important;
|
| 665 |
+
padding: 0 !important;
|
| 666 |
+
}
|
| 667 |
+
.col-cat label {
|
| 668 |
+
padding: 0 !important;
|
| 669 |
+
margin-left: 0 !important;
|
| 670 |
+
background: transparent !important;
|
| 671 |
+
border: 0 !important;
|
| 672 |
+
box-shadow: none !important;
|
| 673 |
+
gap: 6px;
|
| 674 |
+
min-height: 0;
|
| 675 |
+
}
|
| 676 |
+
.col-cat-master,
|
| 677 |
+
.col-cat-master > *,
|
| 678 |
+
.col-cat-master label {
|
| 679 |
+
padding-left: 0 !important;
|
| 680 |
+
margin-left: 0 !important;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
/* Master "toggle all" checkbox sitting above each category group. Uses
|
| 684 |
+
* the same per-category color variable, but renders bolder/bigger so it
|
| 685 |
+
* reads as the category header rather than another option. It's aligned
|
| 686 |
+
* flush-left with the option checkboxes below by zeroing its padding. */
|
| 687 |
+
.col-cat-master {
|
| 688 |
+
padding: 0 !important;
|
| 689 |
+
margin: 0 0 6px !important;
|
| 690 |
+
background: transparent !important;
|
| 691 |
+
border: 0 !important;
|
| 692 |
+
}
|
| 693 |
+
.col-cat-master label {
|
| 694 |
+
padding: 0 !important;
|
| 695 |
+
gap: 6px !important;
|
| 696 |
+
background: transparent !important;
|
| 697 |
+
border: 0 !important;
|
| 698 |
+
display: inline-flex !important;
|
| 699 |
+
align-items: center !important;
|
| 700 |
+
}
|
| 701 |
+
.col-cat-master,
|
| 702 |
+
.col-cat-master > *,
|
| 703 |
+
.col-cat-master label,
|
| 704 |
+
.col-cat-master label > span,
|
| 705 |
+
.col-cat,
|
| 706 |
+
.col-cat > *,
|
| 707 |
+
.col-cat .wrap,
|
| 708 |
+
.col-cat label,
|
| 709 |
+
.col-cat label > span {
|
| 710 |
+
overflow: visible !important;
|
| 711 |
+
}
|
| 712 |
+
.col-cat-master label,
|
| 713 |
+
.col-cat label {
|
| 714 |
+
padding: 4px 0 !important;
|
| 715 |
+
line-height: 1.6 !important;
|
| 716 |
+
min-height: 28px !important;
|
| 717 |
+
align-items: center !important;
|
| 718 |
+
}
|
| 719 |
+
.col-cat-master input[type="checkbox"],
|
| 720 |
+
.col-cat input[type="checkbox"] {
|
| 721 |
+
width: 18px !important;
|
| 722 |
+
height: 18px !important;
|
| 723 |
+
min-width: 18px !important;
|
| 724 |
+
min-height: 18px !important;
|
| 725 |
+
flex: 0 0 18px !important;
|
| 726 |
+
margin: 0 !important;
|
| 727 |
+
padding: 0 !important;
|
| 728 |
+
border-radius: 4px !important;
|
| 729 |
+
box-sizing: border-box !important;
|
| 730 |
+
overflow: visible !important;
|
| 731 |
+
display: inline-block !important;
|
| 732 |
+
vertical-align: middle !important;
|
| 733 |
+
}
|
| 734 |
+
.col-cat-master label > span:last-child,
|
| 735 |
+
.col-cat-master span[data-testid="block-info"] {
|
| 736 |
+
font-size: var(--fs-sm);
|
| 737 |
font-weight: var(--fw-semibold);
|
| 738 |
+
letter-spacing: 0.02em;
|
| 739 |
+
color: var(--cat-color) !important;
|
| 740 |
text-transform: uppercase;
|
|
|
|
|
|
|
| 741 |
}
|
| 742 |
+
.col-cat-master input[type="checkbox"] {
|
| 743 |
+
accent-color: var(--cat-color);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
}
|
| 745 |
+
.col-cat-master input[type="checkbox"]:checked {
|
| 746 |
+
background-color: var(--cat-color) !important;
|
| 747 |
+
border-color: var(--cat-color) !important;
|
|
|
|
| 748 |
}
|
| 749 |
+
|
| 750 |
+
/* --- Results table ---
|
| 751 |
+
* Each metric value is wrapped in `<span class="heatmap-chip">` by the
|
| 752 |
+
* Python renderer, with the rdylgn color applied inline as
|
| 753 |
+
* `background`. The span is inline-block so the chip sizes to the value,
|
| 754 |
+
* leaving the rest of the cell neutral — the row stays readable and the
|
| 755 |
+
* color reads as a badge behind the number, not a column fill.
|
| 756 |
+
*
|
| 757 |
+
* We kill the default Gradio row / column separators and lighten the
|
| 758 |
+
* header so the colored chips carry the visual weight. */
|
| 759 |
+
/* Gradio 6 renders the Dataframe as a virtualized table — column widths
|
| 760 |
+
* are measured in a hidden sizing body and then applied by JS, so we
|
| 761 |
+
* deliberately do NOT override the table's own `width` / `table-layout`
|
| 762 |
+
* here. Doing so collapses every column but the first. With the
|
| 763 |
+
* Dataframe configured as `wrap=False`, columns already expand to their
|
| 764 |
+
* content and the `.virtual-table-viewport` scrolls horizontally.
|
| 765 |
+
*
|
| 766 |
+
* All the rules below are cosmetic only (colors, borders, paddings) —
|
| 767 |
+
* they don't touch the table layout. */
|
| 768 |
+
.gradio-container .table-wrap {
|
| 769 |
+
--table-head-bg: var(--bg-2);
|
| 770 |
+
--table-head-fg: var(--fg-1);
|
| 771 |
+
--table-head-border: var(--border-1);
|
| 772 |
+
}
|
| 773 |
+
.gradio-container .table-wrap thead th,
|
| 774 |
+
.gradio-container .table-wrap tbody td,
|
| 775 |
+
.gradio-container .table-wrap tbody tr {
|
| 776 |
+
border: 0 !important;
|
| 777 |
+
box-shadow: none !important;
|
| 778 |
+
}
|
| 779 |
+
.gradio-container .table-wrap thead th {
|
| 780 |
+
border-bottom: 1px solid var(--table-head-border) !important;
|
| 781 |
+
background: var(--table-head-bg) !important;
|
| 782 |
+
color: var(--table-head-fg) !important;
|
| 783 |
+
}
|
| 784 |
+
.gradio-container .table-wrap tbody td {
|
| 785 |
+
padding: 3px 6px !important;
|
| 786 |
+
background: transparent !important;
|
| 787 |
+
}
|
| 788 |
+
/* Results-tab header: wrap long column names onto two lines instead of
|
| 789 |
+
* forcing a single line, which would either truncate (with ellipsis) or
|
| 790 |
+
* blow up the column width on labels like "Decoding (ms)" and
|
| 791 |
+
* "Composite rank". ``min-height: 2lh`` reserves two lines on every
|
| 792 |
+
* header cell so single-word columns stay baseline-aligned with the
|
| 793 |
+
* wrapped ones. ``max-width`` is what actually triggers the wrap; set
|
| 794 |
+
* it tight enough that two-word headers break naturally. */
|
| 795 |
+
/* Header wraps onto two lines maximum.
|
| 796 |
+
*
|
| 797 |
+
* Headers use the schema's ``short`` form (see ``_render_results_table``
|
| 798 |
+
* in app.py): "Decoding", "Video/Image size", "PSNR" — concise enough
|
| 799 |
+
* that two equal-share columns can hold them on one or two lines.
|
| 800 |
+
* ``column_widths`` on the Dataframe flips the table to ``table-
|
| 801 |
+
* layout: fixed`` so each column is a fixed percentage of the wrap
|
| 802 |
+
* width rather than content-sized.
|
| 803 |
+
*
|
| 804 |
+
* The hard two-line cap is enforced two ways for belt-and-braces:
|
| 805 |
+
* 1. ``max-height`` + ``overflow: hidden`` on the ``<th>`` — bullet-
|
| 806 |
+
* proof against any DOM Gradio renders inside the cell.
|
| 807 |
+
* 2. ``-webkit-line-clamp: 2`` on the inner wrapper — adds the
|
| 808 |
+
* ellipsis when truncation actually fires.
|
| 809 |
+
*
|
| 810 |
+
* The ``th *`` mirror is needed because Gradio sets ``white-space:
|
| 811 |
+
* nowrap`` on an inner span at higher specificity; normalising only
|
| 812 |
+
* the ``th`` would leave the wrapper nowrap. */
|
| 813 |
+
.results-table .table-wrap thead th,
|
| 814 |
+
.results-table .table-wrap thead th * {
|
| 815 |
+
white-space: normal !important;
|
| 816 |
+
/* ``keep-all`` only allows the browser to break at whitespace (and
|
| 817 |
+
* CJK boundaries) — *not* at characters like ``/`` inside a token.
|
| 818 |
+
* Without it, Chrome will happily split "Video/Image" into "Video/"
|
| 819 |
+
* + "Image" because the slash is a soft break opportunity, which
|
| 820 |
+
* pushes the trailing word ("size" / "load") onto a third line and
|
| 821 |
+
* straight into the ``-webkit-line-clamp: 2`` cap below — making the
|
| 822 |
+
* trailing word vanish. ``overflow-wrap: break-word`` is kept as a
|
| 823 |
+
* fallback so a header longer than its column still breaks somewhere
|
| 824 |
+
* instead of overflowing horizontally. */
|
| 825 |
+
word-break: keep-all !important;
|
| 826 |
+
overflow-wrap: break-word !important;
|
| 827 |
+
hyphens: none;
|
| 828 |
+
}
|
| 829 |
+
/* Reserve a fixed slot for Gradio's per-header sort/menu button so it's
|
| 830 |
+
* counted in the column's width budget instead of popping in on hover
|
| 831 |
+
* and squeezing the label. The button is laid out (``display: flex``)
|
| 832 |
+
* at all times — only its paint is toggled via ``visibility``, which
|
| 833 |
+
* keeps the slot occupied while still hiding the chrome until the user
|
| 834 |
+
* actually hovers/focuses the cell. Pair this with the matching weight
|
| 835 |
+
* bump in ``_column_widths_for`` so the column track is wide enough to
|
| 836 |
+
* hold ``label + gap + icon`` without truncating the label. */
|
| 837 |
+
.results-table .table-wrap thead th .cell-menu-button {
|
| 838 |
+
display: flex !important;
|
| 839 |
+
visibility: hidden;
|
| 840 |
+
}
|
| 841 |
+
.results-table .table-wrap thead th:hover .cell-menu-button,
|
| 842 |
+
.results-table .table-wrap thead th.focus .cell-menu-button {
|
| 843 |
+
visibility: visible;
|
| 844 |
+
}
|
| 845 |
+
.results-table .table-wrap thead th {
|
| 846 |
+
/* Explicit pixel font-size + line-height instead of ``lh`` units,
|
| 847 |
+
* so the cell height isn't sensitive to whichever font-size Gradio's
|
| 848 |
+
* theme inherits onto the ``th``.
|
| 849 |
+
*
|
| 850 |
+
* Cell budget: 60px outer = 6px padding-top + 48px content (two
|
| 851 |
+
* line-heights at 24px) + 6px padding-bottom. ``overflow: hidden``
|
| 852 |
+
* on the cell + ``max-height: 60px`` is what actually enforces the
|
| 853 |
+
* two-line cap (the inner wrapper is left as a plain block — see
|
| 854 |
+
* ``> *`` below — so that ``vertical-align: middle`` on this
|
| 855 |
+
* ``table-cell`` can actually see the inner's natural height as
|
| 856 |
+
* less-than-cell and center it).
|
| 857 |
+
*
|
| 858 |
+
* Why padding lives on the cell, not the inner: ``overflow: hidden``
|
| 859 |
+
* on a box clips at its *padding-box* edge, not its content edge —
|
| 860 |
+
* so the cell's 6px of padding-bottom is a clip-safe runoff band
|
| 861 |
+
* that catches bold IBM Plex Mono descenders (``p`` in "fps" /
|
| 862 |
+
* "ratio", ``g`` in "Encoding") which extend a couple of pixels
|
| 863 |
+
* below the second line's content box. The matching 6px on top
|
| 864 |
+
* keeps two-line headers visually balanced. */
|
| 865 |
+
font-size: 14px !important;
|
| 866 |
+
line-height: 24px !important;
|
| 867 |
+
height: 60px;
|
| 868 |
+
max-height: 60px;
|
| 869 |
+
overflow: hidden;
|
| 870 |
+
vertical-align: middle;
|
| 871 |
+
padding-top: 6px !important;
|
| 872 |
+
padding-bottom: 6px !important;
|
| 873 |
+
/* Horizontal breathing room so the column labels don't kiss the
|
| 874 |
+
* vertical separators between cells. Matches the ~12px gutter the
|
| 875 |
+
* body cells get from Gradio's default ``td`` padding, so the
|
| 876 |
+
* header text aligns visually with the values below it. */
|
| 877 |
+
padding-left: 12px !important;
|
| 878 |
+
padding-right: 12px !important;
|
| 879 |
+
}
|
| 880 |
+
/* Gradio's dataframe renders each header as
|
| 881 |
+
*
|
| 882 |
+
* <th>
|
| 883 |
+
* <div class="cell-wrap"> (display:flex, height:100%, padding:8px)
|
| 884 |
+
* <div class="header-content"> (display:flex, overflow:hidden)
|
| 885 |
+
* text
|
| 886 |
+
* </div>
|
| 887 |
+
* </div>
|
| 888 |
+
* </th>
|
| 889 |
+
*
|
| 890 |
+
* Two things in that default styling actively fight vertical centering
|
| 891 |
+
* on single-line headers like "Codec" / "PSNR":
|
| 892 |
+
*
|
| 893 |
+
* 1. ``.cell-wrap`` has ``height: 100%`` — it stretches to fill the
|
| 894 |
+
* ``th``, leaving zero slack for the cell's ``vertical-align:
|
| 895 |
+
* middle`` to center anything.
|
| 896 |
+
* 2. ``.cell-wrap`` is a flex *row* (``flex-direction: row`` is the
|
| 897 |
+
* default), so a 1-line text child sits at the top of the row's
|
| 898 |
+
* cross axis baseline, not in its center.
|
| 899 |
+
*
|
| 900 |
+
* Solution: knock both Gradio wrappers down to plain ``display: block``
|
| 901 |
+
* with ``height: auto``. Now the wrapper chain is sized to its actual
|
| 902 |
+
* text content (24px for single-line headers, 48px for two-line ones),
|
| 903 |
+
* and the cell's ``vertical-align: middle`` distributes the leftover
|
| 904 |
+
* vertical slack equally above and below. */
|
| 905 |
+
.results-table .table-wrap thead th > *,
|
| 906 |
+
.results-table .table-wrap thead th > * > * {
|
| 907 |
+
display: block !important;
|
| 908 |
+
height: auto !important;
|
| 909 |
+
min-height: 0 !important;
|
| 910 |
+
max-height: none !important;
|
| 911 |
+
margin: 0 !important;
|
| 912 |
+
padding: 0 !important;
|
| 913 |
+
gap: 0 !important;
|
| 914 |
+
overflow: visible !important;
|
| 915 |
+
font-size: 14px !important;
|
| 916 |
+
line-height: 24px !important;
|
| 917 |
+
max-width: 100%;
|
| 918 |
+
}
|
| 919 |
+
/* Body cells: keep numeric values + heatmap chips on a single line so
|
| 920 |
+
* the table reads as a comparison grid, even though the dataframe runs
|
| 921 |
+
* in ``wrap=True`` mode (which is what lets the header wrap). The
|
| 922 |
+
* matching rule on the chip itself stops "42.13 ± 0.45" from breaking
|
| 923 |
+
* between the median and the std. */
|
| 924 |
+
.results-table .table-wrap tbody td,
|
| 925 |
+
.results-table .table-wrap tbody td * {
|
| 926 |
+
white-space: nowrap !important;
|
| 927 |
+
}
|
| 928 |
+
.results-table .heatmap-chip {
|
| 929 |
+
white-space: nowrap;
|
| 930 |
+
}
|
| 931 |
+
.heatmap-chip {
|
| 932 |
+
display: inline-block;
|
| 933 |
+
padding: 2px 8px;
|
| 934 |
border-radius: var(--radius-sm);
|
| 935 |
+
color: var(--hf-gray-900);
|
| 936 |
+
font-variant-numeric: tabular-nums;
|
| 937 |
+
line-height: 1.4;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
}
|
| 939 |
+
|
| 940 |
+
/* Row-count caption sits below the table as a quiet, right-aligned
|
| 941 |
+
* annotation so it reads as metadata about the table above rather than
|
| 942 |
+
* a heading. */
|
| 943 |
+
.results-count {
|
| 944 |
+
margin-top: var(--space-2) !important;
|
| 945 |
+
text-align: right;
|
| 946 |
+
color: var(--fg-3);
|
| 947 |
+
font-size: var(--fs-xs);
|
| 948 |
+
letter-spacing: 0.04em;
|
| 949 |
+
}
|
| 950 |
+
/* Dark mode: flip the header to a deep slate with light text so the
|
| 951 |
+
* column names stay legible against Gradio's dark table surface.
|
| 952 |
+
* Cover both the `.dark` root class Gradio sets on theme toggle and the
|
| 953 |
+
* OS-level `prefers-color-scheme` fallback. */
|
| 954 |
+
.dark .gradio-container .table-wrap,
|
| 955 |
+
body.dark .gradio-container .table-wrap {
|
| 956 |
+
--table-head-bg: #2A2E3E;
|
| 957 |
+
--table-head-fg: #F5F6F8;
|
| 958 |
+
--table-head-border: rgba(255, 255, 255, 0.08);
|
| 959 |
}
|
| 960 |
+
@media (prefers-color-scheme: dark) {
|
| 961 |
+
.gradio-container .table-wrap:not(.light *) {
|
| 962 |
+
--table-head-bg: #2A2E3E;
|
| 963 |
+
--table-head-fg: #F5F6F8;
|
| 964 |
+
--table-head-border: rgba(255, 255, 255, 0.08);
|
| 965 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 966 |
}
|
| 967 |
|
| 968 |
+
/* --- Leaderboards tab ---
|
| 969 |
+
* Two-column layout: a Plotly radar on the left, a stack of "podium"
|
| 970 |
+
* cards on the right. The card color (rank badge + left accent bar)
|
| 971 |
+
* maps 1:1 to its polygon in the radar via schema.PODIUM_COLORS, so
|
| 972 |
+
* users can trace any shape back to its config at a glance.
|
| 973 |
+
*
|
| 974 |
+
* Controls row uses a gr.Radio restyled into pill buttons for the
|
| 975 |
+
* Ranking selector (Overall / Quality / Encoding / Decoding); the
|
| 976 |
+
* pattern mirrors .filter-chips but in single-select semantics. */
|
| 977 |
+
/* One-card header: promote .lb-controls from a multi-column Row of card
|
| 978 |
+
* surfaces to a single flat surface, then strip the background / border /
|
| 979 |
+
* shadow from every nested wrapper Gradio inserts between the row and the
|
| 980 |
+
* actual controls. The three columns (Access pattern / Top N / Ranking)
|
| 981 |
+
* sit on one unified card with the dropdown + slider on the left and the
|
| 982 |
+
* access-pattern pill buttons aligned to the right. */
|
| 983 |
+
.lb-controls {
|
| 984 |
+
gap: var(--space-6) !important;
|
| 985 |
+
align-items: start !important;
|
| 986 |
+
margin-bottom: var(--space-4);
|
| 987 |
+
padding: var(--space-3) var(--space-4);
|
| 988 |
+
background: var(--bg-1);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 989 |
border: 1px solid var(--border-1);
|
| 990 |
border-radius: var(--radius-lg);
|
| 991 |
+
overflow: visible !important;
|
| 992 |
+
}
|
| 993 |
+
/* Gradio's slider renders its number-input + reset overlay above the
|
| 994 |
+
* trough; keep the column non-clipping so its rounded corners stay
|
| 995 |
+
* visible inside .lb-controls' padded card. */
|
| 996 |
+
.lb-controls,
|
| 997 |
+
.lb-controls > *,
|
| 998 |
+
.lb-controls .block,
|
| 999 |
+
.lb-controls .form,
|
| 1000 |
+
.lb-controls .gr-form {
|
| 1001 |
+
overflow: visible !important;
|
| 1002 |
+
}
|
| 1003 |
+
/* All three columns of the controls card align to the top of the row
|
| 1004 |
+
* (driven by ``align-items: start`` on ``.lb-controls`` above). Each
|
| 1005 |
+
* column then lays out its label + control naturally from the top,
|
| 1006 |
+
* so the three column labels — "Access pattern", "Top N configs",
|
| 1007 |
+
* "Ranking" (visually replaced by the pills since
|
| 1008 |
+
* ``show_label=False``) — sit on a shared baseline. */
|
| 1009 |
+
.lb-controls .lb-topn-col {
|
| 1010 |
+
display: flex !important;
|
| 1011 |
+
flex-direction: column !important;
|
| 1012 |
+
align-items: center !important;
|
| 1013 |
+
justify-content: flex-start !important;
|
| 1014 |
}
|
| 1015 |
+
.lb-controls .lb-topn-col .lb-topn {
|
| 1016 |
+
width: 100%;
|
| 1017 |
+
max-width: 360px;
|
| 1018 |
+
}
|
| 1019 |
+
/* Right-align the ranking pills horizontally so they hug the right edge
|
| 1020 |
+
* of the controls card, and vertically center the whole column within
|
| 1021 |
+
* the row using ``align-self: center`` (which overrides the parent's
|
| 1022 |
+
* ``align-items: start``). The dropdown column is taller, so the row's
|
| 1023 |
+
* height is driven by it; the shorter pills column then sits in the
|
| 1024 |
+
* vertical middle while the dropdown / slider keep their top-aligned
|
| 1025 |
+
* labels. */
|
| 1026 |
+
.lb-controls .lb-cats-col {
|
| 1027 |
+
display: flex !important;
|
| 1028 |
+
flex-direction: column !important;
|
| 1029 |
+
align-items: flex-end !important;
|
| 1030 |
+
align-self: center !important;
|
| 1031 |
+
}
|
| 1032 |
+
.lb-controls > .gr-column,
|
| 1033 |
+
.lb-controls .gr-form,
|
| 1034 |
+
.lb-controls .form,
|
| 1035 |
+
.lb-controls .block,
|
| 1036 |
+
.lb-controls > * > .block,
|
| 1037 |
+
.lb-controls > * > .form {
|
| 1038 |
+
background: transparent !important;
|
| 1039 |
+
border: 0 !important;
|
| 1040 |
+
box-shadow: none !important;
|
| 1041 |
+
padding: 0 !important;
|
| 1042 |
+
}
|
| 1043 |
+
/* Dark-mode surface: every "window" in the app (controls bar, podium
|
| 1044 |
+
* cards, compare-controls strip, param cards) shares one background
|
| 1045 |
+
* token (``--bg-1``) so the page reads as a consistent visual system
|
| 1046 |
+
* instead of a stack of subtly-different gray slabs. ``--bg-1`` already
|
| 1047 |
+
* flips between light (white) and dark (#1E2233), so per-theme
|
| 1048 |
+
* overrides only need to soften the border. */
|
| 1049 |
+
.dark .lb-controls,
|
| 1050 |
+
body.dark .lb-controls {
|
| 1051 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 1052 |
+
}
|
| 1053 |
+
@media (prefers-color-scheme: dark) {
|
| 1054 |
+
.lb-controls:not(.light *) {
|
| 1055 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 1056 |
+
}
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
/* Ranking pills — restyled gr.Radio. Gradio's default renders each
|
| 1060 |
+
* option as a stacked radio+label row; we hide the native input and
|
| 1061 |
+
* paint the wrapping label as a pill, using :has(input:checked) to
|
| 1062 |
+
* pick up the current selection. */
|
| 1063 |
+
.ranking-pills,
|
| 1064 |
+
.ranking-pills > *,
|
| 1065 |
+
.ranking-pills .wrap {
|
| 1066 |
+
background: transparent !important;
|
| 1067 |
+
border: 0 !important;
|
| 1068 |
+
box-shadow: none !important;
|
| 1069 |
+
padding: 0 !important;
|
| 1070 |
}
|
| 1071 |
+
.ranking-pills .wrap {
|
| 1072 |
+
display: flex !important;
|
| 1073 |
+
flex-direction: row !important;
|
| 1074 |
+
flex-wrap: wrap !important;
|
| 1075 |
+
justify-content: flex-end !important;
|
| 1076 |
+
gap: 6px !important;
|
| 1077 |
+
}
|
| 1078 |
+
.ranking-pills > .block-label,
|
| 1079 |
+
.ranking-pills > label > span:first-child {
|
| 1080 |
font-size: var(--fs-xs);
|
| 1081 |
+
font-weight: var(--fw-semibold);
|
| 1082 |
+
letter-spacing: 0.06em;
|
| 1083 |
+
text-transform: uppercase;
|
| 1084 |
+
color: var(--fg-3);
|
| 1085 |
+
margin-bottom: var(--space-2);
|
| 1086 |
+
}
|
| 1087 |
+
.ranking-pills label {
|
| 1088 |
+
display: inline-flex;
|
| 1089 |
+
align-items: center;
|
| 1090 |
+
justify-content: center;
|
| 1091 |
+
gap: 0 !important;
|
| 1092 |
+
padding: 6px 14px;
|
| 1093 |
+
min-width: 72px;
|
| 1094 |
+
border: 1px solid var(--border-2);
|
| 1095 |
+
border-radius: var(--radius-pill);
|
| 1096 |
+
background: var(--bg-1);
|
| 1097 |
+
color: var(--fg-2);
|
| 1098 |
+
font-size: var(--fs-sm);
|
| 1099 |
font-weight: var(--fw-medium);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
cursor: pointer;
|
| 1101 |
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
| 1102 |
user-select: none;
|
| 1103 |
+
}
|
| 1104 |
+
.ranking-pills label > * {
|
| 1105 |
+
margin: 0 !important;
|
| 1106 |
+
}
|
| 1107 |
+
.ranking-pills label:hover {
|
| 1108 |
+
border-color: var(--fg-3);
|
| 1109 |
+
color: var(--fg-1);
|
| 1110 |
+
}
|
| 1111 |
+
.ranking-pills label input[type="radio"] {
|
| 1112 |
+
position: absolute !important;
|
| 1113 |
+
opacity: 0 !important;
|
| 1114 |
+
width: 0 !important;
|
| 1115 |
+
height: 0 !important;
|
| 1116 |
+
margin: 0 !important;
|
| 1117 |
+
pointer-events: none !important;
|
| 1118 |
+
}
|
| 1119 |
+
.ranking-pills label:has(input:checked),
|
| 1120 |
+
.ranking-pills label[aria-checked="true"] {
|
| 1121 |
+
background: var(--hf-gray-900) !important;
|
| 1122 |
+
border-color: var(--hf-gray-900) !important;
|
| 1123 |
+
color: var(--hf-white) !important;
|
| 1124 |
+
}
|
| 1125 |
+
.dark .ranking-pills label:has(input:checked),
|
| 1126 |
+
.dark .ranking-pills label[aria-checked="true"] {
|
| 1127 |
+
background: var(--hf-yellow) !important;
|
| 1128 |
+
border-color: var(--hf-yellow) !important;
|
| 1129 |
+
color: var(--hf-gray-900) !important;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
/* Main row: radar plot | cards. Kill the plot's own card chrome so the
|
| 1133 |
+
* radar floats inside the same surface as the controls above it. */
|
| 1134 |
+
.lb-main {
|
| 1135 |
+
gap: var(--space-4) !important;
|
| 1136 |
+
align-items: stretch !important;
|
| 1137 |
+
}
|
| 1138 |
+
.lb-radar .plot-container,
|
| 1139 |
+
.lb-cards-wrap {
|
| 1140 |
+
background: transparent;
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
/* Card list — sized to match the 520px radar plot on the left so both
|
| 1144 |
+
* columns share the same bottom edge. When the Top-N slider produces
|
| 1145 |
+
* more cards than fit in that height, the column scrolls internally
|
| 1146 |
+
* instead of pushing the whole tab taller. The header is `position:
|
| 1147 |
+
* sticky` so the "Top configurations" title stays visible while the
|
| 1148 |
+
* reader scrolls through the podium. */
|
| 1149 |
+
.lb-cards-wrap {
|
| 1150 |
width: 100%;
|
| 1151 |
+
max-height: 520px;
|
| 1152 |
+
overflow-y: auto;
|
| 1153 |
+
scrollbar-width: thin;
|
| 1154 |
+
scrollbar-color: var(--border-2) transparent;
|
| 1155 |
+
}
|
| 1156 |
+
.lb-cards-wrap::-webkit-scrollbar {
|
| 1157 |
+
width: 8px;
|
| 1158 |
+
}
|
| 1159 |
+
.lb-cards-wrap::-webkit-scrollbar-track {
|
| 1160 |
+
background: transparent;
|
| 1161 |
+
}
|
| 1162 |
+
.lb-cards-wrap::-webkit-scrollbar-thumb {
|
| 1163 |
+
background: var(--border-2);
|
| 1164 |
+
border-radius: var(--radius-pill);
|
| 1165 |
+
}
|
| 1166 |
+
.lb-cards-wrap::-webkit-scrollbar-thumb:hover {
|
| 1167 |
+
background: var(--fg-3);
|
| 1168 |
+
}
|
| 1169 |
+
.lb-cards {
|
| 1170 |
+
display: flex;
|
| 1171 |
+
flex-direction: column;
|
| 1172 |
+
gap: var(--space-3);
|
| 1173 |
+
padding-right: var(--space-2);
|
| 1174 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1175 |
|
| 1176 |
+
/* Per-tab section heading rendered by ``_tab_intro`` in app.py. The
|
| 1177 |
+
* <h2> is visually matched to the Parameters page's <h3>s: yellow
|
| 1178 |
+
* underline, semibold, inline-block so the bar only stretches under
|
| 1179 |
+
* the title text. The muted subtitle paragraph sits below, capped at
|
| 1180 |
+
* a comfortable reading width. */
|
| 1181 |
+
.tab-intro-wrap {
|
| 1182 |
+
margin-top: var(--space-2);
|
| 1183 |
margin-bottom: var(--space-4);
|
|
|
|
| 1184 |
}
|
| 1185 |
+
.tab-intro {
|
| 1186 |
+
display: flex;
|
| 1187 |
+
flex-direction: column;
|
| 1188 |
+
gap: 4px;
|
| 1189 |
+
}
|
| 1190 |
+
.tab-intro h2 {
|
| 1191 |
+
font-size: var(--fs-xl);
|
| 1192 |
+
font-weight: var(--fw-bold);
|
| 1193 |
+
margin: 0;
|
| 1194 |
+
padding-bottom: 6px;
|
| 1195 |
+
border-bottom: 2px solid var(--hf-yellow);
|
| 1196 |
+
display: inline-block;
|
| 1197 |
+
align-self: flex-start;
|
| 1198 |
+
color: var(--fg-1);
|
| 1199 |
+
letter-spacing: -0.01em;
|
| 1200 |
+
}
|
| 1201 |
+
.tab-intro p {
|
| 1202 |
+
margin: 0;
|
| 1203 |
+
color: var(--fg-3);
|
| 1204 |
+
font-size: var(--fs-sm);
|
| 1205 |
+
line-height: 1.5;
|
| 1206 |
+
max-width: 760px;
|
| 1207 |
+
}
|
| 1208 |
+
.dark .tab-intro h2,
|
| 1209 |
+
body.dark .tab-intro h2 {
|
| 1210 |
+
color: var(--hf-white);
|
| 1211 |
+
}
|
| 1212 |
+
.dark .tab-intro p,
|
| 1213 |
+
body.dark .tab-intro p {
|
| 1214 |
+
color: var(--hf-gray-500);
|
| 1215 |
+
}
|
| 1216 |
+
@media (prefers-color-scheme: dark) {
|
| 1217 |
+
.tab-intro h2:not(.light *) { color: var(--hf-white); }
|
| 1218 |
+
.tab-intro p:not(.light *) { color: var(--hf-gray-500); }
|
| 1219 |
+
}
|
| 1220 |
+
.lb-empty {
|
| 1221 |
+
padding: var(--space-5);
|
| 1222 |
+
text-align: center;
|
| 1223 |
+
color: var(--fg-3);
|
| 1224 |
+
border: 1px dashed var(--border-2);
|
| 1225 |
+
border-radius: var(--radius-lg);
|
| 1226 |
+
}
|
| 1227 |
|
| 1228 |
+
.lb-card {
|
| 1229 |
+
position: relative;
|
| 1230 |
+
display: flex;
|
| 1231 |
+
align-items: stretch;
|
| 1232 |
+
gap: var(--space-3);
|
| 1233 |
+
padding: var(--space-3) var(--space-4) var(--space-3) calc(var(--space-4) + 6px);
|
| 1234 |
+
background: var(--bg-1);
|
| 1235 |
border: 1px solid var(--border-1);
|
| 1236 |
border-radius: var(--radius-lg);
|
| 1237 |
+
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
|
| 1238 |
+
}
|
| 1239 |
+
.lb-card::before {
|
| 1240 |
+
/* Left accent strip keyed to the polygon color — lets users eyeball
|
| 1241 |
+
* "which card goes with which shape" even before reading the badge. */
|
| 1242 |
+
content: "";
|
| 1243 |
+
position: absolute;
|
| 1244 |
+
left: 0; top: 10px; bottom: 10px;
|
| 1245 |
+
width: 4px;
|
| 1246 |
+
background: var(--rank-color, var(--hf-gray-300));
|
| 1247 |
+
border-radius: 0 4px 4px 0;
|
| 1248 |
}
|
| 1249 |
+
.lb-card:hover {
|
| 1250 |
+
border-color: var(--border-2);
|
| 1251 |
+
box-shadow: 0 2px 8px rgba(27, 27, 29, 0.06);
|
| 1252 |
+
}
|
| 1253 |
+
.lb-badge {
|
| 1254 |
+
flex: 0 0 auto;
|
| 1255 |
+
display: inline-flex;
|
| 1256 |
+
align-items: center;
|
| 1257 |
+
justify-content: center;
|
| 1258 |
+
width: 34px; height: 34px;
|
| 1259 |
+
border-radius: 999px;
|
| 1260 |
+
background: var(--rank-color, var(--hf-gray-500));
|
| 1261 |
+
color: var(--hf-white);
|
| 1262 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 1263 |
+
font-size: var(--fs-sm);
|
| 1264 |
+
font-weight: var(--fw-bold);
|
| 1265 |
+
line-height: 1;
|
| 1266 |
+
align-self: center;
|
| 1267 |
+
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9) inset;
|
| 1268 |
+
}
|
| 1269 |
+
.lb-card-body {
|
| 1270 |
+
flex: 1 1 auto;
|
| 1271 |
+
min-width: 0;
|
| 1272 |
+
display: flex;
|
| 1273 |
+
flex-direction: column;
|
| 1274 |
+
gap: var(--space-2);
|
| 1275 |
+
}
|
| 1276 |
+
.lb-card-head {
|
| 1277 |
+
display: flex;
|
| 1278 |
+
align-items: center;
|
| 1279 |
+
gap: var(--space-2);
|
| 1280 |
+
flex-wrap: wrap;
|
| 1281 |
+
}
|
| 1282 |
+
.lb-codec {
|
| 1283 |
+
display: inline-flex;
|
| 1284 |
+
align-items: center;
|
| 1285 |
+
padding: 2px 0;
|
| 1286 |
+
color: var(--fg-1);
|
| 1287 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 1288 |
font-size: var(--fs-xs);
|
| 1289 |
+
font-weight: var(--fw-semibold);
|
| 1290 |
+
letter-spacing: 0.02em;
|
| 1291 |
}
|
| 1292 |
+
.lb-config {
|
| 1293 |
+
color: var(--fg-2);
|
| 1294 |
+
font-size: var(--fs-sm);
|
| 1295 |
+
}
|
| 1296 |
+
.lb-config code {
|
| 1297 |
+
background: var(--bg-3);
|
| 1298 |
+
color: var(--fg-1);
|
| 1299 |
+
padding: 1px 6px;
|
| 1300 |
+
border-radius: 4px;
|
| 1301 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 1302 |
+
font-size: 0.88em;
|
| 1303 |
+
}
|
| 1304 |
+
.lb-metrics {
|
| 1305 |
+
display: flex;
|
| 1306 |
+
flex-wrap: wrap;
|
| 1307 |
+
gap: 6px;
|
| 1308 |
+
}
|
| 1309 |
+
.lb-metric {
|
| 1310 |
+
display: inline-flex;
|
| 1311 |
+
align-items: center;
|
| 1312 |
+
gap: 6px;
|
| 1313 |
+
padding: 3px 8px;
|
| 1314 |
+
border-radius: var(--radius-sm);
|
| 1315 |
+
background: var(--bg-2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1316 |
border: 1px solid var(--border-1);
|
| 1317 |
+
font-size: var(--fs-xs);
|
| 1318 |
+
white-space: nowrap;
|
| 1319 |
+
}
|
| 1320 |
+
.lb-metric-k {
|
| 1321 |
+
color: var(--fg-3);
|
| 1322 |
+
text-transform: uppercase;
|
| 1323 |
+
letter-spacing: 0.06em;
|
| 1324 |
+
font-weight: var(--fw-semibold);
|
| 1325 |
+
font-size: 10px;
|
| 1326 |
}
|
| 1327 |
+
.lb-metric-v {
|
| 1328 |
+
color: var(--fg-1);
|
| 1329 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 1330 |
+
font-weight: var(--fw-semibold);
|
| 1331 |
+
font-variant-numeric: tabular-nums;
|
|
|
|
| 1332 |
}
|
|
|
|
|
|
|
| 1333 |
|
| 1334 |
+
/* Dark-mode tweaks — cards need a slightly lifted background and
|
| 1335 |
+
* lighter metric chips so they don't disappear into the tab surface. */
|
| 1336 |
+
.dark .lb-card,
|
| 1337 |
+
body.dark .lb-card {
|
| 1338 |
+
border-color: rgba(255, 255, 255, 0.08);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1339 |
}
|
| 1340 |
+
.dark .lb-card:hover,
|
| 1341 |
+
body.dark .lb-card:hover {
|
| 1342 |
+
border-color: rgba(255, 255, 255, 0.18);
|
| 1343 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
|
| 1344 |
+
}
|
| 1345 |
+
.dark .lb-metric,
|
| 1346 |
+
body.dark .lb-metric {
|
| 1347 |
+
background: rgba(255, 255, 255, 0.05);
|
| 1348 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 1349 |
+
}
|
| 1350 |
+
.dark .lb-metric-v,
|
| 1351 |
+
body.dark .lb-metric-v {
|
| 1352 |
+
color: var(--hf-gray-100);
|
| 1353 |
+
}
|
| 1354 |
+
.dark .lb-metric-k,
|
| 1355 |
+
body.dark .lb-metric-k {
|
| 1356 |
+
color: var(--hf-gray-500);
|
| 1357 |
+
}
|
| 1358 |
+
.dark .lb-config,
|
| 1359 |
+
body.dark .lb-config {
|
| 1360 |
+
color: var(--hf-gray-300);
|
| 1361 |
+
}
|
| 1362 |
+
.dark .lb-config code,
|
| 1363 |
+
body.dark .lb-config code {
|
| 1364 |
+
background: rgba(255, 255, 255, 0.07);
|
| 1365 |
+
color: var(--hf-gray-100);
|
| 1366 |
+
}
|
| 1367 |
+
.dark .lb-badge,
|
| 1368 |
+
body.dark .lb-badge {
|
| 1369 |
+
box-shadow: 0 0 0 3px rgba(27, 27, 29, 0.9) inset;
|
| 1370 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1371 |
|
| 1372 |
+
@media (prefers-color-scheme: dark) {
|
| 1373 |
+
.lb-card:not(.light *) {
|
| 1374 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 1375 |
+
}
|
| 1376 |
+
.lb-metric:not(.light *) {
|
| 1377 |
+
background: rgba(255, 255, 255, 0.05);
|
| 1378 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 1379 |
+
}
|
| 1380 |
+
}
|
|
|
|
|
|
|
|
|
|
| 1381 |
|
| 1382 |
+
/* "Selected configurations" section heading sits under the radar/card
|
| 1383 |
+
* row and introduces the dataframe. Match the prose scale used in the
|
| 1384 |
+
* params page so the tab reads as header → content → header → content. */
|
| 1385 |
+
.lb-table-head {
|
| 1386 |
+
margin-top: var(--space-6) !important;
|
| 1387 |
+
margin-bottom: var(--space-2) !important;
|
| 1388 |
+
}
|
| 1389 |
+
.lb-table-head h3 {
|
| 1390 |
+
font-size: var(--fs-lg);
|
| 1391 |
+
font-weight: var(--fw-semibold);
|
| 1392 |
+
margin: 0;
|
| 1393 |
padding-bottom: 6px;
|
| 1394 |
border-bottom: 2px solid var(--hf-yellow);
|
| 1395 |
display: inline-block;
|
| 1396 |
+
color: var(--fg-1);
|
| 1397 |
}
|
| 1398 |
+
.dark .lb-table-head h3,
|
| 1399 |
+
body.dark .lb-table-head h3 {
|
| 1400 |
+
color: var(--hf-white);
|
|
|
|
|
|
|
| 1401 |
}
|
| 1402 |
+
/* Kill the dataframe's outer card so the table reads as part of the
|
| 1403 |
+
* tab (matching the Results table treatment), and bump the Score
|
| 1404 |
+
* column to monospace tabular numerals for quick top-down scanning.
|
| 1405 |
+
* Rank is now a colored badge (rendered as HTML inside the cell), so
|
| 1406 |
+
* the span's monospace font handles alignment — no need to restyle
|
| 1407 |
+
* the td. */
|
| 1408 |
+
.lb-table .table-wrap {
|
| 1409 |
+
border: 0 !important;
|
| 1410 |
+
box-shadow: none !important;
|
| 1411 |
+
background: transparent !important;
|
| 1412 |
}
|
| 1413 |
+
.lb-table .table-wrap tbody td:last-child {
|
| 1414 |
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
| 1415 |
+
font-variant-numeric: tabular-nums;
|
| 1416 |
+
font-weight: var(--fw-semibold);
|
| 1417 |
}
|
| 1418 |
+
/* Center header labels in the leaderboard table. Gradio nests the label
|
| 1419 |
+
* text inside ``.cell-wrap`` > ``.header-content`` flex wrappers, so we
|
| 1420 |
+
* also need to neutralize their ``justify-content`` (defaults to
|
| 1421 |
+
* ``flex-start``) — otherwise ``text-align: center`` on the ``th``
|
| 1422 |
+
* alone is overridden by the inner flex layout. */
|
| 1423 |
+
.lb-table .table-wrap thead th,
|
| 1424 |
+
.lb-table .table-wrap thead th * {
|
| 1425 |
+
text-align: center !important;
|
| 1426 |
+
justify-content: center !important;
|
| 1427 |
}
|
| 1428 |
+
|
| 1429 |
+
/* Inline rank badge used inside the table's Rank column. Same visual
|
| 1430 |
+
* as the card badge, just smaller and without the inset ring that
|
| 1431 |
+
* makes sense only on the card's white surface. */
|
| 1432 |
+
.lb-badge-inline {
|
| 1433 |
+
width: 26px;
|
| 1434 |
+
height: 26px;
|
| 1435 |
+
font-size: var(--fs-xs);
|
| 1436 |
+
box-shadow: none;
|
| 1437 |
}
|
|
|
|
| 1438 |
|
| 1439 |
+
/* --- Compare tab plots ---
|
| 1440 |
+
* Plotly renders text via SVG `<text>` with a `fill=...` attribute
|
| 1441 |
+
* set server-side (see `_PLOTLY_FG` in app.py). That baked-in color
|
| 1442 |
+
* is a single mid-slate for both themes, which works but leaves
|
| 1443 |
+
* titles / legends a bit washed-out. CSS `fill` overrides the SVG
|
| 1444 |
+
* attribute, so we swap in theme-aware tokens (`--fg-1` / `--fg-2`)
|
| 1445 |
+
* only for Compare plots — the Leaderboards radar keeps its muted
|
| 1446 |
+
* gray on purpose, to avoid stealing attention from the polygons. */
|
| 1447 |
+
.compare-plot .gtitle,
|
| 1448 |
+
.compare-plot .g-gtitle {
|
| 1449 |
+
fill: var(--fg-1) !important;
|
| 1450 |
+
font-weight: var(--fw-semibold) !important;
|
| 1451 |
+
}
|
| 1452 |
+
.compare-plot .xtitle,
|
| 1453 |
+
.compare-plot .ytitle,
|
| 1454 |
+
.compare-plot .legendtitletext {
|
| 1455 |
+
fill: var(--fg-1) !important;
|
| 1456 |
+
}
|
| 1457 |
+
.compare-plot .legendtext,
|
| 1458 |
+
.compare-plot .xtick > text,
|
| 1459 |
+
.compare-plot .ytick > text,
|
| 1460 |
+
.compare-plot .annotation-text {
|
| 1461 |
+
fill: var(--fg-2) !important;
|
| 1462 |
+
}
|
| 1463 |
+
/* Axis spines + grid lines: use the neutral border token so they
|
| 1464 |
+
* stay visible on dark without turning harsh on light. */
|
| 1465 |
+
.compare-plot .xaxislayer-above path.domain,
|
| 1466 |
+
.compare-plot .yaxislayer-above path.domain,
|
| 1467 |
+
.compare-plot .zerolinelayer path {
|
| 1468 |
+
stroke: var(--border-2) !important;
|
| 1469 |
+
}
|
| 1470 |
+
.compare-plot .gridlayer path {
|
| 1471 |
+
stroke: var(--border-1) !important;
|
| 1472 |
+
}
|
| 1473 |
+
/* The caption sits above each bar plot and explains the current
|
| 1474 |
+
* selection; keep it readable but subordinate to the plot title. */
|
| 1475 |
+
.compare-caption {
|
| 1476 |
+
color: var(--fg-2) !important;
|
| 1477 |
+
font-size: var(--fs-sm) !important;
|
| 1478 |
+
margin: var(--space-1) 0 var(--space-2) !important;
|
| 1479 |
+
}
|
| 1480 |
+
/* The configurable bar chart + its title + its dropdowns live in one
|
| 1481 |
+
* gr.Group so they read as a single "patch". The group itself stays
|
| 1482 |
+
* transparent — it's the `.compare-controls` sub-column (title +
|
| 1483 |
+
* dropdowns) that gets a darker tinted surface. The chart sits on
|
| 1484 |
+
* the page background so it reads as "on the page", not "on a card".
|
| 1485 |
+
*
|
| 1486 |
+
* Together this produces: darker strip for the controls → seamless
|
| 1487 |
+
* transition → chart on page bg — i.e. darker background everywhere
|
| 1488 |
+
* except on the chart. */
|
| 1489 |
+
.compare-panel {
|
| 1490 |
+
margin-bottom: var(--space-4);
|
| 1491 |
+
background: transparent !important;
|
| 1492 |
+
border: 0 !important;
|
| 1493 |
+
box-shadow: none !important;
|
| 1494 |
+
}
|
| 1495 |
+
.compare-controls {
|
| 1496 |
+
background: var(--bg-1) !important;
|
| 1497 |
+
border: 1px solid var(--border-1) !important;
|
| 1498 |
+
padding: var(--space-4) !important;
|
| 1499 |
+
border-radius: var(--radius-lg);
|
| 1500 |
+
margin-bottom: var(--space-4) !important;
|
| 1501 |
+
}
|
| 1502 |
+
.compare-controls .compare-caption {
|
| 1503 |
+
margin-top: 0 !important;
|
| 1504 |
+
margin-bottom: var(--space-3) !important;
|
| 1505 |
+
}
|
| 1506 |
+
/* Control-label treatment for the Leaderboards "Top N configs" slider
|
| 1507 |
+
* and the Compare-tab "Metric" / "Group by" dropdowns — small uppercase
|
| 1508 |
+
* semibold cap in ``--fg-3`` so they all match the ``.filter-chips`` /
|
| 1509 |
+
* ``.ranking-pills`` label family. */
|
| 1510 |
+
.lb-topn > .block-label,
|
| 1511 |
+
.lb-topn > label > span:first-child,
|
| 1512 |
+
.lb-topn span[data-testid="block-info"],
|
| 1513 |
+
.cmp-control > .block-label,
|
| 1514 |
+
.cmp-control > label > span:first-child,
|
| 1515 |
+
.cmp-control span[data-testid="block-info"] {
|
| 1516 |
+
font-size: var(--fs-xs);
|
| 1517 |
+
font-weight: var(--fw-semibold);
|
| 1518 |
+
letter-spacing: 0.06em;
|
| 1519 |
+
text-transform: uppercase;
|
| 1520 |
+
color: var(--fg-3);
|
| 1521 |
+
margin-bottom: var(--space-2);
|
| 1522 |
+
}
|
| 1523 |
+
/* Kill the white/light card chrome Gradio wraps around every gr.Plot —
|
| 1524 |
+
* the plot itself is transparent (see `_PLOTLY_LAYOUT` in app.py), so
|
| 1525 |
+
* the default border + background read as a stray outline against the
|
| 1526 |
+
* `.compare-panel` surface. Strip border, background and shadow on
|
| 1527 |
+
* every wrapper between `.compare-plot` and the plotly SVG. */
|
| 1528 |
+
.compare-plot,
|
| 1529 |
+
.compare-plot > div,
|
| 1530 |
+
.compare-plot .plot-container,
|
| 1531 |
+
.compare-plot .plotly,
|
| 1532 |
+
.compare-plot .svg-container {
|
| 1533 |
+
background: transparent !important;
|
| 1534 |
+
border: 0 !important;
|
| 1535 |
+
box-shadow: none !important;
|
| 1536 |
+
}
|
| 1537 |
|
| 1538 |
+
/* --- Gradio component tweaks to tighten the HF look --- */
|
| 1539 |
+
/* Slightly softer tabs bar — default Gradio tabs ship a heavy underline. */
|
| 1540 |
+
.tab-nav button.selected {
|
| 1541 |
+
border-bottom-color: var(--hf-yellow) !important;
|
| 1542 |
+
color: var(--hf-blue-deep) !important;
|
| 1543 |
+
font-weight: var(--fw-semibold);
|
| 1544 |
+
}
|
| 1545 |
|
| 1546 |
+
/* Give plot cards the same card radius as the param cards. */
|
| 1547 |
+
.gradio-container .plot-container {
|
| 1548 |
+
border-radius: var(--radius-lg);
|
| 1549 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1550 |
|
| 1551 |
+
/* The whole layout — hero and every tab — shares one wide column that tracks
|
| 1552 |
+
* the window. Using `100%` (not the Gradio default `1440px` cap) means the
|
| 1553 |
+
* hero always occupies most of the window. */
|
| 1554 |
+
.gradio-container {
|
| 1555 |
+
max-width: 100% !important;
|
| 1556 |
+
width: 100% !important;
|
| 1557 |
+
margin: 0 auto !important;
|
| 1558 |
+
padding: var(--space-6) var(--space-8) !important;
|
| 1559 |
+
box-sizing: border-box;
|
| 1560 |
+
}
|
|
|
|
|
|
|
|
|
|
| 1561 |
|
| 1562 |
+
@media (max-width: 820px) {
|
| 1563 |
+
.hero {
|
| 1564 |
+
padding: var(--space-6);
|
| 1565 |
+
}
|
| 1566 |
+
.hero .hero-mascot,
|
| 1567 |
+
.hero .hero-mascot img {
|
| 1568 |
+
max-width: 140px;
|
| 1569 |
+
}
|
| 1570 |
+
.hero .hero-stats {
|
| 1571 |
+
gap: var(--space-4);
|
| 1572 |
+
}
|
| 1573 |
+
}
|
| 1574 |
|
| 1575 |
+
/* Footer row hosting the "Refresh from Hub" button. Sits flush against
|
| 1576 |
+
* the bottom of the page, right-aligned so the action reads as ambient
|
| 1577 |
+
* tooling rather than a primary CTA, with a soft top border separating
|
| 1578 |
+
* it from the last tab's content. */
|
| 1579 |
+
.refresh-footer {
|
| 1580 |
+
margin-top: var(--space-6);
|
| 1581 |
+
padding-top: var(--space-4);
|
| 1582 |
+
border-top: 1px solid var(--border-1);
|
| 1583 |
+
align-items: center;
|
| 1584 |
+
justify-content: flex-end;
|
| 1585 |
+
gap: var(--space-4);
|
| 1586 |
+
}
|
| 1587 |
+
.refresh-footer .refresh-status {
|
| 1588 |
+
flex: 1 1 auto;
|
| 1589 |
+
color: var(--fg-3);
|
| 1590 |
+
font-size: 13px;
|
| 1591 |
+
}
|
| 1592 |
+
.refresh-footer button {
|
| 1593 |
+
flex: 0 0 auto;
|
| 1594 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|