Skip to content

Add interactive bar charts to Scores - #5

Merged
5uck1ess merged 3 commits into
5uck1ess:masterfrom
estebanstifli:scores-bar-charts
Aug 19, 2026
Merged

Add interactive bar charts to Scores#5
5uck1ess merged 3 commits into
5uck1ess:masterfrom
estebanstifli:scores-bar-charts

Conversation

@estebanstifli

@estebanstifli estebanstifli commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Hi! First of all, congratulations on the project. I think it’s a genuinely useful resource for comparing local TTS models, and I wanted to contribute a small improvement to the way the results are displayed.

This PR adds horizontal bar charts to the Scores page. The idea is to make it easier to see at a glance which models stand out, while keeping the existing tables available for detailed inspection.

The chart:

  • Shows the top 15 models for the selected metric.
  • Shows UTMOS for Default Voice and allows switching between SIM and UTMOS for Voice Cloning.
  • Respects the existing Default Voice / Voice Cloning selector.
  • Uses fixed metric domains rather than normalizing against the visible top 15.
  • Uses the same data source as the existing table.
  • Preserves WER in the full table as a failure detector rather than charting it as a fine ranking.
  • Keeps the full table below for detailed inspection.
  • Adds no new dependencies; it uses the existing HTML, CSS, and JavaScript structure.

I’ve also tried to preserve the project’s current visual style so that the chart feels like a natural addition to the page.

I hope you find it useful! I’d be happy to adjust the design or behaviour if you would prefer a different approach. Thanks for all the work you’re putting into tts-bench!

@5uck1ess 5uck1ess left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @estebanstifli — and for the unusually careful integration work. Reusing the existing CSS custom properties, wiring into the existing #filter / #reset-sort controls, building the bars with createElement + textContent rather than string HTML, adding aria-labels, and shipping it with zero new dependencies is exactly the right shape for a contribution here. Ruff is clean and scoring/tests/test_build_scores.py passes 4/4.

I built the page against a fixture, drove it in headless Chromium, and ran a second independent review pass over the diff. There are three things I need changed before I can merge, plus one judgment call I'd like your read on.

1. Bar lengths are min–max normalized, which misrepresents this particular data

fill.style.width = (12 + relative * 88) + '%';
// relative = span ? (lowerBetter ? max - row.value : row.value - min) / span : 1

min/max come from the 15 charted rows, not from the metric's domain — so the lowest bar is always 12% and the highest is always 100%, whatever the actual spread is.

That's fine when the spread is wide. On this board it isn't. Measured against the live scoring/scores.csv, via the same _model_scores path the table uses:

chart true value spread rendered as exaggeration
Default UTMOS, top 15 4.487 → 4.232 (+6.0%) 100% vs 12% (8.33×) 7.9×
Default WER, top 15 0.0232 → 0.0657 (2.83×) 100% vs 12% (8.33×) 2.9×

So the #1 and #15 models on the UTMOS chart are within 6% of each other, and the chart draws one bar eight times longer than the other. On a page whose entire job is fair comparison between models, that's the one thing the visualization can't do.

The Relative range 4.232–4.487 caption is a real mitigation and I appreciate that you added it, but it's 0.78em muted text below-right of a chart whose whole visual argument is bar length — and it discloses the min–max normalization without explaining the arbitrary 12% floor. Exact labels don't correct the encoding.

Suggested fix: normalize against the metric's domain rather than the visible extremes. UTMOS is a MOS-scale prediction (~1–5), SIM is a cosine similarity (0–1), WER is a rate. Something like width = (value / domainMax) * 100 for higher-better, and (1 - value) * 100 for WER, gives honest bars.

If that flattens the chart too much to be useful — which it will, and that's the real signal here — a dot plot on a labelled axis shows small differences legibly without implying magnitude. I'd be happy with that.

2. Uncaught TypeError when a board has no scored models

var section = chart.closest('.subsection');
var table = section.querySelector('table');

_scores_table() returns <p class="mode-empty muted">No scored models yet.</p> with no <table> when a board has nothing scored, but the chart is emitted unconditionally. section.querySelector('table') is then null and the initial render() throws.

Reproduced in Chromium with an empty cloning board:

PAGEERROR: Cannot read properties of null (reading 'querySelectorAll')

The throw aborts the whole IIFE. It only looks survivable today because the empty section happens to come last in document order — if the default board is the empty one (a cloning-only bench run, which is a normal thing to do), the throw lands on the first iteration and the cloning chart never gets wired at all.

Fix is one line at the top of the forEach:

if (!table) { chart.remove(); return; }

3. Flagged models lose their warning in the chart

Rows whose mean WER exceeds WER_FAIL_THRESHOLD (0.5) get class="flagged" — dimmed to 55% opacity with a ⚠ appended — because at that error rate the model is producing broken speech and its other scores shouldn't be read at face value.

The chart pulls those rows in at full brightness with no marker. This isn't hypothetical on current data: miso (UTMOS 4.232, WER 0.586) is rank 15 of the live default-board UTMOS chart. It renders as a normal bar in a "top 15" list while the table three inches below dims it and flags it as broken.

Carrying tr.flagged through to .score-bar-row would cover it.

4. Judgment call: should WER be charted at all?

The page tells the reader twice that it shouldn't be read this way — _SCORES_GUIDE:

WER = ASR word-error rate vs the intended text — a failure-detector, not a fine ranking

and scores-foot:

Scored over the 5 bench prompts (thin — WER is a failure-detector, not a fine ranking)

A ranked top-15 WER bar chart is a fine-ranking presentation of exactly that metric, and the scale caption reads longer is better, which is true of the bar but parses as "higher WER is better" at a glance.

My instinct is to drop the WER button and chart only UTMOS and SIM, or render WER as a pass/fail marker instead of a ranked bar. Interested in your view — you may see a framing I'm missing.

Smaller notes

  • The @media(max-width:600px) block restyles .controls .lens-tabs and .controls input. Those are the shared top control bar, not the chart. Since scores_style is only injected on scores.html, the control bar would wrap differently on Scores than on Listen / Speed / Capabilities. Worth scoping to the chart selectors.
  • The new test assertions check exact implementation bytes ("var limit = 15" in html, '.score-bar-fill{display:block' in html, "longer is better" in html), so any cosmetic edit breaks them. The data-metric and score-chart count assertions are the useful ones — I'd keep those and drop the rest.
  • CI hasn't run on this yet; it's waiting on my approval for a first-time contributor. I'll kick it off.

Genuinely good work on the plumbing — items 2 and 3 are small, and item 1 is the one that matters. Happy to talk through the axis question if you'd rather change direction than patch it.

@estebanstifli

estebanstifli commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. All four points made sense, and I pushed d0179ce to address them. A subsequent correction in 986f14d updates the SIM chart to its full -1..1 cosine-similarity domain.

  • Bars now use fixed metric domains rather than being min-max normalized over the visible top 15.
  • Charts are omitted when their board has no score table, and there is a regression test for the empty-board case.
  • Flagged table rows now propagate to the chart, including the dimmed warning marker and an accessible high-WER warning in the row label.
  • I agree that WER should remain a failure detector, so I removed it from the chart controls. It remains in the sortable tables and existing warning logic.
  • I also removed the Scores-only mobile overrides for shared top controls and reduced the tests to output behaviour rather than cosmetic implementation bytes.

Validation after the update: 128 passed; ruff check publish.py scoring/tests/test_build_scores.py and the compile check pass.

@5uck1ess

5uck1ess commented Aug 4, 2026

Copy link
Copy Markdown
Owner

🔍 REVIEW — Codex merge-readiness

Review only — a verdict on this PR; no code was changed. Corrections, when applied, arrive as a separate ## 🔧 Corrections applied comment.

gpt-5.6-sol · high effort · reviewed at d0179ce

Reviewed head d0179ce20074d188f126fca587ef48d797c2606f.

One finding survives:

  • Blocking — SIM chart uses an incorrect fixed domain. publish.py:1011 declares SIM as 0–1, and publish.py:1059 clamps negative values to 0%. The real path is _read_scores_csv_model_scores_scores_tablerender("sim"). Using the checked-in -0.0519 score as the only picked clip reproduced data-sort="-0.0519" and a zero-width bar labeled against a 0–1 scale. The scorer explicitly returns normalized cosine similarity over −1…1. Existing tests use only positive SIM values and do not cover this case. The negative data and scorer existed at the merge base, but the faulty chart did not, so the visualization regression is introduced by this PR.

VERDICT: CHANGES_REQUESTED - SIM chart mishandles valid negative scores


Automated pre-merge review by pr-merge-brief. Advisory only — not a merge gate; the merge decision stays with the maintainer.

@estebanstifli

estebanstifli commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Corrections applied

Thanks, confirmed. SimScorer defines SIM as normalized cosine similarity over -1..1, so the prior 0..1 chart domain was incorrect.

Pushed 986f14d:

  • SIM now uses the fixed -1..1 domain, mapped linearly so -1 renders at 0%, 0 at 50%, and 1 at 100%.
  • The chart caption now states SIM scale: -1 to 1.
  • Added a regression fixture with negative SIM (-0.05) and asserts that the generated score cell preserves it and the full SIM domain is configured.

Validation: 128 passed; compile check and Ruff pass.

@5uck1ess

5uck1ess commented Aug 4, 2026

Copy link
Copy Markdown
Owner

🔍 REVIEW — Codex merge-readiness

Review only — a verdict on this PR; no code was changed. Corrections, when applied, arrive as a separate ## 🔧 Corrections applied comment.

gpt-5.6-sol · high effort · reviewed at 986f14d

Reviewed head 986f14de72c0faeeb6f98550d53692d764d4b5b6.

  • Previous SIM-domain finding: fixed. SIM now uses the scorer’s valid −1…1 range and negative scores are preserved.
  • Reviewed the incremental and complete PR diffs plus reached filtering, sorting, mode-selection, scoring, and clip-selection paths.
  • No new merge-blocking defects found.
  • Python AST, JavaScript syntax, and git diff --check passed.
  • Full tests could not run because pytest is unavailable and the read-only environment prevents uv from acquiring its cache lock. CI status is none.
  • AGENTS.md is not present in this checkout or revision history.

VERDICT: READY


Automated pre-merge review by pr-merge-brief. Advisory only — not a merge gate; the merge decision stays with the maintainer.

@5uck1ess

5uck1ess commented Aug 4, 2026

Copy link
Copy Markdown
Owner

@estebanstifli thanks for your work. how are you automating your PRs? cronjobs like me? or something else?

@5uck1ess
5uck1ess merged commit eda3da0 into 5uck1ess:master Aug 19, 2026
1 check passed
5uck1ess added a commit that referenced this pull request Aug 19, 2026
Republished gh-pages (b4b792a) to carry PR #5's Scores bar charts and the
12 linux-default models that were sitting in an unpushed local commit. That
also pushed the corrected 'checkmark (80+)' cell for fish_s2 live, which the
task had wanted held until the FR clip existed. Recording the tradeoff so the
inconsistency is tracked rather than rediscovered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants