Artem Plastinkin commited on
Commit
13b8f21
Β·
1 Parent(s): b4f7aa3

- NEW styles

Browse files
.claude/skills/regenerate-catalog/SKILL.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: regenerate-catalog
3
+ description: Regenerate the dashboard's models.json from the live Hugging Face repos by running generate_models_json.py. Use when the user wants to refresh, update, or rebuild the model catalog, import benchmark numbers, or after repos change on Hugging Face.
4
+ ---
5
+
6
+ # Regenerate the model catalog (`models.json`)
7
+
8
+ This skill runs `generate_models_json.py` to rebuild `models.json` β€” the auto-generated
9
+ catalog the static dashboard reads. **Never hand-edit `models.json`; always regenerate it.**
10
+
11
+ ## Steps
12
+
13
+ 1. **Work from the project root** (where `generate_models_json.py` lives). Confirm it exists:
14
+ ```bash
15
+ ls generate_models_json.py
16
+ ```
17
+
18
+ 2. **Ensure dependencies** are installed (idempotent β€” safe to re-run):
19
+ ```bash
20
+ pip install requests pyyaml
21
+ ```
22
+ `huggingface_hub` is optional β€” only used to auto-detect a cached login token.
23
+
24
+ 3. **Decide whether a token is needed.** Gated Renesas repos are *listed* without a token, but
25
+ their *benchmark numbers* require an authorized one. Resolve a token from (in order):
26
+ `--token hf_xxx`, env `HF_TOKEN` / `HUGGINGFACE_TOKEN` / `HUGGING_FACE_HUB_TOKEN`, or a prior
27
+ `huggingface-cli login`. If the user hasn't supplied one and wants gated metrics, ask them to
28
+ either run `! huggingface-cli login` in this session or pass a token. Do **not** print or echo
29
+ a token; pass it through the environment or the `--token` flag they provide.
30
+
31
+ 4. **Preview first (recommended).** A dry run prints the JSON and a summary without writing:
32
+ ```bash
33
+ python generate_models_json.py --dry-run -v
34
+ ```
35
+ Read the per-repo summary: configs, metric blocks, and any `[gated: metrics skipped]` flags.
36
+
37
+ 5. **Write the catalog** once the preview looks right:
38
+ ```bash
39
+ python generate_models_json.py # rewrites ./models.json (anonymous)
40
+ python generate_models_json.py --token hf_xxx # also imports gated repos' metrics
41
+ ```
42
+ Other flags: `--org <name>` (default `Renesas`), `--output <path>`, `--include-private`
43
+ (needs a token with access), `--force` (write even if 0 models β€” overwrites existing file).
44
+
45
+ 6. **Check the exit code and report it:**
46
+ - `0` β€” success (or a valid dry run).
47
+ - `2` β€” Hugging Face network/API error (rate-limited or unreachable). Suggest retrying later.
48
+ - `3` β€” could not write the output file (check path/permissions).
49
+ - `4` β€” produced **0 models** and refused to overwrite the existing `models.json`. This is a
50
+ safety guard β€” investigate *why* zero models came back (org name? token? API down?) before
51
+ ever reaching for `--force`.
52
+
53
+ ## After regenerating
54
+
55
+ - Skim the run summary for gated repos whose metrics were skipped β€” if the user expected those
56
+ numbers, they need a token with access (request access on HF, then re-run with `--token`).
57
+ - The dashboard reads `models.json` over HTTP, so to eyeball the result locally:
58
+ ```bash
59
+ python -m http.server 8000 # then open http://localhost:8000
60
+ ```
61
+ (Opening `index.html` via `file://` will not work β€” `app.js` `fetch`es `models.json`.)
62
+ - If the user wants the refresh committed, stage and commit `models.json` only after confirming
63
+ the run succeeded and the diff looks reasonable.
64
+
65
+ ## Notes
66
+
67
+ - The generator is the source of truth for the catalog schema; if a new accelerator/EP or
68
+ benchmark `type` appears and shows up wrong, the fix belongs in the Python (`EP_TO_UNIT`,
69
+ `HIL_TYPES`), not in `models.json`.
70
+ - Run this whenever the Hugging Face repos change to keep the dashboard current.
.vscode/settings.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "python-envs.defaultEnvManager": "ms-python.python:conda",
3
+ "python-envs.defaultPackageManager": "ms-python.python:conda"
4
+ }
CLAUDE.md ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ A **static** Hugging Face Space (`sdk: static` in `README.md` front matter) that lets users
8
+ browse the Renesas AI model catalog and route each model to the correct benchmark/estimation
9
+ Space. There is **no build step, no framework, no package manager, and no test suite** β€” it is
10
+ plain `index.html` + `app.js` (vanilla ES, loaded with `defer`) + `styles.css`, served as-is.
11
+
12
+ ## Running and updating
13
+
14
+ ```bash
15
+ # Serve locally (any static server works; the app fetches models.json/links.json over HTTP)
16
+ python -m http.server 8000 # then open http://localhost:8000
17
+
18
+ # Regenerate the catalog from the live Hugging Face repos
19
+ pip install requests pyyaml
20
+ python generate_models_json.py # rewrites ./models.json
21
+ python generate_models_json.py --token hf_xxx # also import gated repos' benchmark numbers
22
+ python generate_models_json.py --dry-run -v # print, don't write
23
+ ```
24
+
25
+ Opening `index.html` via `file://` will not work β€” `app.js` `fetch`es `models.json` and
26
+ `links.json`, which requires an HTTP origin.
27
+
28
+ Generator exit codes: `0` ok, `2` network/API error, `3` write error, `4` produced 0 models
29
+ (refuses to overwrite an existing file β€” use `--force`).
30
+
31
+ ## Architecture
32
+
33
+ The whole app is **data-driven from two JSON files** loaded at boot in `app.js` `init()`:
34
+
35
+ - **`models.json`** β€” the catalog. **Auto-generated; do not hand-edit.** Regenerate with the
36
+ Python script instead. Schema is defined implicitly by what `app.js` reads (see below).
37
+ - **`links.json`** β€” routing targets: HF Space ids for the benchmark tools (`spaces`) and
38
+ optional collection links. Hand-maintained.
39
+
40
+ ### Data flow: `generate_models_json.py` β†’ `models.json` β†’ `app.js`
41
+
42
+ `generate_models_json.py` is the source of truth for the catalog schema. It scrapes the public
43
+ HF API for an org (default `Renesas`) and, for each model repo, reads:
44
+
45
+ - the repo info + recursive file tree (public even for gated repos),
46
+ - the root `.metadata.yaml` (identity: name, upstream, architecture, modality),
47
+ - each `<artifact>/.metadata.yaml` (per-precision artifact, e.g. `fp16`, `w4a16`),
48
+ - each `<artifact>/benchmarks/<chip>_<ep>.yaml` (the actual benchmark numbers).
49
+
50
+ Key transforms to know when changing either side:
51
+
52
+ - **Repos are grouped into one logical model** by `group_key()` (strips `-ONNX`/`-GGUF`
53
+ suffix). One model entry can therefore carry both `onnx_repo` + `gguf_repo` and their
54
+ separate `*_artifacts` lists.
55
+ - **`key`** is the join id between Python and JS β€” `app.js` builds `KEY_TO_MODEL` keyed by it.
56
+ The generator guarantees uniqueness (collisions get a `-2` suffix), because a duplicate key
57
+ silently drops a model from the dashboard.
58
+ - **Execution provider β†’ target/unit** mapping lives in `EP_TO_UNIT` (`cpu`/`npu`/`dsp`).
59
+ Unknown EPs default to NPU with a warning β€” add new accelerators there.
60
+ - **`benchmark.type`** decides where numbers land: values in `HIL_TYPES` β†’ the `hil` block
61
+ (measured on hardware), everything else β†’ the `estimate` block. The UI shows both per
62
+ artifact per hardware config under `hardware_configs[].metrics.artifacts[<art>].{estimate,hil}`.
63
+ - Artifacts that actually have measured data are sorted first, because the table/Explore open
64
+ on the first artifact and should show real numbers, not `β€”`.
65
+
66
+ ### `app.js` UI (three sections, switched by the top nav)
67
+
68
+ `app.js` is a single file of plain functions (no modules). The three sections map to the
69
+ `<section>` blocks in `index.html`:
70
+
71
+ 1. **Overview** (`initOverview`, `drawConstellation`) β€” a `<canvas>` scatter plotting every
72
+ model/artifact by throughput vs size, plus stat tiles and a "recently updated" carousel.
73
+ 2. **Catalog** (`initCatalog`, `renderCatalog`) β€” faceted card grid / table view with
74
+ search, sort, and a filter rail built from the catalog (`buildFacetRail`, `modelMatches`).
75
+ 3. **Explore** (`updateExploreAny`, `renderActions`) β€” the routing tool: pick
76
+ architecture/modality/format/target/artifact and get the `hf download` command and a link to
77
+ the matching benchmark Space from `links.json`. Format↔purpose rules are enforced by
78
+ `validatePurpose` (PerformanceEstimate/ORT = ONNX-only; llama.cpp = GGUF-only).
79
+
80
+ A shared **model-details modal** (`openModelModal`) is reused by the Catalog table and cards.
81
+
82
+ Global mutable state (`CATALOG`, `KEY_TO_MODEL`, `LINKS`, the `*_SELECTED_ARTIFACT` vars) is
83
+ module-level. Defaults (`DEFAULT_FMT`, `DEFAULT_COMPUTE`, etc.) are derived from the loaded
84
+ catalog so the UI always opens on data that actually exists.
85
+
86
+ ## Conventions
87
+
88
+ - Build HTML via string templates; **always pass user/catalog strings through `esc()`** (the
89
+ existing builders do β€” match that).
90
+ - Element lookups use the `$`/`$$` helpers; missing data renders as `β€”` (`safeList`, null-guards
91
+ everywhere) rather than throwing.
92
+ - The on-page disclaimer banner shows `catalog_note` / `generated_at` from `models.json`.
README.md CHANGED
@@ -7,20 +7,40 @@ sdk: static
7
  pinned: true
8
  short_description: Find models and route to the correct estimation or benchmark
9
  ---
10
- # Renesas AI Dashboard (POC)
11
 
12
- > ⚠️ **DEMO / POC ONLY β€” FAKE DATA**
13
- > This Space uses **faked models** and **placeholder metrics** for UI prototyping.
14
- > Model names and numbers may **not match real repositories or real performance data**.
15
- > Do not use this Space for performance claims.
 
16
 
17
  ## Semantics
18
  - πŸ“Š **ONNX** β†’ PerformanceEstimate now; later custom ONNX Runtime provider benchmarking
19
  - πŸ§ͺ **GGUF** β†’ llama.cpp execution benchmarking (**CPU-only** / **NPU-only**)
20
 
21
- ## How to update (POC)
22
- - Add/edit entries in `models.json`
23
- - Update routing links in `links.json`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
7
  pinned: true
8
  short_description: Find models and route to the correct estimation or benchmark
9
  ---
10
+ # Renesas AI Dashboard
11
 
12
+ The catalog (`models.json`) is **auto-generated from the public Hugging Face
13
+ `Renesas` org repositories** β€” model identity, artifacts (e.g. `fp16` / `w4a16`),
14
+ hardware configs and benchmark numbers all come straight from each repo's
15
+ `.metadata.yaml` and `<artifact>/benchmarks/<chip>_<ep>.yaml` files. No hand
16
+ editing required.
17
 
18
  ## Semantics
19
  - πŸ“Š **ONNX** β†’ PerformanceEstimate now; later custom ONNX Runtime provider benchmarking
20
  - πŸ§ͺ **GGUF** β†’ llama.cpp execution benchmarking (**CPU-only** / **NPU-only**)
21
 
22
+ ## How to update the catalog
23
+
24
+ Re-run the generator whenever the repos change:
25
+
26
+ ```bash
27
+ pip install requests pyyaml
28
+ python generate_models_json.py # rewrites models.json
29
+ ```
30
+
31
+ - **Gated repos** (e.g. `Llama-3.2-1B/3B-Instruct-GGUF`): their listing is public,
32
+ but their benchmark numbers need an authorized token. Provide one via
33
+ `--token hf_xxx`, the `HF_TOKEN` env var, or a prior `huggingface-cli login`
34
+ (the cached token is picked up automatically). Without access, those repos are
35
+ still listed but their metrics are reported as skipped.
36
+ - Useful flags: `--org <owner>` (default `Renesas`), `--output <path>`,
37
+ `--dry-run` (print, don't write), `-v` (verbose). Exit codes: `0` ok,
38
+ `2` network/API error, `3` write error, `4` produced 0 models (refused to
39
+ overwrite β€” use `--force`).
40
+ - Update routing links (benchmark Spaces) in `links.json`.
41
+
42
+ > The on-page banner shows when the catalog was generated; metrics reflect each
43
+ > repo's published benchmark YAML at that time.
44
 
45
 
46
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
__pycache__/generate_models_json.cpython-313.pyc ADDED
Binary file (45.9 kB). View file
 
app.js CHANGED
@@ -21,7 +21,7 @@ function esc(str) {
21
 
22
  let CATALOG_ROOT = { models: [] };
23
  let CATALOG = [];
24
- let CATALOG_NOTE = "POC ONLY: Fake models/metrics for UI prototyping. Not real performance data.";
25
  let LINKS = { spaces: {}, collections: {} };
26
 
27
  let KEY_TO_MODEL = {};
@@ -90,10 +90,10 @@ function hwBadgesHTML(targets) {
90
  function repoButtonsSmallHTML(model) {
91
  const parts = [];
92
  if (model.onnx_repo) {
93
- parts.push(`<a class="link small" href="${esc(hfModelUrl(model.onnx_repo))}" target="_blank" rel="noopener noreferrer">ONNX β†—</a>`);
94
  }
95
  if (model.gguf_repo) {
96
- parts.push(`<a class="link small" href="${esc(hfModelUrl(model.gguf_repo))}" target="_blank" rel="noopener noreferrer">GGUF β†—</a>`);
97
  }
98
  return `<div class="badges" style="gap:8px;">${parts.join("") || `<span class="subtle">β€”</span>`}</div>`;
99
  }
@@ -195,6 +195,33 @@ function getArtifactsFor(modelKey, fmt) {
195
  return (fmt === "ONNX") ? safeList(m.onnx_artifacts, []) : safeList(m.gguf_artifacts, []);
196
  }
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  /**
199
  * Artifact-layer aware:
200
  * - Preferred: cfg.metrics.artifacts[artifact].estimate/hil
@@ -315,6 +342,16 @@ function readRadio(name, fallback) {
315
  return el ? el.value : fallback;
316
  }
317
 
 
 
 
 
 
 
 
 
 
 
318
  function readChecks(name) {
319
  return Array.from(document.querySelectorAll(`input[name="${CSS.escape(name)}"]:checked`)).map(x => x.value);
320
  }
@@ -353,7 +390,7 @@ function refreshModelList(keepSelection = true) {
353
 
354
  /* ---------- Mini per-model scatter (accuracy vs runtime) (Explore only) ---------- */
355
  function palette(i) {
356
- const base = ["#4c6ef5", "#16a34a", "#f59e0b", "#ef4444", "#06b6d4", "#a855f7", "#f97316", "#10b981"];
357
  return base[i % base.length];
358
  }
359
 
@@ -404,7 +441,7 @@ function drawArtifactScatter(canvas, points) {
404
  const innerH = Math.max(1, h - pad.t - pad.b);
405
 
406
  if (!points.length) {
407
- ctx.fillStyle = "rgba(31,41,55,0.65)";
408
  ctx.font = "13px system-ui, -apple-system, Segoe UI, Roboto, Arial";
409
  ctx.fillText("No artifact points available.", pad.l, pad.t + 24);
410
  ARTIFACT_POINTS = [];
@@ -425,9 +462,9 @@ function drawArtifactScatter(canvas, points) {
425
 
426
  const ticks = 4;
427
 
428
- ctx.strokeStyle = "rgba(17,24,39,0.08)";
429
  ctx.lineWidth = 1;
430
- ctx.fillStyle = "rgba(31,41,55,0.75)";
431
  ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Arial";
432
 
433
  for (let i = 0; i <= ticks; i++) {
@@ -447,7 +484,7 @@ function drawArtifactScatter(canvas, points) {
447
  ctx.fillText(`${Math.round(val)} ms`, tx - 18, h - 16);
448
  }
449
 
450
- ctx.fillStyle = "rgba(31,41,55,0.60)";
451
  ctx.font = "11px system-ui, -apple-system, Segoe UI, Roboto, Arial";
452
  ctx.fillText("Runtime total (ms) β€” wheel=zoom β€’ drag=pan β€’ dblclick=reset", pad.l, h - 4);
453
  ctx.fillText("Accuracy", pad.l, 12);
@@ -530,13 +567,39 @@ function renderDetails(modelKey, cfgId, fmt, compute, purpose, artifact) {
530
  const cfgTargets = safeList(cfg.targets, safeList(m.targets, []));
531
  const sizeM = m.model_size_m;
532
 
533
- const arts = getArtifactsFor(modelKey, fmt);
534
  const safeArt = (artifact && arts.includes(artifact)) ? artifact : (arts[0] || "default");
535
  const { estimate: est, hil } = metricsForArtifact(cfg.metrics || {}, safeArt);
536
 
537
  const onnxRepo = m.onnx_repo || "";
538
  const ggufRepo = m.gguf_repo || "";
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  return `
541
  <div class="md">
542
  <h3>Summary</h3>
@@ -552,47 +615,54 @@ function renderDetails(modelKey, cfgId, fmt, compute, purpose, artifact) {
552
  <li><b>Config Targets:</b> <code>${esc(cfgTargets.join(", ") || "β€”")}</code></li>
553
  <li><b>Format:</b> <code>${esc(fmt)}</code></li>
554
  <li><b>Compute:</b> <code>${esc(compute)}</code></li>
555
- <li><b>Precision/Quant:</b> <code>${esc(safeArt)}</code></li>
556
  </ul>
 
 
 
 
 
 
 
557
 
558
- <div style="margin-top:12px;">
559
- <div class="subtle" style="font-weight:950; margin-bottom:8px;">
560
- Available precision/quant (click to select)
561
- </div>
562
 
563
- <div class="chips" role="list" aria-label="Available precision/quant">
564
- ${arts.length
565
- ? arts.map(a => `
 
 
 
 
 
566
  <button type="button"
567
- class="chip-btn ${a === safeArt ? "active" : ""}"
568
  data-artifact="${esc(a)}"
569
- aria-pressed="${a === safeArt ? "true" : "false"}"
570
  title="Select ${esc(a)}"
571
- >${esc(a)}</button>
572
- `).join("")
573
- : `<span class="subtle">β€”</span>`
574
- }
575
- </div>
 
 
 
576
  </div>
577
  </div>
578
 
579
- <div class="repo-buttons" aria-label="Model repos">
580
- ${onnxRepo ? `<a class="link" href="${esc(hfModelUrl(onnxRepo))}" target="_blank" rel="noopener noreferrer">Open ONNX Repo β†—</a>` : `<span class="subtle">ONNX: β€”</span>`}
581
- ${ggufRepo ? `<a class="link" href="${esc(hfModelUrl(ggufRepo))}" target="_blank" rel="noopener noreferrer">Open GGUF Repo β†—</a>` : `<span class="subtle">GGUF: β€”</span>`}
 
 
582
  </div>
583
- </div>
584
 
585
- <h3>Metrics (POC)</h3>
586
- <div class="metric-banner">Precision/Quant: <code>${esc(safeArt)}</code></div>
587
-
588
- <div class="metric-grid">
589
- <div class="metric-card">
590
- <div class="metric-card-title">Estimate</div>
591
- <pre class="metric-pre">${esc(metricSummaryV2(est))}</pre>
592
- </div>
593
- <div class="metric-card">
594
- <div class="metric-card-title">HIL</div>
595
- <pre class="metric-pre">${esc(metricSummaryV2(hil))}</pre>
596
  </div>
597
  </div>
598
  </div>
@@ -640,9 +710,8 @@ function buildArtifactAccuracyRuntimePoints(modelKey, cfgId, fmt) {
640
  }
641
 
642
  /**
643
- * mode:
644
- * - "explore": includes Accuracy vs Runtime plot
645
- * - "modal": omits that section (requested)
646
  */
647
  function renderActions(modelKey, fmt, compute, purpose, artifact, cfgId, mode = "explore") {
648
  const m = KEY_TO_MODEL[modelKey];
@@ -655,52 +724,10 @@ function renderActions(modelKey, fmt, compute, purpose, artifact, cfgId, mode =
655
  const include = downloadIncludePattern(fmt, artifact);
656
  const cmd = repoId ? `hf download ${repoId} --repo-type=model --include "${include}"` : "";
657
 
658
- const sp = (LINKS || {}).spaces || {};
659
- let toolSection = "";
660
- if (purpose === "ORT Benchmarking") {
661
- const sid = sp.ort_benchmark;
662
- toolSection = sid
663
- ? `<h3>Tool</h3><a class="link" href="${esc(hfSpaceUrl(sid))}" target="_blank" rel="noopener noreferrer">Open ORT Benchmark Space β†—</a>`
664
- : `<h3>Tool</h3><i>ORT Benchmark Space: (set links.json)</i>`;
665
- } else if (purpose === "llama.cpp Benchmarking") {
666
- const sid = sp.gguf_llamacpp_benchmark;
667
- toolSection = sid
668
- ? `<h3>Tool</h3><a class="link" href="${esc(hfSpaceUrl(sid))}" target="_blank" rel="noopener noreferrer">Open llama.cpp Benchmark Space β†—</a>`
669
- : `<h3>Tool</h3><i>llama.cpp Benchmark Space: (set links.json)</i>`;
670
- }
671
-
672
- let accRuntimeSection = "";
673
- if (mode === "explore") {
674
- const { points, allAccMissing } = buildArtifactAccuracyRuntimePoints(modelKey, cfgId, fmt);
675
- window.__artifactPlotPoints = points;
676
-
677
- const accWarn = allAccMissing ? `
678
- <div class="note note-warn tight" style="margin-top:10px;">
679
- <b>Accuracy not yet published.</b> Plot uses <b>0</b> for accuracy until values are available.
680
- </div>
681
- ` : "";
682
-
683
- accRuntimeSection = `
684
- <h3 style="margin-top:14px;">Precision/Quant β€” Accuracy vs Runtime</h3>
685
- ${accWarn}
686
- <div class="mini-chart-wrap">
687
- <div class="mini-chart-head">
688
- <div class="mini-chart-title">Accuracy vs Runtime total</div>
689
- <div class="mini-chart-subtle">${points.length ? `${points.length} variant(s)` : "No points"}</div>
690
- </div>
691
- <div style="padding:10px;">
692
- <canvas id="artifactCanvas" style="width:100%; height:260px;"></canvas>
693
- <div id="artifactTooltip" class="mini-tooltip"></div>
694
- </div>
695
- </div>
696
- `;
697
- }
698
-
699
  return `
700
  <div class="md">
701
- <h3>Download</h3>
702
  ${repoId ? `
703
- <div class="subtle tight">Downloads files matching: <code>${esc(include)}</code></div>
704
  <div class="cmd-row">
705
  <pre>${esc(cmd)}</pre>
706
  <button class="copy-btn" type="button"
@@ -709,15 +736,11 @@ function renderActions(modelKey, fmt, compute, purpose, artifact, cfgId, mode =
709
  aria-label="Copy download command"
710
  title="Copy to clipboard"
711
  >
712
- <span class="icon">πŸ“‹</span>
713
  <span class="txt">Copy</span>
714
  </button>
715
  </div>
716
  ` : `<i>Repo not set for the selected format (${esc(fmt)}).</i>`}
717
-
718
- ${accRuntimeSection}
719
-
720
- ${toolSection ? `<div style="margin-top:12px;">${toolSection}</div>` : ""}
721
  </div>
722
  `;
723
  }
@@ -729,13 +752,15 @@ function setActiveSection(targetId) {
729
  $$(".nav-item").forEach(btn => btn.classList.remove("active"));
730
  document.querySelector(`.nav-item[data-target="${CSS.escape(targetId)}"]`)?.classList.add("active");
731
 
732
- if (targetId === "sec-all") requestAnimationFrame(() => setupPinnedColumns($("modelsTable"), 4));
733
- if (targetId === "sec-scatter") requestAnimationFrame(() => updateScatter());
 
 
734
  }
735
 
736
  /* ---------- Explore pipeline ---------- */
737
  function ensureExploreArtifact(modelKey, fmt) {
738
- const arts = getArtifactsFor(modelKey, fmt);
739
  if (!arts.length) { EXPLORE_SELECTED_ARTIFACT = null; return null; }
740
  if (!EXPLORE_SELECTED_ARTIFACT || !arts.includes(EXPLORE_SELECTED_ARTIFACT)) EXPLORE_SELECTED_ARTIFACT = arts[0];
741
  return EXPLORE_SELECTED_ARTIFACT;
@@ -750,6 +775,30 @@ function updateExploreFromFilters() {
750
  refreshModelList(false);
751
  }
752
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  function updateExploreOnModelChange() {
754
  const key = $("modelKey").value;
755
  const m = KEY_TO_MODEL[key];
@@ -758,7 +807,17 @@ function updateExploreOnModelChange() {
758
  const def = ids.length ? ids[0] : null;
759
  fillSelect($("hwCfg"), ids, def);
760
 
761
- const fmt = readRadio("fmt", "ONNX");
 
 
 
 
 
 
 
 
 
 
762
  ensureExploreArtifact(key, fmt);
763
  updateExploreAny();
764
  }
@@ -792,27 +851,6 @@ function updateExploreAny() {
792
 
793
  $("details").innerHTML = renderDetails(key, cfgId, fmt, compute, purpose, artifact);
794
  $("actions").innerHTML = renderActions(key, fmt, compute, purpose, artifact, cfgId, "explore");
795
-
796
- requestAnimationFrame(() => {
797
- const canvas = $("artifactCanvas");
798
- if (!canvas) return;
799
-
800
- drawArtifactScatter(canvas, window.__artifactPlotPoints || []);
801
-
802
- const tip = $("artifactTooltip");
803
- if (!tip) return;
804
-
805
- if (canvas.dataset.bound !== "1") {
806
- canvas.dataset.bound = "1";
807
-
808
- canvas.addEventListener("mousemove", (evt) => {
809
- const hit = hitTestArtifact(evt, canvas);
810
- if (!hit) return hideArtifactTip(tip);
811
- showArtifactTip(hit, evt, tip, canvas);
812
- });
813
- canvas.addEventListener("mouseleave", () => hideArtifactTip(tip));
814
- }
815
- });
816
  }
817
 
818
  /* ---------- Modal (All Models -> Details) ---------- */
@@ -820,7 +858,7 @@ function updateModalAny() {
820
  if (!MODAL_CTX) return;
821
  const { key, cfgId, fmt, compute, purpose } = MODAL_CTX;
822
 
823
- const arts = getArtifactsFor(key, fmt);
824
  if (!arts.length) MODAL_SELECTED_ARTIFACT = null;
825
  else if (!MODAL_SELECTED_ARTIFACT || !arts.includes(MODAL_SELECTED_ARTIFACT)) MODAL_SELECTED_ARTIFACT = arts[0];
826
 
@@ -829,8 +867,6 @@ function updateModalAny() {
829
  }
830
 
831
  /* ---------- All Models (table) ---------- */
832
- let FULL_ROWS = [];
833
- let FILTERED_ROWS = [];
834
 
835
  function defaultRowFmt(model) {
836
  return model.onnx_repo ? "ONNX" : (model.gguf_repo ? "GGUF" : "ONNX");
@@ -852,7 +888,6 @@ function buildTableRows(models) {
852
  const defArt = defaultRowArtifact(m, fmt);
853
 
854
  const { estimate, hil } = metricsForArtifact(cfg.metrics || {}, defArt);
855
- const cfgTargets = safeList(cfg.targets, safeList(m.targets, []));
856
 
857
  const detailsBtn = `<button class="mini-btn" type="button"
858
  data-open-model="1"
@@ -866,11 +901,9 @@ function buildTableRows(models) {
866
  _cfg: cfg.id || "",
867
  _fmt: fmt,
868
 
869
- "Name": esc(m.display_name || m.key || ""),
870
  "Modality": esc(m.modality || "LLM"),
871
- "Task": "",
872
- "Format": formatBadgesHTML(m),
873
- "Supported HW": hwBadgesHTML(cfgTargets.length ? cfgTargets : safeList(m.targets, [])),
874
  "Repo": repoButtonsSmallHTML(m),
875
  "Precision/Quant": artifactsBadgesHTML(m),
876
  "Estimate": metricBriefHTML(estimate),
@@ -882,38 +915,6 @@ function buildTableRows(models) {
882
  return rows;
883
  }
884
 
885
- function applyAllFilters(rows, modality, formatFilter, hwFilter, needEst, needHil, searchQ) {
886
- let out = rows.slice();
887
-
888
- if (modality !== "All") out = out.filter(r => r["Modality"] === modality);
889
-
890
- const fmtList = safeList(formatFilter, []);
891
- if (fmtList.length) {
892
- out = out.filter(r => fmtList.some(f => String(r["Format"] || "").toUpperCase().includes(f)));
893
- }
894
-
895
- const hwList = safeList(hwFilter, []);
896
- if (hwList.length) {
897
- out = out.filter(r => hwList.some(h => String(r["Supported HW"] || "").toUpperCase().includes(h)));
898
- }
899
-
900
- if (needEst) out = out.filter(r => !String(r["Estimate"] || "").includes("β€”"));
901
- if (needHil) out = out.filter(r => !String(r["HIL"] || "").includes("β€”"));
902
-
903
- const q = String(searchQ || "").trim().toLowerCase();
904
- if (q) {
905
- out = out.filter(r => {
906
- const flat = Object.entries(r)
907
- .filter(([k]) => !k.startsWith("_"))
908
- .map(([, v]) => String(v ?? ""))
909
- .join(" | ")
910
- .toLowerCase();
911
- return flat.includes(q);
912
- });
913
- }
914
- return out;
915
- }
916
-
917
  function renderTable(rows) {
918
  const table = $("modelsTable");
919
  if (!table) return;
@@ -927,30 +928,34 @@ function renderTable(rows) {
927
  }
928
 
929
  const cols = Object.keys(rows[0]).filter(k => !k.startsWith("_"));
930
- const pinCount = 4; // Name, Modality, Task, Format
931
 
932
  thead.innerHTML = `<tr>${cols.map((c, i) => {
933
  const isPinned = i < pinCount;
934
  const isDetails = (c === "");
935
- const cls = `${isPinned ? "pin" : ""}${isDetails ? " details-col" : ""}`.trim();
 
936
  const pinAttr = isPinned ? ` data-pin="${i}"` : "";
937
  const header = isDetails ? "" : esc(c);
938
  return `<th class="${cls}"${pinAttr}>${header}</th>`;
939
  }).join("")}</tr>`;
940
 
941
- const htmlCols = new Set(["Format", "Supported HW", "Repo", "Precision/Quant", "Estimate", "HIL", ""]);
942
 
943
  tbody.innerHTML = rows.map((r, ri) => {
944
  return `<tr data-row="${ri}" data-key="${esc(r._key)}" data-cfg="${esc(r._cfg)}" data-fmt="${esc(r._fmt)}">${
945
  cols.map((c, i) => {
946
  const isPinned = i < pinCount;
947
  const isDetails = (c === "");
948
- const cls = `${isPinned ? "pin" : ""}${isDetails ? " details-col" : ""}`.trim();
 
949
  const pinAttr = isPinned ? ` data-pin="${i}"` : "";
 
 
950
  const val = r[c];
951
 
952
- if (htmlCols.has(c)) return `<td class="${cls}"${pinAttr}>${val ?? ""}</td>`;
953
- return `<td class="${cls}"${pinAttr}>${esc(val)}</td>`;
954
  }).join("")
955
  }</tr>`;
956
  }).join("\n");
@@ -976,19 +981,6 @@ function setupPinnedColumns(table, count) {
976
  }
977
  }
978
 
979
- function updateAllModels() {
980
- const modality = $("t_modality").value;
981
- const fmtList = readChecks("t_format");
982
- const hwList = readChecks("t_hw");
983
- const needEst = $("t_need_est").checked;
984
- const needHil = $("t_need_hil").checked;
985
- const searchQ = $("t_search").value;
986
-
987
- FILTERED_ROWS = applyAllFilters(FULL_ROWS, modality, fmtList, hwList, needEst, needHil, searchQ);
988
- $("t_status").innerHTML = `Showing <b>${FILTERED_ROWS.length}</b> / <b>${FULL_ROWS.length}</b> models.`;
989
- renderTable(FILTERED_ROWS);
990
- }
991
-
992
  /* ---------- Model modal ---------- */
993
  function openModelModal(modelKey, cfgId, fmt) {
994
  const m = KEY_TO_MODEL[modelKey];
@@ -1022,224 +1014,559 @@ function closeModelModal() {
1022
  MODAL_SELECTED_ARTIFACT = null;
1023
  }
1024
 
1025
- /* ---------- Scatter tab (unchanged) ---------- */
1026
- function latencyForCompute(block, compute) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1027
  if (!block) return null;
1028
- const unit = block[(compute || "").toLowerCase()];
1029
- if (!unit) return null;
1030
- const lat = unit.latency_ms_p50;
1031
- return (lat != null) ? Number(lat) : null;
1032
  }
1033
 
1034
- function buildScatterRows(arch, modality, variant, compute, metricSource, colorBy, pointMode) {
1035
- const rows = [];
 
 
1036
 
 
 
1037
  for (const m of CATALOG) {
1038
- if (arch !== "All" && (m.architecture || "Unknown") !== arch) continue;
1039
- if (modality !== "All" && (m.modality || "LLM") !== modality) continue;
1040
- if (variant !== "All" && (m.variant || "Default") !== variant) continue;
1041
-
1042
- const sizeM = m.model_size_m;
1043
- if (sizeM == null) continue;
1044
-
1045
- const cfgs = safeList(m.hardware_configs, []);
1046
- if (!cfgs.length) continue;
1047
-
1048
- const defArt = safeList(m.onnx_artifacts, [])[0] || safeList(m.gguf_artifacts, [])[0] || null;
1049
-
1050
- const addRow = (cfg, suffix = "") => {
1051
- const { estimate, hil } = metricsForArtifact(cfg.metrics || {}, defArt);
1052
-
1053
- let block = null;
1054
- let label = "β€”";
1055
- if (metricSource === "Estimate") { block = estimate; label = "Estimate"; }
1056
- else if (metricSource === "HIL") { block = hil; label = "HIL"; }
1057
- else { block = hil || estimate; label = hil ? "HIL" : (estimate ? "Estimate" : "β€”"); }
1058
-
1059
- if (!block) return;
1060
-
1061
- const lat = latencyForCompute(block, compute);
1062
- if (lat == null) return;
1063
-
1064
- const fmtBadge = (m.onnx_repo ? "ONNX" : "") + (m.gguf_repo ? (m.onnx_repo ? "+GGUF" : "GGUF") : "");
1065
- const hwBadge = safeList(cfg.targets, safeList(m.targets, [])).join("+") || "β€”";
1066
-
1067
- let colorVal = m.modality || "LLM";
1068
- if (colorBy === "Architecture") colorVal = m.architecture || "Unknown";
1069
- else if (colorBy === "Modality") colorVal = m.modality || "LLM";
1070
- else if (colorBy === "Variant") colorVal = m.variant || "Default";
1071
- else if (colorBy === "Format") colorVal = fmtBadge || "β€”";
1072
- else if (colorBy === "Supported HW") colorVal = hwBadge || "β€”";
1073
-
1074
- rows.push({
1075
- _key: m.key || "",
1076
- _cfg: cfg.id || "",
1077
- Model: (m.display_name || m.key || "") + suffix,
1078
- "Size (M)": Number(sizeM),
1079
- "Runtime p50 (ms)": Number(lat),
1080
- "Runtime Source": label,
1081
- Compute: compute,
1082
- "HW Config": cfg.id || "",
1083
- "HW Label": cfg.label || cfg.id || "",
1084
- Architecture: m.architecture || "Unknown",
1085
- Modality: m.modality || "LLM",
1086
- Variant: m.variant || "Default",
1087
- Color: String(colorVal)
1088
- });
1089
- };
1090
-
1091
- if (pointMode === "Per hardware config") {
1092
- for (const cfg of cfgs) addRow(cfg);
1093
- } else {
1094
- let bestCfg = null;
1095
- let bestLat = null;
1096
- for (const cfg of cfgs) {
1097
- const { estimate, hil } = metricsForArtifact(cfg.metrics || {}, defArt);
1098
- const block = (metricSource === "Estimate") ? estimate :
1099
- (metricSource === "HIL") ? hil :
1100
- (hil || estimate);
1101
- if (!block) continue;
1102
- const lat = latencyForCompute(block, compute);
1103
- if (lat == null) continue;
1104
- if (bestLat == null || lat < bestLat) { bestLat = lat; bestCfg = cfg; }
1105
  }
1106
- if (bestCfg && bestLat != null) addRow(bestCfg, " (best)");
1107
  }
1108
  }
 
 
 
 
 
1109
 
1110
- rows.sort((a, b) => (a["Size (M)"] - b["Size (M)"]) || (a["Runtime p50 (ms)"] - b["Runtime p50 (ms)"]));
1111
- return rows;
 
 
 
 
 
 
 
 
 
1112
  }
1113
 
1114
- let SCATTER_POINTS = [];
1115
- function niceCeil(v) {
1116
- const x = (v && isFinite(v)) ? v : 1;
1117
- const pow = Math.pow(10, Math.floor(Math.log10(x || 1)));
1118
- const n = x / pow;
1119
- let nice = 1;
1120
- if (n <= 1) nice = 1;
1121
- else if (n <= 2) nice = 2;
1122
- else if (n <= 5) nice = 5;
1123
- else nice = 10;
1124
- return nice * pow;
1125
- }
1126
- function paletteFor(categories) {
1127
- const base = ["#4c6ef5","#16a34a","#f59e0b","#ef4444","#06b6d4","#a855f7","#f97316","#10b981","#3b82f6","#64748b"];
1128
- const map = {}; let i = 0;
1129
- for (const c of categories) { map[c] = base[i % base.length]; i++; }
1130
- return map;
1131
- }
1132
- function drawScatter(canvas, rows) {
1133
  const ctx = canvas.getContext("2d");
1134
- const rect = canvas.getBoundingClientRect();
1135
  const dpr = window.devicePixelRatio || 1;
1136
- canvas.width = Math.max(1, Math.floor(rect.width * dpr));
1137
- canvas.height = Math.max(1, Math.floor(rect.height * dpr));
1138
- ctx.setTransform(dpr,0,0,dpr,0,0);
1139
-
1140
- const w = rect.width, h = rect.height;
1141
- ctx.clearRect(0,0,w,h);
1142
 
1143
- const pad = { l: 62, r: 18, t: 18, b: 52 };
1144
  const innerW = Math.max(1, w - pad.l - pad.r);
1145
  const innerH = Math.max(1, h - pad.t - pad.b);
 
 
 
 
1146
 
1147
- if (!rows.length) { SCATTER_POINTS = []; return; }
1148
-
1149
- const xs = rows.map(r => r["Size (M)"]);
1150
- const ys = rows.map(r => r["Runtime p50 (ms)"]);
1151
- const xMin = Math.min(...xs), xMax = Math.max(...xs);
1152
- const yMin = 0, yMax = niceCeil(Math.max(...ys));
1153
 
1154
- const xOf = (x) => (xMax === xMin) ? pad.l + innerW/2 : pad.l + ((x - xMin)/(xMax - xMin))*innerW;
1155
- const yOf = (y) => pad.t + innerH - ((y - yMin)/(yMax - yMin))*innerH;
 
 
 
 
 
 
1156
 
1157
- const cats = uniqueSorted(rows.map(r => r.Color));
1158
- const pal = paletteFor(cats);
1159
 
1160
- SCATTER_POINTS = [];
1161
- for (const r of rows) {
1162
- const x = xOf(r["Size (M)"]);
1163
- const y = yOf(r["Runtime p50 (ms)"]);
1164
- const col = pal[r.Color] || "#4c6ef5";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
 
1166
- ctx.beginPath();
1167
- ctx.fillStyle = col + "B3";
1168
- ctx.strokeStyle = col;
1169
- ctx.lineWidth = 1.5;
1170
- ctx.arc(x, y, 6.5, 0, Math.PI * 2);
1171
- ctx.fill();
1172
- ctx.stroke();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1173
 
1174
- SCATTER_POINTS.push({ x, y, r: 10, row: r, color: col });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  }
1176
- }
1177
- function hitTestScatter(evt) {
1178
- const canvas = $("scatterCanvas");
1179
- if (!canvas) return null;
1180
- const rect = canvas.getBoundingClientRect();
1181
- const x = evt.clientX - rect.left;
1182
- const y = evt.clientY - rect.top;
1183
- let best = null, bestD2 = Infinity;
1184
- for (const p of SCATTER_POINTS) {
1185
- const dx = x - p.x, dy = y - p.y;
1186
- const d2 = dx*dx + dy*dy;
1187
- if (d2 <= p.r*p.r && d2 < bestD2) { best = p; bestD2 = d2; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
  }
1189
  return best;
1190
  }
1191
- function showTooltip(p, evt) {
1192
- const tip = $("scatterTooltip");
1193
- if (!tip || !p) return;
1194
- const wrap = $("scatterCanvas").parentElement;
1195
- const rect = wrap.getBoundingClientRect();
1196
- const x = evt.clientX - rect.left;
1197
- const y = evt.clientY - rect.top;
1198
- const r = p.row;
1199
- tip.innerHTML = `<b>${esc(r.Model)}<\/b><br/>
1200
- Size: ${esc(r["Size (M)"])}M<br/>
1201
- p50: ${esc(r["Runtime p50 (ms)"])} ms<br/>
1202
- HW: ${esc(r["HW Config"])} β€” ${esc(r["HW Label"])}<br/>
1203
- Source: ${esc(r["Runtime Source"])}
1204
- `;
1205
- tip.style.display = "block";
1206
- tip.style.left = `${Math.min(x + 14, rect.width - 340)}px`;
1207
- tip.style.top = `${Math.max(8, y - 8)}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1208
  }
1209
- function hideTooltip(){ const tip = $("scatterTooltip"); if (tip) tip.style.display = "none"; }
1210
 
1211
- let SCATTER_ROWS = [];
 
 
 
 
 
 
 
1212
 
1213
- function updateScatter() {
1214
- const arch = $("s_arch").value;
1215
- const modality = $("s_modality").value;
1216
- const variant = $("s_variant").value;
1217
- const compute = readRadio("s_compute", "CPU");
1218
- const src = readRadio("s_source", "Best available");
1219
- const pointMode = readRadio("s_point_mode", "Per hardware config");
1220
- const colorBy = $("s_color_by").value;
1221
 
1222
- SCATTER_ROWS = buildScatterRows(arch, modality, variant, compute, src, colorBy, pointMode);
1223
- $("s_status").innerHTML = `Plotted <b>${SCATTER_ROWS.length}</b> point(s) for <b>${esc(compute)}</b> using <b>${esc(src)}</b>.`;
1224
 
1225
- const canvas = $("scatterCanvas");
1226
- if (canvas) drawScatter(canvas, SCATTER_ROWS);
1227
- $("s_details").innerHTML = "Select a point to see details.";
1228
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1229
 
1230
- function onScatterMove(evt) { const hit = hitTestScatter(evt); if (!hit) return hideTooltip(); showTooltip(hit, evt); }
1231
- function onScatterLeave(){ hideTooltip(); }
1232
- function onScatterClick(evt){
1233
- const hit = hitTestScatter(evt);
1234
- if (!hit) return;
1235
- $("s_details").innerHTML = renderDetails(
1236
- hit.row._key,
1237
- hit.row._cfg,
1238
- "ONNX",
1239
- hit.row.Compute,
1240
- defaultPurposeForFormat("ONNX"),
1241
- safeList(KEY_TO_MODEL[hit.row._key]?.onnx_artifacts, [])[0] || null
1242
- );
1243
  }
1244
 
1245
  /* ---------- Boot ---------- */
@@ -1343,14 +1670,20 @@ async function init() {
1343
  MODALITIES = ["All"].concat(uniqueSorted(CATALOG.map(m => m.modality || "LLM")));
1344
  VARIANTS = ["All"].concat(uniqueSorted(CATALOG.map(m => m.variant || "Default")));
1345
 
1346
- $("disclaimer").innerHTML = `<b>⚠️ POC ONLY β€” FAKE MODELS &amp; FAKE METRICS</b><br/>${esc(CATALOG_NOTE)}`;
 
 
 
 
 
 
1347
 
1348
  /* Explore init */
1349
  fillSelect($("arch"), ARCHS, "All");
1350
  buildRadioGroup($("modality"), "modality", MODALITIES, "All");
1351
- buildRadioGroup($("fmt"), "fmt", ["ONNX", "GGUF"], "ONNX");
1352
- buildRadioGroup($("compute"), "compute", ["CPU", "NPU", "DSP"], "CPU");
1353
- buildRadioGroup($("purpose"), "purpose", ["PerformanceEstimate", "ORT Benchmarking", "llama.cpp Benchmarking"], "PerformanceEstimate");
1354
 
1355
  EXPLORE_KEYS = filteredKeys("All", "All", "All");
1356
  updateExploreFromFilters();
@@ -1366,62 +1699,16 @@ async function init() {
1366
  $("modelKey").addEventListener("change", updateExploreOnModelChange);
1367
  $("hwCfg").addEventListener("change", updateExploreAny);
1368
 
1369
- /* All models init */
1370
- fillSelect($("t_modality"), MODALITIES, "All");
1371
- buildCheckGroup($("t_format"), "t_format", ALL_FORMATS, []);
1372
- buildCheckGroup($("t_hw"), "t_hw", ALL_HW, []);
1373
 
1374
- FULL_ROWS = buildTableRows(CATALOG);
1375
- updateAllModels();
1376
-
1377
- $("t_modality").addEventListener("change", updateAllModels);
1378
- $("t_need_est").addEventListener("change", updateAllModels);
1379
- $("t_need_hil").addEventListener("change", updateAllModels);
1380
-
1381
- $("t_search").addEventListener("input", () => {
1382
- clearTimeout(window.__tSearchT);
1383
- window.__tSearchT = setTimeout(updateAllModels, 80);
1384
- });
1385
-
1386
- document.addEventListener("change", (e) => {
1387
- if (e.target && e.target.name === "t_format") { e.target.closest(".pill")?.classList.toggle("active", e.target.checked); updateAllModels(); }
1388
- if (e.target && e.target.name === "t_hw") { e.target.closest(".pill")?.classList.toggle("active", e.target.checked); updateAllModels(); }
1389
- });
1390
-
1391
- /* Scatter init */
1392
- fillSelect($("s_arch"), ARCHS, "All");
1393
- fillSelect($("s_modality"), MODALITIES, "All");
1394
- fillSelect($("s_variant"), VARIANTS, "All");
1395
- fillSelect($("s_color_by"), ["Architecture", "Modality", "Variant", "Format", "Supported HW"], "Modality");
1396
- buildRadioGroup($("s_compute"), "s_compute", ["CPU", "NPU", "DSP"], "CPU");
1397
- buildRadioGroup($("s_source"), "s_source", ["Best available", "Estimate", "HIL"], "Best available");
1398
- buildRadioGroup($("s_point_mode"), "s_point_mode", ["Per hardware config", "Per model (best across configs)"], "Per hardware config");
1399
-
1400
- $("s_arch").addEventListener("change", updateScatter);
1401
- $("s_modality").addEventListener("change", updateScatter);
1402
- $("s_variant").addEventListener("change", updateScatter);
1403
- $("s_color_by").addEventListener("change", updateScatter);
1404
-
1405
- document.addEventListener("change", (e) => {
1406
- if (e.target && (e.target.name === "s_compute" || e.target.name === "s_source" || e.target.name === "s_point_mode")) updateScatter();
1407
- });
1408
-
1409
- const sc = $("scatterCanvas");
1410
- sc.addEventListener("mousemove", onScatterMove);
1411
- sc.addEventListener("mouseleave", onScatterLeave);
1412
- sc.addEventListener("click", onScatterClick);
1413
 
1414
  window.addEventListener("resize", () => {
1415
- setupPinnedColumns($("modelsTable"), 4);
1416
- updateScatter();
1417
- requestAnimationFrame(() => {
1418
- const c = $("artifactCanvas");
1419
- const modalOpen = $("modelModal")?.classList.contains("open");
1420
- if (!modalOpen && c) drawArtifactScatter(c, window.__artifactPlotPoints || []);
1421
- });
1422
  });
1423
-
1424
- updateScatter();
1425
  }
1426
 
1427
  document.addEventListener("DOMContentLoaded", init);
 
21
 
22
  let CATALOG_ROOT = { models: [] };
23
  let CATALOG = [];
24
+ let CATALOG_NOTE = "Auto-generated from Hugging Face repositories.";
25
  let LINKS = { spaces: {}, collections: {} };
26
 
27
  let KEY_TO_MODEL = {};
 
90
  function repoButtonsSmallHTML(model) {
91
  const parts = [];
92
  if (model.onnx_repo) {
93
+ parts.push(`<a class="link small" href="${esc(hfModelUrl(model.onnx_repo))}" target="_blank" rel="noopener noreferrer">ONNX</a>`);
94
  }
95
  if (model.gguf_repo) {
96
+ parts.push(`<a class="link small" href="${esc(hfModelUrl(model.gguf_repo))}" target="_blank" rel="noopener noreferrer">GGUF</a>`);
97
  }
98
  return `<div class="badges" style="gap:8px;">${parts.join("") || `<span class="subtle">β€”</span>`}</div>`;
99
  }
 
195
  return (fmt === "ONNX") ? safeList(m.onnx_artifacts, []) : safeList(m.gguf_artifacts, []);
196
  }
197
 
198
+ /* True only if the artifact actually carries benchmark metadata (from its
199
+ benchmarks/ subfolder β†’ an estimate or hil block). Checked against the
200
+ per-artifact map directly, NOT metricsForArtifact (whose legacy fallback
201
+ would make every artifact look benchmarked). A legacy config with no
202
+ artifacts map at all is treated as benchmarked so it isn't hidden. */
203
+ function artifactHasBenchmark(m, art) {
204
+ for (const cfg of safeList(m?.hardware_configs, [])) {
205
+ const metrics = cfg.metrics || {};
206
+ const artMap = metrics.artifacts;
207
+ if (artMap && typeof artMap === "object") {
208
+ const a = artMap[art];
209
+ if (a && (a.estimate || a.hil)) return true;
210
+ } else if (metrics.estimate || metrics.hil) {
211
+ return true;
212
+ }
213
+ }
214
+ return false;
215
+ }
216
+
217
+ /* Artifacts to actually surface in the results UI: only those with benchmark
218
+ data (e.g. 8B's fp16 folder has no benchmark, so it must not appear). */
219
+ function benchmarkedArtifactsFor(modelKey, fmt) {
220
+ const m = KEY_TO_MODEL[modelKey];
221
+ if (!m) return [];
222
+ return getArtifactsFor(modelKey, fmt).filter(a => artifactHasBenchmark(m, a));
223
+ }
224
+
225
  /**
226
  * Artifact-layer aware:
227
  * - Preferred: cfg.metrics.artifacts[artifact].estimate/hil
 
342
  return el ? el.value : fallback;
343
  }
344
 
345
+ /* Programmatically select a radio option and sync its .pill active state
346
+ (does not fire a change event β€” callers re-render explicitly). */
347
+ function setRadioValue(name, value) {
348
+ document.querySelectorAll(`input[name="${CSS.escape(name)}"]`).forEach(inp => {
349
+ const on = inp.value === value;
350
+ inp.checked = on;
351
+ inp.closest(".pill")?.classList.toggle("active", on);
352
+ });
353
+ }
354
+
355
  function readChecks(name) {
356
  return Array.from(document.querySelectorAll(`input[name="${CSS.escape(name)}"]:checked`)).map(x => x.value);
357
  }
 
390
 
391
  /* ---------- Mini per-model scatter (accuracy vs runtime) (Explore only) ---------- */
392
  function palette(i) {
393
+ const base = ["#C96442", "#6E8B6A", "#C9A24B", "#5B7A99", "#A6573F", "#8E6E9E", "#4C8C7D", "#B07A3C"];
394
  return base[i % base.length];
395
  }
396
 
 
441
  const innerH = Math.max(1, h - pad.t - pad.b);
442
 
443
  if (!points.length) {
444
+ ctx.fillStyle = "rgba(31,30,28,0.62)";
445
  ctx.font = "13px system-ui, -apple-system, Segoe UI, Roboto, Arial";
446
  ctx.fillText("No artifact points available.", pad.l, pad.t + 24);
447
  ARTIFACT_POINTS = [];
 
462
 
463
  const ticks = 4;
464
 
465
+ ctx.strokeStyle = "rgba(31,30,28,0.08)";
466
  ctx.lineWidth = 1;
467
+ ctx.fillStyle = "rgba(31,30,28,0.72)";
468
  ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Arial";
469
 
470
  for (let i = 0; i <= ticks; i++) {
 
484
  ctx.fillText(`${Math.round(val)} ms`, tx - 18, h - 16);
485
  }
486
 
487
+ ctx.fillStyle = "rgba(31,30,28,0.52)";
488
  ctx.font = "11px system-ui, -apple-system, Segoe UI, Roboto, Arial";
489
  ctx.fillText("Runtime total (ms) β€” wheel=zoom β€’ drag=pan β€’ dblclick=reset", pad.l, h - 4);
490
  ctx.fillText("Accuracy", pad.l, 12);
 
567
  const cfgTargets = safeList(cfg.targets, safeList(m.targets, []));
568
  const sizeM = m.model_size_m;
569
 
570
+ const arts = benchmarkedArtifactsFor(modelKey, fmt);
571
  const safeArt = (artifact && arts.includes(artifact)) ? artifact : (arts[0] || "default");
572
  const { estimate: est, hil } = metricsForArtifact(cfg.metrics || {}, safeArt);
573
 
574
  const onnxRepo = m.onnx_repo || "";
575
  const ggufRepo = m.gguf_repo || "";
576
 
577
+ // Est-vs-HIL throughput (tok/s is the metric currently populated) + accuracy
578
+ // for the selected precision (HIL-first). These drive the small graph and the
579
+ // accuracy column of the merged block.
580
+ const estTok = tokSOf(est);
581
+ const hilTok = tokSOf(hil);
582
+ const maxTok = Math.max(estTok || 0, hilTok || 0) || 1;
583
+ const pctOf = (t) => (t != null && t > 0) ? Math.max(6, Math.round((t / maxTok) * 100)) : 0;
584
+ const estPct = pctOf(estTok), hilPct = pctOf(hilTok);
585
+ const graphHasData = (estTok != null || hilTok != null);
586
+
587
+ let acc = accuracyFromBlock(hil), accSrc = "HIL";
588
+ if (acc == null) { acc = accuracyFromBlock(est); accSrc = "Estimate"; }
589
+ const accIsPct = acc != null && acc <= 1.5;
590
+ const accText = acc == null ? "β€”" : `${accIsPct ? Math.round(acc * 1000) / 10 : Math.round(acc * 10) / 10}${accIsPct ? "%" : ""}`;
591
+ const updated = (hil && hil.last_updated) || (est && est.last_updated) || null; // benchmark freshness (HIL-first)
592
+
593
+ const barCol = (label, tok, pct, cls) => `
594
+ <div class="pm-bar-col">
595
+ <div class="pm-bar-val">${tok != null ? esc(Math.round(tok * 10) / 10) : "β€”"}</div>
596
+ <div class="pm-bar-track">
597
+ <div class="pm-bar-spacer" style="flex-grow:${100 - pct}"></div>
598
+ ${tok != null ? `<div class="pm-bar-fill ${cls}" style="flex-grow:${pct}"></div>` : ""}
599
+ </div>
600
+ <div class="pm-bar-lbl">${label}</div>
601
+ </div>`;
602
+
603
  return `
604
  <div class="md">
605
  <h3>Summary</h3>
 
615
  <li><b>Config Targets:</b> <code>${esc(cfgTargets.join(", ") || "β€”")}</code></li>
616
  <li><b>Format:</b> <code>${esc(fmt)}</code></li>
617
  <li><b>Compute:</b> <code>${esc(compute)}</code></li>
 
618
  </ul>
619
+ </div>
620
+
621
+ <div class="repo-buttons" aria-label="Model repos">
622
+ ${onnxRepo ? `<a class="link" href="${esc(hfModelUrl(onnxRepo))}" target="_blank" rel="noopener noreferrer">Open ONNX Repo β†—</a>` : `<span class="link link-na" aria-disabled="true">No ONNX repo available</span>`}
623
+ ${ggufRepo ? `<a class="link" href="${esc(hfModelUrl(ggufRepo))}" target="_blank" rel="noopener noreferrer">Open GGUF Repo β†—</a>` : `<span class="link link-na" aria-disabled="true">No GGUF repo available</span>`}
624
+ </div>
625
+ </div>
626
 
627
+ <h3 class="pm-heading">Precision &amp; performance</h3>
 
 
 
628
 
629
+ <div class="pm-block">
630
+ <div class="pm-rail" role="group" aria-label="Available precision/quant β€” select to view its metrics">
631
+ <div class="pm-rail-cap">Precision / Quant</div>
632
+ <div class="pm-rail-list">
633
+ ${arts.length
634
+ ? arts.map(a => {
635
+ const on = a === safeArt;
636
+ return `
637
  <button type="button"
638
+ class="chip-btn pm-opt ${on ? "active" : ""}"
639
  data-artifact="${esc(a)}"
640
+ aria-pressed="${on ? "true" : "false"}"
641
  title="Select ${esc(a)}"
642
+ >
643
+ <span class="pm-opt-dot" aria-hidden="true"></span>
644
+ <span class="pm-opt-label">${esc(a)}</span>
645
+ <span class="pm-opt-badge" aria-hidden="true">Selected</span>
646
+ </button>`;
647
+ }).join("")
648
+ : `<div class="pm-opt-empty subtle">No precision/quant variants</div>`
649
+ }
650
  </div>
651
  </div>
652
 
653
+ <div class="metric-card pm-graph" aria-live="polite">
654
+ <div class="metric-card-title">Est vs HIL Β· tok/s</div>
655
+ ${graphHasData
656
+ ? `<div class="pm-bars">${barCol("Est", estTok, estPct, "est")}${barCol("HIL", hilTok, hilPct, "hil")}</div>${updated ? `<div class="pm-graph-foot">Updated ${esc(updated)}</div>` : ""}`
657
+ : `<div class="pm-graph-empty subtle">No throughput yet</div>`}
658
  </div>
 
659
 
660
+ <div class="metric-card pm-acc" aria-live="polite">
661
+ <div class="metric-card-title">Accuracy</div>
662
+ <div class="pm-acc-body">
663
+ <div class="pm-acc-num ${acc == null ? "empty" : ""}">${esc(accText)}</div>
664
+ <div class="pm-acc-sub">${acc == null ? "Not published yet" : esc(accSrc) + " Β· " + esc(safeArt)}</div>
665
+ </div>
 
 
 
 
 
666
  </div>
667
  </div>
668
  </div>
 
710
  }
711
 
712
  /**
713
+ * mode: "explore" (Deploy result card) or "modal" (model details).
714
+ * Only differs in the Download heading's top spacing.
 
715
  */
716
  function renderActions(modelKey, fmt, compute, purpose, artifact, cfgId, mode = "explore") {
717
  const m = KEY_TO_MODEL[modelKey];
 
724
  const include = downloadIncludePattern(fmt, artifact);
725
  const cmd = repoId ? `hf download ${repoId} --repo-type=model --include "${include}"` : "";
726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  return `
728
  <div class="md">
729
+ <h3${mode === "explore" ? ' style="margin-top:18px;"' : ""}>Download</h3>
730
  ${repoId ? `
 
731
  <div class="cmd-row">
732
  <pre>${esc(cmd)}</pre>
733
  <button class="copy-btn" type="button"
 
736
  aria-label="Copy download command"
737
  title="Copy to clipboard"
738
  >
739
+ <span class="icon"><svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><rect x="4" y="3" width="9" height="11" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M6 3.2V2.2A1.2 1.2 0 0 1 7.2 1h1.6A1.2 1.2 0 0 1 10 2.2v1" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg></span>
740
  <span class="txt">Copy</span>
741
  </button>
742
  </div>
743
  ` : `<i>Repo not set for the selected format (${esc(fmt)}).</i>`}
 
 
 
 
744
  </div>
745
  `;
746
  }
 
752
  $$(".nav-item").forEach(btn => btn.classList.remove("active"));
753
  document.querySelector(`.nav-item[data-target="${CSS.escape(targetId)}"]`)?.classList.add("active");
754
 
755
+ if (targetId === "sec-overview") requestAnimationFrame(redrawConstellation);
756
+ if (targetId === "sec-catalog" && CATALOG_STATE.view === "table") {
757
+ requestAnimationFrame(() => setupPinnedColumns($("modelsTable"), 1));
758
+ }
759
  }
760
 
761
  /* ---------- Explore pipeline ---------- */
762
  function ensureExploreArtifact(modelKey, fmt) {
763
+ const arts = benchmarkedArtifactsFor(modelKey, fmt);
764
  if (!arts.length) { EXPLORE_SELECTED_ARTIFACT = null; return null; }
765
  if (!EXPLORE_SELECTED_ARTIFACT || !arts.includes(EXPLORE_SELECTED_ARTIFACT)) EXPLORE_SELECTED_ARTIFACT = arts[0];
766
  return EXPLORE_SELECTED_ARTIFACT;
 
775
  refreshModelList(false);
776
  }
777
 
778
+ /* Pick a format the model actually provides, keeping the current one when it's
779
+ valid. Prefers artifacts (which drive the precision selector + graph) and
780
+ falls back to repo presence only when a model lists no artifacts at all. */
781
+ function bestFormatForModel(m, preferred) {
782
+ if (!m) return preferred;
783
+ // Prefer a format that has at least one benchmarked variant to display.
784
+ const onnxBench = safeList(m.onnx_artifacts, []).some(a => artifactHasBenchmark(m, a));
785
+ const ggufBench = safeList(m.gguf_artifacts, []).some(a => artifactHasBenchmark(m, a));
786
+ if (onnxBench || ggufBench) {
787
+ if (preferred === "ONNX" && onnxBench) return "ONNX";
788
+ if (preferred === "GGUF" && ggufBench) return "GGUF";
789
+ return onnxBench ? "ONNX" : "GGUF";
790
+ }
791
+ // No benchmarked variants anywhere β€” fall back to artifact/repo presence so
792
+ // the download command still resolves.
793
+ const hasOnnx = safeList(m.onnx_artifacts, []).length > 0 || !!m.onnx_repo;
794
+ const hasGguf = safeList(m.gguf_artifacts, []).length > 0 || !!m.gguf_repo;
795
+ if (preferred === "ONNX" && hasOnnx) return "ONNX";
796
+ if (preferred === "GGUF" && hasGguf) return "GGUF";
797
+ if (hasOnnx) return "ONNX";
798
+ if (hasGguf) return "GGUF";
799
+ return preferred;
800
+ }
801
+
802
  function updateExploreOnModelChange() {
803
  const key = $("modelKey").value;
804
  const m = KEY_TO_MODEL[key];
 
807
  const def = ids.length ? ids[0] : null;
808
  fillSelect($("hwCfg"), ids, def);
809
 
810
+ /* Keep the format valid for the chosen model. Without this, selecting a model
811
+ that lacks the current format yields no artifacts/repo for it, so only the
812
+ (format-independent) Summary appears to refresh while the precision/quant,
813
+ graph, accuracy and download stay empty. */
814
+ const prevFmt = readRadio("fmt", "ONNX");
815
+ const fmt = bestFormatForModel(m, prevFmt);
816
+ if (fmt !== prevFmt) {
817
+ setRadioValue("fmt", fmt);
818
+ setRadioValue("purpose", defaultPurposeForFormat(fmt));
819
+ }
820
+
821
  ensureExploreArtifact(key, fmt);
822
  updateExploreAny();
823
  }
 
851
 
852
  $("details").innerHTML = renderDetails(key, cfgId, fmt, compute, purpose, artifact);
853
  $("actions").innerHTML = renderActions(key, fmt, compute, purpose, artifact, cfgId, "explore");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
854
  }
855
 
856
  /* ---------- Modal (All Models -> Details) ---------- */
 
858
  if (!MODAL_CTX) return;
859
  const { key, cfgId, fmt, compute, purpose } = MODAL_CTX;
860
 
861
+ const arts = benchmarkedArtifactsFor(key, fmt);
862
  if (!arts.length) MODAL_SELECTED_ARTIFACT = null;
863
  else if (!MODAL_SELECTED_ARTIFACT || !arts.includes(MODAL_SELECTED_ARTIFACT)) MODAL_SELECTED_ARTIFACT = arts[0];
864
 
 
867
  }
868
 
869
  /* ---------- All Models (table) ---------- */
 
 
870
 
871
  function defaultRowFmt(model) {
872
  return model.onnx_repo ? "ONNX" : (model.gguf_repo ? "GGUF" : "ONNX");
 
888
  const defArt = defaultRowArtifact(m, fmt);
889
 
890
  const { estimate, hil } = metricsForArtifact(cfg.metrics || {}, defArt);
 
891
 
892
  const detailsBtn = `<button class="mini-btn" type="button"
893
  data-open-model="1"
 
901
  _cfg: cfg.id || "",
902
  _fmt: fmt,
903
 
904
+ "Name": `<span class="tc-name"><span class="mc-logo">${designerOf(m).svg}</span><span class="tc-name-txt">${esc(m.display_name || m.key || "")}</span></span>`,
905
  "Modality": esc(m.modality || "LLM"),
906
+ "Task": esc(safeList(m.tasks, [])[0] || "β€”"),
 
 
907
  "Repo": repoButtonsSmallHTML(m),
908
  "Precision/Quant": artifactsBadgesHTML(m),
909
  "Estimate": metricBriefHTML(estimate),
 
915
  return rows;
916
  }
917
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  function renderTable(rows) {
919
  const table = $("modelsTable");
920
  if (!table) return;
 
928
  }
929
 
930
  const cols = Object.keys(rows[0]).filter(k => !k.startsWith("_"));
931
+ const pinCount = 1; // pin only the identifier (Name) column
932
 
933
  thead.innerHTML = `<tr>${cols.map((c, i) => {
934
  const isPinned = i < pinCount;
935
  const isDetails = (c === "");
936
+ const isName = (c === "Name");
937
+ const cls = `${isPinned ? "pin" : ""}${isDetails ? " details-col" : ""}${isName ? " cell-name" : ""}`.trim();
938
  const pinAttr = isPinned ? ` data-pin="${i}"` : "";
939
  const header = isDetails ? "" : esc(c);
940
  return `<th class="${cls}"${pinAttr}>${header}</th>`;
941
  }).join("")}</tr>`;
942
 
943
+ const htmlCols = new Set(["Name", "Repo", "Precision/Quant", "Estimate", "HIL", ""]);
944
 
945
  tbody.innerHTML = rows.map((r, ri) => {
946
  return `<tr data-row="${ri}" data-key="${esc(r._key)}" data-cfg="${esc(r._cfg)}" data-fmt="${esc(r._fmt)}">${
947
  cols.map((c, i) => {
948
  const isPinned = i < pinCount;
949
  const isDetails = (c === "");
950
+ const isName = (c === "Name");
951
+ const cls = `${isPinned ? "pin" : ""}${isDetails ? " details-col" : ""}${isName ? " cell-name" : ""}`.trim();
952
  const pinAttr = isPinned ? ` data-pin="${i}"` : "";
953
+ // data-label drives the stacked-card layout's field labels on narrow screens
954
+ const labelAttr = ` data-label="${esc(c)}"`;
955
  const val = r[c];
956
 
957
+ if (htmlCols.has(c)) return `<td class="${cls}"${pinAttr}${labelAttr}>${val ?? ""}</td>`;
958
+ return `<td class="${cls}"${pinAttr}${labelAttr}>${esc(val)}</td>`;
959
  }).join("")
960
  }</tr>`;
961
  }).join("\n");
 
981
  }
982
  }
983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  /* ---------- Model modal ---------- */
985
  function openModelModal(modelKey, cfgId, fmt) {
986
  const m = KEY_TO_MODEL[modelKey];
 
1014
  MODAL_SELECTED_ARTIFACT = null;
1015
  }
1016
 
1017
+ /* ====================================================================== */
1018
+ /* OVERVIEW β€” constellation (size vs throughput) */
1019
+ /* ====================================================================== */
1020
+ const FAMILY_PALETTE = ["#C96442", "#6E8B6A", "#C9A24B", "#5B7A99", "#A6573F", "#8E6E9E", "#4C8C7D", "#B07A3C", "#9A6B6B", "#7C8B5A"];
1021
+ const COLOR_CACHE = {};
1022
+ function colorFor(label) {
1023
+ if (COLOR_CACHE[label] != null) return COLOR_CACHE[label];
1024
+ const c = FAMILY_PALETTE[Object.keys(COLOR_CACHE).length % FAMILY_PALETTE.length];
1025
+ COLOR_CACHE[label] = c;
1026
+ return c;
1027
+ }
1028
+
1029
+ function allArtifactsOf(m) {
1030
+ return uniqueSorted([...safeList(m.onnx_artifacts, []), ...safeList(m.gguf_artifacts, [])]);
1031
+ }
1032
+ function fmtOfArtifact(m, art) {
1033
+ if (safeList(m.gguf_artifacts, []).includes(art)) return "GGUF";
1034
+ if (safeList(m.onnx_artifacts, []).includes(art)) return "ONNX";
1035
+ return "β€”";
1036
+ }
1037
+ function pickBlock(cfgMetrics, art, source) {
1038
+ const { estimate, hil } = metricsForArtifact(cfgMetrics || {}, art);
1039
+ if (source === "HIL") return hil ? { block: hil, source: "HIL" } : null;
1040
+ if (source === "Estimate") return estimate ? { block: estimate, source: "Estimate" } : null;
1041
+ if (hil) return { block: hil, source: "HIL" };
1042
+ if (estimate) return { block: estimate, source: "Estimate" };
1043
+ return null;
1044
+ }
1045
+ function tokSOf(block) {
1046
  if (!block) return null;
1047
+ let t = (block.total && block.total.tok_s != null) ? block.total.tok_s : null;
1048
+ if (t == null) t = block.npu?.tok_s ?? block.cpu?.tok_s ?? block.dsp?.tok_s ?? null;
1049
+ return (t != null && isFinite(Number(t))) ? Number(t) : null;
 
1050
  }
1051
 
1052
+ const AXES = {
1053
+ tok: { get: p => p.tok, label: "Throughput (tok/s)", tick: v => `${Math.round(v * 10) / 10}` },
1054
+ size: { get: p => p.sizeM, label: "Model size", tick: v => (v >= 1000 ? `${Math.round(v / 100) / 10}B` : `${Math.round(v)}M`) },
1055
+ };
1056
 
1057
+ function buildConstellationPoints(source) {
1058
+ const pts = [];
1059
  for (const m of CATALOG) {
1060
+ const sizeM = (m.model_size_m != null) ? Number(m.model_size_m) : null;
1061
+ for (const cfg of safeList(m.hardware_configs, [])) {
1062
+ for (const art of allArtifactsOf(m)) {
1063
+ const got = pickBlock(cfg.metrics || {}, art, source);
1064
+ if (!got) continue;
1065
+ const tok = tokSOf(got.block);
1066
+ if (tok == null) continue;
1067
+ pts.push({
1068
+ key: m.key, cfgId: cfg.id, fmt: fmtOfArtifact(m, art),
1069
+ name: m.display_name || m.key, family: m.architecture || "Unknown",
1070
+ quant: art, target: safeList(cfg.targets, safeList(m.targets, []))[0] || "β€”",
1071
+ hwLabel: cfg.label || cfg.id || "β€”", sizeM, tok, source: got.source,
1072
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
  }
 
1074
  }
1075
  }
1076
+ return pts;
1077
+ }
1078
+ function colorKey(p, colorBy) {
1079
+ return colorBy === "quant" ? p.quant : colorBy === "format" ? p.fmt : colorBy === "target" ? p.target : p.family;
1080
+ }
1081
 
1082
+ let CSTL_HIT = [];
1083
+
1084
+ function hitTest(list, evt, canvas) {
1085
+ const rect = canvas.getBoundingClientRect();
1086
+ const x = evt.clientX - rect.left, y = evt.clientY - rect.top;
1087
+ let best = null, bestD = Infinity;
1088
+ for (const p of list) {
1089
+ const dx = x - p.x, dy = y - p.y, d = dx * dx + dy * dy;
1090
+ if (d <= p.r * p.r && d < bestD) { best = p; bestD = d; }
1091
+ }
1092
+ return best;
1093
  }
1094
 
1095
+ function drawConstellation(canvas, points, opts) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
  const ctx = canvas.getContext("2d");
1097
+ const rect = canvas.parentElement.getBoundingClientRect();
1098
  const dpr = window.devicePixelRatio || 1;
1099
+ const h = canvas.clientHeight || 460, w = rect.width;
1100
+ canvas.width = Math.max(1, Math.floor(w * dpr));
1101
+ canvas.height = Math.max(1, Math.floor(h * dpr));
1102
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1103
+ ctx.clearRect(0, 0, w, h);
 
1104
 
1105
+ const pad = { l: 66, r: 18, t: 22, b: 48 };
1106
  const innerW = Math.max(1, w - pad.l - pad.r);
1107
  const innerH = Math.max(1, h - pad.t - pad.b);
1108
+ const ax = AXES[opts.xKey] || AXES.tok;
1109
+ const ay = AXES[opts.yKey] || AXES.size;
1110
+ const txt = "rgba(31,30,28,0.72)";
1111
+ ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Arial";
1112
 
1113
+ CSTL_HIT = [];
1114
+ if (!points.length) {
1115
+ ctx.fillStyle = txt;
1116
+ ctx.fillText("No data points for this selection.", pad.l, pad.t + 24);
1117
+ return;
1118
+ }
1119
 
1120
+ const xs = points.map(ax.get).filter(v => v != null && isFinite(v));
1121
+ const ys = points.map(ay.get).filter(v => v != null && isFinite(v));
1122
+ let xMin = Math.min(0, ...xs), xMax = Math.max(...xs);
1123
+ let yMin = Math.min(0, ...ys), yMax = Math.max(...ys);
1124
+ if (xMax - xMin < 1e-9) xMax = xMin + 1;
1125
+ if (yMax - yMin < 1e-9) yMax = yMin + 1;
1126
+ xMax += (xMax - xMin) * 0.08;
1127
+ yMax += (yMax - yMin) * 0.12;
1128
 
1129
+ const xOf = v => pad.l + ((v - xMin) / (xMax - xMin)) * innerW;
1130
+ const yOf = v => pad.t + innerH - ((v - yMin) / (yMax - yMin)) * innerH;
1131
 
1132
+ const ticks = 4;
1133
+ ctx.lineWidth = 1;
1134
+ for (let i = 0; i <= ticks; i++) {
1135
+ const ty = pad.t + innerH - (innerH * i / ticks);
1136
+ ctx.strokeStyle = "rgba(31,30,28,0.08)";
1137
+ ctx.beginPath(); ctx.moveTo(pad.l, ty); ctx.lineTo(pad.l + innerW, ty); ctx.stroke();
1138
+ ctx.fillStyle = txt;
1139
+ ctx.fillText(ay.tick(yMin + (yMax - yMin) * (i / ticks)), 8, ty + 4);
1140
+ }
1141
+ for (let i = 0; i <= ticks; i++) {
1142
+ const tx = pad.l + (innerW * i / ticks);
1143
+ ctx.fillStyle = txt;
1144
+ ctx.fillText(ax.tick(xMin + (xMax - xMin) * (i / ticks)), tx - 12, h - 16);
1145
+ }
1146
+ ctx.fillStyle = "rgba(31,30,28,0.52)";
1147
+ ctx.fillText(ax.label + " β†’", pad.l, h - 2);
1148
+ ctx.fillText("↑ " + ay.label, 8, 14);
1149
+
1150
+ const maxSz = Math.max(1, ...points.map(p => p.sizeM || 0));
1151
+ const rOf = s => 6 + 16 * Math.sqrt((s || 0) / maxSz);
1152
+
1153
+ points.forEach(p => {
1154
+ const xv = ax.get(p), yv = ay.get(p);
1155
+ if (xv == null || yv == null) return;
1156
+ const x = xOf(xv), y = yOf(yv), r = rOf(p.sizeM);
1157
+ const col = colorFor(colorKey(p, opts.colorBy));
1158
+ ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2);
1159
+ if (p.source === "HIL") { ctx.fillStyle = col + "C0"; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke(); }
1160
+ else { ctx.fillStyle = col + "33"; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.8; ctx.stroke(); }
1161
+ CSTL_HIT.push({ x, y, r: Math.max(r, 10), data: p });
1162
+ });
1163
+ }
1164
 
1165
+ function buildLegend(points, colorBy) {
1166
+ const el = $("cstlLegend");
1167
+ if (!el) return;
1168
+ const seen = [];
1169
+ for (const p of points) { const k = colorKey(p, colorBy); if (!seen.includes(k)) seen.push(k); }
1170
+ el.innerHTML = seen.map(k =>
1171
+ `<span class="legend-item"><span class="legend-dot" style="background:${colorFor(k)}"></span>${esc(k)}</span>`
1172
+ ).join("") + `<span class="legend-item">● HIL&nbsp;&nbsp;β—Œ Estimate</span>`;
1173
+ }
1174
+
1175
+ function redrawConstellation() {
1176
+ const canvas = $("cstlCanvas");
1177
+ if (!canvas) return;
1178
+ const xKey = $("cstlX")?.value || "tok";
1179
+ const yKey = $("cstlY")?.value || "size";
1180
+ const colorBy = $("cstlColor")?.value || "family";
1181
+ const srcRaw = readRadio("cstlSource", "Best available");
1182
+ const source = (srcRaw === "HIL" || srcRaw === "Estimate") ? srcRaw : "Best";
1183
+ const points = buildConstellationPoints(source);
1184
+ drawConstellation(canvas, points, { xKey, yKey, colorBy });
1185
+ buildLegend(points, colorBy);
1186
+ const status = $("cstlStatus");
1187
+ if (status) status.textContent = `${points.length} variant point(s) Β· ${srcRaw}`;
1188
+ }
1189
+
1190
+ function fillStatTiles() {
1191
+ const el = $("statTiles");
1192
+ if (!el) return;
1193
+ const variants = new Set();
1194
+ for (const m of CATALOG) {
1195
+ for (const cfg of safeList(m.hardware_configs, []))
1196
+ for (const art of allArtifactsOf(m))
1197
+ if (pickBlock(cfg.metrics || {}, art, "Best")) variants.add(`${m.key}|${art}`);
1198
+ }
1199
+ const tiles = [
1200
+ [CATALOG.length, "Models"],
1201
+ [variants.size, "Benchmarked variants"],
1202
+ ];
1203
+ el.innerHTML = tiles.map(([n, l]) =>
1204
+ `<div class="stat-tile"><div class="stat-num">${esc(n)}</div><div class="stat-label">${esc(l)}</div></div>`
1205
+ ).join("");
1206
+ }
1207
+
1208
+ /* ---------- Recently-added carousel ---------- */
1209
+ const RECENT_WINDOW_DAYS = 31;
1210
+
1211
+ function modelRecency(m) {
1212
+ const c = m.created_at ? Date.parse(m.created_at) : NaN;
1213
+ const u = m.last_modified ? Date.parse(m.last_modified) : NaN;
1214
+ const v = [c, u].filter(x => !isNaN(x));
1215
+ return v.length ? Math.max(...v) : NaN;
1216
+ }
1217
+ function timeAgo(ts) {
1218
+ if (isNaN(ts)) return "";
1219
+ const d = Math.floor((Date.now() - ts) / 86400000);
1220
+ if (d <= 0) return "today";
1221
+ if (d === 1) return "yesterday";
1222
+ if (d < 30) return `${d} days ago`;
1223
+ const mo = Math.round(d / 30);
1224
+ return `${mo} month${mo > 1 ? "s" : ""} ago`;
1225
+ }
1226
+ function recentLabel(m) {
1227
+ const c = m.created_at ? Date.parse(m.created_at) : NaN;
1228
+ const u = m.last_modified ? Date.parse(m.last_modified) : NaN;
1229
+ const cut = Date.now() - RECENT_WINDOW_DAYS * 86400000;
1230
+ if (!isNaN(c) && c >= cut) return { isNew: true, text: `added ${timeAgo(c)}` };
1231
+ if (!isNaN(u)) return { isNew: false, text: `updated ${timeAgo(u)}` };
1232
+ return { isNew: false, text: "" };
1233
+ }
1234
+ function recentModels() {
1235
+ const cut = Date.now() - RECENT_WINDOW_DAYS * 86400000;
1236
+ return CATALOG
1237
+ .filter(m => { const r = modelRecency(m); return !isNaN(r) && r >= cut; })
1238
+ .sort((a, b) => modelRecency(b) - modelRecency(a));
1239
+ }
1240
+ function recentCardHTML(m) {
1241
+ const col = colorFor(m.architecture || "Unknown");
1242
+ const best = bestHilFirstVariantOf(m);
1243
+ const firstCfg = safeFirstCfg(m);
1244
+ const defFmt = m.gguf_repo ? "GGUF" : (m.onnx_repo ? "ONNX" : "GGUF");
1245
+ const lbl = recentLabel(m);
1246
+ const srcTag = best
1247
+ ? (best.source === "HIL"
1248
+ ? `<span class="rc-src rc-src-hil">HIL</span>`
1249
+ : `<span class="rc-src rc-src-est">est</span>`)
1250
+ : "";
1251
+ const stat = best
1252
+ ? `<div class="rc-stat"><b>${esc(Math.round(best.tok * 10) / 10)}</b> tok/s Β· ${esc(best.quant)} ${srcTag}</div>`
1253
+ : `<div class="rc-stat empty">No throughput yet</div>`;
1254
+ return `
1255
+ <div class="model-card rc-card" data-card="1" data-key="${esc(m.key)}" data-cfg="${esc(firstCfg?.id || "")}" data-fmt="${esc(defFmt)}">
1256
+ <div class="rc-meta ${lbl.isNew ? "is-new" : ""}">${lbl.isNew ? "✦" : "⟳"} ${esc(lbl.text)}</div>
1257
+ ${designerRowHTML(m)}
1258
+ <div class="mc-head">
1259
+ <div class="mc-monogram" style="background:linear-gradient(135deg, ${col}, ${col}99)">${esc(monogramText(m))}</div>
1260
+ <div style="min-width:0">
1261
+ <div class="mc-title">${esc(m.display_name || m.key)}</div>
1262
+ <div class="mc-sub">${esc(m.architecture || "")}${m.variant ? " Β· " + esc(m.variant) : ""}</div>
1263
+ </div>
1264
+ </div>
1265
+ ${stat}
1266
+ </div>`;
1267
+ }
1268
+ function renderRecentCarousel() {
1269
+ const sec = $("recentSection"), track = $("recentTrack");
1270
+ if (!sec || !track) return;
1271
+ const list = recentModels();
1272
+ if (!list.length) { sec.hidden = true; return; }
1273
+ sec.hidden = false;
1274
+ track.innerHTML = list.map(recentCardHTML).join("");
1275
+ const sub = $("recentSub");
1276
+ if (sub) sub.textContent = `${list.length} model${list.length > 1 ? "s" : ""} Β· last 30 days`;
1277
+ }
1278
+
1279
+ function initOverview() {
1280
+ fillStatTiles();
1281
+ renderRecentCarousel();
1282
+
1283
+ const track = $("recentTrack");
1284
+ $("recentPrev")?.addEventListener("click", () => track?.scrollBy({ left: -276, behavior: "smooth" }));
1285
+ $("recentNext")?.addEventListener("click", () => track?.scrollBy({ left: 276, behavior: "smooth" }));
1286
+ track?.addEventListener("click", (e) => {
1287
+ if (e.target.closest("a")) return;
1288
+ const card = e.target.closest('.model-card[data-card="1"]');
1289
+ if (card) openModelModal(card.getAttribute("data-key"), card.getAttribute("data-cfg") || "", card.getAttribute("data-fmt") || "GGUF");
1290
+ });
1291
 
1292
+ buildRadioGroup($("cstlSource"), "cstlSource", ["Best available", "HIL", "Estimate"], "Best available");
1293
+ ["cstlX", "cstlY", "cstlColor"].forEach(id => $(id)?.addEventListener("change", redrawConstellation));
1294
+ document.addEventListener("change", (e) => { if (e.target && e.target.name === "cstlSource") redrawConstellation(); });
1295
+
1296
+ const canvas = $("cstlCanvas");
1297
+ if (canvas && canvas.dataset.bound !== "1") {
1298
+ canvas.dataset.bound = "1";
1299
+ const tip = $("cstlTooltip");
1300
+ canvas.addEventListener("mousemove", (evt) => {
1301
+ const hit = hitTest(CSTL_HIT, evt, canvas);
1302
+ if (!hit || !tip) { if (tip) tip.style.display = "none"; return; }
1303
+ const rect = canvas.parentElement.getBoundingClientRect();
1304
+ const x = evt.clientX - rect.left, y = evt.clientY - rect.top;
1305
+ const sz = hit.data.sizeM >= 1000 ? `${hit.data.sizeM / 1000}B` : `${hit.data.sizeM}M`;
1306
+ tip.innerHTML = `<b>${esc(hit.data.name)}</b><br/>${esc(hit.data.quant)} Β· ${esc(hit.data.target)} Β· ${esc(hit.data.source)}<br/>${esc(Math.round(hit.data.tok * 10) / 10)} tok/s Β· ${esc(sz)}`;
1307
+ tip.style.display = "block";
1308
+ tip.style.left = `${Math.min(x + 14, rect.width - 250)}px`;
1309
+ tip.style.top = `${Math.max(8, y - 10)}px`;
1310
+ });
1311
+ canvas.addEventListener("mouseleave", () => { if (tip) tip.style.display = "none"; });
1312
+ canvas.addEventListener("click", (evt) => {
1313
+ const hit = hitTest(CSTL_HIT, evt, canvas);
1314
+ if (hit) openModelModal(hit.data.key, hit.data.cfgId, hit.data.fmt);
1315
+ });
1316
  }
1317
+ redrawConstellation();
1318
+ }
1319
+
1320
+ /* ====================================================================== */
1321
+ /* CATALOG β€” card gallery + facet rail + table view */
1322
+ /* ====================================================================== */
1323
+ const SIZE_BUCKETS = [
1324
+ { id: "≀1B", test: s => s != null && s <= 1000 },
1325
+ { id: "1–4B", test: s => s != null && s > 1000 && s <= 4000 },
1326
+ { id: "4–10B", test: s => s != null && s > 4000 && s <= 10000 },
1327
+ { id: ">10B", test: s => s != null && s > 10000 },
1328
+ ];
1329
+ const CATALOG_STATE = {
1330
+ search: "", sort: "size_desc", view: "grid",
1331
+ facets: { family: new Set(), size: new Set(), format: new Set(), target: new Set(), quant: new Set() },
1332
+ hil: false,
1333
+ };
1334
+
1335
+ function bestVariantBySource(m, source) {
1336
+ let best = null;
1337
+ for (const cfg of safeList(m.hardware_configs, [])) {
1338
+ for (const art of allArtifactsOf(m)) {
1339
+ const got = pickBlock(cfg.metrics || {}, art, source);
1340
+ const tok = got ? tokSOf(got.block) : null;
1341
+ if (tok == null) continue;
1342
+ if (!best || tok > best.tok) {
1343
+ best = { tok, quant: art, source: got.source, target: safeList(cfg.targets, safeList(m.targets, []))[0] || "β€”" };
1344
+ }
1345
+ }
1346
  }
1347
  return best;
1348
  }
1349
+ function bestVariantOf(m) { return bestVariantBySource(m, "Best"); }
1350
+ /* Recency cards prioritise measured HIL numbers: show the best HIL variant
1351
+ when any exists, only falling back to the best Estimate when it has none. */
1352
+ function bestHilFirstVariantOf(m) {
1353
+ return bestVariantBySource(m, "HIL") || bestVariantBySource(m, "Estimate");
1354
+ }
1355
+ function modelHasHil(m) {
1356
+ for (const cfg of safeList(m.hardware_configs, []))
1357
+ for (const art of allArtifactsOf(m))
1358
+ if (metricsForArtifact(cfg.metrics || {}, art).hil) return true;
1359
+ return false;
1360
+ }
1361
+ function sizeBucketOf(s) { const b = SIZE_BUCKETS.find(x => x.test(s)); return b ? b.id : "β€”"; }
1362
+ function modelFormats(m) { const f = []; if (m.gguf_repo) f.push("GGUF"); if (m.onnx_repo) f.push("ONNX"); return f; }
1363
+
1364
+ function facetDefs() {
1365
+ return [
1366
+ { group: "family", title: "Family", opts: uniqueSorted(CATALOG.map(m => m.architecture || "Unknown")) },
1367
+ { group: "size", title: "Size", opts: SIZE_BUCKETS.map(b => b.id).filter(id => CATALOG.some(m => sizeBucketOf(m.model_size_m) === id)) },
1368
+ { group: "quant", title: "Quantization", opts: uniqueSorted(CATALOG.flatMap(allArtifactsOf)) },
1369
+ ];
1370
+ }
1371
+
1372
+ function buildFacetRail() {
1373
+ const el = $("cgFacets");
1374
+ if (!el) return;
1375
+ let html = facetDefs().map(d => `
1376
+ <div class="facet-group">
1377
+ <div class="facet-group-title">${esc(d.title)}</div>
1378
+ ${d.opts.map(o => {
1379
+ const checked = CATALOG_STATE.facets[d.group].has(o) ? "checked" : "";
1380
+ return `<label class="facet-opt"><input type="checkbox" data-facet-group="${esc(d.group)}" value="${esc(o)}" ${checked}/> <span>${esc(o)}</span></label>`;
1381
+ }).join("")}
1382
+ </div>`).join("");
1383
+ html += `
1384
+ <div class="facet-group">
1385
+ <div class="facet-group-title">Metrics</div>
1386
+ <label class="facet-opt"><input type="checkbox" id="cgHil" ${CATALOG_STATE.hil ? "checked" : ""}/> <span>Has HIL measurement</span></label>
1387
+ </div>`;
1388
+ el.innerHTML = html;
1389
+ }
1390
+
1391
+ function modelMatches(m) {
1392
+ const f = CATALOG_STATE.facets;
1393
+ if (f.family.size && !f.family.has(m.architecture || "Unknown")) return false;
1394
+ if (f.size.size && !f.size.has(sizeBucketOf(m.model_size_m))) return false;
1395
+ if (f.format.size && !modelFormats(m).some(x => f.format.has(x))) return false;
1396
+ if (f.target.size && !safeList(m.targets, []).some(x => f.target.has(x))) return false;
1397
+ if (f.quant.size && !allArtifactsOf(m).some(x => f.quant.has(x))) return false;
1398
+ if (CATALOG_STATE.hil && !modelHasHil(m)) return false;
1399
+ const q = CATALOG_STATE.search.trim().toLowerCase();
1400
+ if (q) {
1401
+ const hay = [m.display_name, m.key, m.architecture, m.base_model, m.license, safeList(m.tasks, []).join(" "), allArtifactsOf(m).join(" ")].join(" ").toLowerCase();
1402
+ if (!hay.includes(q)) return false;
1403
+ }
1404
+ return true;
1405
+ }
1406
+
1407
+ function sortModels(list) {
1408
+ const s = CATALOG_STATE.sort;
1409
+ const tok = m => { const b = bestVariantOf(m); return b ? b.tok : -1; };
1410
+ const arr = list.slice();
1411
+ if (s === "size_desc") arr.sort((a, b) => (b.model_size_m || 0) - (a.model_size_m || 0));
1412
+ else if (s === "size_asc") arr.sort((a, b) => (a.model_size_m || 0) - (b.model_size_m || 0));
1413
+ else if (s === "tok_desc") arr.sort((a, b) => tok(b) - tok(a));
1414
+ else if (s === "name_asc") arr.sort((a, b) => String(a.display_name || a.key).localeCompare(String(b.display_name || b.key)));
1415
+ return arr;
1416
+ }
1417
+
1418
+ function monogramText(m) {
1419
+ const s = m.model_size_m;
1420
+ if (s != null) return s >= 1000 ? `${Math.round(s / 100) / 10}B` : `${s}M`;
1421
+ return (m.display_name || m.key || "?").slice(0, 2).toUpperCase();
1422
+ }
1423
+
1424
+ /* ---- Model designer / publisher logos (derived from base_model org or architecture) ---- */
1425
+ const DESIGNERS = {
1426
+ meta: { name: "Meta", svg: `<svg viewBox="0 0 32 18" width="22" height="13" aria-hidden="true"><path d="M16 9.2C13.1 4 10.2 2.8 7.6 2.8 4.5 2.8 2 5.6 2 9s2.5 6.2 5.6 6.2c2.6 0 5.5-1.2 8.4-6.4 2.9 5.2 5.8 6.4 8.4 6.4 3.1 0 5.6-2.8 5.6-6.2s-2.5-6.2-5.6-6.2C21.8 2.8 18.9 4 16 9.2Z" fill="none" stroke="#0866FF" stroke-width="2.7" stroke-linejoin="round"/></svg>` },
1427
+ nvidia: { name: "NVIDIA", svg: `<svg viewBox="0 0 26 22" width="18" height="15" aria-hidden="true"><path d="M9 4C4.4 4.6 1.8 7.8 1 11c2.6 4.4 8 6.4 13 5.9 5.3-.5 8.8-3.4 10.4-6.4C22 6.2 15 3.4 9 4Z" fill="none" stroke="#76B900" stroke-width="2"/><path d="M9.7 7.6c-1.9.8-2.9 2.1-3.1 3.6 1.6 2.2 4.5 2.7 6.6 1.8 1.9-1 2.5-3.2 1.3-5C13.4 6.2 11.5 6.8 9.7 7.6Z" fill="#76B900"/></svg>` },
1428
+ google: { name: "Google", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M23 12.2c0-.8-.1-1.6-.2-2.3H12v4.5h6.2a5.3 5.3 0 0 1-2.3 3.5v2.9h3.7C21.7 18.7 23 15.8 23 12.2Z" fill="#4285F4"/><path d="M12 24c3.1 0 5.7-1 7.6-2.8l-3.7-2.9c-1 .7-2.3 1.1-3.9 1.1-3 0-5.5-2-6.4-4.7H1.8v3C3.7 21.4 7.6 24 12 24Z" fill="#34A853"/><path d="M5.6 14.7a7.2 7.2 0 0 1 0-4.6V7.1H1.8a12 12 0 0 0 0 10.6l3.8-3Z" fill="#FBBC05"/><path d="M12 4.8c1.7 0 3.2.6 4.4 1.7l3.3-3.3C17.7 1.2 15.1 0 12 0 7.6 0 3.7 2.6 1.8 6.4l3.8 3C6.5 6.7 9 4.8 12 4.8Z" fill="#EA4335"/></svg>` },
1429
+ microsoft: { name: "Microsoft", svg: `<svg viewBox="0 0 23 23" width="14" height="14" aria-hidden="true"><rect x="1" y="1" width="10" height="10" fill="#F25022"/><rect x="12" y="1" width="10" height="10" fill="#7FBA00"/><rect x="1" y="12" width="10" height="10" fill="#00A4EF"/><rect x="12" y="12" width="10" height="10" fill="#FFB900"/></svg>` },
1430
+ mistral: { name: "Mistral AI", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><g><rect x="2" y="3" width="5" height="4.5" fill="#F7D046"/><rect x="17" y="3" width="5" height="4.5" fill="#F7D046"/><rect x="2" y="8" width="5" height="4.5" fill="#F2A73B"/><rect x="9.5" y="8" width="5" height="4.5" fill="#F2A73B"/><rect x="17" y="8" width="5" height="4.5" fill="#F2A73B"/><rect x="2" y="13" width="5" height="4.5" fill="#EE792F"/><rect x="17" y="13" width="5" height="4.5" fill="#EE792F"/><rect x="2" y="18" width="5" height="4.5" fill="#EA3326"/><rect x="17" y="18" width="5" height="4.5" fill="#EA3326"/></g></svg>` },
1431
+ qwen: { name: "Qwen", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#615CED"/><path d="M7.5 7l4.5 10 4.5-10M9.3 13.2h5.4" stroke="#fff" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>` },
1432
+ deepseek: { name: "DeepSeek", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#4D6BFE"/><path d="M6 9.5c3.2.2 4 2.2 6 2.2s2.2-2 .2-3.2" stroke="#fff" stroke-width="1.6" fill="none" stroke-linecap="round"/><circle cx="9.2" cy="14.2" r="1.3" fill="#fff"/></svg>` },
1433
+ renesas: { name: "Renesas", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><rect width="24" height="24" rx="4" fill="#2A289D"/><text x="12" y="16.5" font-family="Arial, sans-serif" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">R</text></svg>` },
1434
+ generic: { name: "AI Model", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#8E8DA3"/><circle cx="12" cy="12" r="4" fill="#fff"/></svg>` },
1435
+ };
1436
+ function designerOf(m) {
1437
+ const base = (m.base_model || "").toLowerCase();
1438
+ const org = base.includes("/") ? base.split("/")[0] : "";
1439
+ const hay = `${org} ${(m.architecture || "").toLowerCase()} ${(m.key || "").toLowerCase()}`;
1440
+ if (org.includes("nvidia") || hay.includes("nemotron") || hay.includes("nemo")) return DESIGNERS.nvidia;
1441
+ if (org.includes("meta") || org.includes("facebook") || hay.includes("llama")) return DESIGNERS.meta;
1442
+ if (org.includes("google") || hay.includes("gemma")) return DESIGNERS.google;
1443
+ if (org.includes("microsoft") || hay.includes("phi")) return DESIGNERS.microsoft;
1444
+ if (org.includes("mistral") || hay.includes("mistral") || hay.includes("mixtral")) return DESIGNERS.mistral;
1445
+ if (org.includes("qwen") || org.includes("alibaba") || hay.includes("qwen")) return DESIGNERS.qwen;
1446
+ if (org.includes("deepseek") || hay.includes("deepseek")) return DESIGNERS.deepseek;
1447
+ if (org.includes("renesas")) return DESIGNERS.renesas;
1448
+ if (org) return { name: org.split(/[-_]/)[0].replace(/^\w/, c => c.toUpperCase()), svg: DESIGNERS.generic.svg };
1449
+ return DESIGNERS.generic;
1450
+ }
1451
+ function designerRowHTML(m) {
1452
+ const d = designerOf(m);
1453
+ return `<div class="mc-designer"><span class="mc-logo">${d.svg}</span><span class="mc-designer-name">${esc(d.name)}</span></div>`;
1454
+ }
1455
+
1456
+ function cardHTML(m) {
1457
+ const best = bestHilFirstVariantOf(m);
1458
+ const col = colorFor(m.architecture || "Unknown");
1459
+ const quantChips = allArtifactsOf(m).map(a => `<span class="badge art clickable" data-facet="quant" data-value="${esc(a)}">${esc(a)}</span>`).join("");
1460
+ const modalityChip = `<span class="badge modality">${esc(m.modality || "LLM")}</span>`;
1461
+ const _lbl = recentLabel(m);
1462
+ const statusChips = `${modelHasHil(m) ? `<span class="badge status-hil">HIL</span>` : ""}${_lbl.isNew ? `<span class="badge status-new">New</span>` : ""}`;
1463
+ const stat = best
1464
+ ? `<div class="mc-stat"><span class="mc-stat-num">${esc(Math.round(best.tok * 10) / 10)}</span><span class="mc-stat-sub">tok/s Β· ${esc(best.target)} Β· ${esc(best.quant)}${best.source === "Estimate" ? " (est)" : ""}</span></div>`
1465
+ : `<div class="mc-stat empty"><span class="mc-stat-sub">No published throughput yet</span></div>`;
1466
+ const repo = m.gguf_repo || m.onnx_repo || "";
1467
+ const repoLink = repo ? `<a class="link small" href="${esc(hfModelUrl(repo))}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">View repo</a>` : "";
1468
+ const firstCfg = safeFirstCfg(m);
1469
+ const defFmt = m.gguf_repo ? "GGUF" : (m.onnx_repo ? "ONNX" : "GGUF");
1470
+ return `
1471
+ <div class="model-card" data-card="1" data-key="${esc(m.key)}" data-cfg="${esc(firstCfg?.id || "")}" data-fmt="${esc(defFmt)}">
1472
+ ${designerRowHTML(m)}
1473
+ <div class="mc-head">
1474
+ <div class="mc-monogram" style="background:linear-gradient(135deg, ${col}, ${col}99)">${esc(monogramText(m))}</div>
1475
+ <div style="min-width:0">
1476
+ <div class="mc-title">${esc(m.display_name || m.key)}</div>
1477
+ <div class="mc-sub">${esc(m.architecture || "")}${m.variant ? " Β· " + esc(m.variant) : ""}</div>
1478
+ </div>
1479
+ </div>
1480
+ <div class="mc-chips">${modalityChip}${statusChips}</div>
1481
+ ${stat}
1482
+ ${quantChips ? `<div class="mc-quants">${quantChips}</div>` : ""}
1483
+ ${(m.license || repoLink) ? `<div class="mc-foot">
1484
+ <span class="mc-meta">${esc(m.license || "")}</span>
1485
+ ${repoLink}
1486
+ </div>` : ""}
1487
+ </div>`;
1488
+ }
1489
+
1490
+ function renderCards(models) {
1491
+ const el = $("cardGrid");
1492
+ if (!el) return;
1493
+ el.innerHTML = models.length
1494
+ ? models.map(cardHTML).join("")
1495
+ : `<div class="subtle" style="padding:24px;">No models match the current filters.</div>`;
1496
+ }
1497
+
1498
+ function renderCatalog() {
1499
+ const filtered = sortModels(CATALOG.filter(modelMatches));
1500
+ const count = $("cgCount");
1501
+ if (count) count.innerHTML = `<b>${filtered.length}</b> of ${CATALOG.length} models`;
1502
+ const grid = $("cardGrid"), table = $("tableView");
1503
+ if (CATALOG_STATE.view === "grid") {
1504
+ if (grid) grid.hidden = false;
1505
+ if (table) table.hidden = true;
1506
+ renderCards(filtered);
1507
+ } else {
1508
+ if (grid) grid.hidden = true;
1509
+ if (table) table.hidden = false;
1510
+ renderTable(buildTableRows(filtered));
1511
+ requestAnimationFrame(() => setupPinnedColumns($("modelsTable"), 1));
1512
+ }
1513
  }
 
1514
 
1515
+ function setView(view) {
1516
+ CATALOG_STATE.view = view;
1517
+ $("cgViewGrid")?.classList.toggle("active", view === "grid");
1518
+ $("cgViewTable")?.classList.toggle("active", view === "table");
1519
+ $("cgViewGrid")?.setAttribute("aria-pressed", String(view === "grid"));
1520
+ $("cgViewTable")?.setAttribute("aria-pressed", String(view === "table"));
1521
+ renderCatalog();
1522
+ }
1523
 
1524
+ function toggleFacet(group, value) {
1525
+ const set = CATALOG_STATE.facets[group];
1526
+ if (!set) return;
1527
+ if (set.has(value)) set.delete(value); else set.add(value);
1528
+ buildFacetRail();
1529
+ renderCatalog();
1530
+ }
 
1531
 
1532
+ function initCatalog() {
1533
+ buildFacetRail();
1534
 
1535
+ $("cgSearch")?.addEventListener("input", () => {
1536
+ clearTimeout(window.__cgT);
1537
+ window.__cgT = setTimeout(() => { CATALOG_STATE.search = $("cgSearch").value; renderCatalog(); }, 90);
1538
+ });
1539
+ $("cgSort")?.addEventListener("change", () => { CATALOG_STATE.sort = $("cgSort").value; renderCatalog(); });
1540
+ $("cgReset")?.addEventListener("click", () => {
1541
+ CATALOG_STATE.search = ""; CATALOG_STATE.sort = "size_desc"; CATALOG_STATE.hil = false;
1542
+ Object.values(CATALOG_STATE.facets).forEach(s => s.clear());
1543
+ if ($("cgSearch")) $("cgSearch").value = "";
1544
+ if ($("cgSort")) $("cgSort").value = "size_desc";
1545
+ buildFacetRail();
1546
+ renderCatalog();
1547
+ });
1548
+ $("cgViewGrid")?.addEventListener("click", () => setView("grid"));
1549
+ $("cgViewTable")?.addEventListener("click", () => setView("table"));
1550
+
1551
+ $("cgFacets")?.addEventListener("change", (e) => {
1552
+ const t = e.target;
1553
+ if (t.id === "cgHil") { CATALOG_STATE.hil = t.checked; renderCatalog(); return; }
1554
+ const group = t.getAttribute("data-facet-group");
1555
+ if (!group) return;
1556
+ const set = CATALOG_STATE.facets[group];
1557
+ if (t.checked) set.add(t.value); else set.delete(t.value);
1558
+ renderCatalog();
1559
+ });
1560
 
1561
+ $("cardGrid")?.addEventListener("click", (e) => {
1562
+ const chip = e.target.closest("[data-facet]");
1563
+ if (chip) { e.stopPropagation(); toggleFacet(chip.getAttribute("data-facet"), chip.getAttribute("data-value")); return; }
1564
+ if (e.target.closest("a")) return;
1565
+ const card = e.target.closest('.model-card[data-card="1"]');
1566
+ if (card) openModelModal(card.getAttribute("data-key"), card.getAttribute("data-cfg") || "", card.getAttribute("data-fmt") || "GGUF");
1567
+ });
1568
+
1569
+ renderCatalog();
 
 
 
 
1570
  }
1571
 
1572
  /* ---------- Boot ---------- */
 
1670
  MODALITIES = ["All"].concat(uniqueSorted(CATALOG.map(m => m.modality || "LLM")));
1671
  VARIANTS = ["All"].concat(uniqueSorted(CATALOG.map(m => m.variant || "Default")));
1672
 
1673
+ /* Defaults derived from the loaded catalog (so the UI opens on data that exists). */
1674
+ const hasOnnx = CATALOG.some(m => m.onnx_repo);
1675
+ const DEFAULT_FMT = hasOnnx ? "ONNX" : "GGUF";
1676
+ const DEFAULT_PURPOSE = defaultPurposeForFormat(DEFAULT_FMT);
1677
+ const allTargets = new Set();
1678
+ CATALOG.forEach(m => safeList(m.targets, []).forEach(t => allTargets.add(t)));
1679
+ const DEFAULT_COMPUTE = allTargets.has("CPU") ? "CPU" : (allTargets.has("NPU") ? "NPU" : "DSP");
1680
 
1681
  /* Explore init */
1682
  fillSelect($("arch"), ARCHS, "All");
1683
  buildRadioGroup($("modality"), "modality", MODALITIES, "All");
1684
+ buildRadioGroup($("fmt"), "fmt", ["ONNX", "GGUF"], DEFAULT_FMT);
1685
+ buildRadioGroup($("compute"), "compute", ["CPU", "NPU", "DSP"], DEFAULT_COMPUTE);
1686
+ buildRadioGroup($("purpose"), "purpose", ["PerformanceEstimate", "ORT Benchmarking", "llama.cpp Benchmarking"], DEFAULT_PURPOSE);
1687
 
1688
  EXPLORE_KEYS = filteredKeys("All", "All", "All");
1689
  updateExploreFromFilters();
 
1699
  $("modelKey").addEventListener("change", updateExploreOnModelChange);
1700
  $("hwCfg").addEventListener("change", updateExploreAny);
1701
 
1702
+ /* Catalog (cards + table + facet rail) */
1703
+ initCatalog();
 
 
1704
 
1705
+ /* Overview (constellation) */
1706
+ initOverview();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1707
 
1708
  window.addEventListener("resize", () => {
1709
+ if ($("sec-overview")?.classList.contains("visible")) redrawConstellation();
1710
+ if (CATALOG_STATE.view === "table") setupPinnedColumns($("modelsTable"), 1);
 
 
 
 
 
1711
  });
 
 
1712
  }
1713
 
1714
  document.addEventListener("DOMContentLoaded", init);
assets/logo_renesas_blue.svg ADDED
assets/logo_renesas_light.svg CHANGED
assets/logo_renesas_white.svg ADDED
generate_models_json.py ADDED
@@ -0,0 +1,842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ generate_models_json.py
4
+ =======================
5
+
6
+ Regenerate the dashboard's `models.json` directly from the Hugging Face repos of
7
+ an organization (default: "Renesas"), instead of maintaining the catalog by hand.
8
+
9
+ It reads, for every model repo in the org:
10
+ * the model-list / per-repo info API (always public, even for gated repos)
11
+ * the recursive file tree API (always public, even for gated repos)
12
+ * each `.metadata.yaml` (repo-level identity + per-artifact info)
13
+ * each `<artifact>/benchmarks/<chip>_<ep>.yaml` (the actual benchmark numbers)
14
+
15
+ and emits a `models.json` whose schema is the one `app.js` already consumes.
16
+
17
+ Gated repos
18
+ -----------
19
+ The org has gated ("gated: manual") repos. Their *listing* (ids, tags, file
20
+ tree, artifact dirs, benchmark file paths) is public, but their raw *file
21
+ contents* require an authorized token. Provide one to import their metrics:
22
+
23
+ * `--token hf_xxx`, or
24
+ * env HF_TOKEN / HUGGINGFACE_TOKEN / HUGGING_FACE_HUB_TOKEN, or
25
+ * a prior `huggingface-cli login` (cached token is picked up automatically).
26
+
27
+ Without an authorized token, gated repos are still listed (with their artifacts,
28
+ and -- where benchmark files exist in the public file tree -- their hardware
29
+ targets/config derived from the file paths) but their benchmark numbers are left
30
+ empty and reported as skipped.
31
+
32
+ Usage
33
+ -----
34
+ pip install requests pyyaml
35
+ python generate_models_json.py # writes ./models.json
36
+ python generate_models_json.py --org Renesas --output models.json
37
+ python generate_models_json.py --token hf_xxx # include gated repos' metrics
38
+ python generate_models_json.py --dry-run -v # print, don't write
39
+
40
+ Run it whenever the repos change to refresh the dashboard.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import argparse
46
+ import datetime as _dt
47
+ import json
48
+ import os
49
+ import re
50
+ import sys
51
+ import time
52
+ from pathlib import Path
53
+
54
+ try:
55
+ import requests
56
+ except ImportError: # pragma: no cover
57
+ sys.exit("Missing dependency 'requests'. Install with: pip install requests pyyaml")
58
+
59
+ try:
60
+ import yaml
61
+ except ImportError: # pragma: no cover
62
+ sys.exit("Missing dependency 'pyyaml'. Install with: pip install requests pyyaml")
63
+
64
+
65
+ HF_BASE = "https://huggingface.co"
66
+ DEFAULT_ORG = "Renesas"
67
+
68
+ # execution-provider / accelerator token -> (unit key in metric block, UI target)
69
+ EP_TO_UNIT = {
70
+ "cpu": ("cpu", "CPU"),
71
+ "npu": ("npu", "NPU"),
72
+ "dsp": ("dsp", "DSP"),
73
+ }
74
+ # benchmark.type values that mean "measured on real hardware" -> HIL block; everything
75
+ # else (projected / simulated / static estimate) -> Estimate block.
76
+ HIL_TYPES = {"hil", "measured", "hardware", "device", "hw", "hardware_in_the_loop"}
77
+
78
+ VARIANT_KEYWORDS = ["Instruct", "Chat", "Vision", "Guard", "Code", "Base"]
79
+ FORMAT_SUFFIXES = ["GGUF", "ONNX"]
80
+
81
+ # status codes worth retrying (rate limit + transient server errors)
82
+ RETRYABLE_STATUS = {429, 500, 502, 503, 504}
83
+
84
+
85
+ def warn(msg: str) -> None:
86
+ print(f" ! {msg}", file=sys.stderr)
87
+
88
+
89
+ # --------------------------------------------------------------------------- #
90
+ # HTTP helpers #
91
+ # --------------------------------------------------------------------------- #
92
+ class Client:
93
+ def __init__(self, token: str | None, verbose: bool = False):
94
+ self.session = requests.Session()
95
+ self.token = token
96
+ self.verbose = verbose
97
+ if token:
98
+ self.session.headers["Authorization"] = f"Bearer {token}"
99
+ self.session.headers["User-Agent"] = "renesas-ai-dashboard-generator/1.0"
100
+
101
+ def _get(self, url: str, *, params=None, retries: int = 3):
102
+ last = None
103
+ for attempt in range(retries + 1):
104
+ try:
105
+ r = self.session.get(url, params=params, timeout=30)
106
+ except requests.RequestException as exc: # network hiccup -> retry
107
+ last = exc
108
+ if attempt < retries:
109
+ time.sleep(1.0 + attempt)
110
+ continue
111
+ raise
112
+ # retry rate-limit / transient 5xx, honoring Retry-After when present
113
+ if r.status_code in RETRYABLE_STATUS and attempt < retries:
114
+ ra = r.headers.get("Retry-After")
115
+ try:
116
+ delay = float(ra) if ra is not None else None
117
+ except ValueError:
118
+ delay = None
119
+ if self.verbose:
120
+ warn(f"HTTP {r.status_code} from {url} β€” retrying (attempt {attempt + 1})")
121
+ time.sleep(delay if delay is not None else 1.5 * (attempt + 1))
122
+ continue
123
+ return r
124
+ raise last # type: ignore[misc]
125
+
126
+ def _json(self, r, url: str):
127
+ try:
128
+ return r.json()
129
+ except ValueError as exc: # non-JSON 200 (proxy / login interstitial / HTML)
130
+ snippet = (r.text or "")[:200].replace("\n", " ")
131
+ raise RuntimeError(
132
+ f"Expected JSON from {url} (HTTP {r.status_code}) but got non-JSON: {snippet!r}"
133
+ ) from exc
134
+
135
+ def get_json(self, url: str, *, params=None):
136
+ r = self._get(url, params=params)
137
+ r.raise_for_status()
138
+ return self._json(r, url)
139
+
140
+ def get_json_pages(self, url: str, *, params=None):
141
+ """Follow HF `Link: rel=next` pagination; return the concatenated array."""
142
+ out, next_url, next_params = [], url, params
143
+ while next_url:
144
+ r = self._get(next_url, params=next_params)
145
+ r.raise_for_status()
146
+ page = self._json(r, next_url)
147
+ if not isinstance(page, list):
148
+ raise RuntimeError(
149
+ f"Expected a JSON array from {next_url}, got {type(page).__name__}"
150
+ )
151
+ out.extend(page)
152
+ next_url = (r.links.get("next") or {}).get("url")
153
+ next_params = None # the cursor for later pages is embedded in the URL
154
+ return out
155
+
156
+ def get_text(self, path_url: str):
157
+ """Fetch a raw file. Returns (text, status).
158
+
159
+ status: "ok" | "gated" | "missing" | "error".
160
+ """
161
+ r = self._get(path_url)
162
+ if r.status_code == 200:
163
+ return r.text, "ok"
164
+ if r.status_code in (401, 403):
165
+ return None, "gated"
166
+ if r.status_code == 404:
167
+ return None, "missing"
168
+ return None, f"error:{r.status_code}"
169
+
170
+
171
+ # --------------------------------------------------------------------------- #
172
+ # Token resolution #
173
+ # --------------------------------------------------------------------------- #
174
+ def resolve_token(cli_token: str | None) -> str | None:
175
+ if cli_token:
176
+ return cli_token.strip()
177
+ for var in ("HF_TOKEN", "HUGGINGFACE_TOKEN", "HUGGING_FACE_HUB_TOKEN"):
178
+ val = os.environ.get(var)
179
+ if val:
180
+ return val.strip()
181
+ # huggingface_hub cached login token (huggingface-cli login)
182
+ try:
183
+ from huggingface_hub import get_token # type: ignore
184
+
185
+ tok = get_token()
186
+ if tok:
187
+ return tok.strip()
188
+ except Exception:
189
+ pass
190
+ for p in (
191
+ Path.home() / ".cache" / "huggingface" / "token",
192
+ Path.home() / ".huggingface" / "token",
193
+ ):
194
+ try:
195
+ if p.is_file():
196
+ txt = p.read_text(encoding="utf-8").strip()
197
+ if txt:
198
+ return txt
199
+ except Exception:
200
+ pass
201
+ return None
202
+
203
+
204
+ # --------------------------------------------------------------------------- #
205
+ # Small parsing helpers #
206
+ # --------------------------------------------------------------------------- #
207
+ def tag_value(tags: list[str], prefix: str) -> str | None:
208
+ for t in tags:
209
+ if t.startswith(prefix):
210
+ return t[len(prefix):]
211
+ return None
212
+
213
+
214
+ def parse_base_model(tags: list[str], upstream: str | None) -> str | None:
215
+ """The non-quantized base_model:* tag, else the metadata upstream."""
216
+ candidate = None
217
+ for t in tags:
218
+ if t.startswith("base_model:") and ":quantized:" not in t:
219
+ candidate = t.split(":", 1)[1]
220
+ break
221
+ if not candidate:
222
+ for t in tags:
223
+ if t.startswith("base_model:"):
224
+ candidate = t.split(":", 1)[-1]
225
+ break
226
+ return candidate or upstream
227
+
228
+
229
+ def short_name(repo_or_model_id: str) -> str:
230
+ return repo_or_model_id.split("/")[-1]
231
+
232
+
233
+ def strip_format_suffix(name: str) -> str:
234
+ for suf in FORMAT_SUFFIXES:
235
+ name = re.sub(rf"[-_ ]{suf}$", "", name, flags=re.IGNORECASE)
236
+ return name
237
+
238
+
239
+ def detect_variant(name: str) -> str:
240
+ low = name.lower()
241
+ for v in VARIANT_KEYWORDS:
242
+ if re.search(rf"(?:^|[-_ ]){v.lower()}(?:$|[-_ ])", low):
243
+ return v
244
+ return "Base"
245
+
246
+
247
+ def architecture_from(name: str, variant: str) -> str:
248
+ """Family + size label, e.g. 'Llama-3.1-8B' from 'Llama-3.1-8B-Instruct'."""
249
+ arch = strip_format_suffix(short_name(name))
250
+ arch = re.sub(rf"[-_ ]{re.escape(variant)}$", "", arch, flags=re.IGNORECASE)
251
+ return arch.strip("-_ ") or arch
252
+
253
+
254
+ def parse_size_m(*texts: str | None) -> int | None:
255
+ """Parameter count in millions. Handles '8.0B', '8B', '500M', '1.5b'."""
256
+ for text in texts:
257
+ if not text:
258
+ continue
259
+ m = re.search(r"(\d+(?:\.\d+)?)\s*([BM])\b", str(text), flags=re.IGNORECASE)
260
+ if m:
261
+ val = float(m.group(1))
262
+ return int(round(val * (1000 if m.group(2).upper() == "B" else 1)))
263
+ return None
264
+
265
+
266
+ def map_modality(meta_modality, tags: list[str], pipeline_tag: str | None) -> str:
267
+ vals = []
268
+ if isinstance(meta_modality, list):
269
+ vals = [str(v).lower() for v in meta_modality]
270
+ elif isinstance(meta_modality, str):
271
+ vals = [meta_modality.lower()]
272
+ if any(v in ("vision", "image", "multimodal", "vlm") for v in vals):
273
+ return "VLM"
274
+ if any(v in ("audio", "speech") for v in vals):
275
+ return "ALM"
276
+ if vals: # language / text
277
+ return "LLM"
278
+ # fallback from tags / pipeline tag
279
+ low_tags = [t.lower() for t in tags]
280
+ if "vlm" in low_tags or "image-text-to-text" == (pipeline_tag or ""):
281
+ return "VLM"
282
+ return "LLM"
283
+
284
+
285
+ def detect_format(repo_id: str, meta_format_type: str | None) -> str:
286
+ ft = (meta_format_type or "").lower()
287
+ if "onnx" in ft:
288
+ return "ONNX"
289
+ if "gguf" in ft:
290
+ return "GGUF"
291
+ low = repo_id.lower()
292
+ if low.endswith("-onnx") or "onnx" in low:
293
+ return "ONNX"
294
+ if low.endswith("-gguf") or "gguf" in low:
295
+ return "GGUF"
296
+ return "GGUF" # sensible default for this org
297
+
298
+
299
+ def normalize_chip_id(chip: str) -> str:
300
+ return re.sub(r"[^A-Za-z0-9]+", "-", chip).strip("-").upper()
301
+
302
+
303
+ def prettify_chip(chip: str) -> str:
304
+ parts = re.split(r"[-_]", chip)
305
+ out = ["R-Car" if p.lower() == "rcar" else p.upper() for p in parts if p]
306
+ return " ".join(out) if out else chip
307
+
308
+
309
+ def block_kind_for(bench_type: str | None) -> str:
310
+ return "hil" if str(bench_type or "").lower() in HIL_TYPES else "estimate"
311
+
312
+
313
+ def as_number(v):
314
+ if v is None or isinstance(v, bool):
315
+ return None
316
+ if isinstance(v, (int, float)):
317
+ return v
318
+ try:
319
+ s = str(v).strip()
320
+ return float(s) if s else None
321
+ except (TypeError, ValueError):
322
+ return None
323
+
324
+
325
+ def normalize_accuracy(v):
326
+ """app.js expects top-level accuracy as a 0..1 fraction; rescale percents."""
327
+ n = as_number(v)
328
+ if n is None:
329
+ return None
330
+ return n / 100.0 if n > 1.5 else n
331
+
332
+
333
+ # --------------------------------------------------------------------------- #
334
+ # Per-repo gathering #
335
+ # --------------------------------------------------------------------------- #
336
+ class RepoData:
337
+ def __init__(self, info: dict):
338
+ self.id: str = info.get("id") or info.get("modelId") or ""
339
+ if not self.id:
340
+ raise ValueError("repo info has no 'id'")
341
+ self.gated = info.get("gated") # False | "auto" | "manual"
342
+ self.private = bool(info.get("private"))
343
+ self.tags: list[str] = list(info.get("tags") or [])
344
+ self.pipeline_tag = info.get("pipeline_tag")
345
+ self.last_modified = info.get("lastModified") or info.get("last_modified")
346
+ self.created_at = info.get("createdAt") or info.get("created_at")
347
+ self.downloads = info.get("downloads")
348
+ self.likes = info.get("likes")
349
+ self.files: list[str] = []
350
+ self.artifact_dirs: list[str] = []
351
+ self.benchmark_files: list[str] = []
352
+ self.root_meta: dict | None = None
353
+ self.artifact_meta: dict[str, dict] = {}
354
+ self.bench_docs: dict[str, dict] = {} # path -> parsed yaml
355
+ self.bench_errors: list[tuple] = [] # (path, status) for fetch/parse failures
356
+ self.content_blocked = False # gated/forbidden file contents
357
+ self.license = (tag_value(self.tags, "license:") or info.get("license"))
358
+ self.base_model = parse_base_model(self.tags, None)
359
+ self.format = detect_format(self.id, None)
360
+
361
+
362
+ def list_org_repos(client: Client, org: str, include_private: bool) -> list[str]:
363
+ data = client.get_json_pages(
364
+ f"{HF_BASE}/api/models", params={"author": org, "limit": 100}
365
+ )
366
+ ids = []
367
+ for m in data:
368
+ if not isinstance(m, dict):
369
+ warn(f"unexpected listing entry (not an object) skipped: {m!r:.80}")
370
+ continue
371
+ rid = m.get("id")
372
+ if not rid:
373
+ warn(f"listing entry without 'id' skipped: {m!r:.80}")
374
+ continue
375
+ if m.get("private") and not include_private:
376
+ continue
377
+ ids.append(rid)
378
+ return sorted(ids)
379
+
380
+
381
+ def fetch_tree(client: Client, repo: str) -> list[str]:
382
+ """Recursive file list. Public even for gated repos."""
383
+ try:
384
+ tree = client.get_json(
385
+ f"{HF_BASE}/api/models/{repo}/tree/main", params={"recursive": "true"}
386
+ )
387
+ return [e["path"] for e in tree if e.get("type") == "file"]
388
+ except requests.HTTPError:
389
+ return []
390
+
391
+
392
+ def raw_url(repo: str, path: str) -> str:
393
+ return f"{HF_BASE}/{repo}/raw/main/{path}"
394
+
395
+
396
+ def fetch_yaml(client: Client, repo: str, path: str):
397
+ """Return (parsed_dict_or_None, status)."""
398
+ text, status = client.get_text(raw_url(repo, path))
399
+ if status != "ok" or text is None:
400
+ return None, status
401
+ try:
402
+ doc = yaml.safe_load(text)
403
+ return (doc if isinstance(doc, dict) else {}), "ok"
404
+ except yaml.YAMLError:
405
+ return None, "error:yaml"
406
+
407
+
408
+ def gather_repo(client: Client, repo: str, verbose: bool) -> RepoData:
409
+ info = client.get_json(f"{HF_BASE}/api/models/{repo}")
410
+ rd = RepoData(info)
411
+ rd.files = fetch_tree(client, repo) or [
412
+ s.get("rfilename") for s in info.get("siblings", []) if s.get("rfilename")
413
+ ]
414
+ rd.format = detect_format(repo, None)
415
+
416
+ # artifact dirs = top-level dirs that carry a `.metadata.yaml`
417
+ art_dirs: list[str] = []
418
+ for f in rd.files:
419
+ parts = f.split("/")
420
+ if len(parts) == 2 and parts[1] == ".metadata.yaml":
421
+ art_dirs.append(parts[0])
422
+ rd.artifact_dirs = sorted(set(art_dirs))
423
+
424
+ # benchmark files: <artifact>/benchmarks/<name>.yaml|yml
425
+ rd.benchmark_files = sorted(
426
+ f for f in rd.files if re.search(r"/benchmarks/[^/]+\.ya?ml$", f)
427
+ )
428
+
429
+ # root .metadata.yaml
430
+ if ".metadata.yaml" in rd.files:
431
+ doc, status = fetch_yaml(client, repo, ".metadata.yaml")
432
+ if status == "gated":
433
+ rd.content_blocked = True
434
+ rd.root_meta = doc
435
+ if doc:
436
+ rd.format = detect_format(repo, (doc.get("format") or {}).get("type"))
437
+ up = (doc.get("model") or {}).get("upstream")
438
+ if not rd.base_model:
439
+ rd.base_model = up
440
+
441
+ # per-artifact .metadata.yaml (optional enrichment)
442
+ for d in rd.artifact_dirs:
443
+ p = f"{d}/.metadata.yaml"
444
+ if p in rd.files:
445
+ doc, status = fetch_yaml(client, repo, p)
446
+ if status == "gated":
447
+ rd.content_blocked = True
448
+ if doc:
449
+ rd.artifact_meta[d] = doc
450
+
451
+ # benchmark docs
452
+ for bf in rd.benchmark_files:
453
+ doc, status = fetch_yaml(client, repo, bf)
454
+ if status == "gated":
455
+ rd.content_blocked = True
456
+ elif status not in ("ok", "missing"):
457
+ rd.bench_errors.append((bf, status))
458
+ warn(f"{repo}: could not read benchmark {bf} ({status})")
459
+ if doc:
460
+ rd.bench_docs[bf] = doc
461
+
462
+ # warn if we could not confidently determine the repo format
463
+ low = repo.lower()
464
+ mft = str(((rd.root_meta or {}).get("format") or {}).get("type", "")).lower()
465
+ if not any(tok in low or tok in mft for tok in ("onnx", "gguf")):
466
+ warn(f"{repo}: could not determine format; defaulting to {rd.format}. "
467
+ f"Add explicit handling if this is a new format.")
468
+
469
+ if verbose:
470
+ state = "gated/no-access" if rd.content_blocked else "readable"
471
+ print(
472
+ f" Β· {repo}: format={rd.format} artifacts={rd.artifact_dirs} "
473
+ f"benchmarks={len(rd.benchmark_files)} contents={state}"
474
+ )
475
+ return rd
476
+
477
+
478
+ # --------------------------------------------------------------------------- #
479
+ # Benchmark file -> hardware config / metric block #
480
+ # --------------------------------------------------------------------------- #
481
+ def parse_bench_path(path: str):
482
+ """Return (artifact_dir, chip_token, ep_token) from the file path."""
483
+ parts = path.split("/")
484
+ artifact = parts[0]
485
+ base = re.sub(r"\.ya?ml$", "", parts[-1])
486
+ seg = base.split("_")
487
+ if len(seg) >= 2:
488
+ ep = seg[-1]
489
+ chip = "_".join(seg[:-1])
490
+ else:
491
+ ep = seg[-1]
492
+ chip = seg[-1]
493
+ return artifact, chip, ep
494
+
495
+
496
+ def build_metric_block(doc: dict, ep: str, kind: str, repo_date: str | None, chip_label: str):
497
+ hardware = doc.get("hardware") or {}
498
+ runtime = doc.get("runtime") or {}
499
+ perf = doc.get("performance") or {}
500
+ metrics = doc.get("metrics") or {}
501
+ memory = doc.get("memory") or {}
502
+ bench = doc.get("benchmark") or {}
503
+
504
+ ep_token = (runtime.get("execution_provider") or ep or "").lower()
505
+ unit_key, _target = EP_TO_UNIT.get(ep_token, ("npu", "NPU"))
506
+
507
+ tok = as_number(perf.get("tokens_per_sec"))
508
+ if tok is None:
509
+ tok = as_number(perf.get("tok_s"))
510
+ peak = as_number(memory.get("peak_mb"))
511
+ if peak is None:
512
+ peak = as_number(memory.get("peak_mem_mb"))
513
+ lat = as_number(perf.get("latency_ms_p50"))
514
+ acc = normalize_accuracy(metrics.get("accuracy"))
515
+
516
+ # Nothing measured -> no block (keeps null so the UI shows "β€”").
517
+ if tok is None and peak is None and lat is None and acc is None:
518
+ return None
519
+
520
+ unit = {"tok_s": tok, "latency_ms_p50": lat, "peak_mem_mb": peak}
521
+ block = {
522
+ "accuracy": acc,
523
+ unit_key: unit,
524
+ "total": {"tok_s": tok, "latency_ms_p50": lat, "peak_mem_mb": peak},
525
+ "engine": runtime.get("engine"),
526
+ "execution_provider": ep_token or None,
527
+ "last_updated": (repo_date or "")[:10] or None,
528
+ }
529
+ bs = ((bench.get("parameters") or {}).get("batch_size"))
530
+ if bs is not None:
531
+ block["batch_size"] = bs
532
+ if kind == "hil":
533
+ block["setup"] = chip_label
534
+ return block
535
+
536
+
537
+ # --------------------------------------------------------------------------- #
538
+ # Model assembly (group repos that are the same model in different formats) #
539
+ # --------------------------------------------------------------------------- #
540
+ def group_key(repo_id: str) -> str:
541
+ return strip_format_suffix(repo_id).lower()
542
+
543
+
544
+ def build_model(repos: list[RepoData], verbose: bool) -> tuple[dict, dict]:
545
+ """Build one models.json entry from one or more repos (same model, diff formats)."""
546
+ # Identity: prefer a repo whose root metadata is readable.
547
+ primary = next((r for r in repos if r.root_meta), repos[0])
548
+ rm = primary.root_meta or {}
549
+ rm_model = rm.get("model") or {}
550
+ rm_arch = rm.get("architecture") or {}
551
+
552
+ base_model = next((r.base_model for r in repos if r.base_model), None)
553
+ name_src = base_model or short_name(primary.id)
554
+ short = short_name(name_src)
555
+
556
+ variant = detect_variant(short)
557
+ display_name = rm_model.get("display_name") or strip_format_suffix(short)
558
+ key = (rm_model.get("name") or strip_format_suffix(short)).lower()
559
+ key = re.sub(r"[^a-z0-9.]+", "-", key).strip("-.")
560
+ architecture = architecture_from(name_src, variant)
561
+ size_m = parse_size_m(rm_arch.get("parameters"), short, primary.id)
562
+ modality = map_modality(rm_arch.get("modality"), primary.tags, primary.pipeline_tag)
563
+ license_ = next((r.license for r in repos if r.license), None)
564
+ tasks = rm.get("tasks") if isinstance(rm.get("tasks"), list) else None
565
+
566
+ model = {
567
+ "key": key,
568
+ "display_name": display_name,
569
+ "architecture": architecture,
570
+ "model_size_m": size_m,
571
+ "modality": modality,
572
+ "variant": variant,
573
+ "targets": [],
574
+ "onnx_repo": "",
575
+ "gguf_repo": "",
576
+ "onnx_artifacts": [],
577
+ "gguf_artifacts": [],
578
+ "base_model": base_model,
579
+ "license": license_,
580
+ "tasks": tasks,
581
+ "created_at": min([r.created_at for r in repos if r.created_at], default=None),
582
+ "last_modified": max([r.last_modified for r in repos if r.last_modified], default=None),
583
+ "hardware_configs": [],
584
+ }
585
+
586
+ # Per-format repo + artifacts
587
+ all_artifacts: list[str] = []
588
+ for r in repos:
589
+ if r.format == "ONNX":
590
+ model["onnx_repo"] = r.id
591
+ model["onnx_artifacts"] = r.artifact_dirs
592
+ else: # GGUF (and any default)
593
+ model["gguf_repo"] = r.id
594
+ model["gguf_artifacts"] = r.artifact_dirs
595
+ all_artifacts.extend(r.artifact_dirs)
596
+ all_artifacts = list(dict.fromkeys(all_artifacts)) # de-dupe, keep order
597
+
598
+ # Hardware configs keyed by chip, with metrics.artifacts[<art>].{estimate,hil}
599
+ configs: dict[str, dict] = {}
600
+ targets_seen: set[str] = set()
601
+ stats = {"id": primary.id, "configs": 0, "metric_blocks": 0,
602
+ "content_blocked": any(r.content_blocked for r in repos),
603
+ "bench_files": sum(len(r.benchmark_files) for r in repos),
604
+ "bench_errors": sum(len(r.bench_errors) for r in repos)}
605
+
606
+ for r in repos:
607
+ for bf in r.benchmark_files:
608
+ artifact, chip_tok, ep_tok = parse_bench_path(bf)
609
+ doc = r.bench_docs.get(bf)
610
+
611
+ chip = ((doc or {}).get("hardware") or {}).get("chip") or chip_tok
612
+ cfg_id = normalize_chip_id(chip)
613
+ ep = (((doc or {}).get("runtime") or {}).get("execution_provider")
614
+ or ep_tok).lower()
615
+ if ep not in EP_TO_UNIT:
616
+ warn(f"{r.id}: unknown execution_provider '{ep}' in {bf}; "
617
+ f"attributing to NPU β€” add it to EP_TO_UNIT if that is wrong.")
618
+ unit_key, target = EP_TO_UNIT.get(ep, ("npu", "NPU"))
619
+ targets_seen.add(target)
620
+
621
+ if cfg_id not in configs:
622
+ npu = ((doc or {}).get("hardware") or {}).get("npu")
623
+ label = prettify_chip(chip) + (f" ({str(npu).upper()})" if npu else "")
624
+ configs[cfg_id] = {
625
+ "id": cfg_id,
626
+ "label": label,
627
+ "targets": set(),
628
+ "notes": _hw_notes((doc or {}).get("hardware") or {}),
629
+ "metrics": {"artifacts": {a: {"estimate": None, "hil": None}
630
+ for a in all_artifacts}},
631
+ }
632
+ cfg = configs[cfg_id]
633
+ cfg["targets"].add(target)
634
+ cfg["metrics"]["artifacts"].setdefault(
635
+ artifact, {"estimate": None, "hil": None}
636
+ )
637
+
638
+ if doc:
639
+ kind = block_kind_for((doc.get("benchmark") or {}).get("type"))
640
+ block = build_metric_block(doc, ep, kind, r.last_modified, cfg["label"])
641
+ if block:
642
+ cfg["metrics"]["artifacts"][artifact][kind] = block
643
+ stats["metric_blocks"] += 1
644
+
645
+ # finalize configs (sets -> sorted lists)
646
+ hw_configs = []
647
+ for cfg in configs.values():
648
+ cfg["targets"] = sorted(cfg["targets"])
649
+ hw_configs.append(cfg)
650
+ hw_configs.sort(key=lambda c: c["id"])
651
+ model["hardware_configs"] = hw_configs
652
+ model["targets"] = sorted(targets_seen)
653
+ stats["configs"] = len(hw_configs)
654
+
655
+ # The dashboard's table + Explore open on the FIRST artifact. Put artifacts that
656
+ # actually have measured metrics first (stable sort keeps upstream order otherwise),
657
+ # so the default view shows real numbers instead of an empty "β€”" row.
658
+ def _has_data(art: str) -> bool:
659
+ for cfg in hw_configs:
660
+ a = cfg["metrics"]["artifacts"].get(art) or {}
661
+ if a.get("estimate") or a.get("hil"):
662
+ return True
663
+ return False
664
+
665
+ model["gguf_artifacts"] = sorted(model["gguf_artifacts"], key=lambda a: 0 if _has_data(a) else 1)
666
+ model["onnx_artifacts"] = sorted(model["onnx_artifacts"], key=lambda a: 0 if _has_data(a) else 1)
667
+
668
+ if verbose:
669
+ print(f" -> model '{key}' ({display_name}): {len(hw_configs)} config(s), "
670
+ f"{stats['metric_blocks']} metric block(s)")
671
+ return model, stats
672
+
673
+
674
+ def _hw_notes(hardware: dict) -> str:
675
+ if not hardware:
676
+ return ""
677
+ bits = []
678
+ for k in ("vendor", "chip", "cpu", "npu", "memory_mb"):
679
+ v = hardware.get(k)
680
+ if v not in (None, ""):
681
+ bits.append(f"{k}={v}")
682
+ return "; ".join(bits)
683
+
684
+
685
+ # --------------------------------------------------------------------------- #
686
+ # Main #
687
+ # --------------------------------------------------------------------------- #
688
+ def build_catalog(client: Client, org: str, include_private: bool, verbose: bool):
689
+ print(f"Listing models for org '{org}' ...")
690
+ repo_ids = list_org_repos(client, org, include_private)
691
+ print(f"Found {len(repo_ids)} repo(s): {', '.join(repo_ids) or '(none)'}")
692
+
693
+ repos: list[RepoData] = []
694
+ for rid in repo_ids:
695
+ try:
696
+ repos.append(gather_repo(client, rid, verbose))
697
+ except requests.HTTPError as exc:
698
+ print(f" ! skipping {rid}: HTTP {exc.response.status_code if exc.response else '?'}")
699
+ except Exception as exc: # noqa: BLE001 (keep going on a single bad repo)
700
+ print(f" ! skipping {rid}: {exc}")
701
+
702
+ # group repos that are the same logical model in different formats
703
+ groups: dict[str, list[RepoData]] = {}
704
+ for r in repos:
705
+ groups.setdefault(group_key(r.id), []).append(r)
706
+
707
+ models, all_stats = [], []
708
+ for gkey in sorted(groups):
709
+ model, stats = build_model(groups[gkey], verbose)
710
+ models.append(model)
711
+ all_stats.append(stats)
712
+ models.sort(key=lambda m: (m.get("model_size_m") or 0, m["key"]))
713
+
714
+ # guarantee unique keys β€” app.js builds KEY_TO_MODEL keyed by `key`, so a
715
+ # collision would silently drop a model from the dashboard.
716
+ seen: dict[str, int] = {}
717
+ for m in models:
718
+ base = m["key"] or "model"
719
+ key = base
720
+ while key in seen:
721
+ seen[base] = seen.get(base, 1) + 1
722
+ key = f"{base}-{seen[base]}"
723
+ if key != m["key"]:
724
+ warn(f"duplicate model key '{m['key']}' -> '{key}' (renamed to avoid collision)")
725
+ seen[key] = 1
726
+ m["key"] = key
727
+ return models, all_stats
728
+
729
+
730
+ def main(argv=None):
731
+ parser = argparse.ArgumentParser(description=__doc__,
732
+ formatter_class=argparse.RawDescriptionHelpFormatter)
733
+ parser.add_argument("--org", default=DEFAULT_ORG, help="HF org/owner (default: Renesas)")
734
+ parser.add_argument("--output", "-o", default=str(Path(__file__).with_name("models.json")),
735
+ help="output path (default: ./models.json next to this script)")
736
+ parser.add_argument("--token", default=None, help="HF token (else env / cached login)")
737
+ parser.add_argument("--include-private", action="store_true",
738
+ help="also include private repos (requires token with access)")
739
+ parser.add_argument("--dry-run", action="store_true", help="print JSON, do not write")
740
+ parser.add_argument("--force", action="store_true",
741
+ help="write even if 0 models were produced (overwrites existing file)")
742
+ parser.add_argument("--verbose", "-v", action="store_true")
743
+ args = parser.parse_args(argv)
744
+
745
+ token = resolve_token(args.token)
746
+ print(f"Auth: {'token present' if token else 'anonymous (gated repo metrics will be skipped)'}")
747
+ client = Client(token, verbose=args.verbose)
748
+
749
+ # Top-level network guard: a failure on the initial listing must not dump a
750
+ # raw traceback on the admin β€” report it and exit with a distinct code.
751
+ try:
752
+ models, stats = build_catalog(client, args.org, args.include_private, args.verbose)
753
+ except requests.HTTPError as exc:
754
+ code = exc.response.status_code if exc.response is not None else "?"
755
+ print(f"ERROR: Hugging Face API returned HTTP {code} while listing org '{args.org}' "
756
+ f"(rate-limited or unavailable). Try again later.", file=sys.stderr)
757
+ return 2
758
+ except requests.RequestException as exc:
759
+ print(f"ERROR: could not reach {HF_BASE} (network / DNS / proxy): {exc}", file=sys.stderr)
760
+ return 2
761
+ except RuntimeError as exc: # e.g. non-JSON response from a proxy/interstitial
762
+ print(f"ERROR: {exc}", file=sys.stderr)
763
+ return 2
764
+
765
+ now = _dt.datetime.now().astimezone()
766
+ gated_skipped = [s["id"] for s in stats if s["content_blocked"] and s["metric_blocks"] == 0]
767
+ note = (
768
+ f"Auto-generated from Hugging Face '{args.org}' repositories on "
769
+ f"{now.strftime('%Y-%m-%d %H:%M %Z')}. Metrics reflect each repo's published "
770
+ f"benchmark YAML files."
771
+ )
772
+ if gated_skipped:
773
+ reason = ("token not authorized for these repos" if token
774
+ else "no authorized token β€” re-run with --token")
775
+ note += (f" Benchmark metrics for gated repo(s) were not imported "
776
+ f"({reason}): {', '.join(gated_skipped)}.")
777
+
778
+ catalog = {
779
+ "version": "1.0-auto",
780
+ "generated_at": now.isoformat(timespec="seconds"),
781
+ "source": "huggingface",
782
+ "org": args.org,
783
+ "catalog_note": note,
784
+ "models": models,
785
+ }
786
+
787
+ payload = json.dumps(catalog, indent=2, ensure_ascii=False)
788
+
789
+ # ---- summary ----
790
+ print("\nSummary")
791
+ print("-------")
792
+ for s in stats:
793
+ flag = ""
794
+ if s["content_blocked"] and s["metric_blocks"] == 0:
795
+ flag = (" [gated: token lacks access β€” request access on HF]" if token
796
+ else " [gated: metrics skipped β€” provide --token]")
797
+ elif s["content_blocked"]:
798
+ flag = " [gated: imported with token]"
799
+ print(f" {s['id']}: {s['configs']} config(s), {s['metric_blocks']} metric block(s){flag}")
800
+ if s["bench_errors"]:
801
+ print(f" ! {s['bench_errors']} benchmark file(s) failed to parse/fetch "
802
+ f"(see warnings above) β€” those metrics were not imported")
803
+ elif not s["content_blocked"] and s["bench_files"] > s["metric_blocks"]:
804
+ missing = s["bench_files"] - s["metric_blocks"]
805
+ print(f" note: {missing} benchmark file(s) had no recognized metric values")
806
+ print(f"\n{len(models)} model(s) total.")
807
+ if gated_skipped and not token:
808
+ print("Tip: pass --token (or set HF_TOKEN / run `huggingface-cli login`) "
809
+ "to import gated repos' benchmark numbers.")
810
+
811
+ if args.dry_run:
812
+ print("\n--dry-run: not writing file. JSON follows:\n")
813
+ print(payload)
814
+ return 0
815
+
816
+ out = Path(args.output)
817
+ if not models and not args.force:
818
+ if out.exists():
819
+ print(f"\nERROR: produced 0 models; refusing to overwrite existing {out} "
820
+ f"(use --force to override).", file=sys.stderr)
821
+ return 4
822
+ warn("produced 0 models β€” writing an empty catalog (no existing file to protect)")
823
+
824
+ # Atomic write: write a sibling temp file then replace, so an interrupted or
825
+ # failed write never leaves the live models.json truncated.
826
+ tmp = out.with_name(out.name + ".tmp")
827
+ try:
828
+ tmp.write_text(payload + "\n", encoding="utf-8")
829
+ os.replace(tmp, out)
830
+ except OSError as exc:
831
+ print(f"\nERROR: could not write {out}: {exc}", file=sys.stderr)
832
+ try:
833
+ tmp.unlink(missing_ok=True)
834
+ except OSError:
835
+ pass
836
+ return 3
837
+ print(f"\nWrote {out} ({len(payload)} bytes)")
838
+ return 0
839
+
840
+
841
+ if __name__ == "__main__":
842
+ raise SystemExit(main())
index.html CHANGED
@@ -3,153 +3,258 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Renesas AI Dashboard (POC) β€” Static</title>
 
 
 
7
  <link rel="stylesheet" href="styles.css" />
8
  </head>
9
 
10
  <body>
 
 
 
 
 
11
  <header class="topbar" role="banner">
12
  <div class="topbar-inner">
13
- <div class="brand" aria-label="Renesas AI Dashboard">
14
- <img class="brand-icon" src="assets/logo_renesas_light.svg" alt="Company logo" onerror="this.style.display='none'"/>
 
15
  <div class="brand-text">
16
- <div class="brand-title">AI Dashboard (POC)</div>
17
- <div class="brand-subtitle">Renesas AI Model Explorer</div>
18
  </div>
19
  </div>
20
 
21
- <nav class="tabs" role="navigation" aria-label="Primary">
22
- <button class="nav-item active" data-target="sec-explore" type="button">Explore</button>
23
- <button class="nav-item" data-target="sec-all" type="button">All Models</button>
 
24
  </nav>
 
 
 
 
25
  </div>
26
  </header>
27
 
28
  <main class="content" role="main">
29
 
30
- <!-- Explore -->
31
- <section id="sec-explore" class="section visible">
32
- <div class="section-scroll">
33
- <div class="grid explore-grid" style="align-items:start;">
34
- <div class="card">
35
- <h3 class="no-border">Filters</h3>
 
 
 
 
 
 
 
36
 
37
- <div class="form-grid explore-form">
38
- <label class="field">
39
- <span class="label">Architecture</span>
40
- <select id="arch" class="filter-select"></select>
41
- </label>
42
-
43
- <label class="field">
44
- <span class="label">Modality</span>
45
- <div class="radio-row" id="modality"></div>
46
- </label>
47
-
48
- <label class="field">
49
- <span class="label">Purpose</span>
50
- <div class="radio-col" id="purpose"></div>
51
- </label>
52
-
53
- <div class="field-row two">
54
- <label class="field">
55
- <span class="label">Format</span>
56
- <div class="radio-row" id="fmt"></div>
57
- </label>
58
 
59
- <label class="field">
60
- <span class="label">Compute Target</span>
61
- <div class="radio-row" id="compute"></div>
62
- </label>
63
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- <label class="field">
66
- <span class="label">Hardware Config</span>
67
- <select id="hwCfg" class="filter-select"></select>
68
- </label>
 
 
 
 
 
 
69
 
70
- <div class="matchline" id="matchCount"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- <div class="model-picker">
73
- <div class="model-picker-head">
74
- <div class="model-picker-title">Model</div>
75
- <div class="model-picker-subtle" id="modelCountHint"></div>
76
- </div>
77
- <select id="modelKey" class="filter-select model-list" size="12"></select>
78
- </div>
79
  </div>
80
  </div>
81
 
82
- <div class="card">
83
- <div id="warning" class="note note-warn" style="display:none"></div>
84
- <div id="details"></div>
85
- <div id="actions"></div>
 
 
 
 
 
 
 
86
  </div>
87
  </div>
88
  </div>
89
  </section>
90
 
91
- <!-- All Models (updated) -->
92
- <section id="sec-all" class="section">
93
- <div class="section-scroll">
94
  <div class="section-head">
95
- <h1 class="section-title">All Models</h1>
96
- <p class="subtle tight">
97
- Table shows <b>default hardware config</b> main metrics per model. Use <b>Details</b> to open full model card.
98
- </p>
 
 
 
99
  </div>
 
 
 
100
 
101
- <div class="card">
102
- <div class="filters-row compact" id="t_filters_row">
103
- <label class="field compact">
104
- <span class="label">Modality</span>
105
- <select id="t_modality" class="filter-select compact"></select>
106
- </label>
107
-
108
- <div class="field compact">
109
- <span class="label">Format</span>
110
- <div class="check-row compact" id="t_format"></div>
111
- </div>
 
112
 
113
- <div class="field compact">
114
- <span class="label">Supported HW</span>
115
- <div class="check-row compact" id="t_hw"></div>
116
- </div>
 
 
 
 
 
 
117
 
118
- <label class="field inline compact">
119
- <input type="checkbox" id="t_need_est" />
120
- <span>Has Estimate</span>
121
- </label>
 
 
 
 
122
 
123
- <label class="field inline compact">
124
- <input type="checkbox" id="t_need_hil" />
125
- <span>Has HIL</span>
126
- </label>
 
 
 
 
 
 
 
 
127
 
128
- <label class="field compact grow">
129
- <span class="label">Search</span>
130
- <input id="t_search" class="filter-input compact" placeholder="Search…" />
131
- </label>
132
- </div>
 
133
 
134
- <div class="subtle" id="t_status"></div>
135
 
136
- <div class="table-shell">
137
- <div class="table-wrap">
138
- <table class="table" id="modelsTable" aria-label="Models table">
139
- <thead></thead>
140
- <tbody></tbody>
141
- </table>
 
142
  </div>
143
  </div>
 
 
 
 
 
 
144
  </div>
145
- </div>
146
  </section>
147
 
148
  </main>
149
 
150
- <div id="disclaimer" class="poc-overlay" aria-live="polite"></div>
151
-
152
- <!-- Model details modal (All Models -> Details) -->
153
  <div id="modelModal" class="modal" aria-hidden="true" role="dialog" aria-label="Model details">
154
  <div class="modal-backdrop" data-modal-close="1"></div>
155
  <div class="modal-panel" role="document">
@@ -164,6 +269,120 @@
164
  </div>
165
  </div>
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  <script src="app.js" defer></script>
168
  </body>
169
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Renesas AI Models</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@400;500;600;700&display=swap" />
10
  <link rel="stylesheet" href="styles.css" />
11
  </head>
12
 
13
  <body>
14
+ <div class="bg-fx" aria-hidden="true">
15
+ <canvas id="bgfx"></canvas>
16
+ <span class="blob b1"></span>
17
+ <span class="blob b2"></span>
18
+ </div>
19
  <header class="topbar" role="banner">
20
  <div class="topbar-inner">
21
+ <div class="brand" aria-label="Renesas AI Models">
22
+ <img class="brand-icon" src="assets/logo_renesas_blue.svg" alt="Renesas" onerror="this.style.display='none'"/>
23
+ <span class="brand-divider" aria-hidden="true"></span>
24
  <div class="brand-text">
25
+ <div class="brand-title">AI Models</div>
26
+ <div class="brand-subtitle">Model Explorer</div>
27
  </div>
28
  </div>
29
 
30
+ <nav class="tabs" role="navigation" aria-label="Primary" id="primaryNav">
31
+ <button class="nav-item active" data-target="sec-overview" type="button">Overview</button>
32
+ <button class="nav-item" data-target="sec-catalog" type="button">Catalog</button>
33
+ <button class="nav-item" data-target="sec-explore" type="button">Deploy</button>
34
  </nav>
35
+
36
+ <button class="nav-burger" id="navBurger" type="button" aria-label="Open menu" aria-expanded="false" aria-controls="primaryNav">
37
+ <span></span><span></span><span></span>
38
+ </button>
39
  </div>
40
  </header>
41
 
42
  <main class="content" role="main">
43
 
44
+ <!-- ===================== OVERVIEW (constellation) ===================== -->
45
+ <section id="sec-overview" class="section visible">
46
+ <section class="carousel-section featured" id="recentSection" hidden>
47
+ <div class="carousel-head">
48
+ <h2 class="carousel-title">Last Update</h2>
49
+ <div class="carousel-actions">
50
+ <span class="carousel-sub" id="recentSub"></span>
51
+ <button class="carousel-nav" id="recentPrev" type="button" aria-label="Scroll left">β€Ή</button>
52
+ <button class="carousel-nav" id="recentNext" type="button" aria-label="Scroll right">β€Ί</button>
53
+ </div>
54
+ </div>
55
+ <div class="carousel-track" id="recentTrack"></div>
56
+ </section>
57
 
58
+ <div class="stat-tiles" id="statTiles"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ <div class="card constellation-card">
61
+ <div class="card-head">
62
+ <h2 class="card-head-title">Model constellation</h2>
63
+ <p class="subtle tight" style="margin:0;">Every model and quantization plotted by throughput and size. Bubble size ∝ parameters. Click a point for details.</p>
64
+ </div>
65
+ <button class="filter-toggle" type="button" data-target="cstlControls" id="cstlFilterToggle" aria-controls="cstlControls" aria-expanded="false">
66
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M1.5 3h13M3.5 8h9M6 13h4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
67
+ <span>Chart filters</span>
68
+ <svg class="ft-chevron" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
69
+ </button>
70
+ <div class="cstl-controls" id="cstlControls">
71
+ <label class="field compact">
72
+ <span class="label">X axis</span>
73
+ <select id="cstlX" class="filter-select compact">
74
+ <option value="tok">Throughput (tok/s)</option>
75
+ <option value="size">Model size (params)</option>
76
+ </select>
77
+ </label>
78
+ <label class="field compact">
79
+ <span class="label">Y axis</span>
80
+ <select id="cstlY" class="filter-select compact">
81
+ <option value="size">Model size (params)</option>
82
+ <option value="tok">Throughput (tok/s)</option>
83
+ </select>
84
+ </label>
85
+ <label class="field compact">
86
+ <span class="label">Color by</span>
87
+ <select id="cstlColor" class="filter-select compact">
88
+ <option value="family">Family</option>
89
+ <option value="quant">Quantization</option>
90
+ <option value="format">Format</option>
91
+ <option value="target">Target</option>
92
+ </select>
93
+ </label>
94
+ <div class="field compact">
95
+ <span class="label">Source</span>
96
+ <div class="radio-row" id="cstlSource"></div>
97
+ </div>
98
+ </div>
99
 
100
+ <div class="constellation">
101
+ <canvas id="cstlCanvas"></canvas>
102
+ <div id="cstlTooltip" class="mini-tooltip"></div>
103
+ </div>
104
+ <div class="cstl-foot">
105
+ <div id="cstlLegend" class="legend"></div>
106
+ <div id="cstlStatus" class="subtle tight"></div>
107
+ </div>
108
+ </div>
109
+ </section>
110
 
111
+ <!-- ===================== CATALOG (cards + table) ===================== -->
112
+ <section id="sec-catalog" class="section">
113
+ <div class="catalog-layout">
114
+ <aside class="facets" aria-label="Filters">
115
+ <div class="facets-head">
116
+ <button class="filter-toggle" type="button" data-target="facetsBody" aria-controls="facetsBody" aria-expanded="false">
117
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M1.5 3h13M3.5 8h9M6 13h4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
118
+ <span>Filters</span>
119
+ <svg class="ft-chevron" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
120
+ </button>
121
+ <span class="facets-title">Filters</span>
122
+ <button class="link-btn" id="cgReset" type="button">Reset</button>
123
+ </div>
124
+ <div class="facets-body" id="facetsBody">
125
+ <label class="field">
126
+ <span class="label">Search</span>
127
+ <input id="cgSearch" class="filter-input" placeholder="Search models…" />
128
+ </label>
129
+ <label class="field">
130
+ <span class="label">Sort</span>
131
+ <select id="cgSort" class="filter-select">
132
+ <option value="size_desc">Largest first</option>
133
+ <option value="size_asc">Smallest first</option>
134
+ <option value="tok_desc">Fastest (tok/s)</option>
135
+ <option value="name_asc">Name A–Z</option>
136
+ </select>
137
+ </label>
138
+ <div id="cgFacets" class="facet-groups"></div>
139
+ </div>
140
+ </aside>
141
 
142
+ <div class="catalog-main">
143
+ <div class="toolbar">
144
+ <div class="toolbar-count" id="cgCount"></div>
145
+ <div class="view-toggle" role="tablist" aria-label="View">
146
+ <button id="cgViewGrid" class="view-btn active" type="button" aria-pressed="true">β–¦ Grid</button>
147
+ <button id="cgViewTable" class="view-btn" type="button" aria-pressed="false">β–€ Table</button>
 
148
  </div>
149
  </div>
150
 
151
+ <div id="cardGrid" class="card-grid"></div>
152
+
153
+ <div id="tableView" class="table-view" hidden>
154
+ <div class="table-shell">
155
+ <div class="table-wrap">
156
+ <table class="table" id="modelsTable" aria-label="Models table">
157
+ <thead></thead>
158
+ <tbody></tbody>
159
+ </table>
160
+ </div>
161
+ </div>
162
  </div>
163
  </div>
164
  </div>
165
  </section>
166
 
167
+ <!-- ===================== EXPLORE (routing / download tool) ===================== -->
168
+ <section id="sec-explore" class="section">
 
169
  <div class="section-head">
170
+ <h1 class="section-title">Configure &amp; deploy</h1>
171
+ <p class="subtle tight">Set your format, target and hardware, pick a matching model, then copy the exact download command and open its benchmark tool.</p>
172
+ <ol class="deploy-steps">
173
+ <li><span class="ds-num">1</span><span class="ds-txt">Set constraints</span></li>
174
+ <li><span class="ds-num">2</span><span class="ds-txt">Pick a model</span></li>
175
+ <li><span class="ds-num">3</span><span class="ds-txt">Copy &amp; run</span></li>
176
+ </ol>
177
  </div>
178
+ <div class="grid explore-grid">
179
+ <div class="card">
180
+ <h3 class="no-border">Configure</h3>
181
 
182
+ <div class="form-grid explore-form">
183
+ <button class="filter-toggle" type="button" data-target="exploreFilters" aria-controls="exploreFilters" aria-expanded="false">
184
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M1.5 3h13M3.5 8h9M6 13h4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
185
+ <span>Filters</span>
186
+ <svg class="ft-chevron" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
187
+ </button>
188
+ <div class="explore-filters" id="exploreFilters">
189
+ <!-- Primary tier: always visible -->
190
+ <label class="field compact">
191
+ <span class="label">Architecture</span>
192
+ <select id="arch" class="filter-select compact"></select>
193
+ </label>
194
 
195
+ <div class="field-row two">
196
+ <label class="field compact">
197
+ <span class="label">Modality</span>
198
+ <div class="radio-row" id="modality"></div>
199
+ </label>
200
+ <label class="field compact">
201
+ <span class="label">Format</span>
202
+ <div class="radio-row" id="fmt"></div>
203
+ </label>
204
+ </div>
205
 
206
+ <!-- Reveal toggle for secondary filters (reuses the generic
207
+ delegated .filter-toggle handler below β€” no JS change) -->
208
+ <button class="filter-toggle filter-toggle--inline" type="button"
209
+ data-target="exploreAdvanced" aria-controls="exploreAdvanced" aria-expanded="false">
210
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M2.5 5h11M4.5 8h7M6.5 11h3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
211
+ <span>Advanced</span>
212
+ <svg class="ft-chevron" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
213
+ </button>
214
 
215
+ <!-- Secondary tier: collapsed by default on desktop, one click to reveal -->
216
+ <div class="explore-advanced" id="exploreAdvanced">
217
+ <div class="field-row two">
218
+ <label class="field compact">
219
+ <span class="label">Compute Target</span>
220
+ <div class="radio-row" id="compute"></div>
221
+ </label>
222
+ <label class="field compact">
223
+ <span class="label">Hardware Config</span>
224
+ <select id="hwCfg" class="filter-select compact"></select>
225
+ </label>
226
+ </div>
227
 
228
+ <label class="field compact">
229
+ <span class="label">Purpose</span>
230
+ <div class="radio-row" id="purpose"></div>
231
+ </label>
232
+ </div>
233
+ </div>
234
 
235
+ <div class="matchline" id="matchCount"></div>
236
 
237
+ <div class="model-picker">
238
+ <div class="model-picker-head">
239
+ <div class="model-picker-title">Model</div>
240
+ <div class="model-picker-subtle" id="modelCountHint"></div>
241
+ </div>
242
+ <select id="modelKey" class="filter-select model-list" size="12"></select>
243
+ </div>
244
  </div>
245
  </div>
246
+
247
+ <div class="card result-card">
248
+ <div id="warning" class="note note-warn" style="display:none"></div>
249
+ <div id="details"></div>
250
+ <div id="actions"></div>
251
+ </div>
252
  </div>
 
253
  </section>
254
 
255
  </main>
256
 
257
+ <!-- Model details modal -->
 
 
258
  <div id="modelModal" class="modal" aria-hidden="true" role="dialog" aria-label="Model details">
259
  <div class="modal-backdrop" data-modal-close="1"></div>
260
  <div class="modal-panel" role="document">
 
269
  </div>
270
  </div>
271
 
272
+ <script>
273
+ /* Brand network-mesh background β€” inspired by the Renesas circuit-network artwork:
274
+ a faint blueprint grid, then a constellation of nodes (small dots + larger glowing
275
+ hubs) connected by hairlines, tinted on a left→right blue→violet gradient. */
276
+ (function () {
277
+ var c = document.getElementById("bgfx");
278
+ if (!c) return;
279
+ var ctx = c.getContext("2d");
280
+ var w = 0, h = 0, dpr = 1, nodes = [], N = 0, LINK = 168, raf = 0;
281
+ var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
282
+
283
+ /* gradient stops: cool blue (left) -> Renesas blue -> violet (right) */
284
+ var C0 = [67, 171, 226], C1 = [42, 40, 157], C2 = [150, 104, 214];
285
+ function lerp(a, b, t) { return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t]; }
286
+ function tint(t) { return t < 0.5 ? lerp(C0, C1, t * 2) : lerp(C1, C2, (t - 0.5) * 2); }
287
+ function rgba(c, a) { return "rgba(" + (c[0]|0) + "," + (c[1]|0) + "," + (c[2]|0) + "," + a + ")"; }
288
+
289
+ function resize() {
290
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
291
+ w = c.clientWidth; h = c.clientHeight;
292
+ c.width = Math.max(1, Math.floor(w * dpr));
293
+ c.height = Math.max(1, Math.floor(h * dpr));
294
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
295
+ }
296
+ function seed() {
297
+ N = Math.max(40, Math.min(110, Math.round((w * h) / 14000)));
298
+ nodes = [];
299
+ for (var i = 0; i < N; i++) {
300
+ var hub = Math.random() < 0.14;
301
+ nodes.push({
302
+ x: Math.random() * w, y: Math.random() * h,
303
+ vx: (Math.random() - 0.5) * 0.20, vy: (Math.random() - 0.5) * 0.20,
304
+ hub: hub, r: hub ? 3.2 + Math.random() * 3.4 : 1.0 + Math.random() * 1.6
305
+ });
306
+ }
307
+ }
308
+ function grid() {
309
+ var step = 44, g = "rgba(67,140,210,0.06)";
310
+ ctx.lineWidth = 1; ctx.strokeStyle = g; ctx.beginPath();
311
+ for (var x = 0; x <= w; x += step) { ctx.moveTo(x, 0); ctx.lineTo(x, h); }
312
+ for (var y = 0; y <= h; y += step) { ctx.moveTo(0, y); ctx.lineTo(w, y); }
313
+ ctx.stroke();
314
+ }
315
+ function draw(step) {
316
+ ctx.clearRect(0, 0, w, h);
317
+ grid();
318
+ if (step) for (var k = 0; k < N; k++) {
319
+ var n = nodes[k]; n.x += n.vx; n.y += n.vy;
320
+ if (n.x < 0 || n.x > w) n.vx *= -1; if (n.y < 0 || n.y > h) n.vy *= -1;
321
+ }
322
+ /* links */
323
+ for (var i = 0; i < N; i++) for (var j = i + 1; j < N; j++) {
324
+ var a = nodes[i], b = nodes[j], dx = a.x - b.x, dy = a.y - b.y, d = Math.sqrt(dx * dx + dy * dy);
325
+ if (d < LINK) {
326
+ var mid = ((a.x + b.x) / 2) / w;
327
+ ctx.strokeStyle = rgba(tint(mid), ((1 - d / LINK) * 0.16).toFixed(3));
328
+ ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
329
+ }
330
+ }
331
+ /* nodes */
332
+ for (var m = 0; m < N; m++) {
333
+ var p = nodes[m], col = tint(p.x / w);
334
+ if (p.hub) {
335
+ var halo = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 4.5);
336
+ halo.addColorStop(0, rgba(col, 0.28)); halo.addColorStop(1, rgba(col, 0));
337
+ ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(p.x, p.y, p.r * 4.5, 0, Math.PI * 2); ctx.fill();
338
+ }
339
+ ctx.fillStyle = rgba(col, p.hub ? 0.85 : 0.5);
340
+ ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
341
+ }
342
+ }
343
+ function tick() { draw(true); raf = requestAnimationFrame(tick); }
344
+ function start() { resize(); seed(); if (raf) cancelAnimationFrame(raf); if (reduce) draw(false); else tick(); }
345
+ window.addEventListener("resize", function () { resize(); seed(); if (reduce) draw(false); });
346
+ start();
347
+ })();
348
+ </script>
349
+ <script>
350
+ /* Mobile nav: burger toggles the menu; selecting an item closes it. */
351
+ (function () {
352
+ var burger = document.getElementById("navBurger");
353
+ var nav = document.getElementById("primaryNav");
354
+ if (!burger || !nav) return;
355
+ function setOpen(open) {
356
+ nav.classList.toggle("open", open);
357
+ burger.classList.toggle("active", open);
358
+ burger.setAttribute("aria-expanded", open ? "true" : "false");
359
+ }
360
+ burger.addEventListener("click", function (e) {
361
+ e.stopPropagation();
362
+ setOpen(!nav.classList.contains("open"));
363
+ });
364
+ nav.addEventListener("click", function (e) {
365
+ if (e.target.closest(".nav-item")) setOpen(false);
366
+ });
367
+ document.addEventListener("click", function (e) {
368
+ if (nav.classList.contains("open") && !nav.contains(e.target) && e.target !== burger) setOpen(false);
369
+ });
370
+ })();
371
+ </script>
372
+ <script>
373
+ /* Collapsible filter clusters (phone): a "Filters" button toggles each group. */
374
+ (function () {
375
+ document.addEventListener("click", function (e) {
376
+ var btn = e.target.closest(".filter-toggle");
377
+ if (!btn) return;
378
+ var t = document.getElementById(btn.getAttribute("data-target"));
379
+ if (!t) return;
380
+ var open = t.classList.toggle("open");
381
+ btn.classList.toggle("active", open);
382
+ btn.setAttribute("aria-expanded", open ? "true" : "false");
383
+ });
384
+ })();
385
+ </script>
386
  <script src="app.js" defer></script>
387
  </body>
388
  </html>
links.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "spaces": {
3
- "onnx_perf_explorer": "Renesas/ONNX-Performance-Explorer",
4
- "ort_benchmark": "Renesas/ORT-Benchmark",
5
- "gguf_llamacpp_benchmark": "Renesas/GGUF-llamacpp-Benchmark"
6
- },
7
- "collections": {
8
- "onnx": "",
9
- "gguf": "",
10
- "performanceestimate": "",
11
- "cpu": "",
12
- "npu": "",
13
- "llm": "",
14
- "vlm": "",
15
- "llama32_1b": ""
16
- }
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models.json CHANGED
@@ -1,659 +1,127 @@
1
  {
2
- "version": "0.7",
3
- "catalog_note": "POC ONLY: Fake models/metrics for UI prototyping. Not real performance data.",
 
 
 
4
  "models": [
5
  {
6
- "key": "llama32-1b-instruct",
7
- "display_name": "Llama3.2-1B Instruct",
8
- "architecture": "Llama3.2-1B",
9
  "model_size_m": 1000,
10
  "modality": "LLM",
11
  "variant": "Instruct",
12
  "targets": [
13
- "CPU",
14
  "NPU"
15
  ],
16
- "onnx_repo": "Renesas/Llama3.2-1B-Instruct-ONNX",
17
- "gguf_repo": "Renesas/Llama3.2-1B-Instruct-GGUF",
18
  "onnx_artifacts": [
19
  "fp16",
20
- "int8"
 
21
  ],
22
  "gguf_artifacts": [
23
- "Q4_K_M",
24
- "Q8_0"
 
 
 
 
 
 
 
25
  ],
 
 
26
  "hardware_configs": [
27
  {
28
- "id": "X5H-200TOPS",
29
- "label": "X5H 200TOPS Reference Platform",
30
  "targets": [
31
- "CPU",
32
  "NPU"
33
  ],
34
- "notes": "Default reference board config",
35
  "metrics": {
36
  "artifacts": {
37
  "fp16": {
38
  "estimate": {
39
- "accuracy": 0.63,
40
- "llm_metrics": {
41
- "overall": 63.0,
42
- "mmlu": 45.0,
43
- "gsm8k": 22.0,
44
- "hellaswag": 64.5,
45
- "truthfulqa": 38.0,
46
- "winogrande": 59.0,
47
- "arc_challenge": 35.0,
48
- "mt_bench": 6.2
49
- },
50
- "cpu": {
51
- "tok_s": 18.0,
52
- "latency_ms_p50": 55.0,
53
- "peak_mem_mb": 820
54
- },
55
  "npu": {
56
- "tok_s": 45.0,
57
- "latency_ms_p50": 22.0,
58
- "peak_mem_mb": 610
59
  },
60
- "dsp": null,
61
  "total": {
62
- "tok_s": 45.0,
63
- "peak_mem_mb": 900,
64
- "latency_ms_p50": 77.0
65
  },
66
- "last_updated": "2026-03-01"
 
 
67
  },
68
  "hil": {
69
- "accuracy": 0.624,
70
- "llm_metrics": {
71
- "overall": 62.4,
72
- "mmlu": 44.4,
73
- "gsm8k": 21.4,
74
- "hellaswag": 63.9,
75
- "truthfulqa": 37.4,
76
- "winogrande": 58.4,
77
- "arc_challenge": 34.4,
78
- "mt_bench": 6.1
79
- },
80
- "cpu": {
81
- "tok_s": 16.5,
82
- "latency_ms_p50": 60.0,
83
- "peak_mem_mb": 840
84
- },
85
  "npu": {
86
- "tok_s": 38.0,
87
- "latency_ms_p50": 25.0,
88
- "peak_mem_mb": 640
89
  },
90
- "dsp": null,
91
  "total": {
92
- "tok_s": 38.0,
93
- "peak_mem_mb": 920,
94
- "latency_ms_p50": 85.0
95
  },
96
- "setup": "DEMO-HIL-RIG-1",
97
- "last_updated": "2026-03-01"
 
 
 
98
  }
99
  },
100
- "int8": {
101
- "estimate": {
102
- "accuracy": 0.622,
103
- "llm_metrics": {
104
- "overall": 62.2,
105
- "mmlu": 44.2,
106
- "gsm8k": 21.2,
107
- "hellaswag": 63.7,
108
- "truthfulqa": 37.2,
109
- "winogrande": 58.2,
110
- "arc_challenge": 34.2,
111
- "mt_bench": 6.1
112
- },
113
- "cpu": {
114
- "tok_s": 18.0,
115
- "latency_ms_p50": 55.0,
116
- "peak_mem_mb": 820
117
- },
118
- "npu": {
119
- "tok_s": 45.0,
120
- "latency_ms_p50": 22.0,
121
- "peak_mem_mb": 610
122
- },
123
- "dsp": null,
124
- "total": {
125
- "tok_s": 45.0,
126
- "peak_mem_mb": 900,
127
- "latency_ms_p50": 77.0
128
- },
129
- "last_updated": "2026-03-01"
130
- },
131
- "hil": {
132
- "accuracy": 0.616,
133
- "llm_metrics": {
134
- "overall": 61.6,
135
- "mmlu": 43.6,
136
- "gsm8k": 20.6,
137
- "hellaswag": 63.1,
138
- "truthfulqa": 36.6,
139
- "winogrande": 57.6,
140
- "arc_challenge": 33.6,
141
- "mt_bench": 6.1
142
- },
143
- "cpu": {
144
- "tok_s": 16.5,
145
- "latency_ms_p50": 60.0,
146
- "peak_mem_mb": 840
147
- },
148
- "npu": {
149
- "tok_s": 38.0,
150
- "latency_ms_p50": 25.0,
151
- "peak_mem_mb": 640
152
- },
153
- "dsp": null,
154
- "total": {
155
- "tok_s": 38.0,
156
- "peak_mem_mb": 920,
157
- "latency_ms_p50": 85.0
158
- },
159
- "setup": "DEMO-HIL-RIG-1",
160
- "last_updated": "2026-03-01"
161
- }
162
- },
163
- "Q4_K_M": {
164
- "estimate": {
165
- "accuracy": 0.595,
166
- "llm_metrics": {
167
- "overall": 59.5,
168
- "mmlu": 41.5,
169
- "gsm8k": 18.5,
170
- "hellaswag": 61.0,
171
- "truthfulqa": 34.5,
172
- "winogrande": 55.5,
173
- "arc_challenge": 31.5,
174
- "mt_bench": 6.0
175
- },
176
- "cpu": {
177
- "tok_s": 18.0,
178
- "latency_ms_p50": 55.0,
179
- "peak_mem_mb": 820
180
- },
181
- "npu": {
182
- "tok_s": 45.0,
183
- "latency_ms_p50": 22.0,
184
- "peak_mem_mb": 610
185
- },
186
- "dsp": null,
187
- "total": {
188
- "tok_s": 45.0,
189
- "peak_mem_mb": 900,
190
- "latency_ms_p50": 77.0
191
- },
192
- "last_updated": "2026-03-01"
193
- },
194
- "hil": {
195
- "accuracy": 0.589,
196
- "llm_metrics": {
197
- "overall": 58.9,
198
- "mmlu": 40.9,
199
- "gsm8k": 17.9,
200
- "hellaswag": 60.4,
201
- "truthfulqa": 33.9,
202
- "winogrande": 54.9,
203
- "arc_challenge": 30.9,
204
- "mt_bench": 5.9
205
- },
206
- "cpu": {
207
- "tok_s": 16.5,
208
- "latency_ms_p50": 60.0,
209
- "peak_mem_mb": 840
210
- },
211
- "npu": {
212
- "tok_s": 38.0,
213
- "latency_ms_p50": 25.0,
214
- "peak_mem_mb": 640
215
- },
216
- "dsp": null,
217
- "total": {
218
- "tok_s": 38.0,
219
- "peak_mem_mb": 920,
220
- "latency_ms_p50": 85.0
221
- },
222
- "setup": "DEMO-HIL-RIG-1",
223
- "last_updated": "2026-03-01"
224
- }
225
- },
226
- "Q8_0": {
227
- "estimate": {
228
- "accuracy": 0.617,
229
- "llm_metrics": {
230
- "overall": 61.7,
231
- "mmlu": 43.7,
232
- "gsm8k": 20.7,
233
- "hellaswag": 63.2,
234
- "truthfulqa": 36.7,
235
- "winogrande": 57.7,
236
- "arc_challenge": 33.7,
237
- "mt_bench": 6.1
238
- },
239
- "cpu": {
240
- "tok_s": 18.0,
241
- "latency_ms_p50": 55.0,
242
- "peak_mem_mb": 820
243
- },
244
- "npu": {
245
- "tok_s": 45.0,
246
- "latency_ms_p50": 22.0,
247
- "peak_mem_mb": 610
248
- },
249
- "dsp": null,
250
- "total": {
251
- "tok_s": 45.0,
252
- "peak_mem_mb": 900,
253
- "latency_ms_p50": 77.0
254
- },
255
- "last_updated": "2026-03-01"
256
- },
257
- "hil": {
258
- "accuracy": 0.611,
259
- "llm_metrics": {
260
- "overall": 61.1,
261
- "mmlu": 43.1,
262
- "gsm8k": 20.1,
263
- "hellaswag": 62.6,
264
- "truthfulqa": 36.1,
265
- "winogrande": 57.1,
266
- "arc_challenge": 33.1,
267
- "mt_bench": 6.1
268
- },
269
- "cpu": {
270
- "tok_s": 16.5,
271
- "latency_ms_p50": 60.0,
272
- "peak_mem_mb": 840
273
- },
274
- "npu": {
275
- "tok_s": 38.0,
276
- "latency_ms_p50": 25.0,
277
- "peak_mem_mb": 640
278
- },
279
- "dsp": null,
280
- "total": {
281
- "tok_s": 38.0,
282
- "peak_mem_mb": 920,
283
- "latency_ms_p50": 85.0
284
- },
285
- "setup": "DEMO-HIL-RIG-1",
286
- "last_updated": "2026-03-01"
287
- }
288
- }
289
- }
290
- }
291
- },
292
- {
293
- "id": "X5H-200TOPS-LOWPWR",
294
- "label": "X5H 200TOPS Reference Platform β€” Low Power",
295
- "targets": [
296
- "CPU",
297
- "NPU"
298
- ],
299
- "notes": "Lower clocks / lower power envelope",
300
- "metrics": {
301
- "artifacts": {
302
- "fp16": {
303
- "estimate": {
304
- "accuracy": 0.628,
305
- "llm_metrics": {
306
- "overall": 62.8,
307
- "mmlu": 44.8,
308
- "gsm8k": 21.8,
309
- "hellaswag": 64.3,
310
- "truthfulqa": 37.8,
311
- "winogrande": 58.8,
312
- "arc_challenge": 34.8,
313
- "mt_bench": 6.1
314
- },
315
- "cpu": {
316
- "tok_s": 17.0,
317
- "latency_ms_p50": 58.0,
318
- "peak_mem_mb": 820
319
- },
320
- "npu": {
321
- "tok_s": 41.0,
322
- "latency_ms_p50": 24.0,
323
- "peak_mem_mb": 610
324
- },
325
- "dsp": null,
326
- "total": {
327
- "tok_s": 41.0,
328
- "peak_mem_mb": 900,
329
- "latency_ms_p50": 82.0
330
- },
331
- "last_updated": "2026-03-01"
332
- },
333
- "hil": null
334
- },
335
- "int8": {
336
- "estimate": {
337
- "accuracy": 0.62,
338
- "llm_metrics": {
339
- "overall": 62.0,
340
- "mmlu": 44.0,
341
- "gsm8k": 21.0,
342
- "hellaswag": 63.5,
343
- "truthfulqa": 37.0,
344
- "winogrande": 58.0,
345
- "arc_challenge": 34.0,
346
- "mt_bench": 6.1
347
- },
348
- "cpu": {
349
- "tok_s": 17.0,
350
- "latency_ms_p50": 58.0,
351
- "peak_mem_mb": 820
352
- },
353
- "npu": {
354
- "tok_s": 41.0,
355
- "latency_ms_p50": 24.0,
356
- "peak_mem_mb": 610
357
- },
358
- "dsp": null,
359
- "total": {
360
- "tok_s": 41.0,
361
- "peak_mem_mb": 900,
362
- "latency_ms_p50": 82.0
363
- },
364
- "last_updated": "2026-03-01"
365
- },
366
- "hil": null
367
- },
368
- "Q4_K_M": {
369
- "estimate": {
370
- "accuracy": 0.593,
371
- "llm_metrics": {
372
- "overall": 59.3,
373
- "mmlu": 41.3,
374
- "gsm8k": 18.3,
375
- "hellaswag": 60.8,
376
- "truthfulqa": 34.3,
377
- "winogrande": 55.3,
378
- "arc_challenge": 31.3,
379
- "mt_bench": 6.0
380
- },
381
- "cpu": {
382
- "tok_s": 17.0,
383
- "latency_ms_p50": 58.0,
384
- "peak_mem_mb": 820
385
- },
386
- "npu": {
387
- "tok_s": 41.0,
388
- "latency_ms_p50": 24.0,
389
- "peak_mem_mb": 610
390
- },
391
- "dsp": null,
392
- "total": {
393
- "tok_s": 41.0,
394
- "peak_mem_mb": 900,
395
- "latency_ms_p50": 82.0
396
- },
397
- "last_updated": "2026-03-01"
398
- },
399
- "hil": null
400
- },
401
- "Q8_0": {
402
- "estimate": {
403
- "accuracy": 0.615,
404
- "llm_metrics": {
405
- "overall": 61.5,
406
- "mmlu": 43.5,
407
- "gsm8k": 20.5,
408
- "hellaswag": 63.0,
409
- "truthfulqa": 36.5,
410
- "winogrande": 57.5,
411
- "arc_challenge": 33.5,
412
- "mt_bench": 6.1
413
- },
414
- "cpu": {
415
- "tok_s": 17.0,
416
- "latency_ms_p50": 58.0,
417
- "peak_mem_mb": 820
418
- },
419
- "npu": {
420
- "tok_s": 41.0,
421
- "latency_ms_p50": 24.0,
422
- "peak_mem_mb": 610
423
- },
424
- "dsp": null,
425
- "total": {
426
- "tok_s": 41.0,
427
- "peak_mem_mb": 900,
428
- "latency_ms_p50": 82.0
429
- },
430
- "last_updated": "2026-03-01"
431
- },
432
- "hil": null
433
- }
434
- }
435
- }
436
- }
437
- ]
438
- },
439
- {
440
- "key": "llama32-1b-base",
441
- "display_name": "Llama3.2-1B Base",
442
- "architecture": "Llama3.2-1B",
443
- "model_size_m": 1000,
444
- "modality": "LLM",
445
- "variant": "Base",
446
- "targets": [
447
- "CPU"
448
- ],
449
- "onnx_repo": "Renesas/Llama3.2-1B-Base-ONNX",
450
- "gguf_repo": "Renesas/Llama3.2-1B-Base-GGUF",
451
- "onnx_artifacts": [
452
- "fp16"
453
- ],
454
- "gguf_artifacts": [
455
- "Q4_K_M"
456
- ],
457
- "hardware_configs": [
458
- {
459
- "id": "X5H-200TOPS",
460
- "label": "X5H 200TOPS Reference Platform",
461
- "targets": [
462
- "CPU"
463
- ],
464
- "metrics": {
465
- "artifacts": {
466
- "fp16": {
467
  "estimate": null,
468
  "hil": {
469
- "accuracy": 0.549,
470
- "llm_metrics": {
471
- "overall": 54.9,
472
- "mmlu": 36.9,
473
- "gsm8k": 13.9,
474
- "hellaswag": 56.4,
475
- "truthfulqa": 29.9,
476
- "winogrande": 50.9,
477
- "arc_challenge": 26.9,
478
- "mt_bench": 5.7
479
- },
480
- "cpu": {
481
- "tok_s": 20.0,
482
- "latency_ms_p50": 52.0,
483
- "peak_mem_mb": 760
484
  },
485
- "npu": null,
486
- "dsp": null,
487
  "total": {
488
- "tok_s": 20.0,
489
- "peak_mem_mb": 810,
490
- "latency_ms_p50": 52.0
491
  },
492
- "setup": "DEMO-HIL-RIG-1",
493
- "last_updated": "2026-03-01"
 
 
 
494
  }
495
  },
496
- "Q4_K_M": {
497
  "estimate": null,
498
- "hil": {
499
- "accuracy": 0.514,
500
- "llm_metrics": {
501
- "overall": 51.4,
502
- "mmlu": 33.4,
503
- "gsm8k": 10.4,
504
- "hellaswag": 52.9,
505
- "truthfulqa": 26.4,
506
- "winogrande": 47.4,
507
- "arc_challenge": 23.4,
508
- "mt_bench": 5.6
509
- },
510
- "cpu": {
511
- "tok_s": 20.0,
512
- "latency_ms_p50": 52.0,
513
- "peak_mem_mb": 760
514
- },
515
- "npu": null,
516
- "dsp": null,
517
- "total": {
518
- "tok_s": 20.0,
519
- "peak_mem_mb": 810,
520
- "latency_ms_p50": 52.0
521
- },
522
- "setup": "DEMO-HIL-RIG-1",
523
- "last_updated": "2026-03-01"
524
- }
525
- }
526
- }
527
- }
528
- }
529
- ]
530
- },
531
- {
532
- "key": "vlm-tiny-visionchat",
533
- "display_name": "VLM Tiny VisionChat",
534
- "architecture": "VLM-Tiny-450M",
535
- "model_size_m": 450,
536
- "modality": "VLM",
537
- "variant": "Instruct",
538
- "targets": [
539
- "CPU",
540
- "NPU"
541
- ],
542
- "onnx_repo": "Renesas/VLM-Tiny-VisionChat-ONNX",
543
- "gguf_repo": "Renesas/VLM-Tiny-VisionChat-GGUF",
544
- "onnx_artifacts": [
545
- "fp16",
546
- "int8"
547
- ],
548
- "gguf_artifacts": [
549
- "Q4_K_M"
550
- ],
551
- "hardware_configs": [
552
- {
553
- "id": "X5H-200TOPS",
554
- "label": "X5H 200TOPS Reference Platform",
555
- "targets": [
556
- "CPU",
557
- "NPU"
558
- ],
559
- "metrics": {
560
- "artifacts": {
561
- "fp16": {
562
- "estimate": {
563
- "accuracy": 0.55,
564
- "vlm_metrics": {
565
- "overall": 55.0,
566
- "mmbench": 50.0,
567
- "vqav2": 57.0,
568
- "seedbench": 47.0,
569
- "pope": 52.0
570
- },
571
- "cpu": {
572
- "tok_s": 5.0,
573
- "latency_ms_p50": 180.0,
574
- "peak_mem_mb": 920
575
- },
576
- "npu": {
577
- "tok_s": 14.0,
578
- "latency_ms_p50": 70.0,
579
- "peak_mem_mb": 690
580
- },
581
- "dsp": {
582
- "latency_ms_p50": 12.0,
583
- "peak_mem_mb": 32
584
- },
585
- "total": {
586
- "tok_s": 14.0,
587
- "peak_mem_mb": 980,
588
- "latency_ms_p50": 262.0
589
- },
590
- "last_updated": "2026-03-01"
591
- },
592
  "hil": null
593
  },
594
- "int8": {
595
  "estimate": {
596
- "accuracy": 0.542,
597
- "vlm_metrics": {
598
- "overall": 54.2,
599
- "mmbench": 49.2,
600
- "vqav2": 56.2,
601
- "seedbench": 46.2,
602
- "pope": 51.2
603
- },
604
- "cpu": {
605
- "tok_s": 5.0,
606
- "latency_ms_p50": 180.0,
607
- "peak_mem_mb": 920
608
- },
609
  "npu": {
610
- "tok_s": 14.0,
611
- "latency_ms_p50": 70.0,
612
- "peak_mem_mb": 690
613
- },
614
- "dsp": {
615
- "latency_ms_p50": 12.0,
616
- "peak_mem_mb": 32
617
- },
618
- "total": {
619
- "tok_s": 14.0,
620
- "peak_mem_mb": 980,
621
- "latency_ms_p50": 262.0
622
- },
623
- "last_updated": "2026-03-01"
624
- },
625
- "hil": null
626
- },
627
- "Q4_K_M": {
628
- "estimate": {
629
- "accuracy": 0.515,
630
- "vlm_metrics": {
631
- "overall": 51.5,
632
- "mmbench": 46.5,
633
- "vqav2": 53.5,
634
- "seedbench": 43.5,
635
- "pope": 48.5
636
- },
637
- "cpu": {
638
- "tok_s": 5.0,
639
- "latency_ms_p50": 180.0,
640
- "peak_mem_mb": 920
641
- },
642
- "npu": {
643
- "tok_s": 14.0,
644
- "latency_ms_p50": 70.0,
645
- "peak_mem_mb": 690
646
- },
647
- "dsp": {
648
- "latency_ms_p50": 12.0,
649
- "peak_mem_mb": 32
650
  },
651
  "total": {
652
- "tok_s": 14.0,
653
- "peak_mem_mb": 980,
654
- "latency_ms_p50": 262.0
655
  },
656
- "last_updated": "2026-03-01"
 
 
657
  },
658
  "hil": null
659
  }
@@ -663,99 +131,81 @@
663
  ]
664
  },
665
  {
666
- "key": "demo-gguf-only",
667
- "display_name": "Demo GGUF-only Model",
668
- "architecture": "Demo-700M",
669
- "model_size_m": 700,
670
  "modality": "LLM",
671
  "variant": "Instruct",
672
  "targets": [
673
- "CPU",
674
  "NPU"
675
  ],
676
  "onnx_repo": "",
677
- "gguf_repo": "Renesas/Demo-700M-GGUF",
678
  "onnx_artifacts": [],
679
  "gguf_artifacts": [
680
- "Q4_K_M",
681
- "Q8_0"
682
  ],
 
 
 
 
 
 
 
 
 
683
  "hardware_configs": [
684
  {
685
- "id": "X5H-100TOPS",
686
- "label": "X5H 100TOPS Reference Platform",
687
  "targets": [
688
- "CPU",
689
  "NPU"
690
  ],
 
691
  "metrics": {
692
  "artifacts": {
693
- "Q4_K_M": {
694
  "estimate": null,
695
  "hil": {
696
- "accuracy": 0.558,
697
- "llm_metrics": {
698
- "overall": 55.8,
699
- "mmlu": 37.8,
700
- "gsm8k": 14.8,
701
- "hellaswag": 57.3,
702
- "truthfulqa": 30.8,
703
- "winogrande": 51.8,
704
- "arc_challenge": 27.8,
705
- "mt_bench": 5.8
706
- },
707
- "cpu": {
708
- "tok_s": 24.0,
709
- "latency_ms_p50": 40.0,
710
- "peak_mem_mb": 740
711
- },
712
  "npu": {
713
- "tok_s": 52.0,
714
- "latency_ms_p50": 18.0,
715
- "peak_mem_mb": 600
716
  },
717
- "dsp": null,
718
  "total": {
719
- "tok_s": 52.0,
720
- "peak_mem_mb": 820,
721
- "latency_ms_p50": 58.0
722
  },
723
- "setup": "DEMO-HIL-RIG-2",
724
- "last_updated": "2026-03-01"
 
 
 
725
  }
726
  },
727
- "Q8_0": {
728
  "estimate": null,
729
  "hil": {
730
- "accuracy": 0.58,
731
- "llm_metrics": {
732
- "overall": 58.0,
733
- "mmlu": 40.0,
734
- "gsm8k": 17.0,
735
- "hellaswag": 59.5,
736
- "truthfulqa": 33.0,
737
- "winogrande": 54.0,
738
- "arc_challenge": 30.0,
739
- "mt_bench": 5.9
740
- },
741
- "cpu": {
742
- "tok_s": 24.0,
743
- "latency_ms_p50": 40.0,
744
- "peak_mem_mb": 740
745
- },
746
  "npu": {
747
- "tok_s": 52.0,
748
- "latency_ms_p50": 18.0,
749
- "peak_mem_mb": 600
750
  },
751
- "dsp": null,
752
  "total": {
753
- "tok_s": 52.0,
754
- "peak_mem_mb": 820,
755
- "latency_ms_p50": 58.0
756
  },
757
- "setup": "DEMO-HIL-RIG-2",
758
- "last_updated": "2026-03-01"
 
 
 
759
  }
760
  }
761
  }
@@ -764,286 +214,82 @@
764
  ]
765
  },
766
  {
767
- "key": "lfm-3b-instruct",
768
- "display_name": "LFM-3B Instruct",
769
- "architecture": "LFM-3B",
770
  "model_size_m": 3000,
771
  "modality": "LLM",
772
  "variant": "Instruct",
773
  "targets": [
774
- "CPU",
775
  "NPU"
776
  ],
777
- "onnx_repo": "Renesas/LFM-3B-Instruct-ONNX",
778
- "gguf_repo": "Renesas/LFM-3B-Instruct-GGUF",
779
- "onnx_artifacts": [
 
780
  "fp16",
781
- "int8"
782
  ],
783
- "gguf_artifacts": [
784
- "Q4_K_M",
785
- "Q8_0"
 
 
 
 
786
  ],
 
 
787
  "hardware_configs": [
788
  {
789
- "id": "X5H-200TOPS",
790
- "label": "X5H 200TOPS Reference Platform",
791
  "targets": [
792
- "CPU",
793
  "NPU"
794
  ],
 
795
  "metrics": {
796
  "artifacts": {
797
  "fp16": {
798
- "estimate": {
799
- "accuracy": 0.7,
800
- "llm_metrics": {
801
- "overall": 70.0,
802
- "mmlu": 52.0,
803
- "gsm8k": 29.0,
804
- "hellaswag": 71.5,
805
- "truthfulqa": 45.0,
806
- "winogrande": 66.0,
807
- "arc_challenge": 42.0,
808
- "mt_bench": 6.5
809
- },
810
- "cpu": {
811
- "tok_s": 7.5,
812
- "latency_ms_p50": 125.0,
813
- "peak_mem_mb": 1850
814
- },
815
- "npu": {
816
- "tok_s": 19.0,
817
- "latency_ms_p50": 52.0,
818
- "peak_mem_mb": 1320
819
- },
820
- "dsp": null,
821
- "total": {
822
- "tok_s": 19.0,
823
- "peak_mem_mb": 1980,
824
- "latency_ms_p50": 177.0
825
- },
826
- "last_updated": "2026-03-01"
827
- },
828
- "hil": {
829
- "accuracy": 0.694,
830
- "llm_metrics": {
831
- "overall": 69.4,
832
- "mmlu": 51.4,
833
- "gsm8k": 28.4,
834
- "hellaswag": 70.9,
835
- "truthfulqa": 44.4,
836
- "winogrande": 65.4,
837
- "arc_challenge": 41.4,
838
- "mt_bench": 6.5
839
- },
840
- "cpu": {
841
- "tok_s": 6.8,
842
- "latency_ms_p50": 138.0,
843
- "peak_mem_mb": 1900
844
- },
845
- "npu": {
846
- "tok_s": 16.5,
847
- "latency_ms_p50": 58.0,
848
- "peak_mem_mb": 1400
849
- },
850
- "dsp": null,
851
- "total": {
852
- "tok_s": 16.5,
853
- "peak_mem_mb": 2050,
854
- "latency_ms_p50": 196.0
855
- },
856
- "setup": "DEMO-HIL-RIG-1",
857
- "last_updated": "2026-03-02"
858
- }
859
- },
860
- "int8": {
861
- "estimate": {
862
- "accuracy": 0.692,
863
- "llm_metrics": {
864
- "overall": 69.2,
865
- "mmlu": 51.2,
866
- "gsm8k": 28.2,
867
- "hellaswag": 70.7,
868
- "truthfulqa": 44.2,
869
- "winogrande": 65.2,
870
- "arc_challenge": 41.2,
871
- "mt_bench": 6.5
872
- },
873
- "cpu": {
874
- "tok_s": 7.5,
875
- "latency_ms_p50": 125.0,
876
- "peak_mem_mb": 1850
877
- },
878
- "npu": {
879
- "tok_s": 19.0,
880
- "latency_ms_p50": 52.0,
881
- "peak_mem_mb": 1320
882
- },
883
- "dsp": null,
884
- "total": {
885
- "tok_s": 19.0,
886
- "peak_mem_mb": 1980,
887
- "latency_ms_p50": 177.0
888
- },
889
- "last_updated": "2026-03-01"
890
- },
891
- "hil": {
892
- "accuracy": 0.686,
893
- "llm_metrics": {
894
- "overall": 68.6,
895
- "mmlu": 50.6,
896
- "gsm8k": 27.6,
897
- "hellaswag": 70.1,
898
- "truthfulqa": 43.6,
899
- "winogrande": 64.6,
900
- "arc_challenge": 40.6,
901
- "mt_bench": 6.4
902
- },
903
- "cpu": {
904
- "tok_s": 6.8,
905
- "latency_ms_p50": 138.0,
906
- "peak_mem_mb": 1900
907
- },
908
- "npu": {
909
- "tok_s": 16.5,
910
- "latency_ms_p50": 58.0,
911
- "peak_mem_mb": 1400
912
- },
913
- "dsp": null,
914
- "total": {
915
- "tok_s": 16.5,
916
- "peak_mem_mb": 2050,
917
- "latency_ms_p50": 196.0
918
- },
919
- "setup": "DEMO-HIL-RIG-1",
920
- "last_updated": "2026-03-02"
921
- }
922
- },
923
- "Q4_K_M": {
924
- "estimate": {
925
- "accuracy": 0.665,
926
- "llm_metrics": {
927
- "overall": 66.5,
928
- "mmlu": 48.5,
929
- "gsm8k": 25.5,
930
- "hellaswag": 68.0,
931
- "truthfulqa": 41.5,
932
- "winogrande": 62.5,
933
- "arc_challenge": 38.5,
934
- "mt_bench": 6.3
935
- },
936
- "cpu": {
937
- "tok_s": 7.5,
938
- "latency_ms_p50": 125.0,
939
- "peak_mem_mb": 1850
940
- },
941
- "npu": {
942
- "tok_s": 19.0,
943
- "latency_ms_p50": 52.0,
944
- "peak_mem_mb": 1320
945
- },
946
- "dsp": null,
947
- "total": {
948
- "tok_s": 19.0,
949
- "peak_mem_mb": 1980,
950
- "latency_ms_p50": 177.0
951
- },
952
- "last_updated": "2026-03-01"
953
- },
954
  "hil": {
955
- "accuracy": 0.659,
956
- "llm_metrics": {
957
- "overall": 65.9,
958
- "mmlu": 47.9,
959
- "gsm8k": 24.9,
960
- "hellaswag": 67.4,
961
- "truthfulqa": 40.9,
962
- "winogrande": 61.9,
963
- "arc_challenge": 37.9,
964
- "mt_bench": 6.3
965
- },
966
- "cpu": {
967
- "tok_s": 6.8,
968
- "latency_ms_p50": 138.0,
969
- "peak_mem_mb": 1900
970
- },
971
  "npu": {
972
- "tok_s": 16.5,
973
- "latency_ms_p50": 58.0,
974
- "peak_mem_mb": 1400
975
  },
976
- "dsp": null,
977
  "total": {
978
- "tok_s": 16.5,
979
- "peak_mem_mb": 2050,
980
- "latency_ms_p50": 196.0
981
  },
982
- "setup": "DEMO-HIL-RIG-1",
983
- "last_updated": "2026-03-02"
 
 
 
984
  }
985
  },
986
- "Q8_0": {
987
- "estimate": {
988
- "accuracy": 0.687,
989
- "llm_metrics": {
990
- "overall": 68.7,
991
- "mmlu": 50.7,
992
- "gsm8k": 27.7,
993
- "hellaswag": 70.2,
994
- "truthfulqa": 43.7,
995
- "winogrande": 64.7,
996
- "arc_challenge": 40.7,
997
- "mt_bench": 6.4
998
- },
999
- "cpu": {
1000
- "tok_s": 7.5,
1001
- "latency_ms_p50": 125.0,
1002
- "peak_mem_mb": 1850
1003
- },
1004
- "npu": {
1005
- "tok_s": 19.0,
1006
- "latency_ms_p50": 52.0,
1007
- "peak_mem_mb": 1320
1008
- },
1009
- "dsp": null,
1010
- "total": {
1011
- "tok_s": 19.0,
1012
- "peak_mem_mb": 1980,
1013
- "latency_ms_p50": 177.0
1014
- },
1015
- "last_updated": "2026-03-01"
1016
- },
1017
  "hil": {
1018
- "accuracy": 0.681,
1019
- "llm_metrics": {
1020
- "overall": 68.1,
1021
- "mmlu": 50.1,
1022
- "gsm8k": 27.1,
1023
- "hellaswag": 69.6,
1024
- "truthfulqa": 43.1,
1025
- "winogrande": 64.1,
1026
- "arc_challenge": 40.1,
1027
- "mt_bench": 6.4
1028
- },
1029
- "cpu": {
1030
- "tok_s": 6.8,
1031
- "latency_ms_p50": 138.0,
1032
- "peak_mem_mb": 1900
1033
- },
1034
  "npu": {
1035
- "tok_s": 16.5,
1036
- "latency_ms_p50": 58.0,
1037
- "peak_mem_mb": 1400
1038
  },
1039
- "dsp": null,
1040
  "total": {
1041
- "tok_s": 16.5,
1042
- "peak_mem_mb": 2050,
1043
- "latency_ms_p50": 196.0
1044
  },
1045
- "setup": "DEMO-HIL-RIG-1",
1046
- "last_updated": "2026-03-02"
 
 
 
1047
  }
1048
  }
1049
  }
@@ -1052,256 +298,82 @@
1052
  ]
1053
  },
1054
  {
1055
- "key": "mamba-1p4b",
1056
- "display_name": "Mamba-1.4B",
1057
- "architecture": "Mamba-1.4B",
1058
- "model_size_m": 1400,
1059
  "modality": "LLM",
1060
- "variant": "Base",
1061
  "targets": [
1062
- "CPU",
1063
  "NPU"
1064
  ],
1065
- "onnx_repo": "Renesas/Mamba-1.4B-ONNX",
1066
- "gguf_repo": "",
1067
- "onnx_artifacts": [
 
1068
  "fp16",
1069
- "int8"
1070
  ],
1071
- "gguf_artifacts": [],
1072
- "hardware_configs": [
1073
- {
1074
- "id": "X5H-200TOPS",
1075
- "label": "X5H 200TOPS Reference Platform",
1076
- "targets": [
1077
- "CPU",
1078
- "NPU"
1079
- ],
1080
- "metrics": {
1081
- "artifacts": {
1082
- "fp16": {
1083
- "estimate": {
1084
- "accuracy": 0.575,
1085
- "llm_metrics": {
1086
- "overall": 57.5,
1087
- "mmlu": 39.5,
1088
- "gsm8k": 16.5,
1089
- "hellaswag": 59.0,
1090
- "truthfulqa": 32.5,
1091
- "winogrande": 53.5,
1092
- "arc_challenge": 29.5,
1093
- "mt_bench": 5.9
1094
- },
1095
- "cpu": {
1096
- "tok_s": 26.0,
1097
- "latency_ms_p50": 38.0,
1098
- "peak_mem_mb": 880
1099
- },
1100
- "npu": {
1101
- "tok_s": 62.0,
1102
- "latency_ms_p50": 15.0,
1103
- "peak_mem_mb": 520
1104
- },
1105
- "dsp": null,
1106
- "total": {
1107
- "tok_s": 62.0,
1108
- "peak_mem_mb": 940,
1109
- "latency_ms_p50": 53.0
1110
- },
1111
- "last_updated": "2026-03-02"
1112
- },
1113
- "hil": null
1114
- },
1115
- "int8": {
1116
- "estimate": {
1117
- "accuracy": 0.567,
1118
- "llm_metrics": {
1119
- "overall": 56.7,
1120
- "mmlu": 38.7,
1121
- "gsm8k": 15.7,
1122
- "hellaswag": 58.2,
1123
- "truthfulqa": 31.7,
1124
- "winogrande": 52.7,
1125
- "arc_challenge": 28.7,
1126
- "mt_bench": 5.8
1127
- },
1128
- "cpu": {
1129
- "tok_s": 26.0,
1130
- "latency_ms_p50": 38.0,
1131
- "peak_mem_mb": 880
1132
- },
1133
- "npu": {
1134
- "tok_s": 62.0,
1135
- "latency_ms_p50": 15.0,
1136
- "peak_mem_mb": 520
1137
- },
1138
- "dsp": null,
1139
- "total": {
1140
- "tok_s": 62.0,
1141
- "peak_mem_mb": 940,
1142
- "latency_ms_p50": 53.0
1143
- },
1144
- "last_updated": "2026-03-02"
1145
- },
1146
- "hil": null
1147
- }
1148
- }
1149
- }
1150
- }
1151
- ]
1152
- },
1153
- {
1154
- "key": "mamba2-1p3b",
1155
- "display_name": "Mamba2-1.3B",
1156
- "architecture": "Mamba2-1.3B",
1157
- "model_size_m": 1300,
1158
- "modality": "LLM",
1159
- "variant": "Base",
1160
- "targets": [
1161
- "CPU",
1162
- "NPU"
1163
  ],
1164
- "onnx_repo": "Renesas/Mamba2-1.3B-ONNX",
1165
- "gguf_repo": "",
1166
- "onnx_artifacts": [
1167
- "fp16",
1168
- "int8"
1169
- ],
1170
- "gguf_artifacts": [],
1171
  "hardware_configs": [
1172
  {
1173
- "id": "X5H-100TOPS",
1174
- "label": "X5H 100TOPS Reference Platform",
1175
  "targets": [
1176
- "CPU",
1177
  "NPU"
1178
  ],
 
1179
  "metrics": {
1180
  "artifacts": {
1181
  "fp16": {
1182
- "estimate": {
1183
- "accuracy": 0.594,
1184
- "llm_metrics": {
1185
- "overall": 59.4,
1186
- "mmlu": 41.4,
1187
- "gsm8k": 18.4,
1188
- "hellaswag": 60.9,
1189
- "truthfulqa": 34.4,
1190
- "winogrande": 55.4,
1191
- "arc_challenge": 31.4,
1192
- "mt_bench": 6.0
1193
- },
1194
- "cpu": {
1195
- "tok_s": 28.0,
1196
- "latency_ms_p50": 35.0,
1197
- "peak_mem_mb": 860
1198
- },
1199
- "npu": {
1200
- "tok_s": 68.0,
1201
- "latency_ms_p50": 14.0,
1202
- "peak_mem_mb": 500
1203
- },
1204
- "dsp": null,
1205
- "total": {
1206
- "tok_s": 68.0,
1207
- "peak_mem_mb": 920,
1208
- "latency_ms_p50": 49.0
1209
- },
1210
- "last_updated": "2026-03-02"
1211
- },
1212
  "hil": {
1213
- "accuracy": 0.588,
1214
- "llm_metrics": {
1215
- "overall": 58.8,
1216
- "mmlu": 40.8,
1217
- "gsm8k": 17.8,
1218
- "hellaswag": 60.3,
1219
- "truthfulqa": 33.8,
1220
- "winogrande": 54.8,
1221
- "arc_challenge": 30.8,
1222
- "mt_bench": 5.9
1223
- },
1224
- "cpu": {
1225
- "tok_s": 25.5,
1226
- "latency_ms_p50": 39.0,
1227
- "peak_mem_mb": 900
1228
- },
1229
  "npu": {
1230
- "tok_s": 60.0,
1231
- "latency_ms_p50": 16.0,
1232
- "peak_mem_mb": 520
1233
  },
1234
- "dsp": null,
1235
  "total": {
1236
- "tok_s": 60.0,
1237
- "peak_mem_mb": 960,
1238
- "latency_ms_p50": 55.0
1239
  },
1240
- "setup": "DEMO-HIL-RIG-2",
1241
- "last_updated": "2026-03-03"
 
 
 
1242
  }
1243
  },
1244
- "int8": {
1245
- "estimate": {
1246
- "accuracy": 0.586,
1247
- "llm_metrics": {
1248
- "overall": 58.6,
1249
- "mmlu": 40.6,
1250
- "gsm8k": 17.6,
1251
- "hellaswag": 60.1,
1252
- "truthfulqa": 33.6,
1253
- "winogrande": 54.6,
1254
- "arc_challenge": 30.6,
1255
- "mt_bench": 5.9
1256
- },
1257
- "cpu": {
1258
- "tok_s": 28.0,
1259
- "latency_ms_p50": 35.0,
1260
- "peak_mem_mb": 860
1261
- },
1262
- "npu": {
1263
- "tok_s": 68.0,
1264
- "latency_ms_p50": 14.0,
1265
- "peak_mem_mb": 500
1266
- },
1267
- "dsp": null,
1268
- "total": {
1269
- "tok_s": 68.0,
1270
- "peak_mem_mb": 920,
1271
- "latency_ms_p50": 49.0
1272
- },
1273
- "last_updated": "2026-03-02"
1274
- },
1275
  "hil": {
1276
- "accuracy": 0.58,
1277
- "llm_metrics": {
1278
- "overall": 58.0,
1279
- "mmlu": 40.0,
1280
- "gsm8k": 17.0,
1281
- "hellaswag": 59.5,
1282
- "truthfulqa": 33.0,
1283
- "winogrande": 54.0,
1284
- "arc_challenge": 30.0,
1285
- "mt_bench": 5.9
1286
- },
1287
- "cpu": {
1288
- "tok_s": 25.5,
1289
- "latency_ms_p50": 39.0,
1290
- "peak_mem_mb": 900
1291
- },
1292
  "npu": {
1293
- "tok_s": 60.0,
1294
- "latency_ms_p50": 16.0,
1295
- "peak_mem_mb": 520
1296
  },
1297
- "dsp": null,
1298
  "total": {
1299
- "tok_s": 60.0,
1300
- "peak_mem_mb": 960,
1301
- "latency_ms_p50": 55.0
1302
  },
1303
- "setup": "DEMO-HIL-RIG-2",
1304
- "last_updated": "2026-03-03"
 
 
 
1305
  }
1306
  }
1307
  }
@@ -1310,533 +382,70 @@
1310
  ]
1311
  },
1312
  {
1313
- "key": "jamba-1p5b-instruct",
1314
- "display_name": "Jamba-1.5B Instruct",
1315
- "architecture": "Jamba-1.5B",
1316
- "model_size_m": 1500,
1317
  "modality": "LLM",
1318
  "variant": "Instruct",
1319
  "targets": [
1320
- "CPU",
1321
  "NPU"
1322
  ],
1323
- "onnx_repo": "Renesas/Jamba-1.5B-Instruct-ONNX",
1324
- "gguf_repo": "Renesas/Jamba-1.5B-Instruct-GGUF",
1325
- "onnx_artifacts": [
1326
- "fp16",
1327
- "int8"
1328
- ],
1329
  "gguf_artifacts": [
1330
- "Q4_K_M",
1331
- "Q8_0"
1332
- ],
1333
- "hardware_configs": [
1334
- {
1335
- "id": "X5H-200TOPS",
1336
- "label": "X5H 200TOPS Reference Platform",
1337
- "targets": [
1338
- "CPU",
1339
- "NPU"
1340
- ],
1341
- "metrics": {
1342
- "artifacts": {
1343
- "fp16": {
1344
- "estimate": {
1345
- "accuracy": 0.66,
1346
- "llm_metrics": {
1347
- "overall": 66.0,
1348
- "mmlu": 48.0,
1349
- "gsm8k": 25.0,
1350
- "hellaswag": 67.5,
1351
- "truthfulqa": 41.0,
1352
- "winogrande": 62.0,
1353
- "arc_challenge": 38.0,
1354
- "mt_bench": 6.3
1355
- },
1356
- "cpu": {
1357
- "tok_s": 14.0,
1358
- "latency_ms_p50": 72.0,
1359
- "peak_mem_mb": 1050
1360
- },
1361
- "npu": {
1362
- "tok_s": 34.0,
1363
- "latency_ms_p50": 28.0,
1364
- "peak_mem_mb": 720
1365
- },
1366
- "dsp": null,
1367
- "total": {
1368
- "tok_s": 34.0,
1369
- "peak_mem_mb": 1120,
1370
- "latency_ms_p50": 100.0
1371
- },
1372
- "last_updated": "2026-03-02"
1373
- },
1374
- "hil": null
1375
- },
1376
- "int8": {
1377
- "estimate": {
1378
- "accuracy": 0.652,
1379
- "llm_metrics": {
1380
- "overall": 65.2,
1381
- "mmlu": 47.2,
1382
- "gsm8k": 24.2,
1383
- "hellaswag": 66.7,
1384
- "truthfulqa": 40.2,
1385
- "winogrande": 61.2,
1386
- "arc_challenge": 37.2,
1387
- "mt_bench": 6.3
1388
- },
1389
- "cpu": {
1390
- "tok_s": 14.0,
1391
- "latency_ms_p50": 72.0,
1392
- "peak_mem_mb": 1050
1393
- },
1394
- "npu": {
1395
- "tok_s": 34.0,
1396
- "latency_ms_p50": 28.0,
1397
- "peak_mem_mb": 720
1398
- },
1399
- "dsp": null,
1400
- "total": {
1401
- "tok_s": 34.0,
1402
- "peak_mem_mb": 1120,
1403
- "latency_ms_p50": 100.0
1404
- },
1405
- "last_updated": "2026-03-02"
1406
- },
1407
- "hil": null
1408
- },
1409
- "Q4_K_M": {
1410
- "estimate": {
1411
- "accuracy": 0.625,
1412
- "llm_metrics": {
1413
- "overall": 62.5,
1414
- "mmlu": 44.5,
1415
- "gsm8k": 21.5,
1416
- "hellaswag": 64.0,
1417
- "truthfulqa": 37.5,
1418
- "winogrande": 58.5,
1419
- "arc_challenge": 34.5,
1420
- "mt_bench": 6.1
1421
- },
1422
- "cpu": {
1423
- "tok_s": 14.0,
1424
- "latency_ms_p50": 72.0,
1425
- "peak_mem_mb": 1050
1426
- },
1427
- "npu": {
1428
- "tok_s": 34.0,
1429
- "latency_ms_p50": 28.0,
1430
- "peak_mem_mb": 720
1431
- },
1432
- "dsp": null,
1433
- "total": {
1434
- "tok_s": 34.0,
1435
- "peak_mem_mb": 1120,
1436
- "latency_ms_p50": 100.0
1437
- },
1438
- "last_updated": "2026-03-02"
1439
- },
1440
- "hil": null
1441
- },
1442
- "Q8_0": {
1443
- "estimate": {
1444
- "accuracy": 0.647,
1445
- "llm_metrics": {
1446
- "overall": 64.7,
1447
- "mmlu": 46.7,
1448
- "gsm8k": 23.7,
1449
- "hellaswag": 66.2,
1450
- "truthfulqa": 39.7,
1451
- "winogrande": 60.7,
1452
- "arc_challenge": 36.7,
1453
- "mt_bench": 6.2
1454
- },
1455
- "cpu": {
1456
- "tok_s": 14.0,
1457
- "latency_ms_p50": 72.0,
1458
- "peak_mem_mb": 1050
1459
- },
1460
- "npu": {
1461
- "tok_s": 34.0,
1462
- "latency_ms_p50": 28.0,
1463
- "peak_mem_mb": 720
1464
- },
1465
- "dsp": null,
1466
- "total": {
1467
- "tok_s": 34.0,
1468
- "peak_mem_mb": 1120,
1469
- "latency_ms_p50": 100.0
1470
- },
1471
- "last_updated": "2026-03-02"
1472
- },
1473
- "hil": null
1474
- }
1475
- }
1476
- }
1477
- }
1478
- ]
1479
- },
1480
- {
1481
- "key": "zamba-2p7b-instruct",
1482
- "display_name": "Zamba-2.7B Instruct",
1483
- "architecture": "Zamba-2.7B",
1484
- "model_size_m": 2700,
1485
- "modality": "LLM",
1486
- "variant": "Instruct",
1487
- "targets": [
1488
- "CPU",
1489
- "NPU"
1490
- ],
1491
- "onnx_repo": "Renesas/Zamba-2.7B-Instruct-ONNX",
1492
- "gguf_repo": "Renesas/Zamba-2.7B-Instruct-GGUF",
1493
- "onnx_artifacts": [
1494
- "fp16",
1495
- "int8"
1496
  ],
1497
- "gguf_artifacts": [
1498
- "Q4_K_M"
 
 
 
 
1499
  ],
 
 
1500
  "hardware_configs": [
1501
  {
1502
- "id": "X5H-100TOPS",
1503
- "label": "X5H 100TOPS Reference Platform",
1504
  "targets": [
1505
- "CPU",
1506
  "NPU"
1507
  ],
 
1508
  "metrics": {
1509
  "artifacts": {
1510
  "fp16": {
1511
- "estimate": {
1512
- "accuracy": 0.719,
1513
- "llm_metrics": {
1514
- "overall": 71.9,
1515
- "mmlu": 53.9,
1516
- "gsm8k": 30.9,
1517
- "hellaswag": 73.4,
1518
- "truthfulqa": 46.9,
1519
- "winogrande": 67.9,
1520
- "arc_challenge": 43.9,
1521
- "mt_bench": 6.6
1522
- },
1523
- "cpu": {
1524
- "tok_s": 8.5,
1525
- "latency_ms_p50": 112.0,
1526
- "peak_mem_mb": 1620
1527
- },
1528
- "npu": {
1529
- "tok_s": 22.0,
1530
- "latency_ms_p50": 46.0,
1531
- "peak_mem_mb": 1180
1532
- },
1533
- "dsp": null,
1534
- "total": {
1535
- "tok_s": 22.0,
1536
- "peak_mem_mb": 1720,
1537
- "latency_ms_p50": 158.0
1538
- },
1539
- "last_updated": "2026-03-02"
1540
- },
1541
- "hil": {
1542
- "accuracy": 0.713,
1543
- "llm_metrics": {
1544
- "overall": 71.3,
1545
- "mmlu": 53.3,
1546
- "gsm8k": 30.3,
1547
- "hellaswag": 72.8,
1548
- "truthfulqa": 46.3,
1549
- "winogrande": 67.3,
1550
- "arc_challenge": 43.3,
1551
- "mt_bench": 6.6
1552
- },
1553
- "cpu": {
1554
- "tok_s": 7.9,
1555
- "latency_ms_p50": 120.0,
1556
- "peak_mem_mb": 1680
1557
- },
1558
- "npu": {
1559
- "tok_s": 19.5,
1560
- "latency_ms_p50": 52.0,
1561
- "peak_mem_mb": 1240
1562
- },
1563
- "dsp": null,
1564
- "total": {
1565
- "tok_s": 19.5,
1566
- "peak_mem_mb": 1800,
1567
- "latency_ms_p50": 172.0
1568
- },
1569
- "setup": "DEMO-HIL-RIG-2",
1570
- "last_updated": "2026-03-03"
1571
- }
1572
- },
1573
- "int8": {
1574
- "estimate": {
1575
- "accuracy": 0.711,
1576
- "llm_metrics": {
1577
- "overall": 71.1,
1578
- "mmlu": 53.1,
1579
- "gsm8k": 30.1,
1580
- "hellaswag": 72.6,
1581
- "truthfulqa": 46.1,
1582
- "winogrande": 67.1,
1583
- "arc_challenge": 43.1,
1584
- "mt_bench": 6.6
1585
- },
1586
- "cpu": {
1587
- "tok_s": 8.5,
1588
- "latency_ms_p50": 112.0,
1589
- "peak_mem_mb": 1620
1590
- },
1591
- "npu": {
1592
- "tok_s": 22.0,
1593
- "latency_ms_p50": 46.0,
1594
- "peak_mem_mb": 1180
1595
- },
1596
- "dsp": null,
1597
- "total": {
1598
- "tok_s": 22.0,
1599
- "peak_mem_mb": 1720,
1600
- "latency_ms_p50": 158.0
1601
- },
1602
- "last_updated": "2026-03-02"
1603
- },
1604
- "hil": {
1605
- "accuracy": 0.705,
1606
- "llm_metrics": {
1607
- "overall": 70.5,
1608
- "mmlu": 52.5,
1609
- "gsm8k": 29.5,
1610
- "hellaswag": 72.0,
1611
- "truthfulqa": 45.5,
1612
- "winogrande": 66.5,
1613
- "arc_challenge": 42.5,
1614
- "mt_bench": 6.5
1615
- },
1616
- "cpu": {
1617
- "tok_s": 7.9,
1618
- "latency_ms_p50": 120.0,
1619
- "peak_mem_mb": 1680
1620
- },
1621
- "npu": {
1622
- "tok_s": 19.5,
1623
- "latency_ms_p50": 52.0,
1624
- "peak_mem_mb": 1240
1625
- },
1626
- "dsp": null,
1627
- "total": {
1628
- "tok_s": 19.5,
1629
- "peak_mem_mb": 1800,
1630
- "latency_ms_p50": 172.0
1631
- },
1632
- "setup": "DEMO-HIL-RIG-2",
1633
- "last_updated": "2026-03-03"
1634
- }
1635
  },
1636
- "Q4_K_M": {
1637
- "estimate": {
1638
- "accuracy": 0.684,
1639
- "llm_metrics": {
1640
- "overall": 68.4,
1641
- "mmlu": 50.4,
1642
- "gsm8k": 27.4,
1643
- "hellaswag": 69.9,
1644
- "truthfulqa": 43.4,
1645
- "winogrande": 64.4,
1646
- "arc_challenge": 40.4,
1647
- "mt_bench": 6.4
1648
- },
1649
- "cpu": {
1650
- "tok_s": 8.5,
1651
- "latency_ms_p50": 112.0,
1652
- "peak_mem_mb": 1620
1653
- },
1654
- "npu": {
1655
- "tok_s": 22.0,
1656
- "latency_ms_p50": 46.0,
1657
- "peak_mem_mb": 1180
1658
- },
1659
- "dsp": null,
1660
- "total": {
1661
- "tok_s": 22.0,
1662
- "peak_mem_mb": 1720,
1663
- "latency_ms_p50": 158.0
1664
- },
1665
- "last_updated": "2026-03-02"
1666
- },
1667
  "hil": {
1668
- "accuracy": 0.678,
1669
- "llm_metrics": {
1670
- "overall": 67.8,
1671
- "mmlu": 49.8,
1672
- "gsm8k": 26.8,
1673
- "hellaswag": 69.3,
1674
- "truthfulqa": 42.8,
1675
- "winogrande": 63.8,
1676
- "arc_challenge": 39.8,
1677
- "mt_bench": 6.4
1678
- },
1679
- "cpu": {
1680
- "tok_s": 7.9,
1681
- "latency_ms_p50": 120.0,
1682
- "peak_mem_mb": 1680
1683
- },
1684
  "npu": {
1685
- "tok_s": 19.5,
1686
- "latency_ms_p50": 52.0,
1687
- "peak_mem_mb": 1240
1688
  },
1689
- "dsp": null,
1690
  "total": {
1691
- "tok_s": 19.5,
1692
- "peak_mem_mb": 1800,
1693
- "latency_ms_p50": 172.0
1694
  },
1695
- "setup": "DEMO-HIL-RIG-2",
1696
- "last_updated": "2026-03-03"
 
 
 
1697
  }
1698
  }
1699
  }
1700
  }
1701
  }
1702
  ]
1703
- },
1704
- {
1705
- "key": "llava-7b",
1706
- "display_name": "LLaVA-7B",
1707
- "architecture": "LLaVA-7B",
1708
- "model_size_m": 7000,
1709
- "modality": "VLM",
1710
- "variant": "Chat",
1711
- "targets": [
1712
- "CPU",
1713
- "NPU",
1714
- "DSP"
1715
- ],
1716
- "onnx_repo": "Renesas/LLaVA-7B-ONNX",
1717
- "gguf_repo": "Renesas/LLaVA-7B-GGUF",
1718
- "onnx_artifacts": [
1719
- "fp16",
1720
- "int8"
1721
- ],
1722
- "gguf_artifacts": [
1723
- "Q4_K_M"
1724
- ],
1725
- "hardware_configs": [
1726
- {
1727
- "id": "X5H-100TOPS",
1728
- "label": "X5H 100TOPS Reference Platform",
1729
- "targets": [
1730
- "CPU",
1731
- "NPU",
1732
- "DSP"
1733
- ],
1734
- "notes": "Example showing DSP used for vision preprocessing",
1735
- "metrics": {
1736
- "artifacts": {
1737
- "fp16": {
1738
- "estimate": {
1739
- "accuracy": 0.699,
1740
- "vlm_metrics": {
1741
- "overall": 69.9,
1742
- "mmbench": 64.9,
1743
- "vqav2": 71.9,
1744
- "seedbench": 61.9,
1745
- "pope": 66.9
1746
- },
1747
- "cpu": {
1748
- "tok_s": 2.2,
1749
- "latency_ms_p50": 420.0,
1750
- "peak_mem_mb": 4100
1751
- },
1752
- "npu": {
1753
- "tok_s": 6.0,
1754
- "latency_ms_p50": 155.0,
1755
- "peak_mem_mb": 2800
1756
- },
1757
- "dsp": {
1758
- "latency_ms_p50": 18.0,
1759
- "peak_mem_mb": 64
1760
- },
1761
- "total": {
1762
- "tok_s": 6.0,
1763
- "peak_mem_mb": 4300,
1764
- "latency_ms_p50": 593.0
1765
- },
1766
- "last_updated": "2026-03-03"
1767
- },
1768
- "hil": null
1769
- },
1770
- "int8": {
1771
- "estimate": {
1772
- "accuracy": 0.691,
1773
- "vlm_metrics": {
1774
- "overall": 69.1,
1775
- "mmbench": 64.1,
1776
- "vqav2": 71.1,
1777
- "seedbench": 61.1,
1778
- "pope": 66.1
1779
- },
1780
- "cpu": {
1781
- "tok_s": 2.2,
1782
- "latency_ms_p50": 420.0,
1783
- "peak_mem_mb": 4100
1784
- },
1785
- "npu": {
1786
- "tok_s": 6.0,
1787
- "latency_ms_p50": 155.0,
1788
- "peak_mem_mb": 2800
1789
- },
1790
- "dsp": {
1791
- "latency_ms_p50": 18.0,
1792
- "peak_mem_mb": 64
1793
- },
1794
- "total": {
1795
- "tok_s": 6.0,
1796
- "peak_mem_mb": 4300,
1797
- "latency_ms_p50": 593.0
1798
- },
1799
- "last_updated": "2026-03-03"
1800
- },
1801
- "hil": null
1802
- },
1803
- "Q4_K_M": {
1804
- "estimate": {
1805
- "accuracy": 0.664,
1806
- "vlm_metrics": {
1807
- "overall": 66.4,
1808
- "mmbench": 61.4,
1809
- "vqav2": 68.4,
1810
- "seedbench": 58.4,
1811
- "pope": 63.4
1812
- },
1813
- "cpu": {
1814
- "tok_s": 2.2,
1815
- "latency_ms_p50": 420.0,
1816
- "peak_mem_mb": 4100
1817
- },
1818
- "npu": {
1819
- "tok_s": 6.0,
1820
- "latency_ms_p50": 155.0,
1821
- "peak_mem_mb": 2800
1822
- },
1823
- "dsp": {
1824
- "latency_ms_p50": 18.0,
1825
- "peak_mem_mb": 64
1826
- },
1827
- "total": {
1828
- "tok_s": 6.0,
1829
- "peak_mem_mb": 4300,
1830
- "latency_ms_p50": 593.0
1831
- },
1832
- "last_updated": "2026-03-03"
1833
- },
1834
- "hil": null
1835
- }
1836
- }
1837
- }
1838
- }
1839
- ]
1840
  }
1841
  ]
1842
- }
 
1
  {
2
+ "version": "1.0-auto",
3
+ "generated_at": "2026-06-23T16:50:24+02:00",
4
+ "source": "huggingface",
5
+ "org": "Renesas",
6
+ "catalog_note": "Auto-generated from Hugging Face 'Renesas' repositories on 2026-06-23 16:50 W. Europe Daylight Time. Metrics reflect each repo's published benchmark YAML files.",
7
  "models": [
8
  {
9
+ "key": "llama-3.2-1b-instruct",
10
+ "display_name": "Llama-3.2-1B-Instruct",
11
+ "architecture": "Llama-3.2-1B",
12
  "model_size_m": 1000,
13
  "modality": "LLM",
14
  "variant": "Instruct",
15
  "targets": [
 
16
  "NPU"
17
  ],
18
+ "onnx_repo": "Renesas/Llama-3.2-1B-Instruct-ONNX",
19
+ "gguf_repo": "Renesas/Llama-3.2-1B-Instruct-GGUF",
20
  "onnx_artifacts": [
21
  "fp16",
22
+ "w4a8",
23
+ "fp32"
24
  ],
25
  "gguf_artifacts": [
26
+ "fp16",
27
+ "w4a16"
28
+ ],
29
+ "base_model": "meta-llama/Llama-3.2-1B-Instruct",
30
+ "license": "llama3.2",
31
+ "tasks": [
32
+ "text-generation",
33
+ "instruction-following",
34
+ "chat"
35
  ],
36
+ "created_at": "2026-02-20T08:59:16.000Z",
37
+ "last_modified": "2026-06-22T09:24:19.000Z",
38
  "hardware_configs": [
39
  {
40
+ "id": "RCAR-X5H",
41
+ "label": "R-Car X5H (NPX6-48K)",
42
  "targets": [
 
43
  "NPU"
44
  ],
45
+ "notes": "vendor=renesas; chip=rcar-x5h; cpu=arm-cortex-a720; npu=npx6-48k; memory_mb=8192",
46
  "metrics": {
47
  "artifacts": {
48
  "fp16": {
49
  "estimate": {
50
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  "npu": {
52
+ "tok_s": 18.73,
53
+ "latency_ms_p50": null,
54
+ "peak_mem_mb": null
55
  },
 
56
  "total": {
57
+ "tok_s": 18.73,
58
+ "latency_ms_p50": null,
59
+ "peak_mem_mb": null
60
  },
61
+ "engine": "ppa-estimator",
62
+ "execution_provider": "npu",
63
+ "last_updated": "2026-03-09"
64
  },
65
  "hil": {
66
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  "npu": {
68
+ "tok_s": 20.25,
69
+ "latency_ms_p50": null,
70
+ "peak_mem_mb": null
71
  },
 
72
  "total": {
73
+ "tok_s": 20.25,
74
+ "latency_ms_p50": null,
75
+ "peak_mem_mb": null
76
  },
77
+ "engine": "llama.cpp",
78
+ "execution_provider": "npu",
79
+ "last_updated": "2026-06-22",
80
+ "batch_size": 1,
81
+ "setup": "R-Car X5H (NPX6-48K)"
82
  }
83
  },
84
+ "w4a16": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  "estimate": null,
86
  "hil": {
87
+ "accuracy": null,
88
+ "npu": {
89
+ "tok_s": 41.38,
90
+ "latency_ms_p50": null,
91
+ "peak_mem_mb": null
 
 
 
 
 
 
 
 
 
 
92
  },
 
 
93
  "total": {
94
+ "tok_s": 41.38,
95
+ "latency_ms_p50": null,
96
+ "peak_mem_mb": null
97
  },
98
+ "engine": "llama.cpp",
99
+ "execution_provider": "npu",
100
+ "last_updated": "2026-06-22",
101
+ "batch_size": 1,
102
+ "setup": "R-Car X5H (NPX6-48K)"
103
  }
104
  },
105
+ "fp32": {
106
  "estimate": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  "hil": null
108
  },
109
+ "w4a8": {
110
  "estimate": {
111
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
112
  "npu": {
113
+ "tok_s": 72.98,
114
+ "latency_ms_p50": null,
115
+ "peak_mem_mb": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  },
117
  "total": {
118
+ "tok_s": 72.98,
119
+ "latency_ms_p50": null,
120
+ "peak_mem_mb": null
121
  },
122
+ "engine": "ppa-estimator",
123
+ "execution_provider": "bw",
124
+ "last_updated": "2026-03-09"
125
  },
126
  "hil": null
127
  }
 
131
  ]
132
  },
133
  {
134
+ "key": "llama-3.2-3b-instruct",
135
+ "display_name": "Llama-3.2-3B-Instruct",
136
+ "architecture": "Llama-3.2-3B",
137
+ "model_size_m": 3000,
138
  "modality": "LLM",
139
  "variant": "Instruct",
140
  "targets": [
 
141
  "NPU"
142
  ],
143
  "onnx_repo": "",
144
+ "gguf_repo": "Renesas/Llama-3.2-3B-Instruct-GGUF",
145
  "onnx_artifacts": [],
146
  "gguf_artifacts": [
147
+ "fp16",
148
+ "w4a16"
149
  ],
150
+ "base_model": "meta-llama/Llama-3.2-3B-Instruct",
151
+ "license": "llama3.2",
152
+ "tasks": [
153
+ "text-generation",
154
+ "instruction-following",
155
+ "chat"
156
+ ],
157
+ "created_at": "2026-05-11T09:18:02.000Z",
158
+ "last_modified": "2026-06-22T09:30:11.000Z",
159
  "hardware_configs": [
160
  {
161
+ "id": "RCAR-X5H",
162
+ "label": "R-Car X5H (NPX6-48K)",
163
  "targets": [
 
164
  "NPU"
165
  ],
166
+ "notes": "vendor=renesas; chip=rcar-x5h; cpu=arm-cortex-a720; npu=npx6-48k; memory_mb=8192",
167
  "metrics": {
168
  "artifacts": {
169
+ "fp16": {
170
  "estimate": null,
171
  "hil": {
172
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  "npu": {
174
+ "tok_s": 8.76,
175
+ "latency_ms_p50": null,
176
+ "peak_mem_mb": null
177
  },
 
178
  "total": {
179
+ "tok_s": 8.76,
180
+ "latency_ms_p50": null,
181
+ "peak_mem_mb": null
182
  },
183
+ "engine": "llama.cpp",
184
+ "execution_provider": "npu",
185
+ "last_updated": "2026-06-22",
186
+ "batch_size": 1,
187
+ "setup": "R-Car X5H (NPX6-48K)"
188
  }
189
  },
190
+ "w4a16": {
191
  "estimate": null,
192
  "hil": {
193
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  "npu": {
195
+ "tok_s": 17.46,
196
+ "latency_ms_p50": null,
197
+ "peak_mem_mb": null
198
  },
 
199
  "total": {
200
+ "tok_s": 17.46,
201
+ "latency_ms_p50": null,
202
+ "peak_mem_mb": null
203
  },
204
+ "engine": "llama.cpp",
205
+ "execution_provider": "npu",
206
+ "last_updated": "2026-06-22",
207
+ "batch_size": 1,
208
+ "setup": "R-Car X5H (NPX6-48K)"
209
  }
210
  }
211
  }
 
214
  ]
215
  },
216
  {
217
+ "key": "qwen2.5-vl-3b-instruct",
218
+ "display_name": "Qwen2.5-VL-3B-Instruct",
219
+ "architecture": "Qwen2.5-VL-3B",
220
  "model_size_m": 3000,
221
  "modality": "LLM",
222
  "variant": "Instruct",
223
  "targets": [
 
224
  "NPU"
225
  ],
226
+ "onnx_repo": "",
227
+ "gguf_repo": "Renesas/Qwen2.5-VL-3B-Instruct-GGUF",
228
+ "onnx_artifacts": [],
229
+ "gguf_artifacts": [
230
  "fp16",
231
+ "w4a16"
232
  ],
233
+ "base_model": "Qwen/Qwen2.5-VL-3B-Instruct",
234
+ "license": "other",
235
+ "tasks": [
236
+ "image-text-to-text",
237
+ "text-generation",
238
+ "instruction-following",
239
+ "chat"
240
  ],
241
+ "created_at": "2026-05-13T09:09:11.000Z",
242
+ "last_modified": "2026-06-23T10:48:46.000Z",
243
  "hardware_configs": [
244
  {
245
+ "id": "RCAR-X5H",
246
+ "label": "R-Car X5H (NPX6-48K)",
247
  "targets": [
 
248
  "NPU"
249
  ],
250
+ "notes": "vendor=renesas; chip=rcar-x5h; cpu=arm-cortex-a720; npu=npx6-48k; memory_mb=8192",
251
  "metrics": {
252
  "artifacts": {
253
  "fp16": {
254
+ "estimate": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  "hil": {
256
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  "npu": {
258
+ "tok_s": 8.21,
259
+ "latency_ms_p50": null,
260
+ "peak_mem_mb": null
261
  },
 
262
  "total": {
263
+ "tok_s": 8.21,
264
+ "latency_ms_p50": null,
265
+ "peak_mem_mb": null
266
  },
267
+ "engine": "llama.cpp",
268
+ "execution_provider": "npu",
269
+ "last_updated": "2026-06-23",
270
+ "batch_size": 1,
271
+ "setup": "R-Car X5H (NPX6-48K)"
272
  }
273
  },
274
+ "w4a16": {
275
+ "estimate": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  "hil": {
277
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  "npu": {
279
+ "tok_s": 14.09,
280
+ "latency_ms_p50": null,
281
+ "peak_mem_mb": null
282
  },
 
283
  "total": {
284
+ "tok_s": 14.09,
285
+ "latency_ms_p50": null,
286
+ "peak_mem_mb": null
287
  },
288
+ "engine": "llama.cpp",
289
+ "execution_provider": "npu",
290
+ "last_updated": "2026-06-23",
291
+ "batch_size": 1,
292
+ "setup": "R-Car X5H (NPX6-48K)"
293
  }
294
  }
295
  }
 
298
  ]
299
  },
300
  {
301
+ "key": "qwen3-vl-4b-instruct",
302
+ "display_name": "Qwen3-VL-4B-Instruct",
303
+ "architecture": "Qwen3-VL-4B",
304
+ "model_size_m": 4000,
305
  "modality": "LLM",
306
+ "variant": "Instruct",
307
  "targets": [
 
308
  "NPU"
309
  ],
310
+ "onnx_repo": "",
311
+ "gguf_repo": "Renesas/Qwen3-VL-4B-Instruct-GGUF",
312
+ "onnx_artifacts": [],
313
+ "gguf_artifacts": [
314
  "fp16",
315
+ "w4a16"
316
  ],
317
+ "base_model": "Qwen/Qwen3-VL-4B-Instruct",
318
+ "license": "apache-2.0",
319
+ "tasks": [
320
+ "image-text-to-text",
321
+ "text-generation",
322
+ "instruction-following",
323
+ "chat"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  ],
325
+ "created_at": "2026-05-15T09:37:33.000Z",
326
+ "last_modified": "2026-06-23T13:01:38.000Z",
 
 
 
 
 
327
  "hardware_configs": [
328
  {
329
+ "id": "RCAR-X5H",
330
+ "label": "R-Car X5H (NPX6-48K)",
331
  "targets": [
 
332
  "NPU"
333
  ],
334
+ "notes": "vendor=renesas; chip=rcar-x5h; cpu=arm-cortex-a720; npu=npx6-48k; memory_mb=8192",
335
  "metrics": {
336
  "artifacts": {
337
  "fp16": {
338
+ "estimate": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  "hil": {
340
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  "npu": {
342
+ "tok_s": 6.29,
343
+ "latency_ms_p50": null,
344
+ "peak_mem_mb": null
345
  },
 
346
  "total": {
347
+ "tok_s": 6.29,
348
+ "latency_ms_p50": null,
349
+ "peak_mem_mb": null
350
  },
351
+ "engine": "llama.cpp",
352
+ "execution_provider": "npu",
353
+ "last_updated": "2026-06-23",
354
+ "batch_size": 1,
355
+ "setup": "R-Car X5H (NPX6-48K)"
356
  }
357
  },
358
+ "w4a16": {
359
+ "estimate": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  "hil": {
361
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  "npu": {
363
+ "tok_s": 11.48,
364
+ "latency_ms_p50": null,
365
+ "peak_mem_mb": null
366
  },
 
367
  "total": {
368
+ "tok_s": 11.48,
369
+ "latency_ms_p50": null,
370
+ "peak_mem_mb": null
371
  },
372
+ "engine": "llama.cpp",
373
+ "execution_provider": "npu",
374
+ "last_updated": "2026-06-23",
375
+ "batch_size": 1,
376
+ "setup": "R-Car X5H (NPX6-48K)"
377
  }
378
  }
379
  }
 
382
  ]
383
  },
384
  {
385
+ "key": "llama-3.1-8b-instruct",
386
+ "display_name": "Llama-3.1-8B-Instruct",
387
+ "architecture": "Llama-3.1-8B",
388
+ "model_size_m": 8000,
389
  "modality": "LLM",
390
  "variant": "Instruct",
391
  "targets": [
 
392
  "NPU"
393
  ],
394
+ "onnx_repo": "",
395
+ "gguf_repo": "Renesas/Llama-3.1-8B-Instruct-GGUF",
396
+ "onnx_artifacts": [],
 
 
 
397
  "gguf_artifacts": [
398
+ "w4a16",
399
+ "fp16"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  ],
401
+ "base_model": "meta-llama/Llama-3.1-8B-Instruct",
402
+ "license": "llama3.1",
403
+ "tasks": [
404
+ "text-generation",
405
+ "instruction-following",
406
+ "chat"
407
  ],
408
+ "created_at": "2026-06-12T09:43:03.000Z",
409
+ "last_modified": "2026-06-19T11:01:35.000Z",
410
  "hardware_configs": [
411
  {
412
+ "id": "RCAR-X5H",
413
+ "label": "R-Car X5H (NPX6-48K)",
414
  "targets": [
 
415
  "NPU"
416
  ],
417
+ "notes": "vendor=renesas; chip=rcar-x5h; cpu=arm-cortex-a720; npu=npx6-48k; memory_mb=8192",
418
  "metrics": {
419
  "artifacts": {
420
  "fp16": {
421
+ "estimate": null,
422
+ "hil": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  },
424
+ "w4a16": {
425
+ "estimate": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  "hil": {
427
+ "accuracy": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  "npu": {
429
+ "tok_s": 8.59,
430
+ "latency_ms_p50": null,
431
+ "peak_mem_mb": null
432
  },
 
433
  "total": {
434
+ "tok_s": 8.59,
435
+ "latency_ms_p50": null,
436
+ "peak_mem_mb": null
437
  },
438
+ "engine": "llama.cpp",
439
+ "execution_provider": "npu",
440
+ "last_updated": "2026-06-19",
441
+ "batch_size": 1,
442
+ "setup": "R-Car X5H (NPX6-48K)"
443
  }
444
  }
445
  }
446
  }
447
  }
448
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
  ]
451
+ }
styles.css CHANGED
@@ -1,18 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
1
  :root {
2
- --bg: #0b1220;
3
- --text: #e8eefc;
4
- --border: rgba(232, 238, 252, 0.12);
5
- --accent: #4c6ef5;
 
6
 
7
- --content-text: #1f2937;
8
- --code-bg: #f6f8fa;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- --pad: 12px;
11
  --radius: 16px;
12
- --card-radius: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
 
 
 
15
  * { box-sizing: border-box; }
 
16
  html, body { height: 100%; }
17
 
18
  body {
@@ -21,559 +99,992 @@ body {
21
  display: flex;
22
  flex-direction: column;
23
  overflow: hidden;
24
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
25
- background:
26
- radial-gradient(1000px 400px at 10% 0%, rgba(76,110,245,.32), transparent),
27
- linear-gradient(180deg, var(--bg), #070b14);
28
  }
 
 
 
 
 
 
 
 
 
 
29
 
30
- /* Topbar */
 
 
31
  .topbar {
32
  flex: 0 0 auto;
33
  z-index: 50;
34
- border-bottom: 1px solid var(--border);
35
- background: linear-gradient(180deg, rgba(15,26,48,0.96), rgba(15,26,48,0.86));
36
- backdrop-filter: blur(10px);
 
 
37
  }
38
-
39
  .topbar-inner {
40
  width: 100%;
41
- margin: 0;
42
- padding: 12px 16px;
43
- display: grid;
44
- grid-template-columns: 1fr auto;
45
- gap: 12px;
46
  align-items: center;
 
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
50
- .brand-icon{ height: 34px; width: auto; max-width: 180px; object-fit: contain; }
51
- .brand-title{ color: var(--text); font-weight: 900; font-size: 1.02rem; line-height: 1.15; }
52
- .brand-subtitle{ color: rgba(232, 238, 252, 0.72); font-size: 0.82rem; margin-top: 4px; }
53
-
54
- /* Tabs */
55
- .tabs { display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
56
  .nav-item {
57
  appearance: none;
58
- border: 1px solid rgba(232, 238, 252, 0.18);
59
- background: rgba(255,255,255,0.04);
60
- color: rgba(232,238,252,0.95);
61
- border-radius: 999px;
62
- padding: 9px 14px;
63
- font-weight: 850;
64
- font-size: 0.92rem;
 
 
65
  cursor: pointer;
 
66
  }
 
67
  .nav-item.active {
68
- background: rgba(76,110,245,0.22);
69
- border-color: rgba(76,110,245,0.55);
70
- box-shadow: 0 0 0 4px rgba(76,110,245,0.10);
71
  }
72
 
73
- /* Content */
74
- .content {
75
- flex: 1 1 auto;
76
- min-height: 0;
77
- width: 100%;
78
- margin: 0;
79
- padding: var(--pad);
80
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- /* Sections */
 
 
 
84
  .section {
85
  display: none;
86
  height: 100%;
87
  min-height: 0;
88
- background: #fff;
89
- color: var(--content-text);
90
  border-radius: var(--radius);
91
- border: 1px solid rgba(17, 24, 39, 0.08);
92
- box-shadow: 0 18px 50px rgba(0,0,0,0.22);
 
 
 
93
  overflow: hidden;
94
  }
95
  .section.visible { display: flex; flex-direction: column; }
96
- .section-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 14px; }
 
 
 
 
 
 
 
 
 
97
 
98
- /* Typography */
99
- h1, h2, h3 { margin-top: 0.2rem; padding-bottom: 0.25rem; border-bottom: 1px solid rgba(17,24,39,0.08); }
100
- pre, code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
101
- pre { background: var(--code-bg); padding: 10px 12px; border-radius: 12px; overflow-x: auto; border: 1px solid rgba(17,24,39,0.08); margin: 0; }
102
- code { background: rgba(246,248,250,0.9); padding: 0.1rem 0.3rem; border-radius: 6px; }
 
 
 
 
103
 
104
- .note { background: #f0f4ff; border-left: 4px solid var(--accent); padding: 10px 12px; margin: 10px 0 12px; border-radius: 10px; }
105
- .note.note-warn{ background: #fff7ed; border-left-color: #f59e0b; }
106
- .subtle { color: #4b5563; margin-top: 0.4rem; }
107
  .tight { margin-top: 0.35rem; }
108
- .no-border{ border-bottom:none !important; padding-bottom:0 !important; }
109
-
110
- /* Disclaimer overlay */
111
- .poc-overlay{
112
- position: fixed;
113
- top: 10px;
114
- left: 50%;
115
- transform: translateX(-50%);
116
- z-index: 9999;
117
- max-width: min(820px, calc(100vw - 32px));
118
- background: rgba(240, 244, 255, 0.70);
119
- border: 1px solid rgba(76, 110, 245, 0.25);
120
- border-left: 4px solid var(--accent);
121
- border-radius: 12px;
122
- padding: 10px 12px;
123
- box-shadow: 0 16px 40px rgba(0,0,0,0.22);
124
- backdrop-filter: blur(10px);
125
- color: #111827;
126
- font-size: 0.92rem;
127
- line-height: 1.25;
128
- pointer-events: none;
129
- }
130
 
131
- /* Layout */
132
- .grid.two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
133
- .grid.explore-grid{ display: grid; grid-template-columns: minmax(320px, 30%) 1fr; gap: 12px; margin-top: 10px; }
134
 
135
- .card {
136
- border: 1px solid rgba(17,24,39,0.08);
137
- border-radius: var(--card-radius);
138
- padding: 12px 12px 10px;
139
- background: linear-gradient(180deg, rgba(76,110,245,0.05), transparent);
140
- min-width: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
- /* Form controls */
144
- .form-grid{ display: grid; grid-template-columns: 1fr; gap: 10px; }
145
- .field{ display:flex; flex-direction:column; gap:6px; min-width: 0; }
146
- .label{ font-size: 0.82rem; color: rgba(75, 85, 99, 0.9); font-weight: 800; }
147
- .radio-row{ display:flex; flex-wrap:wrap; gap:10px; }
148
- .radio-col{ display:grid; gap:8px; }
149
- .field-row.two{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
 
 
150
 
151
- .pill{
152
- display:inline-flex; align-items:center; gap:8px;
153
- padding: 6px 10px; border: 1px solid rgba(17,24,39,0.10);
154
- background: rgba(255,255,255,0.95); border-radius: 999px;
155
- cursor:pointer; user-select:none; font-size: 0.92rem;
 
156
  }
157
- .pill.active{ background: rgba(76,110,245,0.10); border-color: rgba(76,110,245,0.35); box-shadow: 0 0 0 4px rgba(76,110,245,0.08); }
 
 
158
 
159
  .filter-input, .filter-select {
160
- height: 38px; padding: 0 12px; border-radius: 10px;
161
- border: 1px solid rgba(17,24,39,0.14);
162
- background: rgba(255,255,255,0.98); color: var(--content-text);
163
- outline: none; font-size: 0.95rem; width: 100%;
164
  }
165
-
166
- select.filter-select{
167
- appearance:none; -webkit-appearance:none; -moz-appearance:none;
168
- padding-right:40px;
169
  background-image:
170
- linear-gradient(45deg, transparent 50%, rgba(31,41,55,.75) 50%),
171
- linear-gradient(135deg, rgba(31,41,55,.75) 50%, transparent 50%),
172
- linear-gradient(to right, rgba(17,24,39,.08), rgba(17,24,39,.08));
173
- background-position:
174
- calc(100% - 18px) 16px,
175
- calc(100% - 12px) 16px,
176
- calc(100% - 36px) 50%;
177
- background-size: 6px 6px, 6px 6px, 1px 60%;
178
- background-repeat:no-repeat;
179
- }
180
- .filter-input:focus, .filter-select:focus{
181
- border-color: rgba(76,110,245,0.55);
182
- box-shadow: 0 0 0 4px rgba(76,110,245,0.12);
183
- }
184
-
185
- .matchline{
186
- font-size: 0.92rem;
187
- color: #111827;
188
- font-weight: 850;
189
- padding: 8px 10px;
190
- border-radius: 12px;
191
- background: rgba(76,110,245,0.08);
192
- border: 1px solid rgba(76,110,245,0.18);
193
- }
194
-
195
- /* Model list */
196
- .model-picker{ border: 1px solid rgba(17,24,39,0.10); background: rgba(255,255,255,0.85); border-radius: 14px; padding: 10px; }
197
- .model-picker-head{ display:flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
198
- .model-picker-title{ font-weight: 950; color: #111827; font-size: 0.95rem; }
199
- .model-picker-subtle{ font-size: 0.82rem; color: rgba(75,85,99,0.85); font-weight: 700; }
200
- .model-list{ height:auto; min-height:280px; padding: 8px 10px; line-height:1.3; border-radius:12px; background-image:none !important; padding-right: 12px !important; }
201
- .model-list option{ padding: 6px 6px; }
202
-
203
- /* Link buttons */
204
- .link{
205
- display:inline-flex; gap:6px; align-items:center;
206
- font-weight: 850; color: #1d4ed8; text-decoration:none;
207
- padding: 8px 10px; border-radius:10px;
208
- border: 1px solid rgba(29,78,216,0.18);
209
- background: rgba(29,78,216,0.06);
210
- }
211
- .link:hover{ background: rgba(29,78,216,0.10); }
212
- .link.small{
213
- padding: 6px 8px;
214
- font-size: 0.82rem;
215
- border-radius: 9px;
216
- }
217
-
218
- /* Summary grid + repo buttons */
219
- .summary-grid{ display:grid; grid-template-columns: 1fr 320px; gap: 12px; align-items:start; }
220
- .repo-buttons{ display:grid; gap: 10px; justify-items:end; padding: 6px 8px 0 0; }
221
- .repo-buttons .link{ width: 100%; justify-content:center; }
222
-
223
- /* Chips */
224
- .chips{ display:flex; flex-wrap:wrap; gap:10px; }
225
- .chip-btn{
226
- appearance:none; border:1px solid rgba(17,24,39,0.14);
227
- background: rgba(255,255,255,0.96);
228
- border-radius:999px; padding: 7px 12px;
229
- cursor:pointer; font-weight:900; font-size:0.9rem;
230
- color: rgba(17,24,39,0.88);
231
- transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease, background 120ms ease;
232
  }
233
- .chip-btn:hover{ border-color: rgba(76,110,245,0.45); box-shadow: 0 0 0 4px rgba(76,110,245,0.10); transform: translateY(-1px); }
234
- .chip-btn.active{ background: rgba(76,110,245,0.12); border-color: rgba(76,110,245,0.55); box-shadow: 0 0 0 4px rgba(76,110,245,0.12); }
 
 
 
 
235
 
236
- /* Mini chart */
237
- .mini-chart-wrap{ margin-top: 10px; border: 1px solid rgba(17,24,39,0.10); border-radius: 12px; background: linear-gradient(180deg, rgba(76,110,245,0.04), transparent); overflow:hidden; position:relative; }
238
- .mini-chart-head{ display:flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 10px 10px 0; }
239
- .mini-chart-title{ font-weight: 950; color: #111827; }
240
- .mini-chart-subtle{ color: rgba(75,85,99,0.85); font-weight: 700; font-size: 0.85rem; }
241
- .mini-tooltip{
242
- position:absolute; pointer-events:none; padding: 8px 10px; border-radius: 12px;
243
- background: rgba(15,26,48,0.92); color: rgba(232,238,252,0.95);
244
- border: 1px solid rgba(232,238,252,0.18); box-shadow: 0 18px 40px rgba(0,0,0,0.28);
245
- font-size: 0.82rem; max-width: 280px; line-height: 1.25; display:none;
246
- }
247
- .mini-chart-wrap canvas { cursor: grab; }
248
- .mini-chart-wrap canvas:active { cursor: grabbing; }
249
 
250
- /* All Models: compact filters (one line as much as possible) */
251
- .filters-row{
252
- display:flex;
253
- flex-wrap:wrap;
254
- gap: 10px 12px;
255
- align-items:flex-end;
 
256
  }
257
- .filters-row.compact{
258
- padding: 6px 2px 2px;
 
 
 
 
 
 
 
 
 
 
259
  }
260
- .field.compact { gap: 6px; }
261
- .field.compact .label{ font-size: 0.78rem; }
262
- .field.inline.compact{
263
- flex-direction: row;
264
- align-items: center;
265
- gap: 8px;
266
- padding-top: 20px; /* align with controls */
267
- }
268
- .field.grow{ flex: 1 1 260px; min-width: 220px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
- .filter-input.compact, .filter-select.compact{
271
- height: 34px;
272
- border-radius: 10px;
273
- font-size: 0.92rem;
 
274
  }
275
- .check-row.compact{
276
- display:flex;
277
- flex-wrap:wrap;
278
- gap: 8px;
 
 
 
 
279
  }
280
- .check-row.compact .pill{
281
- padding: 5px 9px;
282
- font-size: 0.88rem;
 
 
 
 
 
 
 
 
283
  }
 
 
 
 
 
 
284
 
285
- /* Badges */
286
- .badges{ display:flex; flex-wrap:wrap; gap:6px; }
287
- .badge{
288
- display:inline-flex;
289
- align-items:center;
290
- gap:6px;
291
- padding: 4px 8px;
292
- border-radius: 999px;
293
- font-weight: 900;
294
- font-size: 0.82rem;
295
- border: 1px solid rgba(17,24,39,0.12);
296
- background: rgba(255,255,255,0.9);
297
- color: rgba(17,24,39,0.88);
298
- }
299
- .badge.onnx{ background: rgba(59,130,246,0.10); border-color: rgba(59,130,246,0.25); }
300
- .badge.gguf{ background: rgba(16,185,129,0.10); border-color: rgba(16,185,129,0.25); }
301
- .badge.cpu{ background: rgba(148,163,184,0.14); border-color: rgba(148,163,184,0.35); }
302
- .badge.npu{ background: rgba(245,158,11,0.12); border-color: rgba(245,158,11,0.30); }
303
- .badge.dsp{ background: rgba(168,85,247,0.12); border-color: rgba(168,85,247,0.30); }
304
-
305
- /* Precision/Quant badge */
306
- .badge.art{
307
- background: rgba(17,24,39,0.06);
308
- border-color: rgba(17,24,39,0.14);
309
- font-weight: 950;
310
- }
311
-
312
- /* Table + pinned cols */
313
- .table-wrap{ margin-top:10px; border:1px solid rgba(17,24,39,0.10); border-radius:12px; overflow:auto; max-height:650px; background:#fff; }
314
- table.table{ width:100%; border-collapse: collapse; background:#fff; }
315
- .table th, .table td{ padding:10px 12px; border-bottom:1px solid rgba(17,24,39,0.08); font-size:0.94rem; color:#111827; vertical-align: top; }
316
- .table th{ background: rgba(76,110,245,0.06); font-weight: 900; }
317
- .table thead th{ position: sticky; top: 0; z-index: 3; }
318
- .pin{ position: sticky; left:0; z-index:2; background:#fff; }
319
- .table thead .pin{ z-index:4; background: rgba(76,110,245,0.06); }
320
- .table tr.selected-row td{ background: rgba(76,110,245,0.08); }
321
-
322
- /* Stack badges vertically in table cells by default */
323
- .table .badges{
324
- flex-direction: column;
325
- align-items: flex-start;
326
- gap: 6px;
327
  }
328
- /* Precision/Quant badges: horizontal wrap */
329
- .table .badges.badges-row{
330
- flex-direction: row;
331
- flex-wrap: wrap;
332
- align-items: flex-start;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
 
 
 
 
 
 
 
 
334
 
335
- /* Details column: vertically center button */
336
- .table th.details-col,
337
- .table td.details-col{
338
- width: 1%;
339
- white-space: nowrap;
 
 
340
  }
341
- .table td.details-col{
342
- vertical-align: middle;
343
- display: flex;
344
- align-items: center;
345
- justify-content: center;
 
 
346
  }
347
- .table td.details-col .mini-btn{
348
- margin-top: 0;
349
- align-self: center;
350
  }
 
351
 
352
- /* Compact metrics cell */
353
- .cell-metrics{
354
- display:flex;
355
- flex-direction:column;
356
- gap: 4px;
357
- white-space: normal;
358
- line-height: 1.15;
359
  }
360
- .cell-metrics .row{
361
- display:flex;
362
- gap: 8px;
363
- align-items: baseline;
 
 
 
 
364
  }
365
- .cell-metrics .k{
366
- font-size: 0.78rem;
367
- font-weight: 950;
368
- color: rgba(75,85,99,0.95);
369
- width: 56px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  flex: 0 0 auto;
 
 
371
  }
372
- .cell-metrics .v{
373
- font-weight: 900;
374
- color: #111827;
 
 
 
 
 
 
375
  }
 
 
376
 
377
- /* Small action button for table cells */
378
- .mini-btn{
379
- appearance: none;
380
- border: 1px solid rgba(29,78,216,0.20);
381
- background: rgba(29,78,216,0.06);
382
- color: #1d4ed8;
383
- border-radius: 10px;
384
- padding: 6px 10px;
385
- font-weight: 900;
386
- font-size: 0.84rem;
387
- cursor: pointer;
388
- align-self: flex-start;
389
- margin-top: 4px;
390
  }
391
- .mini-btn:hover{
392
- background: rgba(29,78,216,0.10);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  }
394
 
395
- /* --- Metrics cards (Estimate + HIL side-by-side) --- */
396
- .metric-banner{
397
- margin: 10px 0 10px;
398
- padding: 8px 10px;
399
- border-radius: 12px;
400
- background: rgba(76,110,245,0.08);
401
- border: 1px solid rgba(76,110,245,0.18);
402
- font-weight: 850;
403
- color: #111827;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
- .metric-grid{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  display: grid;
407
- grid-template-columns: 1fr 1fr;
408
- gap: 12px;
409
- align-items: stretch;
410
- margin-top: 10px;
411
- }
412
- .metric-card{
413
- border: 1px solid rgba(17,24,39,0.10);
414
- border-radius: 14px;
415
- background: rgba(255,255,255,0.92);
416
- padding: 10px 10px 12px;
 
 
 
 
 
417
  min-width: 0;
418
  }
419
- .metric-card-title{
420
- font-weight: 950;
421
- color: #111827;
422
- font-size: 0.95rem;
423
- margin-bottom: 8px;
 
 
 
424
  }
425
- .metric-pre{
426
- white-space: pre-wrap;
427
- word-break: break-word;
428
- margin: 0;
429
- border-radius: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  }
 
 
431
 
432
- /* --- Download copy button --- */
433
- .cmd-row{
434
- display:flex;
435
- align-items:center;
436
- gap:10px;
437
- margin-top:10px;
 
 
 
 
438
  }
439
- .cmd-row pre{
 
 
440
  flex: 1 1 auto;
 
 
 
 
 
 
 
 
 
441
  }
442
 
443
- .copy-btn{
444
- appearance:none;
445
- border: 1px solid rgba(17,24,39,0.12);
446
- background: rgba(255,255,255,0.92);
447
- border-radius: 10px;
448
- padding: 8px 10px;
449
- cursor: pointer;
450
- font-weight: 950;
451
- line-height: 1;
452
- display:inline-flex;
453
- align-items:center;
454
- gap:8px;
455
- color: rgba(17,24,39,0.88);
456
- white-space: nowrap;
457
- }
458
- .copy-btn:hover{
459
- background: rgba(76,110,245,0.10);
460
- border-color: rgba(76,110,245,0.35);
461
- box-shadow: 0 0 0 4px rgba(76,110,245,0.10);
462
- }
463
- .copy-btn .icon{ font-size: 1rem; }
464
- .copy-btn.copied{
465
- background: rgba(16,185,129,0.10);
466
- border-color: rgba(16,185,129,0.35);
467
- box-shadow: 0 0 0 4px rgba(16,185,129,0.10);
468
- }
469
-
470
- /* Scatter */
471
- .chart-wrap{
472
- width:100%; border-radius:12px; border:1px solid rgba(17,24,39,0.08);
473
- background: linear-gradient(180deg, rgba(76,110,245,0.04), transparent);
474
- overflow:hidden;
475
- }
476
- .tooltip{
477
- position:absolute; pointer-events:none; padding:10px 10px; border-radius:12px;
478
- background: rgba(15,26,48,0.92); color: rgba(232,238,252,0.95);
479
- border:1px solid rgba(232,238,252,0.18); box-shadow: 0 18px 40px rgba(0,0,0,0.28);
480
- font-size:0.85rem; max-width:320px; line-height:1.25;
481
- }
482
-
483
- /* Modal */
484
- .modal{
485
- position: fixed;
486
- inset: 0;
487
- z-index: 10000;
488
  display: none;
 
 
 
 
 
 
 
 
 
 
489
  }
490
- .modal.open{ display:block; }
491
- .modal-backdrop{
492
- position:absolute;
493
- inset:0;
494
- background: rgba(0,0,0,0.45);
495
- backdrop-filter: blur(4px);
496
- }
497
- .modal-panel{
498
- position: absolute;
499
- top: 24px;
500
- left: 50%;
501
- transform: translateX(-50%);
502
- width: min(980px, calc(100vw - 28px));
503
- max-height: calc(100vh - 48px);
504
- overflow: auto;
505
- background: #fff;
506
- border-radius: 16px;
507
- border: 1px solid rgba(17,24,39,0.10);
508
- box-shadow: 0 25px 80px rgba(0,0,0,0.35);
509
- }
510
- .modal-head{
511
- position: sticky;
512
- top: 0;
513
- z-index: 5;
514
- background: rgba(255,255,255,0.92);
515
- backdrop-filter: blur(8px);
516
- border-bottom: 1px solid rgba(17,24,39,0.08);
517
- padding: 12px 14px;
518
- display:flex;
519
- align-items:center;
520
- justify-content: space-between;
521
- gap: 12px;
522
- }
523
- .modal-title{
524
- font-weight: 950;
525
- font-size: x-large;
526
- color: #111827;
527
- }
528
- .icon-btn{
529
- appearance:none;
530
- border: 1px solid rgba(17,24,39,0.12);
531
- background: rgba(255,255,255,0.9);
532
- border-radius: 12px;
533
- padding: 8px 10px;
534
- cursor: pointer;
535
- font-weight: 950;
536
  }
537
- .modal-body{ padding: 12px 14px 16px; }
538
 
539
- /* All Models table takes all available height */
540
- #sec-all .section-scroll{
541
- display:flex;
542
- flex-direction:column;
543
- min-height:0;
544
- overflow:hidden;
545
  }
546
- #sec-all .section-head{ flex: 0 0 auto; }
547
- #sec-all .card{
548
- flex: 1 1 auto;
549
- min-height:0;
550
- display:flex;
551
- flex-direction:column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  }
553
- #sec-all .table-shell{
554
- flex: 1 1 auto;
555
- min-height:0;
556
- display:flex;
557
- flex-direction:column;
 
 
 
558
  }
559
- #sec-all .table-wrap{
560
- flex: 1 1 auto;
561
- min-height:0;
562
- max-height:none;
563
- height:100%;
564
- overflow:auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
- /* Responsive */
568
- @media (max-width: 980px){
569
- .topbar-inner { grid-template-columns: 1fr; }
570
- .tabs { justify-content: flex-start; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  .grid.two, .grid.explore-grid { grid-template-columns: 1fr; }
572
- .field-row.two{ grid-template-columns: 1fr; }
573
- .summary-grid{ grid-template-columns: 1fr; }
574
- .repo-buttons{ justify-items: start; padding: 0; }
575
- .repo-buttons .link{ width: auto; justify-content: flex-start; }
576
- .field.inline.compact{ padding-top: 0; }
577
-
578
- .metric-grid{ grid-template-columns: 1fr; }
579
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =====================================================================
2
+ Renesas AI Models β€” restyle
3
+ Three switchable directions on data-theme:
4
+ a = Brand Light (true to Renesas brand: flat, squared, hairlines)
5
+ b = Frosted Light (light branded backdrop + liquid-glass surfaces)
6
+ c = Midnight Glass (refined dark, premium frosted glass)
7
+ All share Renesas Blue #2A289D and the brand condensed display type.
8
+ Drop-in: works with the existing app.js unchanged.
9
+ ===================================================================== */
10
+
11
+ /* ---------- Brand constants (all themes) ---------- */
12
  :root {
13
+ --brand: #2A289D; /* Renesas Blue β€” corporate */
14
+ --brand-press: #1F1D78;
15
+ --brand-2: #43ABE2; /* secondary sky blue */
16
+ --font-display: "Archivo Narrow", "Arial Narrow", Arial, sans-serif;
17
+ --font-body: "Helvetica Neue", Arial, "Liberation Sans", sans-serif;
18
 
19
+ /* badge accent colors (brand secondary palette) */
20
+ --bc-onnx: #306683;
21
+ --bc-gguf: #609A3B;
22
+ --bc-cpu: #706F6F;
23
+ --bc-npu: #EB6209;
24
+ --bc-dsp: #883D90;
25
+ --bc-modal: var(--brand);
26
+ --bc-art: #706F6F;
27
+ }
28
+
29
+ /* =====================================================================
30
+ Frosted Light β€” the single theme: liquid glass over a living,
31
+ branded backdrop. Renesas Blue #2A289D + brand condensed display.
32
+ ===================================================================== */
33
+ :root {
34
+ --bg: linear-gradient(165deg, #EEF1F8 0%, #E4E9F4 48%, #EAE6F2 100%);
35
+ --bg-dots: radial-gradient(rgba(42,40,157,0.10) 1.4px, transparent 1.6px);
36
+ --text: #2C2B45;
37
+ --text-dim: #5C5B72;
38
+ --text-mute: #908FA3;
39
 
 
40
  --radius: 16px;
41
+ --radius-sm: 12px;
42
+ --card-radius: 16px;
43
+ --card-blur: blur(20px) saturate(155%);
44
+
45
+ --panel-bg: rgba(255,255,255,0.55);
46
+ --panel-border: rgba(255,255,255,0.75);
47
+ --panel-shadow: 0 18px 44px rgba(42,40,157,0.10), inset 0 1px 0 rgba(255,255,255,0.65);
48
+
49
+ --card-bg: rgba(255,255,255,0.58);
50
+ --card-border: rgba(255,255,255,0.8);
51
+ --card-shadow: 0 12px 32px rgba(42,40,157,0.10), inset 0 1px 0 rgba(255,255,255,0.7);
52
+ --card-rule: 0px;
53
+ --card-hover-bg: rgba(255,255,255,0.74);
54
+ --card-hover-border: rgba(42,40,157,0.45);
55
+ --card-hover-shadow: 0 20px 46px rgba(42,40,157,0.18), inset 0 1px 0 rgba(255,255,255,0.8);
56
+
57
+ --surface: rgba(255,255,255,0.5);
58
+ --surface-strong: rgba(255,255,255,0.7);
59
+ --surface-border: rgba(255,255,255,0.85);
60
+
61
+ --header-bg: rgba(255,255,255,0.6);
62
+ --header-border: rgba(255,255,255,0.7);
63
+ --header-shadow: 0 8px 30px rgba(42,40,157,0.12), inset 0 1px 0 rgba(255,255,255,0.8);
64
+ --header-text: #2C2B45;
65
+ --header-blur: blur(22px) saturate(160%);
66
+
67
+ --input-bg: rgba(255,255,255,0.7);
68
+ --input-border: rgba(255,255,255,0.9);
69
+ --chip-bg: rgba(255,255,255,0.65);
70
+ --chip-border: rgba(255,255,255,0.85);
71
+
72
+ --brand-tint: rgba(42,40,157,0.12);
73
+ --brand-ring: rgba(42,40,157,0.22);
74
+ --hairline: rgba(42,40,157,0.12);
75
+ --canvas-bg: rgba(255,255,255,0.92);
76
+ --canvas-border: rgba(255,255,255,0.8);
77
+ --table-head-bg: var(--brand);
78
+ --table-head-text: #FFFFFF;
79
+ --code-bg: rgba(42,40,157,0.06);
80
+ --scroll-thumb: rgba(42,40,157,0.28);
81
+ --link-bg: rgba(42,40,157,0.12);
82
+ --link-text: var(--brand);
83
+ --link-border: rgba(42,40,157,0.3);
84
+ --modal-backdrop: rgba(38,40,80,0.4);
85
+
86
+ --brand-active: var(--brand);
87
  }
88
 
89
+ /* =====================================================================
90
+ Base
91
+ ===================================================================== */
92
  * { box-sizing: border-box; }
93
+ [hidden] { display: none !important; }
94
  html, body { height: 100%; }
95
 
96
  body {
 
99
  display: flex;
100
  flex-direction: column;
101
  overflow: hidden;
102
+ color: var(--text);
103
+ font-family: var(--font-body);
104
+ background: var(--bg);
105
+ background-attachment: fixed;
106
  }
107
+ /* ---------- Dynamic, living background: brand network mesh + soft color blooms ---------- */
108
+ .bg-fx { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
109
+ .bg-fx canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
110
+ .bg-fx .blob { position: absolute; border-radius: 50%; filter: blur(70px); opacity: 0.42; will-change: transform; }
111
+ .bg-fx .b1 { width: 46vw; height: 46vw; left: -12vw; top: -16vh; background: radial-gradient(circle, rgba(67,171,226,0.70), transparent 68%); animation: fx1 30s ease-in-out infinite; }
112
+ .bg-fx .b2 { width: 42vw; height: 42vw; right: -10vw; bottom: -18vh; background: radial-gradient(circle, rgba(150,104,214,0.62), transparent 68%); animation: fx2 36s ease-in-out infinite; }
113
+ @keyframes fx1 { 0%, 100% { transform: translate(0,0) scale(1); } 50% { transform: translate(14vw,10vh) scale(1.15); } }
114
+ @keyframes fx2 { 0%, 100% { transform: translate(0,0) scale(1.05); } 50% { transform: translate(-12vw,12vh) scale(0.92); } }
115
+ @media (prefers-reduced-motion: reduce) { .bg-fx .blob { animation: none !important; } }
116
+ .topbar, .content { position: relative; z-index: 1; }
117
 
118
+ /* =====================================================================
119
+ Topbar
120
+ ===================================================================== */
121
  .topbar {
122
  flex: 0 0 auto;
123
  z-index: 50;
124
+ border-bottom: 1px solid var(--header-border);
125
+ background: var(--header-bg);
126
+ backdrop-filter: var(--header-blur);
127
+ -webkit-backdrop-filter: var(--header-blur);
128
+ box-shadow: var(--header-shadow);
129
  }
130
+ :root[data-theme="a"] .topbar { border-top: 3px solid var(--brand); }
131
  .topbar-inner {
132
  width: 100%;
133
+ padding: 11px 22px;
134
+ display: flex;
 
 
 
135
  align-items: center;
136
+ gap: 22px;
137
  }
138
+ .brand { display: flex; align-items: center; gap: 14px; min-width: 0; flex: 0 0 auto; }
139
+ .brand-icon { height: 22px; width: auto; max-width: 168px; object-fit: contain; display: block; }
140
+ .brand-divider { width: 1px; height: 26px; background: var(--hairline); flex: 0 0 auto; }
141
+ .brand-title {
142
+ color: var(--header-text);
143
+ font-family: var(--font-display);
144
+ font-weight: 700;
145
+ font-size: 1.18rem;
146
+ line-height: 1;
147
+ letter-spacing: 0.04em;
148
+ text-transform: uppercase;
149
+ }
150
+ .brand-subtitle { color: var(--text-dim); font-size: 0.74rem; margin-top: 3px; letter-spacing: 0.04em; }
151
 
152
+ .tabs { display: flex; gap: 6px; align-items: center; margin-left: auto; flex-wrap: wrap; }
 
 
 
 
 
 
153
  .nav-item {
154
  appearance: none;
155
+ border: 1px solid transparent;
156
+ background: transparent;
157
+ color: var(--text-dim);
158
+ border-radius: var(--radius-sm);
159
+ padding: 9px 16px;
160
+ font-family: var(--font-display);
161
+ font-weight: 600;
162
+ font-size: 0.96rem;
163
+ letter-spacing: 0.02em;
164
  cursor: pointer;
165
+ transition: background 140ms ease, color 140ms ease, box-shadow 140ms ease;
166
  }
167
+ .nav-item:hover { color: var(--text); background: var(--brand-tint); }
168
  .nav-item.active {
169
+ background: var(--brand-active);
170
+ color: #fff;
171
+ box-shadow: 0 4px 14px rgba(42,40,157,0.32);
172
  }
173
 
174
+ /* Burger (mobile nav trigger) β€” hidden until the phone breakpoint */
175
+ .nav-burger {
176
+ display: none;
177
+ appearance: none; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
178
+ width: 44px; height: 44px; flex: 0 0 auto; margin-left: auto; cursor: pointer;
179
+ border: 1px solid var(--chip-border); background: var(--chip-bg);
180
+ border-radius: var(--radius-sm);
181
+ backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
182
+ transition: border-color 140ms ease, background 140ms ease;
183
+ }
184
+ .nav-burger:hover { border-color: var(--brand-ring); }
185
+ .nav-burger span { display: block; width: 19px; height: 2px; border-radius: 2px; background: var(--text); transition: transform 180ms ease, opacity 150ms ease; }
186
+ .nav-burger.active span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
187
+ .nav-burger.active span:nth-child(2) { opacity: 0; }
188
+ .nav-burger.active span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
189
+
190
+ /* Theme switch */
191
+ .theme-switch {
192
+ display: flex;
193
+ gap: 2px;
194
+ padding: 3px;
195
+ border-radius: var(--radius-sm);
196
+ border: 1px solid var(--chip-border);
197
+ background: var(--surface);
198
+ flex: 0 0 auto;
199
  }
200
+ .theme-btn {
201
+ appearance: none;
202
+ border: none;
203
+ background: transparent;
204
+ color: var(--text-dim);
205
+ border-radius: calc(var(--radius-sm) - 2px);
206
+ padding: 6px 10px;
207
+ font-family: var(--font-body);
208
+ font-weight: 600;
209
+ font-size: 0.74rem;
210
+ letter-spacing: 0.02em;
211
+ cursor: pointer;
212
+ display: inline-flex;
213
+ align-items: center;
214
+ gap: 6px;
215
+ transition: background 140ms ease, color 140ms ease;
216
+ }
217
+ .theme-btn:hover { color: var(--text); }
218
+ .theme-btn.active { background: var(--brand-active); color: #fff; box-shadow: 0 2px 8px rgba(42,40,157,0.28); }
219
+ .theme-btn .sw { width: 11px; height: 11px; border-radius: 3px; flex: 0 0 auto; border: 1px solid rgba(0,0,0,0.12); }
220
+ .sw-a { background: #FFFFFF; }
221
+ .sw-b { background: linear-gradient(135deg, #E7ECF8, #DCD9EF); }
222
+ .sw-c { background: linear-gradient(135deg, #1A1C36, #3937C4); border-color: rgba(255,255,255,0.2); }
223
 
224
+ /* =====================================================================
225
+ Content / sections
226
+ ===================================================================== */
227
+ .content { flex: 1 1 auto; min-height: 0; width: 100%; padding: 8px; overflow: hidden; }
228
  .section {
229
  display: none;
230
  height: 100%;
231
  min-height: 0;
232
+ color: var(--text);
 
233
  border-radius: var(--radius);
234
+ border: 1px solid var(--panel-border);
235
+ background: var(--panel-bg);
236
+ backdrop-filter: var(--card-blur);
237
+ -webkit-backdrop-filter: var(--card-blur);
238
+ box-shadow: var(--panel-shadow);
239
  overflow: hidden;
240
  }
241
  .section.visible { display: flex; flex-direction: column; }
242
+ #sec-overview.visible, #sec-explore.visible { overflow: auto; padding: 9px; }
243
+ /* Overview content sits directly on the page (no panel frame). */
244
+ #sec-overview { border: none; background: none; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; }
245
+
246
+ .section-head { margin-bottom: 18px; }
247
+ .section-title {
248
+ margin: 0 0 4px; border: none; padding: 0;
249
+ font-family: var(--font-display); font-weight: 700; font-size: 1.9rem;
250
+ letter-spacing: 0.02em; text-transform: uppercase; color: var(--text);
251
+ }
252
 
253
+ /* =====================================================================
254
+ Typography
255
+ ===================================================================== */
256
+ h1, h2, h3 { color: var(--text); margin-top: 0.2rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--hairline); }
257
+ h3 { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--brand); }
258
+ :root[data-theme="c"] h3 { color: #B9C0FF; }
259
+ pre, code { font-family: "Consolas", ui-monospace, SFMono-Regular, Menlo, monospace; }
260
+ pre { background: var(--code-bg); color: var(--text); padding: 11px 13px; border-radius: var(--radius-sm); overflow-x: auto; border: 1px solid var(--hairline); margin: 0; font-size: 0.86rem; }
261
+ code { background: var(--code-bg); color: var(--text); padding: 0.1rem 0.34rem; border-radius: 4px; font-size: 0.88em; }
262
 
263
+ .note { background: var(--surface); border-left: 3px solid var(--brand); padding: 11px 13px; margin: 10px 0 12px; border-radius: var(--radius-sm); color: var(--text); }
264
+ .note.note-warn { background: color-mix(in srgb, var(--bc-npu) 12%, transparent); border-left-color: var(--bc-npu); }
265
+ .subtle { color: var(--text-dim); margin-top: 0.4rem; }
266
  .tight { margin-top: 0.35rem; }
267
+ .no-border { border-bottom: none !important; padding-bottom: 0 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ .link-btn { appearance: none; background: none; border: none; cursor: pointer; color: var(--brand); font-weight: 700; font-size: 0.82rem; padding: 2px 4px; letter-spacing: 0.02em; }
270
+ :root[data-theme="c"] .link-btn { color: #B9C0FF; }
271
+ .link-btn:hover { text-decoration: underline; }
272
 
273
+ /* =====================================================================
274
+ Card base (big panels) + glass surfaces
275
+ ===================================================================== */
276
+ .card, .stat-tile, .model-card, .metric-card, .model-picker, .constellation {
277
+ background: var(--card-bg);
278
+ border: 1px solid var(--card-border);
279
+ backdrop-filter: var(--card-blur);
280
+ -webkit-backdrop-filter: var(--card-blur);
281
+ box-shadow: var(--card-shadow);
282
+ }
283
+ .card { border-radius: var(--card-radius); padding: 8px; min-width: 0; position: relative; }
284
+ .card::before {
285
+ content: "";
286
+ position: absolute; top: 0; left: 0; right: 0;
287
+ height: var(--card-rule);
288
+ background: var(--brand);
289
+ border-radius: var(--card-radius) var(--card-radius) 0 0;
290
+ }
291
+ .card-head { margin-bottom: 14px; }
292
+ .card-head-title {
293
+ margin: 0 0 4px; border: none; padding: 0;
294
+ font-family: var(--font-display); font-weight: 700; font-size: 1.28rem;
295
+ letter-spacing: 0.03em; text-transform: uppercase; color: var(--text);
296
  }
297
 
298
+ /* =====================================================================
299
+ Form controls
300
+ ===================================================================== */
301
+ .form-grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
302
+ .field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
303
+ .label { font-size: 0.72rem; color: var(--text-dim); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
304
+ .radio-row { display: flex; flex-wrap: wrap; gap: 8px; }
305
+ .radio-col { display: grid; gap: 8px; }
306
+ .field-row.two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
307
 
308
+ .pill {
309
+ display: inline-flex; align-items: center; gap: 8px; padding: 7px 13px;
310
+ border: 1px solid var(--chip-border); background: var(--chip-bg);
311
+ backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
312
+ border-radius: var(--radius-sm); cursor: pointer; user-select: none; font-size: 0.88rem; color: var(--text);
313
+ transition: background 130ms ease, border-color 130ms ease, box-shadow 130ms ease;
314
  }
315
+ .pill:hover { border-color: var(--brand-ring); }
316
+ .pill.active { background: var(--brand-tint); border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-tint); color: var(--text); font-weight: 600; }
317
+ .pill input { accent-color: var(--brand); }
318
 
319
  .filter-input, .filter-select {
320
+ height: 40px; padding: 0 13px; border-radius: var(--radius-sm);
321
+ border: 1px solid var(--input-border); background: var(--input-bg);
322
+ backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
323
+ color: var(--text); outline: none; font-size: 0.92rem; width: 100%; font-family: var(--font-body);
324
  }
325
+ .filter-input::placeholder { color: var(--text-mute); }
326
+ select.filter-select {
327
+ appearance: none; -webkit-appearance: none; -moz-appearance: none; padding-right: 38px; cursor: pointer;
 
328
  background-image:
329
+ linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
330
+ linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
331
+ background-position: calc(100% - 18px) 17px, calc(100% - 12px) 17px;
332
+ background-size: 6px 6px, 6px 6px; background-repeat: no-repeat;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
+ .filter-select option { background: var(--input-bg); color: var(--text); }
335
+ :root[data-theme="c"] .filter-select option { background: #1B1D33; color: var(--text); }
336
+ .filter-input:focus, .filter-select:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); }
337
+ .filter-input.compact, .filter-select.compact { height: 36px; font-size: 0.88rem; }
338
+ .field.compact { gap: 5px; }
339
+ .field.compact .label { font-size: 0.7rem; }
340
 
341
+ /* =====================================================================
342
+ OVERVIEW
343
+ ===================================================================== */
344
+ .stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 12px; }
345
+ .stat-tile { border-radius: var(--card-radius); padding: 8px 9px 8px 10px; position: relative; border-left: 3px solid var(--brand); }
346
+ .stat-num { font-family: var(--font-display); font-size: 2rem; font-weight: 700; color: var(--text); line-height: 1; }
347
+ .stat-label { margin-top: 6px; font-size: 0.78rem; color: var(--text-dim); font-weight: 600; letter-spacing: 0.03em; }
 
 
 
 
 
 
348
 
349
+ /* Carousel */
350
+ .carousel-section { margin-bottom: 20px; }
351
+ .carousel-section.featured {
352
+ padding: 8px 10px 4px; margin-bottom: 14px; border-radius: var(--card-radius);
353
+ background: var(--card-bg); border: 1px solid var(--card-border);
354
+ box-shadow: var(--card-shadow); backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
355
+ position: relative;
356
  }
357
+ :root[data-theme="a"] .carousel-section.featured { border-top: 3px solid var(--brand); }
358
+ .carousel-section.featured .carousel-title { font-size: 1.42rem; }
359
+ .carousel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
360
+ .carousel-title { font-family: var(--font-display); font-size: 1.22rem; font-weight: 700; margin: 0; border: none; padding: 0; letter-spacing: 0.03em; text-transform: uppercase; color: var(--text); }
361
+ .carousel-actions { display: flex; align-items: center; gap: 8px; }
362
+ .carousel-sub { color: var(--text-dim); font-size: 0.8rem; font-weight: 600; margin-right: 4px; }
363
+ .carousel-nav {
364
+ width: 34px; height: 34px; border-radius: var(--radius-sm); display: grid; place-items: center;
365
+ cursor: pointer; font-size: 1.15rem; line-height: 1; color: var(--text);
366
+ border: 1px solid var(--chip-border); background: var(--chip-bg);
367
+ backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
368
+ transition: border-color 130ms ease, color 130ms ease, background 130ms ease;
369
  }
370
+ .carousel-nav:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-tint); }
371
+ .carousel-track { display: flex; gap: 14px; overflow-x: auto; scroll-behavior: smooth; scroll-snap-type: x proximity; padding: 4px 2px 14px; }
372
+ .rc-card { flex: 0 0 252px; scroll-snap-align: start; gap: 10px; }
373
+ .rc-meta { font-size: 0.72rem; font-weight: 700; color: var(--text-dim); display: flex; align-items: center; gap: 5px; letter-spacing: 0.02em; }
374
+ .rc-meta.is-new { color: var(--brand); }
375
+ :root[data-theme="c"] .rc-meta.is-new { color: #B9C0FF; }
376
+ .rc-stat { font-size: 0.84rem; color: var(--text-dim); }
377
+ .rc-stat b { font-family: var(--font-display); font-size: 1.15rem; color: var(--text); font-weight: 700; }
378
+ .rc-stat.empty { color: var(--text-mute); }
379
+ /* Source tag on the recency stat β€” measured HIL is the priority, estimates
380
+ are clearly marked as fallback. */
381
+ .rc-src { display: inline-block; font-family: var(--font-display); font-weight: 700; font-size: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase; padding: 1px 6px; border-radius: 999px; vertical-align: middle; line-height: 1.45; }
382
+ .rc-src-hil { background: var(--brand); color: #fff; }
383
+ .rc-src-est { background: transparent; color: var(--text-mute); border: 1px solid var(--hairline); }
384
+ :root[data-theme="c"] .rc-src-hil { background: #6670E6; }
385
+
386
+ /* Constellation */
387
+ .constellation-card { padding: 8px; }
388
+ .cstl-controls { display: flex; flex-wrap: wrap; gap: 12px 18px; align-items: flex-end; padding-bottom: 14px; border-bottom: 1px solid var(--hairline); margin-bottom: 14px; }
389
+ .cstl-controls .field { min-width: 150px; }
390
+ .constellation { border-radius: var(--radius-sm); overflow: hidden; position: relative; background: var(--canvas-bg); border: 1px solid var(--canvas-border); box-shadow: none; }
391
+ .constellation canvas { width: 100%; height: 460px; display: block; cursor: grab; }
392
+ .constellation canvas:active { cursor: grabbing; }
393
+ .cstl-foot { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; margin-top: 12px; align-items: center; }
394
+ .legend { display: flex; flex-wrap: wrap; gap: 8px 14px; }
395
+ .legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: 0.8rem; color: var(--text-dim); font-weight: 600; }
396
+ .legend-dot { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
397
 
398
+ .mini-tooltip {
399
+ position: absolute; pointer-events: none; padding: 9px 11px; border-radius: var(--radius-sm);
400
+ background: rgba(28,30,52,0.95); color: #F4F5FB; backdrop-filter: blur(6px);
401
+ border: 1px solid rgba(255,255,255,0.14); box-shadow: 0 18px 40px rgba(0,0,0,0.35);
402
+ font-size: 0.8rem; max-width: 280px; line-height: 1.35; display: none; z-index: 5;
403
  }
404
+
405
+ /* =====================================================================
406
+ CATALOG
407
+ ===================================================================== */
408
+ .catalog-layout { flex: 1 1 auto; min-width: 0; min-height: 0; display: grid; grid-template-columns: 196px minmax(0, 1fr); }
409
+ .facets {
410
+ border-right: 1px solid var(--hairline); padding: 9px 8px; overflow: auto;
411
+ display: flex; flex-direction: column; gap: 12px;
412
  }
413
+ .facets-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
414
+ .facets-title { font-family: var(--font-display); font-weight: 700; font-size: 0.95rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text); }
415
+
416
+ /* Collapsible filter clusters (the toggle button is shown only on phones) */
417
+ .filter-toggle {
418
+ display: none; align-items: center; gap: 8px; width: 100%; cursor: pointer;
419
+ appearance: none; border: 1px solid var(--chip-border); background: var(--chip-bg);
420
+ color: var(--text); font-family: var(--font-display); font-weight: 700; font-size: 0.95rem;
421
+ letter-spacing: 0.03em; padding: 11px 14px; border-radius: var(--radius-sm);
422
+ backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
423
+ transition: border-color 140ms ease, background 140ms ease;
424
  }
425
+ .filter-toggle:hover { border-color: var(--brand-ring); }
426
+ .filter-toggle.active { border-color: var(--brand); background: var(--brand-tint); }
427
+ .filter-toggle .ft-chevron { margin-left: auto; transition: transform 200ms ease; }
428
+ .filter-toggle[aria-expanded="true"] .ft-chevron { transform: rotate(180deg); }
429
+ .facets-body { display: flex; flex-direction: column; gap: 12px; }
430
+ .explore-filters { display: grid; gap: 10px; }
431
 
432
+ /* Explore filters: dense progressive-disclosure redesign.
433
+ Secondary ("Advanced") tier is collapsed by default on desktop; the generic
434
+ delegated .filter-toggle handler (index.html) toggles .open on it β€” no JS. */
435
+ .explore-advanced { display: none; }
436
+ .explore-advanced.open { display: grid; gap: 10px; }
437
+ /* Desktop-visible variant of the toggle (base .filter-toggle is display:none
438
+ >680px). Slimmer + dimmer so it reads as a secondary affordance. */
439
+ .filter-toggle--inline {
440
+ display: flex; padding: 7px 12px; font-size: 0.82rem; font-family: var(--font-body);
441
+ letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-dim);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  }
443
+ .filter-toggle--inline svg:first-of-type { opacity: 0.75; }
444
+ /* Dense packing, SCOPED to Explore so Catalog/Overview pills are untouched. */
445
+ .explore-filters .field-row.two, .explore-advanced .field-row.two { gap: 10px; }
446
+ .explore-filters .field.compact { gap: 4px; }
447
+ .explore-filters .field.compact .label { font-size: 0.68rem; }
448
+ .explore-filters .filter-select.compact { height: 34px; font-size: 0.86rem; padding: 0 10px; }
449
+ .explore-filters select.filter-select.compact { background-position: calc(100% - 16px) 14px, calc(100% - 10px) 14px; padding-right: 32px; }
450
+ .explore-filters .radio-row { gap: 6px; row-gap: 6px; }
451
+ .explore-filters .radio-row .pill { padding: 5px 10px; gap: 6px; font-size: 0.82rem; line-height: 1.15; }
452
+ .facet-groups { display: flex; flex-direction: column; gap: 12px; }
453
+ .facet-group-title { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-mute); font-weight: 700; margin-bottom: 5px; }
454
+ .facet-opt { display: flex; align-items: center; gap: 8px; padding: 3px 5px; font-size: 0.82rem; color: var(--text); cursor: pointer; border-radius: 6px; }
455
+ .facet-opt:hover { background: var(--brand-tint); }
456
+ .facet-opt input { appearance: none; -webkit-appearance: none; flex: 0 0 auto; width: 30px; height: 17px; border-radius: 999px; background: rgba(42,40,157,0.18); position: relative; cursor: pointer; transition: background 150ms ease; }
457
+ .facet-opt input::after { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.28); transition: left 150ms ease; }
458
+ .facet-opt input:checked { background: var(--brand); }
459
+ .facet-opt input:checked::after { left: 15px; }
460
+
461
+ .catalog-main { min-width: 0; display: flex; flex-direction: column; min-height: 0; }
462
+ .toolbar {
463
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
464
+ padding: 7px 9px; border-bottom: 1px solid var(--hairline); flex: 0 0 auto;
465
  }
466
+ .toolbar-count { color: var(--text-dim); font-weight: 600; font-size: 0.9rem; }
467
+ .toolbar-count b { color: var(--text); font-family: var(--font-display); }
468
+ .view-toggle { display: inline-flex; border: 1px solid var(--chip-border); border-radius: var(--radius-sm); overflow: hidden; background: var(--chip-bg); }
469
+ .view-btn { appearance: none; border: none; background: transparent; color: var(--text-dim); padding: 8px 14px; font-weight: 700; font-size: 0.84rem; cursor: pointer; font-family: var(--font-body); }
470
+ .view-btn.active { background: var(--brand-active); color: #fff; }
471
+
472
+ .card-grid { flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 9px; display: grid; grid-template-columns: repeat(auto-fill, minmax(247px, 1fr)); gap: 12px; align-content: start; }
473
+ .table-view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; padding: 14px 18px 18px; }
474
 
475
+ /* Model card β€” squared Renesas brand language: a top gradient accent that
476
+ sweeps wider on hover, a framed hero stat with a solid left bar, and
477
+ hairline-divided publisher row + footer with a solid-fill repo CTA. */
478
+ .model-card {
479
+ position: relative; overflow: hidden;
480
+ border-radius: var(--card-radius); padding: 17px 17px 15px; display: flex; flex-direction: column; gap: 13px; cursor: pointer;
481
+ transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease, background 150ms ease;
482
  }
483
+ /* Signature top accent: 3px brand→brand-2 bar, 34% wide at rest, sweeping to
484
+ full width + full opacity on hover. Keep this motif on card-like surfaces. */
485
+ .model-card::before {
486
+ content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
487
+ background: linear-gradient(90deg, var(--brand), var(--brand-2));
488
+ transform: scaleX(0.34); transform-origin: left; opacity: 0.8;
489
+ transition: transform 220ms ease, opacity 220ms ease;
490
  }
491
+ .model-card:hover {
492
+ transform: translateY(-4px); border-color: var(--card-hover-border); background: var(--card-hover-bg);
493
+ box-shadow: var(--card-hover-shadow);
494
  }
495
+ .model-card:hover::before { transform: scaleX(1); opacity: 1; }
496
 
497
+ /* Publisher row β€” uppercase, divided from the body by a hairline rule */
498
+ .mc-designer {
499
+ display: flex; align-items: center; gap: 8px; min-width: 0;
500
+ padding-bottom: 12px; border-bottom: 1px solid var(--hairline);
501
+ text-transform: uppercase; letter-spacing: 0.04em;
 
 
502
  }
503
+ .mc-logo { display: inline-flex; align-items: center; flex: 0 0 auto; }
504
+ .mc-logo svg { display: block; }
505
+ .mc-designer-name { font-family: var(--font-display); font-weight: 700; font-size: 0.85rem; letter-spacing: 0.04em; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
506
+ .mc-head { display: flex; gap: 12px; align-items: center; }
507
+ .mc-monogram {
508
+ flex: 0 0 auto; width: 46px; height: 46px; border-radius: var(--radius-sm); display: grid; place-items: center;
509
+ font-family: var(--font-display); font-weight: 700; font-size: 0.95rem; color: #fff;
510
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), 0 4px 12px rgba(42,40,157,0.18);
511
  }
512
+ .mc-title { font-family: var(--font-display); font-weight: 700; font-size: 1rem; line-height: 1.15; color: var(--text); word-break: break-word; letter-spacing: 0.01em; }
513
+ .mc-sub { font-size: 0.8rem; color: var(--text-dim); margin-top: 2px; }
514
+ .mc-chips { display: flex; flex-wrap: wrap; gap: 6px; }
515
+ /* Throughput hero β€” tinted brand panel, framed all round, with a solid brand
516
+ left bar via ::before (squared accent, not a left-border-only card). */
517
+ .mc-stat {
518
+ position: relative; overflow: hidden;
519
+ display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
520
+ padding: 9px 12px 9px 16px; border-radius: var(--radius-sm);
521
+ background: linear-gradient(115deg, rgba(42,40,157,0.08), rgba(67,171,226,0.05));
522
+ border: 1px solid rgba(42,40,157,0.16);
523
+ }
524
+ .mc-stat::before {
525
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
526
+ background: var(--brand);
527
+ }
528
+ .mc-stat-num { font-family: var(--font-display); font-size: 1.78rem; font-weight: 700; color: var(--brand); line-height: 1; }
529
+ :root[data-theme="c"] .mc-stat-num { color: #C3C8FF; }
530
+ .mc-stat-sub { font-size: 0.74rem; color: var(--text-dim); font-weight: 600; line-height: 1.25; }
531
+ /* Empty state β€” greyed bar + dashed frame, transparent, italic placeholder */
532
+ .mc-stat.empty { align-items: center; text-align: center; background: transparent; border: 1px dashed var(--hairline); }
533
+ .mc-stat.empty::before { background: var(--hairline); }
534
+ .mc-stat.empty .mc-stat-sub { color: var(--text-mute); font-style: italic; font-weight: 500; }
535
+ .mc-quants { display: flex; flex-wrap: wrap; gap: 6px; }
536
+ /* Footer β€” hairline rule on top; uppercase meta, solid-fill repo CTA */
537
+ .mc-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: auto; padding-top: 13px; border-top: 1px solid var(--hairline); }
538
+ .mc-meta { flex: 1 1 0; min-width: 0; font-size: 0.72rem; color: var(--text-mute); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
539
+ .mc-foot .link {
540
  flex: 0 0 auto;
541
+ background: var(--brand); border-color: var(--brand); color: #fff;
542
+ box-shadow: 0 4px 12px rgba(42,40,157,0.24);
543
  }
544
+ .mc-foot .link:hover { background: var(--brand-press); border-color: var(--brand-press); color: #fff; }
545
+
546
+ /* Chips / badges */
547
+ .chips { display: flex; flex-wrap: wrap; gap: 8px; }
548
+ .chip-btn {
549
+ appearance: none; border: 1px solid var(--chip-border); background: var(--chip-bg);
550
+ backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
551
+ border-radius: var(--radius-sm); padding: 7px 12px; cursor: pointer; font-weight: 700; font-size: 0.86rem; color: var(--text);
552
+ transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease, background 120ms ease;
553
  }
554
+ .chip-btn:hover { border-color: var(--brand); transform: translateY(-1px); }
555
+ .chip-btn.active { background: var(--brand-tint); border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-tint); }
556
 
557
+ .badges { display: flex; flex-wrap: wrap; gap: 6px; }
558
+ .badge {
559
+ display: inline-flex; align-items: center; gap: 6px; padding: 4px 11px; border-radius: 999px;
560
+ font-weight: 700; font-size: 0.76rem; letter-spacing: 0.02em;
561
+ background: color-mix(in srgb, var(--bc) 13%, transparent);
562
+ border: 1px solid color-mix(in srgb, var(--bc) 38%, transparent);
563
+ color: var(--bc);
564
+ --bc: var(--bc-art);
 
 
 
 
 
565
  }
566
+ :root[data-theme="c"] .badge { color: color-mix(in srgb, var(--bc) 55%, white); background: color-mix(in srgb, var(--bc) 24%, transparent); border-color: color-mix(in srgb, var(--bc) 45%, transparent); }
567
+ .badge.clickable { cursor: pointer; }
568
+ .badge.clickable:hover { border-color: var(--bc); filter: brightness(0.97); }
569
+ .badge.onnx { --bc: var(--bc-onnx); }
570
+ .badge.gguf { --bc: var(--bc-gguf); }
571
+ .badge.cpu { --bc: var(--bc-cpu); }
572
+ .badge.npu { --bc: var(--bc-npu); }
573
+ .badge.dsp { --bc: var(--bc-dsp); }
574
+ .badge.modality { --bc: var(--bc-modal); }
575
+ .badge.art { --bc: var(--bc-art); }
576
+
577
+ /* status badges (mockup-style: uppercase pills) */
578
+ .badge.status { text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.68rem; padding: 3px 9px; }
579
+ .badge.status-hil { text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.68rem; padding: 3px 9px; background: var(--brand); border-color: var(--brand); color: #fff; }
580
+ .badge.status-new { text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.68rem; padding: 3px 9px; background: rgba(223,177,24,0.16); border-color: rgba(223,177,24,0.5); color: #9C7B0F; }
581
+ :root[data-theme="c"] .badge.status-new { color: #E7C657; }
582
+
583
+ /* =====================================================================
584
+ EXPLORE
585
+ ===================================================================== */
586
+ .grid.two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
587
+ .grid.explore-grid { display: grid; grid-template-columns: minmax(320px, 32%) 1fr; gap: 16px; margin-top: 10px; }
588
+ /* Wide screens: the Configure rail and the result card each fill the full
589
+ section height and scroll internally (the page itself doesn't scroll).
590
+ On smaller screens the columns stack and the section scrolls normally. */
591
+ @media (min-width: 1041px) {
592
+ #sec-explore.visible { overflow: hidden; }
593
+ #sec-explore .explore-grid { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr); }
594
+ /* Both columns fill the section height and scroll internally if content
595
+ overflows. The model list keeps its own natural (size=12) height β€” a
596
+ native <select> does NOT flex reliably, so we don't stretch it. */
597
+ #sec-explore .explore-grid > .card { min-height: 0; overflow: auto; }
598
  }
599
 
600
+ .matchline { font-size: 0.9rem; color: var(--text); font-weight: 700; padding: 10px 12px; border-radius: var(--radius-sm); background: var(--brand-tint); border: 1px solid var(--brand-ring); }
601
+ .matchline b { font-family: var(--font-display); color: var(--brand); }
602
+ :root[data-theme="c"] .matchline b { color: #C3C8FF; }
603
+ .model-picker { border-radius: var(--radius-sm); padding: 12px; }
604
+ .model-picker-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
605
+ .model-picker-title { font-family: var(--font-display); font-weight: 700; color: var(--text); font-size: 1rem; letter-spacing: 0.03em; text-transform: uppercase; }
606
+ .model-picker-subtle { font-size: 0.8rem; color: var(--text-dim); font-weight: 600; }
607
+ .model-list { height: auto; min-height: 280px; padding: 8px 10px; line-height: 1.4; border-radius: var(--radius-sm); background-image: none !important; padding-right: 12px !important; cursor: auto; }
608
+ .model-list option { padding: 7px 8px; border-radius: 4px; }
609
+
610
+ /* Deploy page: numbered step strip under the section head */
611
+ .deploy-steps { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 0; padding: 0; list-style: none; }
612
+ .deploy-steps li {
613
+ display: inline-flex; align-items: center; gap: 9px; padding: 6px 14px 6px 7px;
614
+ border: 1px solid var(--hairline); border-radius: var(--radius-sm); background: var(--surface);
615
+ font-family: var(--font-display); font-weight: 700; font-size: 0.8rem; letter-spacing: 0.03em;
616
+ text-transform: uppercase; color: var(--text-dim);
617
+ }
618
+ .deploy-steps .ds-num {
619
+ flex: 0 0 auto; width: 22px; height: 22px; border-radius: 999px; display: grid; place-items: center;
620
+ background: var(--brand); color: #fff; font-size: 0.8rem; line-height: 1;
621
+ }
622
+ .deploy-steps .ds-txt { white-space: nowrap; }
623
+ /* Hero terminal for the download command (the only non-token colors allowed) */
624
+ #actions .cmd-row pre {
625
+ background: #18172B; color: #EAEBFB; margin: 0; padding: 12px 14px; border-radius: var(--radius-sm);
626
+ border: 1px solid rgba(42,40,157,0.55); border-left: 4px solid var(--brand-2);
627
+ }
628
+
629
+ .link { display: inline-flex; gap: 6px; align-items: center; font-weight: 700; color: var(--link-text); text-decoration: none; padding: 9px 13px; border-radius: var(--radius-sm); border: 1px solid var(--link-border); background: var(--link-bg); transition: background 130ms ease, border-color 130ms ease; }
630
+ .link:hover { border-color: var(--brand); filter: brightness(1.02); }
631
+ .link.small { padding: 6px 10px; font-size: 0.8rem; }
632
+ .link.link-na {
633
+ color: var(--text-mute); background: transparent;
634
+ border: 1px dashed var(--input-border); cursor: default; font-weight: 600;
635
  }
636
+ .link.link-na:hover { border-color: var(--input-border); filter: none; }
637
+
638
+ .summary-grid { display: grid; grid-template-columns: 1fr 300px; gap: 16px; align-items: start; }
639
+ .summary-grid ul { margin: 0; padding-left: 0; list-style: none; }
640
+ .summary-grid li { margin: 6px 0; color: var(--text); padding-left: 18px; position: relative; }
641
+ .summary-grid li::before { content: ""; position: absolute; left: 0; top: 0.5em; width: 8px; height: 8px; background: var(--brand); }
642
+ :root[data-theme="c"] .summary-grid li::before { background: #6670E6; }
643
+ .repo-buttons { display: grid; gap: 10px; justify-items: end; }
644
+ .repo-buttons .link { width: 100%; justify-content: center; }
645
+
646
+ /* Metrics cards */
647
+ .metric-banner { margin: 10px 0; padding: 9px 12px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--surface-border); font-weight: 700; color: var(--text); }
648
+ .metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: stretch; margin-top: 10px; }
649
+ .metric-card { border-radius: var(--radius-sm); padding: 14px; min-width: 0; }
650
+ .metric-card-title { font-family: var(--font-display); font-weight: 700; color: var(--text); font-size: 0.95rem; margin-bottom: 8px; letter-spacing: 0.04em; text-transform: uppercase; }
651
+ .metric-pre { white-space: pre-wrap; word-break: break-word; margin: 0; border-radius: var(--radius-sm); }
652
+
653
+ /* =====================================================================
654
+ Merged "Precision & performance" block (selector + metrics)
655
+ Replaces the old precision/quant chip label and the standalone
656
+ "Metrics" heading. The selector drives the metrics shown beside it β€”
657
+ each option keeps .chip-btn + data-artifact, so the click handler is
658
+ unchanged (zero JS change).
659
+ ===================================================================== */
660
+ .pm-heading { margin-top: 20px; }
661
+
662
+ .pm-block {
663
  display: grid;
664
+ grid-template-columns: minmax(140px, 3fr) minmax(0, 4fr) minmax(0, 3fr); /* precision | est-vs-HIL graph | accuracy */
665
+ gap: 14px;
666
+ align-items: stretch; /* all three columns share the same height */
667
+ margin-top: 12px;
668
+ }
669
+
670
+ /* ---- Selector rail (left) ---- */
671
+ .pm-rail {
672
+ border-radius: var(--radius-sm);
673
+ border: 1px solid var(--card-border);
674
+ background: var(--card-bg);
675
+ backdrop-filter: var(--card-blur);
676
+ -webkit-backdrop-filter: var(--card-blur);
677
+ box-shadow: var(--card-shadow);
678
+ padding: 12px;
679
  min-width: 0;
680
  }
681
+ .pm-rail-cap {
682
+ font-family: var(--font-display);
683
+ font-weight: 700;
684
+ font-size: 0.78rem;
685
+ letter-spacing: 0.05em;
686
+ text-transform: uppercase;
687
+ color: var(--text-dim);
688
+ margin-bottom: 10px;
689
  }
690
+ .pm-rail-list { display: flex; flex-direction: column; gap: 6px; }
691
+
692
+ /* Each selectable row keeps .chip-btn (handler-compatible); override the
693
+ inherited tiny-pill look into a full-width row. */
694
+ .chip-btn.pm-opt {
695
+ display: flex;
696
+ align-items: center;
697
+ gap: 10px;
698
+ width: 100%;
699
+ text-align: left;
700
+ padding: 10px 12px;
701
+ border: 1px solid var(--chip-border);
702
+ background: var(--chip-bg);
703
+ border-radius: var(--radius-sm);
704
+ cursor: pointer;
705
+ position: relative;
706
+ font-size: inherit;
707
+ transition: background 130ms ease, border-color 130ms ease, box-shadow 130ms ease;
708
  }
709
+ .chip-btn.pm-opt:hover { background: var(--brand-tint); border-color: var(--brand-ring); transform: none; }
710
+ .chip-btn.pm-opt:focus-visible { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); }
711
 
712
+ /* Radio-dot indicator */
713
+ .pm-opt-dot {
714
+ flex: 0 0 auto;
715
+ width: 16px; height: 16px;
716
+ border-radius: 50%;
717
+ border: 2px solid var(--text-mute);
718
+ background: transparent;
719
+ display: grid;
720
+ place-items: center; /* centers the check glyph regardless of box metrics */
721
+ transition: border-color 130ms ease, background 130ms ease;
722
  }
723
+
724
+ /* Label (condensed display type, like brand titles) */
725
+ .pm-opt-label {
726
  flex: 1 1 auto;
727
+ min-width: 0;
728
+ font-family: var(--font-display);
729
+ font-weight: 700;
730
+ font-size: 0.96rem;
731
+ letter-spacing: 0.03em;
732
+ text-transform: uppercase;
733
+ color: var(--text);
734
+ line-height: 1.1;
735
+ word-break: break-word;
736
  }
737
 
738
+ /* "Selected" badge β€” only on the active row */
739
+ .pm-opt-badge {
740
+ flex: 0 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  display: none;
742
+ font-family: var(--font-display);
743
+ font-weight: 700;
744
+ font-size: 0.62rem;
745
+ letter-spacing: 0.06em;
746
+ text-transform: uppercase;
747
+ color: #fff;
748
+ background: rgba(255,255,255,0.22);
749
+ border-radius: 999px;
750
+ padding: 2px 8px;
751
+ line-height: 1.3;
752
  }
753
+
754
+ /* ---- ACTIVE row: unmistakable solid brand fill ---- */
755
+ .chip-btn.pm-opt.active {
756
+ background: var(--brand);
757
+ border-color: var(--brand);
758
+ box-shadow: 0 2px 10px rgba(42,40,157,0.28), inset 0 1px 0 rgba(255,255,255,0.18);
759
+ transform: none;
760
+ }
761
+ .chip-btn.pm-opt.active:hover { background: var(--brand-press); border-color: var(--brand-press); }
762
+ .chip-btn.pm-opt.active .pm-opt-label { color: #fff; }
763
+ .chip-btn.pm-opt.active .pm-opt-badge { display: inline-block; }
764
+ .chip-btn.pm-opt.active .pm-opt-dot { border-color: #fff; background: #fff; }
765
+ .chip-btn.pm-opt.active .pm-opt-dot::after {
766
+ content: "";
767
+ width: 3.5px; height: 7px;
768
+ margin-top: -1px; /* optical lift so the tick sits centred in the circle */
769
+ border: solid var(--brand); border-width: 0 2px 2px 0;
770
+ transform: rotate(45deg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  }
772
+ .chip-btn.pm-opt.active:focus-visible { box-shadow: 0 0 0 3px var(--brand-ring), 0 2px 10px rgba(42,40,157,0.28); }
773
 
774
+ .pm-opt-empty {
775
+ padding: 12px;
776
+ border: 1px dashed var(--hairline);
777
+ border-radius: var(--radius-sm);
778
+ font-size: 0.85rem;
779
+ text-align: center;
780
  }
781
+
782
+ /* ---- Column 2: Est-vs-HIL throughput as a small bar graph.
783
+ Bars use flex-grow (not % heights) so they scale to whatever height the
784
+ row stretches to, with no dependence on a definite track height. ---- */
785
+ .pm-graph, .pm-acc { display: flex; flex-direction: column; min-width: 0; }
786
+ .pm-bars { flex: 1 1 auto; display: flex; align-items: stretch; justify-content: center; gap: 22px; min-height: 96px; margin-top: 12px; }
787
+ .pm-bar-col { display: flex; flex-direction: column; align-items: center; gap: 7px; flex: 0 0 auto; }
788
+ .pm-bar-val { font-family: var(--font-display); font-weight: 700; font-size: 0.92rem; line-height: 1; color: var(--text); }
789
+ .pm-bar-track { width: 40px; flex: 1 1 auto; min-height: 56px; display: flex; flex-direction: column; }
790
+ .pm-bar-fill { width: 100%; min-height: 4px; border-radius: 6px 6px 2px 2px; }
791
+ .pm-bar-fill.est { background: var(--brand-2); }
792
+ .pm-bar-fill.hil { background: var(--brand); }
793
+ .pm-bar-lbl { font-family: var(--font-display); font-weight: 700; font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); }
794
+ .pm-graph-empty { flex: 1 1 auto; display: grid; place-items: center; min-height: 80px; font-size: 0.85rem; }
795
+ .pm-graph-foot { margin-top: 10px; text-align: center; font-size: 0.7rem; font-weight: 600; color: var(--text-mute); }
796
+
797
+ /* ---- Column 3: Accuracy (HIL-first; "β€”" until published) ---- */
798
+ .pm-acc-body { flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; text-align: center; }
799
+ .pm-acc-num { font-family: var(--font-display); font-weight: 700; font-size: 1.95rem; line-height: 1; color: var(--brand); }
800
+ :root[data-theme="c"] .pm-acc-num { color: #C3C8FF; }
801
+ .pm-acc-num.empty { color: var(--text-mute); font-size: 1.6rem; }
802
+ .pm-acc-sub { font-size: 0.72rem; font-weight: 600; color: var(--text-dim); }
803
+
804
+ /* ---- Dark variant (data-theme="c") β€” match the established #6670E6 accent ---- */
805
+ :root[data-theme="c"] .chip-btn.pm-opt.active {
806
+ background: #6670E6;
807
+ border-color: #6670E6;
808
+ box-shadow: 0 2px 12px rgba(0,0,0,0.40), inset 0 1px 0 rgba(255,255,255,0.20);
809
  }
810
+ :root[data-theme="c"] .chip-btn.pm-opt.active:hover { background: #565FD6; border-color: #565FD6; }
811
+ :root[data-theme="c"] .chip-btn.pm-opt.active .pm-opt-dot::after { border-color: #6670E6; }
812
+
813
+ /* ---- Responsive: selector ABOVE metrics on narrow screens ---- */
814
+ @media (max-width: 760px) {
815
+ .pm-block { grid-template-columns: 1fr; gap: 14px; }
816
+ .pm-rail-list { flex-direction: row; flex-wrap: wrap; }
817
+ .chip-btn.pm-opt { width: auto; flex: 1 1 150px; }
818
  }
819
+
820
+ /* Mini chart */
821
+ .mini-chart-wrap { margin-top: 12px; border: 1px solid var(--canvas-border); border-radius: var(--radius-sm); background: var(--canvas-bg); overflow: hidden; position: relative; }
822
+ .mini-chart-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 11px 12px 0; }
823
+ .mini-chart-title { font-family: var(--font-display); font-weight: 700; color: #2C2B45; letter-spacing: 0.02em; }
824
+ .mini-chart-subtle { color: #5b5750; font-weight: 600; font-size: 0.85rem; }
825
+ .mini-chart-wrap canvas { cursor: grab; }
826
+ .mini-chart-wrap canvas:active { cursor: grabbing; }
827
+
828
+ /* Download copy */
829
+ .cmd-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
830
+ .cmd-row pre { flex: 1 1 auto; }
831
+ .copy-btn { appearance: none; border: 1px solid var(--chip-border); background: var(--chip-bg); backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur); border-radius: var(--radius-sm); padding: 9px 12px; cursor: pointer; font-weight: 700; line-height: 1; display: inline-flex; align-items: center; gap: 8px; color: var(--text); white-space: nowrap; transition: background 130ms ease, border-color 130ms ease; }
832
+ .copy-btn:hover { background: var(--brand-tint); border-color: var(--brand); }
833
+ .copy-btn.copied { background: color-mix(in srgb, var(--bc-gguf) 16%, transparent); border-color: color-mix(in srgb, var(--bc-gguf) 45%, transparent); color: var(--bc-gguf); }
834
+ .copy-btn .icon { font-size: 1rem; }
835
+
836
+ /* =====================================================================
837
+ TABLE
838
+ ===================================================================== */
839
+ .table-shell { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
840
+ .table-wrap { flex: 1 1 auto; min-height: 0; border: 1px solid var(--card-border); border-radius: var(--radius-sm); overflow: auto; background: var(--card-bg); backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur); }
841
+ table.table { width: 100%; min-width: 720px; border-collapse: collapse; background: transparent; }
842
+ .table th, .table td { padding: 12px 15px; border-bottom: 1px solid var(--hairline); font-size: 0.9rem; color: var(--text); vertical-align: top; text-align: left; }
843
+ /* Header β€” brand band with a brand-2 accent underline (the signature edge accent) */
844
+ .table th {
845
+ background: var(--table-head-bg); color: var(--table-head-text);
846
+ font-family: var(--font-display); font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; font-size: 0.78rem;
847
+ white-space: nowrap; border-bottom: none; box-shadow: inset 0 -3px 0 var(--brand-2);
848
  }
849
+ .table thead th { position: sticky; top: 0; z-index: 3; }
850
+ .table tbody tr { transition: background 130ms ease; }
851
+ /* subtle zebra for cross-row scanning (pinned cell gets the matching tint over its opaque base) */
852
+ .table tbody tr:nth-child(even) td { background: rgba(42,40,157,0.035); }
853
+ .table tbody tr:nth-child(even) .pin { background: linear-gradient(rgba(42,40,157,0.035), rgba(42,40,157,0.035)), var(--panel-bg); }
854
+ .table tbody tr:hover td { background: var(--brand-tint); }
855
+ .table tbody tr:last-child td { border-bottom: none; }
856
+ /* Name reads as the row's title; a left accent bar slides in on hover */
857
+ .table td.cell-name { font-family: var(--font-display); font-weight: 700; font-size: 0.95rem; color: var(--text); white-space: nowrap; }
858
+ /* Publisher logo + model name in the Name cell (mirrors the card's .mc-designer) */
859
+ .tc-name { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
860
+ .tc-name .mc-logo { line-height: 0; }
861
+ .table tbody tr:hover td.cell-name { box-shadow: inset 3px 0 0 var(--brand); }
862
 
863
+ .pin { position: sticky; left: 0; z-index: 2; background: var(--panel-bg); }
864
+ .table thead .pin { z-index: 4; background: var(--table-head-bg); }
865
+ .table tbody tr:hover .pin { background: var(--card-hover-bg); }
866
+
867
+ .table .badges { flex-direction: column; align-items: flex-start; gap: 6px; }
868
+ .table .badges.badges-row { flex-direction: row; flex-wrap: wrap; }
869
+ .table th.details-col, .table td.details-col { width: 1%; white-space: nowrap; }
870
+ .table td.details-col { vertical-align: middle; }
871
+
872
+ /* Metric mini-table β€” label left, value right-aligned in display type */
873
+ .cell-metrics { display: grid; gap: 3px; white-space: normal; line-height: 1.2; min-width: 118px; }
874
+ .cell-metrics .row { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: baseline; }
875
+ .cell-metrics .k { font-size: 0.66rem; font-weight: 700; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.04em; }
876
+ .cell-metrics .v { font-family: var(--font-display); font-weight: 700; color: var(--text); text-align: right; font-size: 0.95rem; line-height: 1; }
877
+
878
+ .mini-btn { appearance: none; border: 1px solid var(--brand-ring); background: var(--brand-tint); color: var(--brand); border-radius: var(--radius-sm); padding: 7px 12px; font-weight: 700; font-size: 0.82rem; cursor: pointer; font-family: var(--font-body); transition: background 130ms ease; }
879
+ :root[data-theme="c"] .mini-btn { color: #C3C8FF; }
880
+ .mini-btn:hover { background: var(--brand); color: #fff; }
881
+
882
+ /* =====================================================================
883
+ MODAL
884
+ ===================================================================== */
885
+ .modal { position: fixed; inset: 0; z-index: 10000; display: none; }
886
+ .modal.open { display: block; }
887
+ .modal-backdrop { position: absolute; inset: 0; background: var(--modal-backdrop); backdrop-filter: blur(4px); }
888
+ .modal-panel {
889
+ position: absolute; top: 30px; left: 50%; transform: translateX(-50%);
890
+ width: min(980px, calc(100vw - 28px)); max-height: calc(100vh - 60px); overflow: hidden;
891
+ display: flex; flex-direction: column;
892
+ background: var(--panel-bg); backdrop-filter: var(--card-blur); -webkit-backdrop-filter: var(--card-blur);
893
+ border-radius: var(--radius); border: 1px solid var(--panel-border); box-shadow: 0 40px 90px rgba(0,0,0,0.35), var(--panel-shadow);
894
+ }
895
+ :root[data-theme="a"] .modal-panel { background: #FFFFFF; border-top: 3px solid var(--brand); }
896
+ .modal-head { flex: 0 0 auto; z-index: 5; background: var(--header-bg); backdrop-filter: var(--header-blur); border-bottom: 1px solid var(--hairline); border-radius: var(--radius) var(--radius) 0 0; padding: 15px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
897
+ .modal-title { font-family: var(--font-display); font-weight: 700; font-size: 1.35rem; color: var(--text); letter-spacing: 0.02em; text-transform: uppercase; }
898
+ .icon-btn { appearance: none; border: 1px solid var(--chip-border); background: var(--chip-bg); border-radius: var(--radius-sm); padding: 8px 11px; cursor: pointer; font-weight: 700; color: var(--text); }
899
+ .icon-btn:hover { background: var(--brand-tint); border-color: var(--brand); }
900
+ .modal-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 16px 18px 20px; }
901
+
902
+ /* =====================================================================
903
+ Scrollbars
904
+ ===================================================================== */
905
+ .facets::-webkit-scrollbar, .card-grid::-webkit-scrollbar, .table-wrap::-webkit-scrollbar,
906
+ .carousel-track::-webkit-scrollbar, #sec-overview::-webkit-scrollbar, #sec-explore::-webkit-scrollbar,
907
+ #sec-explore .explore-grid > .card::-webkit-scrollbar,
908
+ .modal-panel::-webkit-scrollbar { width: 10px; height: 9px; }
909
+ ::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
910
+ ::-webkit-scrollbar-track { background: transparent; }
911
+
912
+ /* =====================================================================
913
+ Responsive
914
+ ---------------------------------------------------------------------
915
+ β‰₯1281px Full layout (tuned for wide / ultrawide screens)
916
+ ≀1280px Laptop β€” tighten padding, narrower rails, shorter chart
917
+ ≀1040px Tablet β€” single-column layouts, catalog filters collapse behind a toggle
918
+ ≀680px Phone β€” switch to natural document scroll, stack everything
919
+ ===================================================================== */
920
+
921
+ /* ---------- Laptop: keep the desktop layout but ease the spacing ---------- */
922
+ @media (max-width: 1280px) {
923
+ .section-title { font-size: 1.7rem; }
924
+ .catalog-layout { grid-template-columns: 184px minmax(0, 1fr); }
925
+ .card-grid { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
926
+ .grid.explore-grid { grid-template-columns: minmax(300px, 36%) 1fr; }
927
+ .constellation canvas { height: 400px; }
928
+ .stat-tiles { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
929
+ }
930
+
931
+ /* ---------- Tablet ---------- */
932
+ @media (max-width: 1040px) {
933
+ /* Nav stays inline on the header row (no wrap) right down to the phone breakpoint */
934
+ .topbar-inner { padding: 10px 16px; }
935
  .grid.two, .grid.explore-grid { grid-template-columns: 1fr; }
936
+ .field-row.two { grid-template-columns: 1fr 1fr; }
937
+ .summary-grid { grid-template-columns: 1fr; }
938
+ .repo-buttons { justify-items: start; }
939
+ .repo-buttons .link { width: auto; }
940
+ .metric-grid { grid-template-columns: 1fr 1fr; }
941
+ .catalog-layout { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
942
+ .facets { border-right: none; border-bottom: 1px solid var(--hairline); max-height: none; overflow: visible; }
943
+ /* Catalog filters collapse behind the "Filters" toggle β€” the old always-open
944
+ top strip is removed, leaving only "left rail" (desktop) and "collapsed".
945
+ Scoped to .facets-head so the Explore toggle is unaffected here. */
946
+ .facets-head .filter-toggle { display: flex; width: auto; flex: 1 1 auto; }
947
+ .facets-title { display: none; }
948
+ .facets-body { display: none; }
949
+ .facets-body.open { display: flex; max-height: 50vh; overflow-y: auto; padding-bottom: 2px; }
950
+ /* Overview model constellation β€” same merge: its "Chart filters" collapse moves
951
+ up here too, so it has only "full" (desktop) and "collapsed" states. */
952
+ #cstlFilterToggle { display: flex; margin-bottom: 12px; }
953
+ .cstl-controls { display: none; margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
954
+ .cstl-controls.open { display: flex; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--hairline); }
955
+ }
956
+
957
+ /* ---------- Phone: natural document scroll, burger nav, fully stacked ---------- */
958
+ @media (max-width: 680px) {
959
+ /* Release the fixed-viewport lock so the page scrolls as one document */
960
+ html, body { height: auto; }
961
+ body { height: auto; min-height: 100dvh; overflow-y: auto; overflow-x: hidden; }
962
+ .topbar { position: sticky; top: 0; }
963
+ .content { flex: 0 0 auto; height: auto; min-height: 0; overflow: visible; }
964
+
965
+ .section { height: auto; min-height: 0; overflow: visible; }
966
+ #sec-overview.visible, #sec-explore.visible { overflow: visible; }
967
+
968
+ /* Header stays on a single line: brand left, burger right */
969
+ .topbar-inner { flex-wrap: nowrap; padding: 9px 12px; gap: 12px; }
970
+ .brand-icon { height: 19px; }
971
+ .brand-subtitle { display: none; }
972
+ .nav-burger { display: inline-flex; }
973
+
974
+ /* Tabs collapse into a dropdown panel under the header */
975
+ .tabs {
976
+ position: absolute; top: 100%; left: 0; right: 0; margin: 0; z-index: 60;
977
+ flex-direction: column; align-items: stretch; gap: 4px; padding: 8px;
978
+ display: none;
979
+ background: rgba(255,255,255,0.95); backdrop-filter: blur(26px) saturate(165%); -webkit-backdrop-filter: blur(26px) saturate(165%);
980
+ border-bottom: 1px solid var(--header-border); box-shadow: 0 18px 40px rgba(42,40,157,0.20);
981
+ }
982
+ .tabs.open { display: flex; }
983
+ .nav-item { width: 100%; text-align: left; padding: 11px 14px; font-size: 0.96rem; }
984
+
985
+ /* Headings scale down */
986
+ .section-title { font-size: 1.45rem; }
987
+ .card-head-title { font-size: 1.12rem; }
988
+ .carousel-section.featured .carousel-title { font-size: 1.18rem; }
989
+
990
+ /* Overview */
991
+ .stat-tiles { grid-template-columns: 1fr 1fr; }
992
+ .stat-num { font-size: 1.7rem; }
993
+ .cstl-controls .field { min-width: calc(50% - 6px); flex: 1 1 calc(50% - 6px); }
994
+ .constellation canvas { height: 320px; }
995
+ .rc-card { flex-basis: 78vw; }
996
+
997
+ /* Catalog β€” a wrapping two-up grid that scrolls vertically with the page
998
+ (NOT a single horizontal swipe lane) */
999
+ .toolbar { flex-wrap: wrap; }
1000
+ .card-grid {
1001
+ flex: 0 0 auto; height: auto; min-height: 0;
1002
+ display: grid; grid-template-columns: repeat(2, 1fr);
1003
+ overflow-x: hidden; overflow-y: visible;
1004
+ gap: 10px; padding-bottom: 16px; scroll-snap-type: none;
1005
+ }
1006
+ .model-card { width: auto; scroll-snap-align: none; padding: 10px; gap: 8px; }
1007
+ .mc-monogram { width: 34px; height: 34px; font-size: 0.8rem; }
1008
+ .mc-head { gap: 9px; }
1009
+ .mc-title { font-size: 0.95rem; }
1010
+ .mc-stat { padding: 8px 10px 8px 11px; }
1011
+ .mc-stat-num { font-size: 1.18rem; }
1012
+ /* Table β†’ cards: collapse each row into a stacked card (no horizontal scroll),
1013
+ flowing with the document. Each <td> becomes a labelled space-between row. */
1014
+ .table-view { display: block; flex: 0 0 auto; height: auto; min-height: 0; overflow: visible; padding: 8px 10px 16px; }
1015
+ .table-wrap { max-height: none; overflow: visible; border: none; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; }
1016
+ table.table { min-width: 0; display: block; width: auto; }
1017
+ .table thead { display: none; }
1018
+ .table tbody { display: flex; flex-direction: column; gap: 12px; }
1019
+ .table tbody tr {
1020
+ position: relative; overflow: hidden; display: block;
1021
+ padding: 15px 16px 13px;
1022
+ border: 1px solid var(--card-border); border-radius: var(--card-radius);
1023
+ background: var(--card-bg); box-shadow: var(--card-shadow);
1024
+ }
1025
+ /* signature top accent (matches .model-card) */
1026
+ .table tbody tr::before {
1027
+ content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
1028
+ background: linear-gradient(90deg, var(--brand), var(--brand-2));
1029
+ transform: scaleX(0.34); transform-origin: left; opacity: 0.85;
1030
+ }
1031
+ /* cancel desktop row tints + sticky pins inside cards */
1032
+ .table tbody tr:hover td, .table tbody tr:nth-child(even) td { background: transparent; }
1033
+ .table .pin { position: static !important; left: auto !important; z-index: auto; background: transparent; }
1034
+ .table tbody tr:hover td.cell-name { box-shadow: none; }
1035
+ .table tbody td {
1036
+ display: flex; justify-content: space-between; align-items: baseline; gap: 14px;
1037
+ width: auto; padding: 8px 0; border-bottom: 1px solid var(--hairline);
1038
+ font-size: 0.88rem; white-space: normal; text-align: right;
1039
+ }
1040
+ .table tbody td::before {
1041
+ content: attr(data-label); flex: 0 0 auto; text-align: left;
1042
+ font-family: var(--font-display); font-weight: 700; font-size: 0.66rem;
1043
+ text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim);
1044
+ }
1045
+ .table tbody td:last-child { border-bottom: none; }
1046
+ /* Name β†’ card title (block, larger, no label) */
1047
+ .table tbody td[data-label="Name"] {
1048
+ display: block; text-align: left; padding: 0 0 11px; margin-bottom: 4px;
1049
+ border-bottom: 1px solid var(--hairline); font-size: 1.18rem; white-space: normal;
1050
+ }
1051
+ .table tbody td[data-label="Name"]::before { content: none; }
1052
+ /* Details β†’ full-width button */
1053
+ .table tbody td.details-col { display: block; width: auto; padding: 12px 0 4px; border-bottom: none; }
1054
+ .table tbody td.details-col::before { content: none; }
1055
+ .table tbody td.details-col .mini-btn { width: 100%; }
1056
+ .table .cell-metrics { min-width: 0; }
1057
+ .table .badges, .table .badges.badges-row { flex-direction: row; flex-wrap: wrap; justify-content: flex-end; }
1058
+
1059
+ /* Explore */
1060
+ .field-row.two { grid-template-columns: 1fr; }
1061
+ .metric-grid { grid-template-columns: 1fr; }
1062
+ .model-list { min-height: 220px; }
1063
+
1064
+ /* Collapsible filter clusters β€” only Explore collapses here now.
1065
+ (Catalog + Overview-constellation filters collapse at ≀1040px above.) */
1066
+ .filter-toggle { display: flex; }
1067
+ .explore-filters { display: none; }
1068
+ .explore-filters.open { display: grid; }
1069
+ /* On phone the whole page scrolls, so let the opened filter lists expand fully */
1070
+ .facets-body.open { max-height: none; overflow: visible; }
1071
+
1072
+ /* Modal goes near full-screen */
1073
+ .modal-panel { top: 8px; width: calc(100vw - 16px); max-height: calc(100dvh - 16px); }
1074
+ .modal-title { font-size: 1.15rem; }
1075
+ .modal-body { padding: 14px; }
1076
+ }
1077
+
1078
+ /* Explore: on phone the single "Filters" button reveals #exploreFilters, so
1079
+ force the nested Advanced tier open (no two-step) and hide its desktop
1080
+ sub-toggle. Must come AFTER the block above to win on source order. */
1081
+ @media (max-width: 680px) {
1082
+ .explore-advanced { display: grid; gap: 10px; }
1083
+ .filter-toggle--inline { display: none; }
1084
+ .explore-filters .field-row.two, .explore-advanced .field-row.two { grid-template-columns: 1fr 1fr; }
1085
+ }
1086
+
1087
+ /* ---------- Small phone ---------- */
1088
+ @media (max-width: 380px) {
1089
+ .stat-tiles { grid-template-columns: 1fr; }
1090
+ }