rashisht commited on
Commit
68869dd
·
verified ·
1 Parent(s): c7c267f

Add Speech Recognition + LLM Judge tabs (absolute heatmap mode, grouped samples with transcripts)

Browse files
Files changed (1) hide show
  1. app.py +131 -31
app.py CHANGED
@@ -1,7 +1,8 @@
1
  """Real World VoiceEQ Benchmark — Gradio Space (Text-to-Speech / Speech-to-Speech /
2
- Voice Controllability / Speech Understanding).
3
 
4
- One Space, four tabs — one per modality. Each tab renders a heatmap-style ranking
 
5
  table from a self-describing JSON file that holds a *list* of boards (an Overall
6
  board + one per factor). The boards are pivoted into one wide table: each factor's
7
  `score` becomes a heatmapped column. Rows are ranked by the first factor column by
@@ -12,10 +13,13 @@ shown as columns.
12
 
13
  Heatmap modes:
14
  * Default (no board-level "heatmap" key): absolute 1–5 rater scale, higher = greener
15
- (all current tabs).
16
- * Board with "heatmap": {"mode": "normalized"}: each factor column is scaled by its
17
- own min/max across providers and the column's "direction" is honoured
18
- ("asc" => lower is better, e.g. WER), so the best value is greenest.
 
 
 
19
 
20
  Theming: light and dark render from the same markup. All card colors live in
21
  `--lb-*` custom properties on `.ttslb` (light values = the canonical look) with a
@@ -51,14 +55,20 @@ MODALITIES = [
51
  ("Voice Controllability", "voice_creation", "voice_creation_leaderboard.json"),
52
  ("Speech-to-Speech", "sts", "sts_leaderboard.json"),
53
  ("Speech Understanding", "stt", "stt_leaderboard.json"),
 
 
54
  ]
55
 
56
  # Curated audio samples (optional): a samples.json manifest next to the board
57
- # JSONs, each record {modality, factor, label?, model?, text?, audio} where
58
- # "audio" is a repo-relative path (e.g. "samples/foo.mp3") resolved against the
59
- # dataset (or ./data locally). A record's modality routes it to that tab, which
60
- # appends a "Sample Generations" section (per-factor category tabs) below the
61
- # board table.
 
 
 
 
62
  SAMPLES_NAME = "samples.json"
63
 
64
  # Each board in the data carries a single primary `score` column plus a `coverage`
@@ -131,8 +141,29 @@ def _heat_t(t):
131
 
132
  def _make_color_fn(board: dict, factor_cols: list):
133
  """Return color_fn(col, value) -> (bg, fg), per the board's heatmap mode."""
134
- if not board.get("heatmap"):
 
135
  return lambda col, v: _heat_abs(v)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  parts = board.get("participants", [])
137
  stats = {}
138
  for c in factor_cols:
@@ -224,9 +255,13 @@ def merge_boards(boards: list) -> dict:
224
 
225
  metric_columns = []
226
  for b in factor_boards:
227
- # The factor board's description doubles as the column-header tooltip.
 
 
 
228
  metric_columns.append({"field": b["id"], "label": _factor_label(b),
229
- "unit": "", "direction": "desc",
 
230
  "description": (b.get("description") or "").strip()})
231
 
232
  by_model: dict = {}
@@ -265,6 +300,9 @@ def merge_boards(boards: list) -> dict:
265
  "title": meta.get("title", ""),
266
  "description": meta.get("description", ""),
267
  "keyMetric": {},
 
 
 
268
  "metricColumns": metric_columns,
269
  "participants": [by_model[m] for m in order],
270
  }
@@ -413,7 +451,19 @@ TABLE_CSS = """
413
  .ttslb .smp-row:last-child { border-bottom:0; }
414
  .ttslb .smp-head { width:300px; flex:none; }
415
  .ttslb .smp-label { font-weight:600; font-size:12.5px; }
 
 
 
 
 
416
  .ttslb .smp-row audio { flex:1; min-width:260px; height:32px; }
 
 
 
 
 
 
 
417
  .ttslb .smp-pending { color:var(--lb-ink-3); font-size:12px; }
418
  /* Header logo swap: each img bakes its default visibility inline as
419
  display:var(--lb-logo-*, ...) — gradio strips class-based styling from
@@ -579,9 +629,12 @@ def _row_html(p, heat_cols, primary_col, cov_col, rank_label, color_fn, marker_o
579
  else:
580
  (bg, fg), (bgd, fgd) = color_fn(c, v)
581
  cls = "pill primary" if c is primary_col else "pill"
 
 
 
582
  cells.append(
583
  f"<td data-v='{v}'><span class='{cls}' style='--hm-bg:{bg};--hm-fg:{fg};"
584
- f"--hm-bg-dark:{bgd};--hm-fg-dark:{fgd}'>{v:.2f}</span></td>"
585
  )
586
  # coverage: plain count (e.g. "9/9"), not heatmapped.
587
  if cov_col is not None:
@@ -675,13 +728,29 @@ def render_board_html(board: dict, lic: str = "all", fname: str = "board") -> st
675
  f"<div><span class='notemark'>{mark}</span> {html.escape(note)}</div>"
676
  for note, mark in marker_of.items()) + "</div>")
677
 
678
- # Toolbar: the 1–5 score-scale legend (absolute rater-scale boards only) and
679
- # an icon button downloading the table as CSV, exactly as displayed (handled
680
- # in APP_JS). Both sit inside .tbl-box, so they align with the table's edges.
681
- legend = ("" if board.get("heatmap") else
682
- "<div class='legend'><span class='legend-end'>1</span>"
683
- "<span class='legend-bar'></span><span class='legend-end'>5</span>"
684
- "<span>mean human rating</span></div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  toolbar = (f"<div class='lb-toolbar'>{legend}"
686
  f"<button class='csv-btn' data-fname='{html.escape(fname, quote=True)}'"
687
  " data-tip='Download CSV' aria-label='Download CSV'></button></div>")
@@ -699,13 +768,18 @@ def render_board_html(board: dict, lic: str = "all", fname: str = "board") -> st
699
  )
700
 
701
 
702
- def render_samples_section(samples: list, factor_labels: dict) -> str:
703
- """A modality tab's "Sample Generations" card, rendered below the board
 
 
704
  table: one category tab per factor (first-seen manifest order, switching in
705
  APP_JS), each a list of label + audio player rows. A single-category
706
  modality (e.g. Voice Controllability) gets no tab row — just the rows. A sample's
707
- "text" (transcript/prompt) and "model" attribution are kept in the manifest
708
- but not displayed."""
 
 
 
709
  if not samples:
710
  return ""
711
  factors = []
@@ -720,18 +794,41 @@ def render_samples_section(samples: list, factor_labels: dict) -> str:
720
  tabs.append(f"<button class='smp-tab{' on' if j == 0 else ''}' data-p='{j}'>"
721
  f"{html.escape(f_label)} <span class='smp-n'>({len(rows)})</span></button>")
722
  rows_html = []
 
723
  for s in rows:
724
- head = (f"<span class='smp-label'>{html.escape(s['label'])}</span>"
725
- if s.get("label") else "")
726
  src = _audio_src(s.get("audio", ""))
727
  player = (f"<audio controls preload='none' src='{src}'></audio>"
728
  if src else "<span class='smp-pending'>audio pending</span>")
729
- rows_html.append(f"<div class='smp-row'><div class='smp-head'>{head}</div>{player}</div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  panels.append(f"<div class='smp-panel' data-p='{j}'{'' if j == 0 else ' hidden'}>"
731
  + "".join(rows_html) + "</div>")
732
 
733
  tab_row = ("<div class='smp-tabs'>" + "".join(tabs) + "</div>") if len(factors) > 1 else ""
734
- return ("<div class='ttslb smp'><h3 class='smp-h'>Sample Generations</h3>"
735
  + tab_row + "".join(panels) + "</div>")
736
 
737
 
@@ -783,7 +880,10 @@ def build_demo() -> gr.Blocks:
783
  # this modality's curated samples, below the board
784
  mod_samples = [s for s in samples if s.get("modality") == key]
785
  if mod_samples:
786
- gr.HTML(render_samples_section(mod_samples, factor_labels))
 
 
 
787
 
788
  return demo
789
 
 
1
  """Real World VoiceEQ Benchmark — Gradio Space (Text-to-Speech / Speech-to-Speech /
2
+ Voice Controllability / Speech Understanding / Speech Recognition / LLM Judge).
3
 
4
+ One Space, six tabs — one per modality (plus the LLM Judge meta-leaderboard of
5
+ judge-vs-human agreement). Each tab renders a heatmap-style ranking
6
  table from a self-describing JSON file that holds a *list* of boards (an Overall
7
  board + one per factor). The boards are pivoted into one wide table: each factor's
8
  `score` becomes a heatmapped column. Rows are ranked by the first factor column by
 
13
 
14
  Heatmap modes:
15
  * Default (no board-level "heatmap" key): absolute 1–5 rater scale, higher = greener
16
+ (all rater-scored tabs).
17
+ * "heatmap": {"mode": "absolute", "stops": [...], "unit", "legend"}: fixed value
18
+ anchors shared by every column, so a color means the same thing across columns;
19
+ each column's "direction" is honoured ("asc" => lower is better) Speech
20
+ Recognition's WER and LLM Judge's Pearson-r tabs.
21
+ * "heatmap": {"mode": "normalized"}: each factor column is scaled by its own
22
+ min/max across providers (direction honoured too), best value greenest.
23
 
24
  Theming: light and dark render from the same markup. All card colors live in
25
  `--lb-*` custom properties on `.ttslb` (light values = the canonical look) with a
 
55
  ("Voice Controllability", "voice_creation", "voice_creation_leaderboard.json"),
56
  ("Speech-to-Speech", "sts", "sts_leaderboard.json"),
57
  ("Speech Understanding", "stt", "stt_leaderboard.json"),
58
+ ("Speech Recognition", "asr", "asr_leaderboard.json"),
59
+ ("LLM Judge", "llm_judge", "llm_judge_leaderboard.json"),
60
  ]
61
 
62
  # Curated audio samples (optional): a samples.json manifest next to the board
63
+ # JSONs, each record {modality, factor, label?, group?, model?, text?,
64
+ # transcript?, audio} where "audio" is a repo-relative path (e.g.
65
+ # "samples/foo.mp3") resolved against the dataset (or ./data locally). A
66
+ # record's modality routes it to that tab, which appends a samples section
67
+ # (per-factor category tabs) below the board table. "text" (generation prompt)
68
+ # is kept but never shown; "transcript" (reference transcript, ASR golden
69
+ # samples) is rendered next to the player. Consecutive records sharing a
70
+ # "group" (e.g. the Accents tab's Native / 2nd language / Foreign buckets)
71
+ # render under one shared subheading instead of per-row labels.
72
  SAMPLES_NAME = "samples.json"
73
 
74
  # Each board in the data carries a single primary `score` column plus a `coverage`
 
141
 
142
  def _make_color_fn(board: dict, factor_cols: list):
143
  """Return color_fn(col, value) -> (bg, fg), per the board's heatmap mode."""
144
+ hm = board.get("heatmap")
145
+ if not hm:
146
  return lambda col, v: _heat_abs(v)
147
+ if hm.get("mode") == "absolute" and hm.get("stops"):
148
+ # Fixed value anchors shared by every column (board-supplied stops, e.g.
149
+ # WER percentages), so a color means the same thing in every column —
150
+ # the absolute-scale analogue of the rater tabs' 1–5 anchors. A column's
151
+ # "asc" direction flips the ramp (low WER = green).
152
+ stops = [float(s) for s in hm["stops"]]
153
+
154
+ def fn_abs(col, v):
155
+ x = max(stops[0], min(stops[-1], float(v)))
156
+ t = 1.0
157
+ for i, (a, b) in enumerate(zip(stops, stops[1:])):
158
+ if x <= b:
159
+ f = 0.0 if b == a else (x - a) / (b - a)
160
+ t = (i + f) / (len(stops) - 1)
161
+ break
162
+ if col.get("direction") == "asc":
163
+ t = 1.0 - t
164
+ return _heat_t(t)
165
+
166
+ return fn_abs
167
  parts = board.get("participants", [])
168
  stats = {}
169
  for c in factor_cols:
 
255
 
256
  metric_columns = []
257
  for b in factor_boards:
258
+ # The factor board's description doubles as the column-header tooltip;
259
+ # its keyMetric supplies the column's unit and sort direction (WER-style
260
+ # boards declare "asc" = lower is better).
261
+ km = b.get("keyMetric") or {}
262
  metric_columns.append({"field": b["id"], "label": _factor_label(b),
263
+ "unit": km.get("unit", ""),
264
+ "direction": km.get("direction", "desc"),
265
  "description": (b.get("description") or "").strip()})
266
 
267
  by_model: dict = {}
 
300
  "title": meta.get("title", ""),
301
  "description": meta.get("description", ""),
302
  "keyMetric": {},
303
+ # any board declaring a heatmap mode (normalized WER-style tabs) sets it
304
+ # for the whole merged table
305
+ "heatmap": next((b.get("heatmap") for b in boards if b.get("heatmap")), None),
306
  "metricColumns": metric_columns,
307
  "participants": [by_model[m] for m in order],
308
  }
 
451
  .ttslb .smp-row:last-child { border-bottom:0; }
452
  .ttslb .smp-head { width:300px; flex:none; }
453
  .ttslb .smp-label { font-weight:600; font-size:12.5px; }
454
+ /* tag-bucket subheadings (ASR golden samples): one heading per group, its rows
455
+ indented beneath it with the head column dropped */
456
+ .ttslb .smp-group { margin:16px 0 2px; text-align:left; font-size:12.5px; font-weight:700; }
457
+ .ttslb .smp-panel > .smp-group:first-child { margin-top:4px; }
458
+ .ttslb .smp-row.grp { padding-left:14px; }
459
  .ttslb .smp-row audio { flex:1; min-width:260px; height:32px; }
460
+ /* transcript rows (ASR golden samples): the category tag is short, so the head
461
+ narrows, the player takes a fixed slot, and the reference transcript fills
462
+ the rest of the row */
463
+ .ttslb .smp-row.txt .smp-head { width:150px; }
464
+ .ttslb .smp-row.txt audio { flex:0 0 300px; min-width:300px; }
465
+ .ttslb .smp-text { flex:1; min-width:220px; text-align:left; font-size:12.5px;
466
+ line-height:1.45; color:var(--lb-ink-2); }
467
  .ttslb .smp-pending { color:var(--lb-ink-3); font-size:12px; }
468
  /* Header logo swap: each img bakes its default visibility inline as
469
  display:var(--lb-logo-*, ...) — gradio strips class-based styling from
 
629
  else:
630
  (bg, fg), (bgd, fgd) = color_fn(c, v)
631
  cls = "pill primary" if c is primary_col else "pill"
632
+ # unit suffix ("%", "x") rides along in the pill; data-v stays the
633
+ # bare number so sorting and CSV export keep working
634
+ txt = f"{v:.2f}{html.escape(c.get('unit') or '')}"
635
  cells.append(
636
  f"<td data-v='{v}'><span class='{cls}' style='--hm-bg:{bg};--hm-fg:{fg};"
637
+ f"--hm-bg-dark:{bgd};--hm-fg-dark:{fgd}'>{txt}</span></td>"
638
  )
639
  # coverage: plain count (e.g. "9/9"), not heatmapped.
640
  if cov_col is not None:
 
728
  f"<div><span class='notemark'>{mark}</span> {html.escape(note)}</div>"
729
  for note, mark in marker_of.items()) + "</div>")
730
 
731
+ # Toolbar: the score-scale legend and an icon button downloading the table as
732
+ # CSV, exactly as displayed (handled in APP_JS). Both sit inside .tbl-box, so
733
+ # they align with the table's edges. The legend follows the heatmap mode:
734
+ # 1–5 rater scale by default, the board's own value anchors for "absolute"
735
+ # boards (ends flipped for asc columns so red stays on the worst end), and a
736
+ # generic worst→best for "normalized" boards.
737
+ hm = board.get("heatmap") or {}
738
+ if hm.get("mode") == "absolute" and hm.get("stops"):
739
+ unit, cap = hm.get("unit", ""), (hm.get("legend") or "").strip()
740
+ ends = [f"{float(s):g}{unit}" for s in (hm["stops"][0], hm["stops"][-1])]
741
+ if value_cols and value_cols[0].get("direction") == "asc":
742
+ ends.reverse()
743
+ legend = (f"<div class='legend'><span class='legend-end'>{html.escape(ends[0])}</span>"
744
+ f"<span class='legend-bar'></span><span class='legend-end'>{html.escape(ends[1])}</span>"
745
+ + (f"<span>{html.escape(cap)}</span>" if cap else "") + "</div>")
746
+ elif hm:
747
+ legend = ("<div class='legend'><span class='legend-end'>worst</span>"
748
+ "<span class='legend-bar'></span><span class='legend-end'>best</span>"
749
+ "<span>scaled per column</span></div>")
750
+ else:
751
+ legend = ("<div class='legend'><span class='legend-end'>1</span>"
752
+ "<span class='legend-bar'></span><span class='legend-end'>5</span>"
753
+ "<span>mean human rating</span></div>")
754
  toolbar = (f"<div class='lb-toolbar'>{legend}"
755
  f"<button class='csv-btn' data-fname='{html.escape(fname, quote=True)}'"
756
  " data-tip='Download CSV' aria-label='Download CSV'></button></div>")
 
768
  )
769
 
770
 
771
+ def render_samples_section(samples: list, factor_labels: dict,
772
+ heading: str = "Sample Generations") -> str:
773
+ """A modality tab's samples card ("Sample Generations", or "Golden Samples"
774
+ for ASR's human-curated references), rendered below the board
775
  table: one category tab per factor (first-seen manifest order, switching in
776
  APP_JS), each a list of label + audio player rows. A single-category
777
  modality (e.g. Voice Controllability) gets no tab row — just the rows. A sample's
778
+ "text" (generation prompt) and "model" attribution are kept in the manifest
779
+ but not displayed; a "transcript" (the human reference for ASR golden
780
+ samples) is rendered next to the player. Rows carrying a "group" cluster
781
+ under one shared subheading (with a sample count) instead of repeating a
782
+ per-row label — the manifest is expected to keep a group's rows adjacent."""
783
  if not samples:
784
  return ""
785
  factors = []
 
794
  tabs.append(f"<button class='smp-tab{' on' if j == 0 else ''}' data-p='{j}'>"
795
  f"{html.escape(f_label)} <span class='smp-n'>({len(rows)})</span></button>")
796
  rows_html = []
797
+ cur_group = None
798
  for s in rows:
 
 
799
  src = _audio_src(s.get("audio", ""))
800
  player = (f"<audio controls preload='none' src='{src}'></audio>"
801
  if src else "<span class='smp-pending'>audio pending</span>")
802
+ transcript = (s.get("transcript") or "").strip()
803
+ group = (s.get("group") or "").strip()
804
+ if group:
805
+ # one shared subheading per run of same-group rows; the rows
806
+ # below it drop the head column and indent under the heading
807
+ if group != cur_group:
808
+ n = sum(1 for r in rows if (r.get("group") or "").strip() == group)
809
+ rows_html.append(f"<div class='smp-group'>{html.escape(group)} "
810
+ f"<span class='smp-n'>({n})</span></div>")
811
+ cur_group = group
812
+ body = (f"{player}<div class='smp-text'>{html.escape(transcript)}</div>"
813
+ if transcript else player)
814
+ rows_html.append(f"<div class='smp-row grp{' txt' if transcript else ''}'>{body}</div>")
815
+ continue
816
+ cur_group = None
817
+ head = (f"<span class='smp-label'>{html.escape(s['label'])}</span>"
818
+ if s.get("label") else "")
819
+ # a reference transcript (ASR golden samples) shares the row with the
820
+ # player; the row's .txt class narrows the head and pins the player
821
+ if transcript:
822
+ rows_html.append(
823
+ f"<div class='smp-row txt'><div class='smp-head'>{head}</div>{player}"
824
+ f"<div class='smp-text'>{html.escape(transcript)}</div></div>")
825
+ else:
826
+ rows_html.append(f"<div class='smp-row'><div class='smp-head'>{head}</div>{player}</div>")
827
  panels.append(f"<div class='smp-panel' data-p='{j}'{'' if j == 0 else ' hidden'}>"
828
  + "".join(rows_html) + "</div>")
829
 
830
  tab_row = ("<div class='smp-tabs'>" + "".join(tabs) + "</div>") if len(factors) > 1 else ""
831
+ return (f"<div class='ttslb smp'><h3 class='smp-h'>{html.escape(heading)}</h3>"
832
  + tab_row + "".join(panels) + "</div>")
833
 
834
 
 
880
  # this modality's curated samples, below the board
881
  mod_samples = [s for s in samples if s.get("modality") == key]
882
  if mod_samples:
883
+ heading = ("Golden Samples" if key == "asr"
884
+ else "Sample Generations")
885
+ gr.HTML(render_samples_section(mod_samples, factor_labels,
886
+ heading))
887
 
888
  return demo
889