CarolinePascal commited on
Commit
1fb78ac
·
unverified ·
1 Parent(s): 5034273

feat(gradio): update to gradio app

Browse files
README.md CHANGED
@@ -3,8 +3,9 @@ title: Video Benchmark
3
  emoji: 🎬
4
  colorFrom: green
5
  colorTo: indigo
6
- sdk: static
7
- app_file: index.html
 
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

  • SHA256: 09395277aee38b0259b8641536f7b80bde19d9e3094038626311ccb5d1515658
  • Pointer size: 130 Bytes
  • Size of remote file: 96 kB
assets/huggy-hello.png DELETED

Git LFS Details

  • SHA256: e6e8a263270116895720013a824f1b967c6421f88e3fd531a8116b424666bccd
  • Pointer size: 130 Bytes
  • Size of remote file: 96.2 kB
assets/huggy-lab.png DELETED

Git LFS Details

  • SHA256: c3d19fb29d0344ed651bce9c3701d970ca1d1d2b469c36cad39e9f36eaaa188a
  • Pointer size: 131 Bytes
  • Size of remote file: 139 kB
assets/huggy-rocket.png DELETED

Git LFS Details

  • SHA256: e2d9c134654c8e4a5c475ed3b94ac0b77ec51754b290a959afe2bc95982ed570
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
assets/huggy-vision.png DELETED

Git LFS Details

  • SHA256: df7b28347d3976c5830a6f775f53195511c451e0190b2d9ee10caa10e65c1fa4
  • Pointer size: 131 Bytes
  • Size of remote file: 112 kB
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 — Hugging Face-styled UI */
 
 
 
 
 
 
 
 
2
 
3
- /* --- Base overrides built on top of colors_and_type.css --- */
4
- * { box-sizing: border-box; }
5
- html, body { margin: 0; padding: 0; }
6
- body {
7
- background: var(--bg-2);
8
- color: var(--fg-1);
9
- font-family: var(--font-sans);
10
- font-size: var(--fs-sm);
11
- line-height: var(--lh-sm);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
- button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
15
-
16
- /* --- Top nav (mimics hf.co) --- */
17
- .nav {
18
- background: var(--hf-white);
19
- border-bottom: 1px solid var(--border-1);
20
- padding: 0 var(--space-6);
21
- height: 56px;
22
- display: flex;
23
- align-items: center;
24
- gap: var(--space-6);
25
- position: sticky; top: 0; z-index: 100;
26
- }
27
- .nav-brand { display: flex; align-items: center; gap: var(--space-3); font-weight: var(--fw-semibold); font-size: var(--fs-md); color: var(--fg-1); text-decoration: none; }
28
- .nav-brand img { width: 28px; height: 28px; }
29
- .nav-brand .wordmark { font-family: var(--font-display); letter-spacing: -0.01em; }
30
- .nav-links { display: flex; gap: var(--space-1); align-items: center; }
31
- .nav-links a { color: var(--fg-2); text-decoration: none; padding: 6px 12px; border-radius: var(--radius-sm); font-weight: var(--fw-medium); font-size: var(--fs-sm); }
32
- .nav-links a:hover { background: var(--bg-3); color: var(--fg-1); opacity: 1; }
33
- .nav-spacer { flex: 1; }
34
- .nav-search {
35
- background: var(--bg-2);
36
- border: 1px solid var(--border-1);
37
- border-radius: var(--radius-md);
38
- padding: 6px 12px 6px 32px;
39
- font-size: var(--fs-sm);
40
- width: 240px;
41
- color: var(--fg-2);
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
- background: var(--hf-cream);
 
 
 
 
 
 
 
76
  border-radius: var(--radius-2xl);
77
- padding: var(--space-10) var(--space-10);
78
- display: grid;
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: 0 0 var(--space-3);
92
- color: var(--hf-blue-deep);
93
  }
94
- .hero .subtitle {
95
  font-size: var(--fs-md);
96
- line-height: var(--lh-md);
97
- color: var(--hf-gray-800);
98
  max-width: 620px;
99
- margin: 0 0 var(--space-5);
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
- .tab-btn {
124
- padding: var(--space-3) var(--space-4);
125
- font-size: var(--fs-sm);
126
- font-weight: var(--fw-medium);
127
- color: var(--fg-2);
128
- border-bottom: 2px solid transparent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  white-space: nowrap;
130
- display: inline-flex; align-items: center; gap: 6px;
131
  }
132
- .tab-btn:hover { color: var(--fg-1); }
133
- .tab-btn.active { color: var(--hf-blue-deep); border-bottom-color: var(--hf-yellow); font-weight: var(--fw-semibold); }
134
- .tab-btn .tab-count { font-family: var(--font-mono); font-size: 11px; background: var(--bg-3); color: var(--fg-2); padding: 1px 6px; border-radius: var(--radius-pill); }
135
- .tab-btn.active .tab-count { background: var(--hf-yellow); color: var(--hf-blue-deep); }
136
-
137
- .tab-panel { display: none; }
138
- .tab-panel.active { display: block; animation: fadeIn 0.2s var(--ease-out); }
139
- @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
140
-
141
- /* --- Cards --- */
142
- .card {
143
- background: var(--hf-white);
144
- border: 1px solid var(--border-1);
145
- border-radius: var(--radius-lg);
146
- padding: var(--space-5);
 
 
 
 
 
 
 
 
 
 
 
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
- .section-title { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: var(--space-4); }
153
- .section-title h2 { font-size: var(--fs-xl); font-weight: var(--fw-semibold); margin: 0; letter-spacing: -0.01em; }
154
- .section-title .muted { color: var(--fg-3); font-size: var(--fs-sm); }
 
 
 
 
 
 
155
 
156
- /* --- Filters --- */
157
- .filters-bar {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  display: grid;
159
- grid-template-columns: repeat(5, 1fr) auto;
160
- grid-template-rows: auto 1fr;
161
  gap: var(--space-3);
162
- padding: var(--space-4);
163
- background: var(--hf-white);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  border: 1px solid var(--border-1);
165
  border-radius: var(--radius-lg);
166
- margin-bottom: var(--space-4);
167
- align-items: end;
168
  }
169
- .filter-field {
170
- display: grid;
171
- grid-row: 1 / span 2;
172
- grid-template-rows: subgrid;
173
- row-gap: 6px;
174
- }
175
- .filter-field label {
176
- display: block; font-size: 11px; font-weight: var(--fw-semibold);
177
- color: var(--fg-3); text-transform: uppercase; letter-spacing: 0.06em;
178
- margin-bottom: 0;
179
- align-self: start;
180
- }
181
- .filter-field .chips { display: flex; gap: 4px; flex-wrap: wrap; align-content: flex-start; align-self: start; }
182
- .filter-chip {
183
- font-family: var(--font-mono);
184
- font-size: 11px;
185
- padding: 3px 8px;
186
  border-radius: var(--radius-sm);
187
- background: var(--bg-2);
188
- border: 1px solid var(--border-1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  color: var(--fg-2);
190
- cursor: pointer;
191
- transition: all var(--t-quick) var(--ease-out);
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
- .filter-actions { grid-row: 2; display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); }
199
- .btn {
200
- padding: 8px 14px; border-radius: var(--radius-md); font-size: var(--fs-sm);
201
- font-weight: var(--fw-semibold); border: 1px solid var(--border-1);
202
- background: var(--hf-white); color: var(--fg-1);
203
- transition: all var(--t-quick) var(--ease-out);
204
- display: inline-flex; align-items: center; gap: 6px;
205
- }
206
- .btn:hover { background: var(--bg-2); }
207
- .btn.primary { background: var(--hf-gray-900); color: var(--hf-white); border-color: var(--hf-gray-900); }
208
- .btn.primary:hover { background: var(--hf-gray-800); }
209
- .btn.brand { background: var(--hf-yellow); color: var(--hf-blue-deep); border-color: var(--hf-yellow-deep); }
210
- .btn.brand:hover { background: var(--hf-yellow-deep); }
211
- .btn.ghost { background: transparent; border-color: transparent; color: var(--fg-2); }
212
- .btn.ghost:hover { background: var(--bg-3); color: var(--fg-1); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- /* --- Column picker dropdown --- */
215
- .columns-dd-wrap { position: relative; display: inline-block; }
216
- .columns-dd-trigger {
217
- display: inline-flex; align-items: center; gap: 8px;
218
- padding: 8px 12px;
219
- background: var(--hf-white);
220
- border: 1px solid var(--border-1);
221
- border-radius: var(--radius-md);
222
- font-family: var(--font-sans);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  font-size: var(--fs-sm);
224
  font-weight: var(--fw-medium);
225
- color: var(--fg-1);
226
  cursor: pointer;
227
- transition: all var(--t-quick) var(--ease-out);
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
- .columns-dd-panel {
241
- position: absolute;
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
- @keyframes ddIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
257
- .dd-panel-head {
258
- display: flex; align-items: center; gap: 8px;
259
- padding: 10px 14px;
260
- border-bottom: 1px solid var(--border-1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  font-size: var(--fs-xs);
262
  font-weight: var(--fw-semibold);
263
- text-transform: uppercase;
264
  letter-spacing: 0.06em;
265
- color: var(--fg-2);
 
 
266
  }
267
- .dd-mini {
268
- font-family: var(--font-sans);
269
- font-size: 11px;
270
- font-weight: var(--fw-medium);
271
- padding: 4px 8px;
272
- background: transparent;
273
- border: 1px solid var(--border-1);
274
- border-radius: 6px;
275
- color: var(--fg-2);
276
- cursor: pointer;
277
- text-transform: none;
278
- letter-spacing: 0;
 
 
 
 
 
 
 
 
 
279
  }
280
- .dd-mini:hover { border-color: var(--hf-blue-deep); color: var(--hf-blue-deep); }
281
- .dd-groups {
282
- overflow-y: auto;
283
- padding: 6px;
284
- display: grid;
285
- grid-template-columns: 1fr 1fr;
286
- gap: 2px 8px;
287
- align-content: start;
 
 
288
  }
289
- .dd-group {
290
- display: flex;
291
- flex-direction: column;
292
- padding: 4px 0 8px;
293
  }
294
- .dd-group-head {
295
- display: flex; align-items: center; gap: 8px;
296
- padding: 6px 8px;
297
- background: transparent;
298
- border: none;
299
- cursor: pointer;
300
- border-radius: var(--radius-sm);
301
- width: 100%;
302
- text-align: left;
303
  }
304
- .dd-group-head:hover { background: var(--bg-2); }
305
- .dd-group-head .sw { width: 3px; height: 14px; border-radius: 2px; flex-shrink: 0; }
306
- .dd-group-name {
307
- font-family: var(--font-sans);
308
- font-size: var(--fs-xs);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  font-weight: var(--fw-semibold);
 
 
310
  text-transform: uppercase;
311
- letter-spacing: 0.06em;
312
- color: var(--fg-1);
313
  }
314
- .dd-group-count {
315
- margin-left: auto;
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
- .dd-group-items {
324
- display: flex;
325
- flex-direction: column;
326
- padding: 2px 0 0 10px;
327
  }
328
- .dd-item {
329
- display: flex; align-items: center; gap: 8px;
330
- padding: 5px 8px;
331
- background: transparent;
332
- border: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  border-radius: var(--radius-sm);
334
- font-family: var(--font-sans);
335
- font-size: var(--fs-sm);
336
- color: var(--fg-2);
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
- .dd-item:hover { background: var(--bg-2); color: var(--fg-1); }
343
- .dd-item.on { color: var(--fg-1); }
344
- .dd-item-label { flex: 1; }
345
- .dd-cbx {
346
- width: 14px; height: 14px;
347
- border-radius: 4px;
348
- border: 1.5px solid var(--border-2);
349
- background: var(--hf-white);
350
- flex-shrink: 0;
351
- transition: all var(--t-quick) var(--ease-out);
352
- position: relative;
 
 
 
 
 
 
 
 
 
353
  }
354
- .dd-cbx.on {
355
- background: var(--hf-blue-deep);
356
- border-color: var(--hf-blue-deep);
357
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6L5 8.5L9.5 3.5' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
358
- background-size: 12px 12px;
359
- background-repeat: no-repeat;
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
- /* Dark mode dropdown */
372
- body.dark .columns-dd-trigger { background: #1A1D24; border-color: #2C3240; color: #F5F6F8; }
373
- body.dark .columns-dd-trigger:hover { border-color: #475064; }
374
- body.dark .columns-dd-trigger.open { border-color: var(--hf-yellow); box-shadow: 0 0 0 3px color-mix(in oklab, var(--hf-yellow) 22%, transparent); }
375
- body.dark .columns-dd-trigger .dd-count { background: #0F1115; color: #C7CDD8; }
376
- body.dark .columns-dd-panel { background: #161A20; border-color: #2C3240; box-shadow: 0 16px 40px rgba(0,0,0,.55), 0 4px 12px rgba(0,0,0,.3); }
377
- body.dark .dd-panel-head { border-bottom-color: #2C3240; color: #C7CDD8; }
378
- body.dark .dd-mini { border-color: #2C3240; color: #C7CDD8; }
379
- body.dark .dd-mini:hover { border-color: var(--hf-yellow); color: var(--hf-yellow); }
380
- body.dark .dd-group-head:hover { background: #1A1D24; }
381
- body.dark .dd-group-name { color: #F5F6F8; }
382
- body.dark .dd-group-count { background: #0F1115; color: #8B93A7; }
383
- body.dark .dd-item { color: #C7CDD8; }
384
- body.dark .dd-item:hover { background: #1A1D24; color: #F5F6F8; }
385
- body.dark .dd-item.on { color: #F5F6F8; }
386
- body.dark .dd-cbx { background: #0F1115; border-color: #475064; }
387
- body.dark .dd-cbx.on, body.dark .dd-cbx.some { background: var(--hf-yellow); border-color: var(--hf-yellow); }
388
- body.dark .dd-cbx.on { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6L5 8.5L9.5 3.5' stroke='%231B1B1D' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
389
- body.dark .dd-cbx.some { 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='%231B1B1D' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); }
390
- .columns-bar {
391
- display: grid;
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
- margin-bottom: var(--space-4);
399
- align-items: start;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  }
401
- .columns-bar .filter-field label {
402
- display: flex; align-items: center; gap: 6px;
403
- font-size: 11px; font-weight: var(--fw-semibold);
404
- color: var(--fg-1); text-transform: uppercase; letter-spacing: 0.06em;
405
- margin-bottom: 6px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  }
407
- .columns-bar .chips { flex-direction: column; gap: 3px; align-items: flex-start; }
408
- .columns-bar .col-chip {
409
- font-family: var(--font-sans);
 
 
 
 
 
 
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
- display: flex; align-items: center; gap: 6px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  width: 100%;
420
- transition: color var(--t-quick) var(--ease-out);
421
- text-align: left;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- /* (legacy details-block kept for any other use) */
451
- .details-block {
452
- background: var(--hf-white);
453
- border: 1px solid var(--border-1);
454
- border-radius: var(--radius-lg);
 
 
455
  margin-bottom: var(--space-4);
456
- overflow: hidden;
457
  }
458
- .details-head {
459
- padding: var(--space-3) var(--space-4);
460
- display: flex; align-items: center; gap: var(--space-3);
461
- cursor: pointer; user-select: none;
462
- font-weight: var(--fw-semibold); font-size: var(--fs-sm);
463
- }
464
- .details-head:hover { background: var(--bg-2); }
465
- .details-head .caret { transition: transform var(--t-quick); }
466
- .details-block.open .caret { transform: rotate(90deg); }
467
- .details-body { padding: 0 var(--space-4) var(--space-4); display: none; }
468
- .details-block.open .details-body { display: block; }
469
-
470
- .col-groups { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }
471
- .col-group-header {
472
- display: flex; align-items: center; gap: 8px; font-weight: var(--fw-semibold);
473
- font-size: var(--fs-sm); color: var(--fg-1); margin-bottom: 8px;
474
- padding-bottom: 8px; border-bottom: 1px solid var(--border-1);
475
- }
476
- .col-group-header .sw { width: 3px; height: 14px; border-radius: 2px; }
477
- .col-item {
478
- display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm);
479
- padding: 4px 0; color: var(--fg-2); cursor: pointer; user-select: none;
480
- }
481
- .col-item input { accent-color: var(--hf-blue); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
- /* --- Table --- */
484
- .table-wrap {
485
- background: var(--hf-white);
 
 
 
 
486
  border: 1px solid var(--border-1);
487
  border-radius: var(--radius-lg);
488
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
489
  }
490
- .table-top { padding: var(--space-3) var(--space-4); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-1); background: var(--bg-2); }
491
- .table-top .row-count { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-2); }
492
- .table-top .row-count b { color: var(--fg-1); }
493
- .table-scroll { overflow-x: auto; max-height: 620px; }
494
- table.bench {
495
- width: 100%; border-collapse: separate; border-spacing: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  font-size: var(--fs-xs);
497
- font-variant-numeric: tabular-nums;
 
498
  }
499
- table.bench thead th {
500
- position: sticky; top: 0; z-index: 2;
501
- background: var(--hf-white); padding: 10px 12px; text-align: left;
502
- font-size: 11px; font-weight: var(--fw-semibold); color: var(--fg-2);
503
- text-transform: uppercase; letter-spacing: 0.05em;
504
- border-bottom: 1px solid var(--border-2);
505
- white-space: pre-line;
506
- vertical-align: middle;
507
- line-height: 1.25;
508
- }
509
- table.bench thead th.group-start { border-left: 2px solid var(--hf-yellow); }
510
- table.bench thead th .info {
511
- display: inline-block; width: 13px; height: 13px; margin-left: 4px;
512
- border: 1px solid var(--border-2); border-radius: 50%;
513
- text-align: center; font-size: 9px; line-height: 12px;
514
- color: var(--fg-3); cursor: help;
515
- font-family: var(--font-sans); font-weight: var(--fw-bold);
516
- }
517
- table.bench thead th .info:hover { border-color: var(--fg-2); color: var(--fg-1); }
518
- table.bench tbody td {
519
- padding: 8px 12px; border-bottom: 1px solid var(--border-1);
520
- white-space: nowrap; color: var(--fg-1);
521
- }
522
- table.bench tbody td.group-start { border-left: 2px solid var(--hf-yellow); }
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
- transition: border-color var(--t-quick);
 
 
 
 
 
 
 
 
591
  }
592
- .legend-item:hover { border-color: var(--border-2); }
593
- .legend-item .rank {
594
- width: 28px; height: 28px; border-radius: 50%;
595
- display: grid; place-items: center;
596
- font-family: var(--font-mono); font-size: 12px; font-weight: var(--fw-bold);
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
- /* --- Compare charts --- */
603
- .compare-grid { display: grid; gap: var(--space-4); }
604
- .chart-card { padding: var(--space-5); }
605
- .chart-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4); }
606
- .chart-head .controls { display: flex; gap: var(--space-3); }
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
- .textarea:focus { outline: 2px solid var(--hf-blue); outline-offset: -1px; border-color: transparent; }
633
-
634
- .summary-card .count-box {
635
- background: var(--hf-cream); border: 1px solid var(--hf-yellow-deep);
636
- border-radius: var(--radius-md); padding: var(--space-4);
637
- display: flex; align-items: center; gap: var(--space-3);
638
- margin-bottom: var(--space-4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- .queue-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-xs); margin-top: var(--space-3); }
650
- .queue-table th { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border-1); color: var(--fg-2); font-weight: var(--fw-semibold); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
651
- .queue-table td { padding: 10px; border-bottom: 1px solid var(--border-1); font-family: var(--font-mono); color: var(--fg-1); }
652
- .queue-status { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: var(--radius-pill); font-size: 11px; font-weight: var(--fw-semibold); }
653
- .queue-status .dot { width: 6px; height: 6px; border-radius: 50%; }
654
- .queue-status.done { background: var(--tag-papers-bg); color: var(--tag-papers-fg); }
655
- .queue-status.done .dot { background: var(--tag-papers-fg); }
656
- .queue-status.running { background: var(--tag-models-bg); color: var(--tag-models-fg); }
657
- .queue-status.running .dot { background: var(--tag-models-fg); animation: pulse 1.4s infinite; }
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
- /* --- Parameters page --- */
663
- .params-page { display: flex; flex-direction: column; gap: var(--space-6); }
664
- .param-group { }
665
- .param-group-head { margin-bottom: var(--space-4); }
666
- .param-group-head h3 {
667
- font-size: var(--fs-md); font-weight: var(--fw-semibold);
668
- margin: 0 0 4px; letter-spacing: -0.01em;
 
 
 
 
669
  padding-bottom: 6px;
670
  border-bottom: 2px solid var(--hf-yellow);
671
  display: inline-block;
 
672
  }
673
- .param-group-head p { margin: 8px 0 0; color: var(--fg-2); font-size: var(--fs-sm); max-width: 760px; }
674
- .param-cards {
675
- display: grid;
676
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
677
- gap: var(--space-3);
678
  }
679
- .param-card {
680
- background: var(--hf-white);
681
- border: 1px solid var(--border-1);
682
- border-radius: var(--radius-lg);
683
- padding: var(--space-4);
684
- transition: border-color var(--t-quick);
 
 
 
 
685
  }
686
- .param-card:hover { border-color: var(--hf-yellow-deep); }
687
- .param-card-head {
688
- display: flex; align-items: baseline; gap: 10px;
689
- flex-wrap: wrap; margin-bottom: 8px;
690
  }
691
- .param-card-head .param-key {
692
- font-family: var(--font-mono); font-size: var(--fs-xs);
693
- background: var(--hf-cream); color: var(--hf-blue-deep);
694
- padding: 2px 8px; border-radius: var(--radius-sm);
695
- font-weight: var(--fw-semibold); letter-spacing: -0.01em;
 
 
 
 
696
  }
697
- .param-card-head .param-label {
698
- font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--fg-1);
 
 
 
 
 
 
 
699
  }
700
- .param-card p { margin: 0; color: var(--fg-2); font-size: var(--fs-sm); line-height: 1.55; }
701
 
702
- /* --- About --- */
703
- .about-grid { display: grid; grid-template-columns: 1fr 280px; gap: var(--space-8); align-items: start; }
704
- .prose { font-size: var(--fs-base); line-height: var(--lh-md); color: var(--fg-1); }
705
- .prose h3 { font-size: var(--fs-lg); font-weight: var(--fw-semibold); margin: var(--space-6) 0 var(--space-2); }
706
- .prose h3:first-child { margin-top: 0; }
707
- .prose p { margin: 0 0 var(--space-3); }
708
- .prose ul { margin: 0 0 var(--space-3); padding-left: 20px; }
709
- .prose li { margin-bottom: 4px; }
710
- .prose code { background: var(--bg-3); padding: 1px 6px; border-radius: var(--radius-xs); font-family: var(--font-mono); font-size: 0.88em; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
 
712
- .about-toc { position: sticky; top: 80px; padding: var(--space-4); background: var(--hf-cream); border-radius: var(--radius-lg); }
713
- .about-toc .t { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--hf-blue-deep); font-weight: var(--fw-bold); margin-bottom: var(--space-2); }
714
- .about-toc a { display: block; padding: 6px 0; font-size: var(--fs-sm); color: var(--hf-blue-deep); text-decoration: none; opacity: 0.8; }
715
- .about-toc a:hover { opacity: 1; }
 
 
 
716
 
717
- /* --- Tweaks panel --- */
718
- .tweaks-panel {
719
- position: fixed; bottom: 20px; right: 20px; width: 280px;
720
- background: var(--hf-white); border: 1px solid var(--border-1);
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
- /* --- Citation --- */
732
- .citation {
733
- background: var(--hf-gray-900); color: var(--hf-gray-300);
734
- border-radius: var(--radius-lg); padding: var(--space-4);
735
- font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.6;
736
- position: relative; overflow: auto;
737
- }
738
- .citation .copy-btn {
739
- position: absolute; top: 10px; right: 10px;
740
- padding: 4px 8px; font-size: 11px; background: rgba(255,255,255,0.1);
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
- /* --- Density modes --- */
746
- body.density-compact table.bench tbody td { padding: 5px 10px; }
747
- body.density-compact table.bench thead th { padding: 7px 10px; }
 
 
 
 
 
 
 
 
 
748
 
749
- /* --- Dark (subtle) --- */
750
- body.dark { background: #0F1115; color: #E5E7EB; }
751
- body.dark h1, body.dark h2, body.dark h3, body.dark h4, body.dark h5, body.dark h6 { color: #F5F6F8; }
752
- body.dark strong, body.dark b { color: #F5F6F8; }
753
- body.dark .card,
754
- body.dark .nav,
755
- body.dark .space-header,
756
- body.dark .filters-bar,
757
- body.dark .columns-bar,
758
- body.dark .table-wrap,
759
- body.dark .details-block,
760
- body.dark .lb-controls,
761
- body.dark .tweaks-panel,
762
- body.dark .param-card { background: #161A20; border-color: #2E3442; }
763
- body.dark .hero { background: #1E1B0F; }
764
- body.dark .hero h1, body.dark .hero .subtitle, body.dark .hero-stat .v, body.dark .hero-stat .l { color: #F5F6F8; }
765
- body.dark .btn { background: #1E232C; border-color: #2E3442; color: #E5E7EB; }
766
- body.dark .btn:hover { background: #262B36; }
767
- body.dark .btn.ghost { background: transparent; color: #9BA3AF; }
768
- body.dark .btn.ghost:hover { background: #262B36; color: #F5F6F8; }
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
+ }