Spaces:
Running
Running
Add Speech Recognition + LLM Judge tabs (absolute heatmap mode, grouped samples with transcripts)
Browse files
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,
|
|
|
|
| 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
|
| 16 |
-
*
|
| 17 |
-
|
| 18 |
-
("asc" => lower is better
|
|
|
|
|
|
|
|
|
|
| 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?,
|
| 58 |
-
# "audio" is a repo-relative path (e.g.
|
| 59 |
-
# dataset (or ./data locally). A
|
| 60 |
-
#
|
| 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 |
-
|
|
|
|
| 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": "", "
|
|
|
|
| 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}'>{
|
| 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
|
| 679 |
-
#
|
| 680 |
-
#
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 703 |
-
|
|
|
|
|
|
|
| 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" (
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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'>
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|