Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit ·
ee561c5
1
Parent(s): 14ce963
new version with new metrics
Browse files- analytics.py +259 -59
- app.py +91 -61
- constants.py +52 -30
- evaluation/orchestrator.py +1 -1
- init.py +65 -13
- job_queue.py +7 -15
- metrics_config.py +30 -46
- scripts/run_eval.py +2 -0
- scripts/run_eval_and_publish.py +11 -18
- utils_display.py +39 -13
analytics.py
CHANGED
|
@@ -17,6 +17,16 @@ from plotly.colors import qualitative
|
|
| 17 |
|
| 18 |
from metrics_config import LIVE_SCENARIO_KEYS, SCENARIO_METRICS, metric_by_key
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# Consistent height for Gradio Plot
|
| 21 |
_FIG_HEIGHT = 460
|
| 22 |
_TEMPLATE = "plotly_white"
|
|
@@ -50,9 +60,86 @@ def _cached_model_size_mb(model_id: str) -> float:
|
|
| 50 |
return mb
|
| 51 |
|
| 52 |
|
| 53 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
"""
|
| 55 |
-
|
| 56 |
|
| 57 |
* `inverse=True` -> lower raw value is better (WER, memory). Strength = 1 - norm.
|
| 58 |
* `inverse=False` -> higher raw value is better (RTF). Strength = norm.
|
|
@@ -64,7 +151,7 @@ def _log_normalize(values: np.ndarray, *, inverse: bool) -> np.ndarray:
|
|
| 64 |
mask = np.isfinite(v) & (v > 0)
|
| 65 |
if not mask.any():
|
| 66 |
return out
|
| 67 |
-
logv = np.
|
| 68 |
lo = float(np.min(logv))
|
| 69 |
hi = float(np.max(logv))
|
| 70 |
if hi - lo < 1e-9:
|
|
@@ -73,6 +160,8 @@ def _log_normalize(values: np.ndarray, *, inverse: bool) -> np.ndarray:
|
|
| 73 |
out[mask] = (logv - lo) / (hi - lo)
|
| 74 |
if inverse:
|
| 75 |
out[mask] = 1.0 - out[mask]
|
|
|
|
|
|
|
| 76 |
return out
|
| 77 |
|
| 78 |
|
|
@@ -82,15 +171,15 @@ def _log_normalize(values: np.ndarray, *, inverse: bool) -> np.ndarray:
|
|
| 82 |
# raw_display_name: human-readable name of the underlying metric, shown in the hover tooltip.
|
| 83 |
# raw_unit : optional unit suffix for the raw value in the tooltip.
|
| 84 |
#
|
| 85 |
-
# Live
|
| 86 |
-
# Plus Speed (RTF) and Compactness (weight-file size).
|
| 87 |
_RADAR_AXES: tuple[tuple[str, str, str | None, bool, str, str], ...] = (
|
| 88 |
-
("Anechoic",
|
| 89 |
-
("
|
| 90 |
-
("
|
| 91 |
-
("
|
| 92 |
-
("
|
| 93 |
-
("
|
|
|
|
| 94 |
# Compactness: smaller model -> more compact -> farther from centre.
|
| 95 |
# `inverse=True` makes `_log_normalize` emit `1 - norm`, so the smallest
|
| 96 |
# observed model maps to strength = 1.0 (radar edge) and the largest
|
|
@@ -154,13 +243,17 @@ def _raw_to_analytics_df(raw: list[dict]) -> pd.DataFrame:
|
|
| 154 |
"""
|
| 155 |
Parse leaderboard rows; coerce scenario WER + timing columns to float (NaN if missing).
|
| 156 |
|
| 157 |
-
|
| 158 |
-
* `avg_wer` — mean of live scenario WERs present for that row.
|
| 159 |
-
* `
|
|
|
|
| 160 |
"""
|
|
|
|
|
|
|
| 161 |
if not raw:
|
| 162 |
return pd.DataFrame()
|
| 163 |
-
|
|
|
|
| 164 |
if "model_id" not in df.columns:
|
| 165 |
return pd.DataFrame()
|
| 166 |
for m in SCENARIO_METRICS:
|
|
@@ -178,6 +271,8 @@ def _raw_to_analytics_df(raw: list[dict]) -> pd.DataFrame:
|
|
| 178 |
df["num_params_m"] = df["num_params"] / 1e6
|
| 179 |
live_cols = [c for c in LIVE_SCENARIO_KEYS if c in df.columns]
|
| 180 |
df["avg_wer"] = df[live_cols].mean(axis=1, skipna=True) if live_cols else np.nan
|
|
|
|
|
|
|
| 181 |
df["model_size_mb"] = df["model_id"].apply(_cached_model_size_mb)
|
| 182 |
return df
|
| 183 |
|
|
@@ -191,10 +286,61 @@ def available_metric_keys(df: pd.DataFrame) -> list[str]:
|
|
| 191 |
return keys
|
| 192 |
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
def plot_robustness_radar(
|
| 195 |
df: pd.DataFrame,
|
| 196 |
model_ids: Sequence[str],
|
| 197 |
-
title: str = "
|
| 198 |
) -> go.Figure:
|
| 199 |
"""
|
| 200 |
Fixed-axis robustness radar — **strength** view (lines only, no fill).
|
|
@@ -203,7 +349,7 @@ def plot_robustness_radar(
|
|
| 203 |
size / parameter count are inverted before normalisation; RTF is not.
|
| 204 |
Strength is a 0–1 score computed per-axis across **all** leaderboard rows
|
| 205 |
with a log transform + global min/max, so axes are directly comparable
|
| 206 |
-
across models. Missing
|
| 207 |
|
| 208 |
The Compactness axis prefers `num_params_m` (recorded for every modern
|
| 209 |
eval) and falls back to `model_size_mb` only when params are unavailable;
|
|
@@ -257,7 +403,10 @@ def plot_robustness_radar(
|
|
| 257 |
for _lab, strength, raw_series, _src, inv, raw_disp, raw_unit, using_fb in axes:
|
| 258 |
s = strength[idx]
|
| 259 |
rv = raw_series[idx]
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
| 261 |
direction = "lower is better" if inv else "higher is better"
|
| 262 |
if not np.isfinite(rv):
|
| 263 |
raw_text.append(f"{raw_disp}: — ({direction})")
|
|
@@ -290,8 +439,11 @@ def plot_robustness_radar(
|
|
| 290 |
fig.update_layout(
|
| 291 |
polar=dict(
|
| 292 |
radialaxis=dict(
|
| 293 |
-
visible=True,
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
| 295 |
),
|
| 296 |
angularaxis=dict(direction="clockwise", rotation=90),
|
| 297 |
),
|
|
@@ -322,7 +474,7 @@ def plot_compare_models_across_scenarios(
|
|
| 322 |
title: str = "WER across models — one line per scenario (click legend to show/hide scenarios)",
|
| 323 |
) -> go.Figure:
|
| 324 |
"""
|
| 325 |
-
Line chart: x = model (sorted by
|
| 326 |
|
| 327 |
Scaling to more models now just extends the x axis instead of adding more traces,
|
| 328 |
which keeps the legend small (≤ #scenarios) and makes cross-scenario comparisons
|
|
@@ -339,8 +491,10 @@ def plot_compare_models_across_scenarios(
|
|
| 339 |
if sub.empty:
|
| 340 |
return _empty_fig("No matching models.")
|
| 341 |
|
| 342 |
-
# Sort models by
|
| 343 |
-
if "
|
|
|
|
|
|
|
| 344 |
sub = sub.sort_values("avg_wer", ascending=True, na_position="last")
|
| 345 |
sub = sub.reset_index(drop=True)
|
| 346 |
|
|
@@ -373,7 +527,7 @@ def plot_compare_models_across_scenarios(
|
|
| 373 |
tickangle = -45 if n_models > 8 else -25
|
| 374 |
fig.update_layout(
|
| 375 |
title=dict(text=title, x=0.5, xanchor="center"),
|
| 376 |
-
xaxis=dict(title="Model (sorted by
|
| 377 |
yaxis=dict(title="WER (lower is better)", rangemode="tozero"),
|
| 378 |
template=_TEMPLATE,
|
| 379 |
height=_FIG_HEIGHT,
|
|
@@ -400,7 +554,7 @@ def plot_scenario_heatmap(
|
|
| 400 |
- and how the top of the leaderboard compares to the rest.
|
| 401 |
|
| 402 |
`metric_keys` selects which scenario columns to show (in given order). Models
|
| 403 |
-
are sorted by
|
| 404 |
stays legible even with hundreds of leaderboard entries.
|
| 405 |
"""
|
| 406 |
metric_keys = [k for k in metric_keys if k in df.columns]
|
|
@@ -408,7 +562,9 @@ def plot_scenario_heatmap(
|
|
| 408 |
return _empty_fig("Not enough data for the heatmap.")
|
| 409 |
|
| 410 |
d = df.copy()
|
| 411 |
-
if "
|
|
|
|
|
|
|
| 412 |
d = d.sort_values("avg_wer", ascending=True, na_position="last")
|
| 413 |
d = d.head(max(1, int(top_n))).reset_index(drop=True)
|
| 414 |
|
|
@@ -465,12 +621,13 @@ def plot_scenario_heatmap(
|
|
| 465 |
height = int(min(900, max(_FIG_HEIGHT, 80 + 18 * n_rows)))
|
| 466 |
fig.update_layout(
|
| 467 |
title=dict(
|
| 468 |
-
text=title
|
|
|
|
| 469 |
x=0.5,
|
| 470 |
xanchor="center",
|
| 471 |
),
|
| 472 |
xaxis=dict(title="Scenario", tickangle=0, side="top", automargin=True),
|
| 473 |
-
yaxis=dict(title="Model (sorted by
|
| 474 |
template=_TEMPLATE,
|
| 475 |
height=height,
|
| 476 |
margin=dict(l=80, r=40, t=80, b=40),
|
|
@@ -480,23 +637,24 @@ def plot_scenario_heatmap(
|
|
| 480 |
|
| 481 |
def plot_clean_vs_reverb_scatter(
|
| 482 |
df: pd.DataFrame,
|
| 483 |
-
title: str = "
|
| 484 |
) -> go.Figure:
|
| 485 |
"""
|
| 486 |
-
Scatter: x =
|
| 487 |
"""
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
d
|
| 493 |
-
d[
|
| 494 |
-
d =
|
|
|
|
| 495 |
if d.empty:
|
| 496 |
-
return _empty_fig("No complete
|
| 497 |
|
| 498 |
fig = go.Figure()
|
| 499 |
-
mx = float(max(d[
|
| 500 |
fig.add_trace(
|
| 501 |
go.Scatter(
|
| 502 |
x=[0, mx],
|
|
@@ -510,21 +668,21 @@ def plot_clean_vs_reverb_scatter(
|
|
| 510 |
short = d["model_id"].str.split("/").str[-1].str[:28]
|
| 511 |
fig.add_trace(
|
| 512 |
go.Scatter(
|
| 513 |
-
x=d[
|
| 514 |
-
y=d[
|
| 515 |
mode="markers",
|
| 516 |
text=short,
|
| 517 |
marker=dict(size=10, opacity=0.85),
|
| 518 |
hovertemplate=(
|
| 519 |
-
"<b>%{text}</b><br>
|
| 520 |
),
|
| 521 |
)
|
| 522 |
)
|
| 523 |
|
| 524 |
fig.update_layout(
|
| 525 |
title=dict(text=title, x=0.5, xanchor="center"),
|
| 526 |
-
xaxis=dict(title="WER —
|
| 527 |
-
yaxis=dict(title="WER —
|
| 528 |
template=_TEMPLATE,
|
| 529 |
height=_FIG_HEIGHT,
|
| 530 |
margin=dict(l=60, r=40, t=60, b=60),
|
|
@@ -556,27 +714,64 @@ def plot_latency_vs_wer(
|
|
| 556 |
"No rows with both timing and WER. Run new evaluations to populate eval_wall_time_s / RTF."
|
| 557 |
)
|
| 558 |
|
| 559 |
-
|
| 560 |
-
y_label = (
|
| 561 |
x_title = {
|
| 562 |
"eval_wall_time_s": "Total inference wall time (s)",
|
| 563 |
"eval_rtf": "RTF (audio seconds / inference seconds)",
|
| 564 |
"num_params_m": "Parameters (millions)",
|
| 565 |
}.get(x_key, x_key)
|
| 566 |
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
go.Scatter(
|
| 571 |
-
x=d[x_key],
|
| 572 |
-
y=d[y_metric_key],
|
| 573 |
mode="markers",
|
| 574 |
-
text=
|
| 575 |
-
|
| 576 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
)
|
| 578 |
-
|
| 579 |
-
|
|
|
|
|
|
|
| 580 |
ttl = title or f"Speed vs accuracy — {y_label}"
|
| 581 |
fig.update_layout(
|
| 582 |
title=dict(text=ttl, x=0.5, xanchor="center"),
|
|
@@ -585,6 +780,8 @@ def plot_latency_vs_wer(
|
|
| 585 |
template=_TEMPLATE,
|
| 586 |
height=_FIG_HEIGHT,
|
| 587 |
margin=dict(l=60, r=40, t=60, b=60),
|
|
|
|
|
|
|
| 588 |
)
|
| 589 |
return fig
|
| 590 |
|
|
@@ -601,8 +798,11 @@ def plot_scenario_bar_summary(df: pd.DataFrame, top_n: int = 8) -> go.Figure:
|
|
| 601 |
return _empty_fig("No core WER columns.")
|
| 602 |
|
| 603 |
d = df.copy()
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
| 606 |
|
| 607 |
scenarios = cols
|
| 608 |
x_labels = [metric_by_key(c).short if metric_by_key(c) else c for c in scenarios]
|
|
@@ -625,7 +825,7 @@ def plot_scenario_bar_summary(df: pd.DataFrame, top_n: int = 8) -> go.Figure:
|
|
| 625 |
fig.update_layout(
|
| 626 |
barmode="group",
|
| 627 |
title=dict(
|
| 628 |
-
text=f"
|
| 629 |
x=0.5,
|
| 630 |
xanchor="center",
|
| 631 |
),
|
|
|
|
| 17 |
|
| 18 |
from metrics_config import LIVE_SCENARIO_KEYS, SCENARIO_METRICS, metric_by_key
|
| 19 |
|
| 20 |
+
# Far-field ranking weights (renormalized per model over metrics that have data).
|
| 21 |
+
FAR_FIELD_WEIGHTS: dict[str, float] = {
|
| 22 |
+
"wer_anechoic_speech": 0.05,
|
| 23 |
+
"wer_lab_measured": 0.10,
|
| 24 |
+
"wer_lab_simulated": 0.10,
|
| 25 |
+
"wer_realistic_high_snr": 0.20,
|
| 26 |
+
"wer_realistic_low_snr": 0.35,
|
| 27 |
+
"wer_moving_sources": 0.20,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
# Consistent height for Gradio Plot
|
| 31 |
_FIG_HEIGHT = 460
|
| 32 |
_TEMPLATE = "plotly_white"
|
|
|
|
| 60 |
return mb
|
| 61 |
|
| 62 |
|
| 63 |
+
def _coerce_positive_float(v) -> float | None:
|
| 64 |
+
if v is None or v == "":
|
| 65 |
+
return None
|
| 66 |
+
try:
|
| 67 |
+
x = float(v)
|
| 68 |
+
return x if np.isfinite(x) and x > 0 else None
|
| 69 |
+
except (TypeError, ValueError):
|
| 70 |
+
return None
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def compute_wer_log_bounds(rows: list[dict], keys: Sequence[str]) -> dict[str, tuple[float, float]]:
|
| 74 |
+
"""Per-key log10(WER) bounds across leaderboard rows (WER > 0 only)."""
|
| 75 |
+
bounds: dict[str, tuple[float, float]] = {}
|
| 76 |
+
for k in keys:
|
| 77 |
+
vals: list[float] = []
|
| 78 |
+
for r in rows:
|
| 79 |
+
x = _coerce_positive_float(r.get(k))
|
| 80 |
+
if x is not None:
|
| 81 |
+
vals.append(float(np.log10(x)))
|
| 82 |
+
if vals:
|
| 83 |
+
bounds[k] = (float(min(vals)), float(max(vals)))
|
| 84 |
+
else:
|
| 85 |
+
bounds[k] = (0.0, 0.0)
|
| 86 |
+
return bounds
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def wer_quality_from_bounds(wer, lo: float, hi: float) -> float | None:
|
| 90 |
+
"""Inverted log10 min-max quality in [0, 1]; lower WER -> higher quality."""
|
| 91 |
+
x = _coerce_positive_float(wer)
|
| 92 |
+
if x is None:
|
| 93 |
+
return None
|
| 94 |
+
lv = float(np.log10(x))
|
| 95 |
+
if hi - lo < 1e-9:
|
| 96 |
+
return 0.5
|
| 97 |
+
t = (lv - lo) / (hi - lo)
|
| 98 |
+
t = float(np.clip(t, 0.0, 1.0))
|
| 99 |
+
return 1.0 - t
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def far_field_score_for_row(
|
| 103 |
+
row: dict,
|
| 104 |
+
bounds: dict[str, tuple[float, float]],
|
| 105 |
+
metric_keys: Sequence[str] | None = None,
|
| 106 |
+
) -> float | None:
|
| 107 |
+
"""Composite score in [0, 100]; higher is better. Missing metrics drop out with weight renorm."""
|
| 108 |
+
keys = list(metric_keys) if metric_keys is not None else list(LIVE_SCENARIO_KEYS)
|
| 109 |
+
num = 0.0
|
| 110 |
+
den = 0.0
|
| 111 |
+
for k in keys:
|
| 112 |
+
w = FAR_FIELD_WEIGHTS.get(k)
|
| 113 |
+
if w is None:
|
| 114 |
+
continue
|
| 115 |
+
lo, hi = bounds.get(k, (0.0, 0.0))
|
| 116 |
+
q = wer_quality_from_bounds(row.get(k), lo, hi)
|
| 117 |
+
if q is None:
|
| 118 |
+
continue
|
| 119 |
+
num += w * q
|
| 120 |
+
den += w
|
| 121 |
+
if den <= 0:
|
| 122 |
+
return None
|
| 123 |
+
return round(100.0 * num / den, 2)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def compute_far_field_score_map(rows: list[dict]) -> dict[str, float]:
|
| 127 |
+
"""model_id -> score for sorting CSV rows."""
|
| 128 |
+
bounds = compute_wer_log_bounds(rows, LIVE_SCENARIO_KEYS)
|
| 129 |
+
out: dict[str, float] = {}
|
| 130 |
+
for r in rows:
|
| 131 |
+
mid = r.get("model_id")
|
| 132 |
+
if not mid:
|
| 133 |
+
continue
|
| 134 |
+
s = far_field_score_for_row(r, bounds)
|
| 135 |
+
if s is not None:
|
| 136 |
+
out[str(mid)] = float(s)
|
| 137 |
+
return out
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _log_normalize(values: np.ndarray, *, inverse: bool, strength_floor: float = 0.1) -> np.ndarray:
|
| 141 |
"""
|
| 142 |
+
Log10-normalize an array across all models to a "strength" score (default display floor 0.1).
|
| 143 |
|
| 144 |
* `inverse=True` -> lower raw value is better (WER, memory). Strength = 1 - norm.
|
| 145 |
* `inverse=False` -> higher raw value is better (RTF). Strength = norm.
|
|
|
|
| 151 |
mask = np.isfinite(v) & (v > 0)
|
| 152 |
if not mask.any():
|
| 153 |
return out
|
| 154 |
+
logv = np.log10(np.maximum(v[mask], 1e-12))
|
| 155 |
lo = float(np.min(logv))
|
| 156 |
hi = float(np.max(logv))
|
| 157 |
if hi - lo < 1e-9:
|
|
|
|
| 160 |
out[mask] = (logv - lo) / (hi - lo)
|
| 161 |
if inverse:
|
| 162 |
out[mask] = 1.0 - out[mask]
|
| 163 |
+
fin = np.isfinite(out)
|
| 164 |
+
out[fin] = np.clip(np.maximum(out[fin], strength_floor), strength_floor, 1.0)
|
| 165 |
return out
|
| 166 |
|
| 167 |
|
|
|
|
| 171 |
# raw_display_name: human-readable name of the underlying metric, shown in the hover tooltip.
|
| 172 |
# raw_unit : optional unit suffix for the raw value in the tooltip.
|
| 173 |
#
|
| 174 |
+
# Live far-field scenario axes + Speed (RTF) + Compactness (parameters / Hub size).
|
|
|
|
| 175 |
_RADAR_AXES: tuple[tuple[str, str, str | None, bool, str, str], ...] = (
|
| 176 |
+
("Anechoic speech", "wer_anechoic_speech", None, True, "Anechoic speech WER", ""),
|
| 177 |
+
("Lab measured", "wer_lab_measured", None, True, "Lab measured WER", ""),
|
| 178 |
+
("Lab simulated", "wer_lab_simulated", None, True, "Lab simulated WER", ""),
|
| 179 |
+
("Realistic Hi SNR", "wer_realistic_high_snr", None, True, "Realistic high-SNR WER", ""),
|
| 180 |
+
("Realistic Lo SNR", "wer_realistic_low_snr", None, True, "Realistic low-SNR WER", ""),
|
| 181 |
+
("Moving sources", "wer_moving_sources", None, True, "Moving sources WER", ""),
|
| 182 |
+
("Speed", "eval_rtf", None, False, "RTF", "× realtime"),
|
| 183 |
# Compactness: smaller model -> more compact -> farther from centre.
|
| 184 |
# `inverse=True` makes `_log_normalize` emit `1 - norm`, so the smallest
|
| 185 |
# observed model maps to strength = 1.0 (radar edge) and the largest
|
|
|
|
| 243 |
"""
|
| 244 |
Parse leaderboard rows; coerce scenario WER + timing columns to float (NaN if missing).
|
| 245 |
|
| 246 |
+
Computes:
|
| 247 |
+
* ``avg_wer`` — mean of live scenario WERs present for that row (reference).
|
| 248 |
+
* ``ff_score`` — far-field composite score (0–100, higher is better).
|
| 249 |
+
* ``model_size_mb`` — total weight-file size fetched from the Hub (cached).
|
| 250 |
"""
|
| 251 |
+
from init import normalize_legacy_csv_row
|
| 252 |
+
|
| 253 |
if not raw:
|
| 254 |
return pd.DataFrame()
|
| 255 |
+
rows = [normalize_legacy_csv_row(dict(r)) for r in raw]
|
| 256 |
+
df = pd.DataFrame(rows)
|
| 257 |
if "model_id" not in df.columns:
|
| 258 |
return pd.DataFrame()
|
| 259 |
for m in SCENARIO_METRICS:
|
|
|
|
| 271 |
df["num_params_m"] = df["num_params"] / 1e6
|
| 272 |
live_cols = [c for c in LIVE_SCENARIO_KEYS if c in df.columns]
|
| 273 |
df["avg_wer"] = df[live_cols].mean(axis=1, skipna=True) if live_cols else np.nan
|
| 274 |
+
bounds = compute_wer_log_bounds(rows, LIVE_SCENARIO_KEYS)
|
| 275 |
+
df["ff_score"] = [far_field_score_for_row(r, bounds) for r in rows]
|
| 276 |
df["model_size_mb"] = df["model_id"].apply(_cached_model_size_mb)
|
| 277 |
return df
|
| 278 |
|
|
|
|
| 286 |
return keys
|
| 287 |
|
| 288 |
|
| 289 |
+
def plot_leaderboard_score_bars(
|
| 290 |
+
df: pd.DataFrame,
|
| 291 |
+
top_n: int = 40,
|
| 292 |
+
title: str | None = None,
|
| 293 |
+
) -> go.Figure:
|
| 294 |
+
"""
|
| 295 |
+
Ranked horizontal bar chart of ``ff_score`` (Artificial Analysis–style highlights).
|
| 296 |
+
"""
|
| 297 |
+
if df.empty or "ff_score" not in df.columns:
|
| 298 |
+
return _empty_fig("No leaderboard scores yet.")
|
| 299 |
+
d = df[["model_id", "ff_score"]].dropna(subset=["ff_score"]).copy()
|
| 300 |
+
if d.empty:
|
| 301 |
+
return _empty_fig("No scores computed.")
|
| 302 |
+
n = max(1, int(top_n))
|
| 303 |
+
d = d.sort_values("ff_score", ascending=False, na_position="last").head(n)
|
| 304 |
+
labels = d["model_id"].str.split("/").str[-1].str[:42]
|
| 305 |
+
scores = d["ff_score"].astype(float)
|
| 306 |
+
fig = go.Figure(
|
| 307 |
+
go.Bar(
|
| 308 |
+
x=scores,
|
| 309 |
+
y=labels,
|
| 310 |
+
orientation="h",
|
| 311 |
+
marker=dict(
|
| 312 |
+
color=scores,
|
| 313 |
+
colorscale="Viridis",
|
| 314 |
+
cmid=50,
|
| 315 |
+
cmin=0,
|
| 316 |
+
cmax=100,
|
| 317 |
+
showscale=True,
|
| 318 |
+
colorbar=dict(title="Score"),
|
| 319 |
+
),
|
| 320 |
+
text=[f"{s:.1f}" for s in scores],
|
| 321 |
+
textposition="outside",
|
| 322 |
+
hovertemplate="<b>%{y}</b><br>Score: %{x:.2f} / 100<extra></extra>",
|
| 323 |
+
)
|
| 324 |
+
)
|
| 325 |
+
fig.update_layout(
|
| 326 |
+
title=dict(
|
| 327 |
+
text=title or f"Far-field score (top {len(d)} models) · Higher is better",
|
| 328 |
+
x=0.5,
|
| 329 |
+
xanchor="center",
|
| 330 |
+
),
|
| 331 |
+
xaxis=dict(title="Score (out of 100)", range=[0, min(110, float(scores.max()) * 1.15 + 5)]),
|
| 332 |
+
yaxis=dict(autorange="reversed"),
|
| 333 |
+
template=_TEMPLATE,
|
| 334 |
+
height=int(min(920, max(_FIG_HEIGHT, 100 + 22 * len(d)))),
|
| 335 |
+
margin=dict(l=200, r=100, t=60, b=50),
|
| 336 |
+
)
|
| 337 |
+
return fig
|
| 338 |
+
|
| 339 |
+
|
| 340 |
def plot_robustness_radar(
|
| 341 |
df: pd.DataFrame,
|
| 342 |
model_ids: Sequence[str],
|
| 343 |
+
title: str = "Robustness radar — log10-scaled strength (floor 0.1), farther from centre is better",
|
| 344 |
) -> go.Figure:
|
| 345 |
"""
|
| 346 |
Fixed-axis robustness radar — **strength** view (lines only, no fill).
|
|
|
|
| 349 |
size / parameter count are inverted before normalisation; RTF is not.
|
| 350 |
Strength is a 0–1 score computed per-axis across **all** leaderboard rows
|
| 351 |
with a log transform + global min/max, so axes are directly comparable
|
| 352 |
+
across models. Missing or invalid raw values use strength **0.1** (display floor).
|
| 353 |
|
| 354 |
The Compactness axis prefers `num_params_m` (recorded for every modern
|
| 355 |
eval) and falls back to `model_size_mb` only when params are unavailable;
|
|
|
|
| 403 |
for _lab, strength, raw_series, _src, inv, raw_disp, raw_unit, using_fb in axes:
|
| 404 |
s = strength[idx]
|
| 405 |
rv = raw_series[idx]
|
| 406 |
+
if np.isfinite(s):
|
| 407 |
+
r_vals.append(float(max(s, 0.1)))
|
| 408 |
+
else:
|
| 409 |
+
r_vals.append(0.1)
|
| 410 |
direction = "lower is better" if inv else "higher is better"
|
| 411 |
if not np.isfinite(rv):
|
| 412 |
raw_text.append(f"{raw_disp}: — ({direction})")
|
|
|
|
| 439 |
fig.update_layout(
|
| 440 |
polar=dict(
|
| 441 |
radialaxis=dict(
|
| 442 |
+
visible=True,
|
| 443 |
+
range=[0.1, 1.0],
|
| 444 |
+
showticklabels=True,
|
| 445 |
+
tickformat=".2f",
|
| 446 |
+
tickvals=[0.1, 0.25, 0.5, 0.75, 1.0],
|
| 447 |
),
|
| 448 |
angularaxis=dict(direction="clockwise", rotation=90),
|
| 449 |
),
|
|
|
|
| 474 |
title: str = "WER across models — one line per scenario (click legend to show/hide scenarios)",
|
| 475 |
) -> go.Figure:
|
| 476 |
"""
|
| 477 |
+
Line chart: x = model (sorted by far-field score), y = WER, one trace per scenario.
|
| 478 |
|
| 479 |
Scaling to more models now just extends the x axis instead of adding more traces,
|
| 480 |
which keeps the legend small (≤ #scenarios) and makes cross-scenario comparisons
|
|
|
|
| 491 |
if sub.empty:
|
| 492 |
return _empty_fig("No matching models.")
|
| 493 |
|
| 494 |
+
# Sort models by far-field score (fallback: Average WER).
|
| 495 |
+
if "ff_score" in sub.columns:
|
| 496 |
+
sub = sub.sort_values("ff_score", ascending=False, na_position="last")
|
| 497 |
+
elif "avg_wer" in sub.columns:
|
| 498 |
sub = sub.sort_values("avg_wer", ascending=True, na_position="last")
|
| 499 |
sub = sub.reset_index(drop=True)
|
| 500 |
|
|
|
|
| 527 |
tickangle = -45 if n_models > 8 else -25
|
| 528 |
fig.update_layout(
|
| 529 |
title=dict(text=title, x=0.5, xanchor="center"),
|
| 530 |
+
xaxis=dict(title="Model (sorted by far-field score)", tickangle=tickangle, automargin=True),
|
| 531 |
yaxis=dict(title="WER (lower is better)", rangemode="tozero"),
|
| 532 |
template=_TEMPLATE,
|
| 533 |
height=_FIG_HEIGHT,
|
|
|
|
| 554 |
- and how the top of the leaderboard compares to the rest.
|
| 555 |
|
| 556 |
`metric_keys` selects which scenario columns to show (in given order). Models
|
| 557 |
+
are sorted by far-field score (fallback: mean WER) and capped at `top_n` rows so the chart
|
| 558 |
stays legible even with hundreds of leaderboard entries.
|
| 559 |
"""
|
| 560 |
metric_keys = [k for k in metric_keys if k in df.columns]
|
|
|
|
| 562 |
return _empty_fig("Not enough data for the heatmap.")
|
| 563 |
|
| 564 |
d = df.copy()
|
| 565 |
+
if "ff_score" in d.columns:
|
| 566 |
+
d = d.sort_values("ff_score", ascending=False, na_position="last")
|
| 567 |
+
elif "avg_wer" in d.columns:
|
| 568 |
d = d.sort_values("avg_wer", ascending=True, na_position="last")
|
| 569 |
d = d.head(max(1, int(top_n))).reset_index(drop=True)
|
| 570 |
|
|
|
|
| 621 |
height = int(min(900, max(_FIG_HEIGHT, 80 + 18 * n_rows)))
|
| 622 |
fig.update_layout(
|
| 623 |
title=dict(
|
| 624 |
+
text=title
|
| 625 |
+
or f"WER heatmap — top {n_rows} model(s) × {len(keep_keys)} scenario(s) (sorted by score)",
|
| 626 |
x=0.5,
|
| 627 |
xanchor="center",
|
| 628 |
),
|
| 629 |
xaxis=dict(title="Scenario", tickangle=0, side="top", automargin=True),
|
| 630 |
+
yaxis=dict(title="Model (sorted by far-field score)", automargin=True, autorange="reversed"),
|
| 631 |
template=_TEMPLATE,
|
| 632 |
height=height,
|
| 633 |
margin=dict(l=80, r=40, t=80, b=40),
|
|
|
|
| 637 |
|
| 638 |
def plot_clean_vs_reverb_scatter(
|
| 639 |
df: pd.DataFrame,
|
| 640 |
+
title: str = "Anechoic vs lab-simulated WER — points above the diagonal degrade more under simulation",
|
| 641 |
) -> go.Figure:
|
| 642 |
"""
|
| 643 |
+
Scatter: x = anechoic WER, y = lab-simulated WER. Models often sit above y=x when simulation is harder.
|
| 644 |
"""
|
| 645 |
+
c1, c2 = "wer_anechoic_speech", "wer_lab_simulated"
|
| 646 |
+
if df.empty or c1 not in df.columns or c2 not in df.columns:
|
| 647 |
+
return _empty_fig("Need anechoic and lab-simulated WER columns in the leaderboard.")
|
| 648 |
+
|
| 649 |
+
d = df[["model_id", c1, c2]].copy()
|
| 650 |
+
d[c1] = pd.to_numeric(d[c1], errors="coerce")
|
| 651 |
+
d[c2] = pd.to_numeric(d[c2], errors="coerce")
|
| 652 |
+
d = d.dropna(subset=[c1, c2])
|
| 653 |
if d.empty:
|
| 654 |
+
return _empty_fig("No complete anechoic / lab-simulated WER pairs yet.")
|
| 655 |
|
| 656 |
fig = go.Figure()
|
| 657 |
+
mx = float(max(d[c1].max(), d[c2].max()))
|
| 658 |
fig.add_trace(
|
| 659 |
go.Scatter(
|
| 660 |
x=[0, mx],
|
|
|
|
| 668 |
short = d["model_id"].str.split("/").str[-1].str[:28]
|
| 669 |
fig.add_trace(
|
| 670 |
go.Scatter(
|
| 671 |
+
x=d[c1],
|
| 672 |
+
y=d[c2],
|
| 673 |
mode="markers",
|
| 674 |
text=short,
|
| 675 |
marker=dict(size=10, opacity=0.85),
|
| 676 |
hovertemplate=(
|
| 677 |
+
"<b>%{text}</b><br>Anechoic WER: %{x:.4f}<br>Lab simulated WER: %{y:.4f}<extra></extra>"
|
| 678 |
),
|
| 679 |
)
|
| 680 |
)
|
| 681 |
|
| 682 |
fig.update_layout(
|
| 683 |
title=dict(text=title, x=0.5, xanchor="center"),
|
| 684 |
+
xaxis=dict(title="WER — anechoic speech (lower is better)", rangemode="tozero"),
|
| 685 |
+
yaxis=dict(title="WER — lab simulated (lower is better)", rangemode="tozero"),
|
| 686 |
template=_TEMPLATE,
|
| 687 |
height=_FIG_HEIGHT,
|
| 688 |
margin=dict(l=60, r=40, t=60, b=60),
|
|
|
|
| 714 |
"No rows with both timing and WER. Run new evaluations to populate eval_wall_time_s / RTF."
|
| 715 |
)
|
| 716 |
|
| 717 |
+
m_met = metric_by_key(y_metric_key)
|
| 718 |
+
y_label = (m_met.short if m_met else y_metric_key) + " WER"
|
| 719 |
x_title = {
|
| 720 |
"eval_wall_time_s": "Total inference wall time (s)",
|
| 721 |
"eval_rtf": "RTF (audio seconds / inference seconds)",
|
| 722 |
"num_params_m": "Parameters (millions)",
|
| 723 |
}.get(x_key, x_key)
|
| 724 |
|
| 725 |
+
xs = d[x_key].to_numpy(dtype=float)
|
| 726 |
+
ys = d[y_metric_key].to_numpy(dtype=float)
|
| 727 |
+
mx_f = float(np.nanmedian(xs))
|
| 728 |
+
my_f = float(np.nanmedian(ys))
|
| 729 |
+
|
| 730 |
+
def _bucket(xv: float, yv: float) -> tuple[str, str]:
|
| 731 |
+
if x_key == "eval_rtf":
|
| 732 |
+
fast = xv >= mx_f
|
| 733 |
+
else:
|
| 734 |
+
fast = xv <= mx_f
|
| 735 |
+
accurate = yv <= my_f
|
| 736 |
+
if fast and accurate:
|
| 737 |
+
return "Fast · accurate", "#22c55e"
|
| 738 |
+
if fast and not accurate:
|
| 739 |
+
return "Fast · higher WER", "#3b82f6"
|
| 740 |
+
if not fast and accurate:
|
| 741 |
+
return "Slower · lower WER", "#f59e0b"
|
| 742 |
+
return "Slower · higher WER", "#ef4444"
|
| 743 |
+
|
| 744 |
+
buckets = [_bucket(float(a), float(b)) for a, b in zip(xs, ys)]
|
| 745 |
+
labels = [b[0] for b in buckets]
|
| 746 |
+
|
| 747 |
+
fig = go.Figure()
|
| 748 |
+
for lab, color in (
|
| 749 |
+
("Fast · accurate", "#22c55e"),
|
| 750 |
+
("Fast · higher WER", "#3b82f6"),
|
| 751 |
+
("Slower · lower WER", "#f59e0b"),
|
| 752 |
+
("Slower · higher WER", "#ef4444"),
|
| 753 |
+
):
|
| 754 |
+
idx = [i for i, lb in enumerate(labels) if lb == lab]
|
| 755 |
+
if not idx:
|
| 756 |
+
continue
|
| 757 |
+
fig.add_trace(
|
| 758 |
go.Scatter(
|
| 759 |
+
x=d.iloc[idx][x_key],
|
| 760 |
+
y=d.iloc[idx][y_metric_key],
|
| 761 |
mode="markers",
|
| 762 |
+
text=d.iloc[idx]["model_id"].str.split("/").str[-1].str[:28],
|
| 763 |
+
name=lab,
|
| 764 |
+
legendgroup=lab,
|
| 765 |
+
marker=dict(size=11, opacity=0.9, color=color, line=dict(width=0.6, color="white")),
|
| 766 |
+
hovertemplate=(
|
| 767 |
+
f"<b>%{{text}}</b><br>{lab}<br>{x_title}: %{{x:.4f}}<br>"
|
| 768 |
+
f"{y_label}: %{{y:.4f}}<extra></extra>"
|
| 769 |
+
),
|
| 770 |
)
|
| 771 |
+
)
|
| 772 |
+
|
| 773 |
+
fig.add_vline(x=mx_f, line_dash="dot", line_color="rgba(0,0,0,0.25)")
|
| 774 |
+
fig.add_hline(y=my_f, line_dash="dot", line_color="rgba(0,0,0,0.25)")
|
| 775 |
ttl = title or f"Speed vs accuracy — {y_label}"
|
| 776 |
fig.update_layout(
|
| 777 |
title=dict(text=ttl, x=0.5, xanchor="center"),
|
|
|
|
| 780 |
template=_TEMPLATE,
|
| 781 |
height=_FIG_HEIGHT,
|
| 782 |
margin=dict(l=60, r=40, t=60, b=60),
|
| 783 |
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
| 784 |
+
showlegend=True,
|
| 785 |
)
|
| 786 |
return fig
|
| 787 |
|
|
|
|
| 798 |
return _empty_fig("No core WER columns.")
|
| 799 |
|
| 800 |
d = df.copy()
|
| 801 |
+
if "ff_score" in d.columns:
|
| 802 |
+
d = d.sort_values("ff_score", ascending=False, na_position="last").head(int(top_n))
|
| 803 |
+
else:
|
| 804 |
+
d["_avg"] = d[cols].mean(axis=1, skipna=True)
|
| 805 |
+
d = d.sort_values("_avg", ascending=True).head(int(top_n))
|
| 806 |
|
| 807 |
scenarios = cols
|
| 808 |
x_labels = [metric_by_key(c).short if metric_by_key(c) else c for c in scenarios]
|
|
|
|
| 825 |
fig.update_layout(
|
| 826 |
barmode="group",
|
| 827 |
title=dict(
|
| 828 |
+
text=f"WER by scenario — top {len(d)} models by score (click legend to hide/show)",
|
| 829 |
x=0.5,
|
| 830 |
xanchor="center",
|
| 831 |
),
|
app.py
CHANGED
|
@@ -3,7 +3,6 @@ import time
|
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
| 6 |
-
import numpy as np
|
| 7 |
from constants import (
|
| 8 |
ABOUT_TEXT,
|
| 9 |
APP_TITLE,
|
|
@@ -16,17 +15,20 @@ from constants import (
|
|
| 16 |
import analytics
|
| 17 |
from backends import default_family_id
|
| 18 |
import job_queue
|
| 19 |
-
from metrics_config import SCENARIO_METRICS
|
| 20 |
|
| 21 |
from init import (
|
| 22 |
is_model_on_hub,
|
| 23 |
load_raw_results,
|
| 24 |
load_results,
|
|
|
|
| 25 |
)
|
| 26 |
from utils_display import (
|
| 27 |
AutoEvalColumn,
|
| 28 |
SCENARIO_DISPLAY_COLS,
|
|
|
|
| 29 |
fields,
|
|
|
|
| 30 |
styled_error,
|
| 31 |
styled_message,
|
| 32 |
styled_warning,
|
|
@@ -38,7 +40,7 @@ from utils_display import (
|
|
| 38 |
|
| 39 |
COLS = [c.name for c in fields(AutoEvalColumn)]
|
| 40 |
TYPES = [c.type for c in fields(AutoEvalColumn)]
|
| 41 |
-
|
| 42 |
MODEL_COL = AutoEvalColumn.model.name # "Model"
|
| 43 |
|
| 44 |
# Load initial data
|
|
@@ -61,31 +63,43 @@ def filter_main_table(search_query, selected_columns):
|
|
| 61 |
)
|
| 62 |
filtered_df = filtered_df[mask]
|
| 63 |
|
| 64 |
-
# Hide toggleable columns that the user unchecked. Model +
|
| 65 |
-
all_toggleable = [c for c in COLS if c not in (MODEL_COL,
|
| 66 |
columns_to_hide = set(all_toggleable) - set(selected_columns)
|
| 67 |
filtered_df = filtered_df[[c for c in filtered_df.columns if c not in columns_to_hide]]
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
]
|
| 73 |
-
if
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
try:
|
| 84 |
-
return (0, float(v))
|
| 85 |
except Exception:
|
| 86 |
return (1, 0.0)
|
| 87 |
|
| 88 |
-
filtered_df = filtered_df.assign(_sort=filtered_df[
|
| 89 |
filtered_df = filtered_df.sort_values(by="_sort").drop(columns="_sort")
|
| 90 |
return filtered_df
|
| 91 |
|
|
@@ -95,6 +109,14 @@ def filter_main_table(search_query, selected_columns):
|
|
| 95 |
# ---------------------------------------------------------------------------
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def submit_model(model_id: str, submission_notes: str) -> str:
|
| 99 |
"""Validate and enqueue evaluation; a background worker runs jobs one at a time.
|
| 100 |
|
|
@@ -113,11 +135,12 @@ def submit_model(model_id: str, submission_notes: str) -> str:
|
|
| 113 |
if row["model_id"] == model_id:
|
| 114 |
return styled_message(
|
| 115 |
f"Model '{model_id}' has already been evaluated. "
|
| 116 |
-
f"Anechoic: {
|
| 117 |
-
f"
|
| 118 |
-
f"
|
| 119 |
-
f"
|
| 120 |
-
f"
|
|
|
|
| 121 |
)
|
| 122 |
|
| 123 |
on_hub, err_msg = is_model_on_hub(model_id)
|
|
@@ -142,11 +165,12 @@ def submit_model(model_id: str, submission_notes: str) -> str:
|
|
| 142 |
if row["model_id"] == model_id:
|
| 143 |
return styled_message(
|
| 144 |
f"Model '{model_id}' has already been evaluated. "
|
| 145 |
-
f"Anechoic: {
|
| 146 |
-
f"
|
| 147 |
-
f"
|
| 148 |
-
f"
|
| 149 |
-
f"
|
|
|
|
| 150 |
)
|
| 151 |
return styled_error("Could not enqueue; please try again.")
|
| 152 |
if err == "queue_full":
|
|
@@ -205,18 +229,21 @@ def _analytics_initial():
|
|
| 205 |
live_keys = [m.key for m in SCENARIO_METRICS if m.status == "live"]
|
| 206 |
line_default = [k for k in live_keys if k in avail] or list(avail)
|
| 207 |
sel = ids[: min(5, len(ids))]
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
| 211 |
fig_b = analytics.plot_scenario_bar_summary(df, 8)
|
| 212 |
return (
|
| 213 |
gr.update(choices=ids, value=sel),
|
| 214 |
gr.update(value=line_default),
|
| 215 |
gr.update(choices=_LATENCY_X_CHOICES, value="eval_wall_time_s"),
|
| 216 |
-
gr.update(choices=_LATENCY_Y_CHOICES, value="
|
| 217 |
-
|
| 218 |
fig_hm,
|
| 219 |
fig_lat,
|
|
|
|
| 220 |
fig_b,
|
| 221 |
)
|
| 222 |
|
|
@@ -233,12 +260,14 @@ def _analytics_apply(models, line_keys, top_n, latency_x, latency_y):
|
|
| 233 |
if not lk:
|
| 234 |
lk = avail
|
| 235 |
lx = latency_x if latency_x in ("eval_wall_time_s", "eval_rtf", "num_params_m") else "eval_wall_time_s"
|
| 236 |
-
ly = latency_y if latency_y in [m.key for m in SCENARIO_METRICS] else "
|
| 237 |
-
|
| 238 |
-
|
|
|
|
| 239 |
fig_lat = analytics.plot_latency_vs_wer(df, lx, ly)
|
| 240 |
-
|
| 241 |
-
|
|
|
|
| 242 |
|
| 243 |
|
| 244 |
# ---------------------------------------------------------------------------
|
|
@@ -481,19 +510,16 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 481 |
gr.Markdown(
|
| 482 |
"Charts are **interactive (Plotly)**: use the **legend** to show or hide traces "
|
| 483 |
"(single click toggles, double‑click isolates), drag to **zoom**, and **hover** for values.\n\n"
|
| 484 |
-
"**
|
| 485 |
-
"
|
| 486 |
-
"
|
| 487 |
-
"
|
| 488 |
-
"
|
| 489 |
-
"
|
| 490 |
-
"**
|
| 491 |
-
"
|
| 492 |
-
"
|
| 493 |
-
"**
|
| 494 |
-
"and a chosen scenario WER on the y axis (timing appears after evaluations that record "
|
| 495 |
-
"`eval_wall_time_s` / `eval_rtf`, and parameter counts after evaluations that record `num_params`).\n\n"
|
| 496 |
-
"**Bars** group core scenarios for the top models."
|
| 497 |
)
|
| 498 |
with gr.Row():
|
| 499 |
an_models = gr.Dropdown(
|
|
@@ -509,7 +535,7 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 509 |
80,
|
| 510 |
value=30,
|
| 511 |
step=1,
|
| 512 |
-
label="Top‑N models (heatmap &
|
| 513 |
scale=1,
|
| 514 |
)
|
| 515 |
an_line_metrics = gr.CheckboxGroup(
|
|
@@ -519,22 +545,24 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 519 |
)
|
| 520 |
an_apply = gr.Button("Apply / refresh charts", variant="primary")
|
| 521 |
with gr.Row():
|
| 522 |
-
|
| 523 |
an_plot_compare = gr.Plot(label="WER heatmap — models × scenarios (greener = better)")
|
| 524 |
gr.Markdown("### Speed vs accuracy")
|
| 525 |
with gr.Row():
|
| 526 |
an_latency_x = gr.Dropdown(
|
| 527 |
-
label="X axis (speed)",
|
| 528 |
choices=_LATENCY_X_CHOICES,
|
| 529 |
value="eval_wall_time_s",
|
| 530 |
)
|
| 531 |
an_latency_y = gr.Dropdown(
|
| 532 |
label="Y axis (WER scenario)",
|
| 533 |
choices=_LATENCY_Y_CHOICES,
|
| 534 |
-
value="
|
| 535 |
)
|
| 536 |
-
an_plot_latency = gr.Plot(label="Speed vs
|
| 537 |
-
|
|
|
|
|
|
|
| 538 |
|
| 539 |
an_apply.click(
|
| 540 |
fn=_analytics_apply,
|
|
@@ -546,9 +574,10 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 546 |
an_latency_y,
|
| 547 |
],
|
| 548 |
outputs=[
|
| 549 |
-
|
| 550 |
an_plot_compare,
|
| 551 |
an_plot_latency,
|
|
|
|
| 552 |
an_plot_bar,
|
| 553 |
],
|
| 554 |
)
|
|
@@ -591,9 +620,10 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 591 |
an_line_metrics,
|
| 592 |
an_latency_x,
|
| 593 |
an_latency_y,
|
| 594 |
-
|
| 595 |
an_plot_compare,
|
| 596 |
an_plot_latency,
|
|
|
|
| 597 |
an_plot_bar,
|
| 598 |
],
|
| 599 |
)
|
|
|
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
import pandas as pd
|
|
|
|
| 6 |
from constants import (
|
| 7 |
ABOUT_TEXT,
|
| 8 |
APP_TITLE,
|
|
|
|
| 15 |
import analytics
|
| 16 |
from backends import default_family_id
|
| 17 |
import job_queue
|
| 18 |
+
from metrics_config import LIVE_SCENARIO_KEYS, SCENARIO_METRICS
|
| 19 |
|
| 20 |
from init import (
|
| 21 |
is_model_on_hub,
|
| 22 |
load_raw_results,
|
| 23 |
load_results,
|
| 24 |
+
normalize_legacy_csv_row,
|
| 25 |
)
|
| 26 |
from utils_display import (
|
| 27 |
AutoEvalColumn,
|
| 28 |
SCENARIO_DISPLAY_COLS,
|
| 29 |
+
SCENARIO_DISPLAY_TO_KEY,
|
| 30 |
fields,
|
| 31 |
+
model_id_from_leaderboard_cell,
|
| 32 |
styled_error,
|
| 33 |
styled_message,
|
| 34 |
styled_warning,
|
|
|
|
| 40 |
|
| 41 |
COLS = [c.name for c in fields(AutoEvalColumn)]
|
| 42 |
TYPES = [c.type for c in fields(AutoEvalColumn)]
|
| 43 |
+
SCORE_COL = AutoEvalColumn.ff_score.name # "Score (out of 100)"
|
| 44 |
MODEL_COL = AutoEvalColumn.model.name # "Model"
|
| 45 |
|
| 46 |
# Load initial data
|
|
|
|
| 63 |
)
|
| 64 |
filtered_df = filtered_df[mask]
|
| 65 |
|
| 66 |
+
# Hide toggleable columns that the user unchecked. Model + Score stay visible.
|
| 67 |
+
all_toggleable = [c for c in COLS if c not in (MODEL_COL, SCORE_COL)]
|
| 68 |
columns_to_hide = set(all_toggleable) - set(selected_columns)
|
| 69 |
filtered_df = filtered_df[[c for c in filtered_df.columns if c not in columns_to_hide]]
|
| 70 |
|
| 71 |
+
raw_rows = load_raw_results()
|
| 72 |
+
for r in raw_rows:
|
| 73 |
+
normalize_legacy_csv_row(r)
|
| 74 |
+
bounds = analytics.compute_wer_log_bounds(raw_rows, LIVE_SCENARIO_KEYS)
|
| 75 |
+
raw_by_mid = {r["model_id"]: r for r in raw_rows}
|
| 76 |
+
|
| 77 |
+
visible_metric_keys = [
|
| 78 |
+
SCENARIO_DISPLAY_TO_KEY[c]
|
| 79 |
+
for c in SCENARIO_DISPLAY_COLS
|
| 80 |
+
if c in filtered_df.columns and c in selected_columns
|
| 81 |
]
|
| 82 |
+
if not visible_metric_keys:
|
| 83 |
+
visible_metric_keys = list(LIVE_SCENARIO_KEYS)
|
| 84 |
+
|
| 85 |
+
def compute_score(row):
|
| 86 |
+
mid = model_id_from_leaderboard_cell(row[MODEL_COL])
|
| 87 |
+
rdict = raw_by_mid.get(mid)
|
| 88 |
+
if not rdict:
|
| 89 |
+
return "NA"
|
| 90 |
+
s = analytics.far_field_score_for_row(rdict, bounds, metric_keys=visible_metric_keys)
|
| 91 |
+
return round(float(s), 2) if s is not None else "NA"
|
| 92 |
+
|
| 93 |
+
filtered_df[SCORE_COL] = filtered_df.apply(compute_score, axis=1)
|
| 94 |
+
|
| 95 |
+
# Sort by Score descending; NA last.
|
| 96 |
+
def _score_key(v):
|
| 97 |
try:
|
| 98 |
+
return (0, -float(v))
|
| 99 |
except Exception:
|
| 100 |
return (1, 0.0)
|
| 101 |
|
| 102 |
+
filtered_df = filtered_df.assign(_sort=filtered_df[SCORE_COL].apply(_score_key))
|
| 103 |
filtered_df = filtered_df.sort_values(by="_sort").drop(columns="_sort")
|
| 104 |
return filtered_df
|
| 105 |
|
|
|
|
| 109 |
# ---------------------------------------------------------------------------
|
| 110 |
|
| 111 |
|
| 112 |
+
def _wer_cell(row: dict, key: str) -> str:
|
| 113 |
+
normalize_legacy_csv_row(row)
|
| 114 |
+
v = row.get(key, "")
|
| 115 |
+
if v is None or (isinstance(v, str) and str(v).strip() == ""):
|
| 116 |
+
return "—"
|
| 117 |
+
return str(v).strip()
|
| 118 |
+
|
| 119 |
+
|
| 120 |
def submit_model(model_id: str, submission_notes: str) -> str:
|
| 121 |
"""Validate and enqueue evaluation; a background worker runs jobs one at a time.
|
| 122 |
|
|
|
|
| 135 |
if row["model_id"] == model_id:
|
| 136 |
return styled_message(
|
| 137 |
f"Model '{model_id}' has already been evaluated. "
|
| 138 |
+
f"Anechoic speech: {_wer_cell(row, 'wer_anechoic_speech')} | "
|
| 139 |
+
f"Lab measured: {_wer_cell(row, 'wer_lab_measured')} | "
|
| 140 |
+
f"Lab simulated: {_wer_cell(row, 'wer_lab_simulated')} | "
|
| 141 |
+
f"Realistic High SNR: {_wer_cell(row, 'wer_realistic_high_snr')} | "
|
| 142 |
+
f"Realistic Low SNR: {_wer_cell(row, 'wer_realistic_low_snr')} | "
|
| 143 |
+
f"Moving sources: {_wer_cell(row, 'wer_moving_sources')}"
|
| 144 |
)
|
| 145 |
|
| 146 |
on_hub, err_msg = is_model_on_hub(model_id)
|
|
|
|
| 165 |
if row["model_id"] == model_id:
|
| 166 |
return styled_message(
|
| 167 |
f"Model '{model_id}' has already been evaluated. "
|
| 168 |
+
f"Anechoic speech: {_wer_cell(row, 'wer_anechoic_speech')} | "
|
| 169 |
+
f"Lab measured: {_wer_cell(row, 'wer_lab_measured')} | "
|
| 170 |
+
f"Lab simulated: {_wer_cell(row, 'wer_lab_simulated')} | "
|
| 171 |
+
f"Realistic High SNR: {_wer_cell(row, 'wer_realistic_high_snr')} | "
|
| 172 |
+
f"Realistic Low SNR: {_wer_cell(row, 'wer_realistic_low_snr')} | "
|
| 173 |
+
f"Moving sources: {_wer_cell(row, 'wer_moving_sources')}"
|
| 174 |
)
|
| 175 |
return styled_error("Could not enqueue; please try again.")
|
| 176 |
if err == "queue_full":
|
|
|
|
| 229 |
live_keys = [m.key for m in SCENARIO_METRICS if m.status == "live"]
|
| 230 |
line_default = [k for k in live_keys if k in avail] or list(avail)
|
| 231 |
sel = ids[: min(5, len(ids))]
|
| 232 |
+
tn = 40
|
| 233 |
+
fig_score = analytics.plot_leaderboard_score_bars(df, top_n=tn)
|
| 234 |
+
fig_hm = analytics.plot_scenario_heatmap(df, line_default, top_n=tn)
|
| 235 |
+
fig_lat = analytics.plot_latency_vs_wer(df, "eval_wall_time_s", "wer_lab_simulated")
|
| 236 |
+
fig_radar = analytics.plot_robustness_radar(df, sel)
|
| 237 |
fig_b = analytics.plot_scenario_bar_summary(df, 8)
|
| 238 |
return (
|
| 239 |
gr.update(choices=ids, value=sel),
|
| 240 |
gr.update(value=line_default),
|
| 241 |
gr.update(choices=_LATENCY_X_CHOICES, value="eval_wall_time_s"),
|
| 242 |
+
gr.update(choices=_LATENCY_Y_CHOICES, value="wer_lab_simulated"),
|
| 243 |
+
fig_score,
|
| 244 |
fig_hm,
|
| 245 |
fig_lat,
|
| 246 |
+
fig_radar,
|
| 247 |
fig_b,
|
| 248 |
)
|
| 249 |
|
|
|
|
| 260 |
if not lk:
|
| 261 |
lk = avail
|
| 262 |
lx = latency_x if latency_x in ("eval_wall_time_s", "eval_rtf", "num_params_m") else "eval_wall_time_s"
|
| 263 |
+
ly = latency_y if latency_y in [m.key for m in SCENARIO_METRICS] else "wer_lab_simulated"
|
| 264 |
+
tn = int(top_n) if top_n else 40
|
| 265 |
+
fig_score = analytics.plot_leaderboard_score_bars(df, top_n=tn)
|
| 266 |
+
fig_hm = analytics.plot_scenario_heatmap(df, lk, top_n=tn)
|
| 267 |
fig_lat = analytics.plot_latency_vs_wer(df, lx, ly)
|
| 268 |
+
fig_radar = analytics.plot_robustness_radar(df, valid_models)
|
| 269 |
+
fig_b = analytics.plot_scenario_bar_summary(df, tn)
|
| 270 |
+
return fig_score, fig_hm, fig_lat, fig_radar, fig_b
|
| 271 |
|
| 272 |
|
| 273 |
# ---------------------------------------------------------------------------
|
|
|
|
| 510 |
gr.Markdown(
|
| 511 |
"Charts are **interactive (Plotly)**: use the **legend** to show or hide traces "
|
| 512 |
"(single click toggles, double‑click isolates), drag to **zoom**, and **hover** for values.\n\n"
|
| 513 |
+
"**Far-field score (bar)** ranks models by the same weighted **Score (out of 100)** as the "
|
| 514 |
+
"leaderboard (higher is better). **WER heatmap** shows the top‑N models (sorted by score) on "
|
| 515 |
+
"the y axis and chosen scenarios on the x axis — greener is lower WER.\n\n"
|
| 516 |
+
"**Speed vs accuracy** scatter uses median splits on the axes: points are **color-coded** "
|
| 517 |
+
"(green = faster-for-axis & lower WER, blue/orange/red = other quadrants). Dotted lines show "
|
| 518 |
+
"the medians used for those buckets.\n\n"
|
| 519 |
+
"**Robustness radar** plots selected models on **log₁₀‑scaled strength** axes for each live "
|
| 520 |
+
"scenario plus **RTF** and **compactness** (parameters / Hub size). Display strengths use a "
|
| 521 |
+
"**0.1 floor** so traces stay readable. Farther from the centre = better on every spoke.\n\n"
|
| 522 |
+
"**Grouped bars** compare raw WER across scenarios for the top models."
|
|
|
|
|
|
|
|
|
|
| 523 |
)
|
| 524 |
with gr.Row():
|
| 525 |
an_models = gr.Dropdown(
|
|
|
|
| 535 |
80,
|
| 536 |
value=30,
|
| 537 |
step=1,
|
| 538 |
+
label="Top‑N models (score bars, heatmap & grouped WER)",
|
| 539 |
scale=1,
|
| 540 |
)
|
| 541 |
an_line_metrics = gr.CheckboxGroup(
|
|
|
|
| 545 |
)
|
| 546 |
an_apply = gr.Button("Apply / refresh charts", variant="primary")
|
| 547 |
with gr.Row():
|
| 548 |
+
an_plot_score = gr.Plot(label="Far-field score — top N models · Higher is better")
|
| 549 |
an_plot_compare = gr.Plot(label="WER heatmap — models × scenarios (greener = better)")
|
| 550 |
gr.Markdown("### Speed vs accuracy")
|
| 551 |
with gr.Row():
|
| 552 |
an_latency_x = gr.Dropdown(
|
| 553 |
+
label="X axis (speed / compute)",
|
| 554 |
choices=_LATENCY_X_CHOICES,
|
| 555 |
value="eval_wall_time_s",
|
| 556 |
)
|
| 557 |
an_latency_y = gr.Dropdown(
|
| 558 |
label="Y axis (WER scenario)",
|
| 559 |
choices=_LATENCY_Y_CHOICES,
|
| 560 |
+
value="wer_lab_simulated",
|
| 561 |
)
|
| 562 |
+
an_plot_latency = gr.Plot(label="Speed vs accuracy — color-coded quadrants")
|
| 563 |
+
with gr.Row():
|
| 564 |
+
an_plot_radar = gr.Plot(label="Robustness radar — strength with 0.1 floor")
|
| 565 |
+
an_plot_bar = gr.Plot(label="WER by scenario — top models (grouped)")
|
| 566 |
|
| 567 |
an_apply.click(
|
| 568 |
fn=_analytics_apply,
|
|
|
|
| 574 |
an_latency_y,
|
| 575 |
],
|
| 576 |
outputs=[
|
| 577 |
+
an_plot_score,
|
| 578 |
an_plot_compare,
|
| 579 |
an_plot_latency,
|
| 580 |
+
an_plot_radar,
|
| 581 |
an_plot_bar,
|
| 582 |
],
|
| 583 |
)
|
|
|
|
| 620 |
an_line_metrics,
|
| 621 |
an_latency_x,
|
| 622 |
an_latency_y,
|
| 623 |
+
an_plot_score,
|
| 624 |
an_plot_compare,
|
| 625 |
an_plot_latency,
|
| 626 |
+
an_plot_radar,
|
| 627 |
an_plot_bar,
|
| 628 |
],
|
| 629 |
)
|
constants.py
CHANGED
|
@@ -38,26 +38,26 @@ BANNER = (
|
|
| 38 |
"<h1>FFASR</h1>"
|
| 39 |
"<div class='ffasr-subtitle'>Far‑Field Automatic Speech Recognition — a multi‑condition leaderboard</div>"
|
| 40 |
"<div class='ffasr-badges'>"
|
| 41 |
-
"<span class='ffasr-badge'>
|
| 42 |
-
"<span class='ffasr-badge'>
|
| 43 |
-
"<span class='ffasr-badge'>
|
| 44 |
-
"<span class='ffasr-badge'>Real RIR</span>"
|
| 45 |
-
"<span class='ffasr-badge'>Difficult</span>"
|
| 46 |
"<span class='ffasr-badge'>WER · RTF</span>"
|
| 47 |
"</div>"
|
| 48 |
"</div>"
|
| 49 |
)
|
| 50 |
|
| 51 |
INTRODUCTION_TEXT = (
|
| 52 |
-
"**FFASR** benchmarks speech‑recognition models on
|
| 53 |
-
"studio. Every model runs on the **same held‑out set**
|
| 54 |
-
"directly comparable.\n\n"
|
| 55 |
-
"
|
| 56 |
-
"**
|
| 57 |
-
"
|
| 58 |
-
"
|
|
|
|
| 59 |
"Paste a Hugging Face model id in the **Submit** tab — scoring runs server‑side and your model never sees "
|
| 60 |
-
"the evaluation audio directly. Use the **Analysis** tab for
|
|
|
|
| 61 |
)
|
| 62 |
|
| 63 |
CITATION_TEXT = """@misc{ffasr_leaderboard_2026,
|
|
@@ -70,33 +70,56 @@ CITATION_TEXT = """@misc{ffasr_leaderboard_2026,
|
|
| 70 |
ABOUT_TEXT = """
|
| 71 |
## About FFASR
|
| 72 |
|
| 73 |
-
**FFASR** (Far‑Field Automatic Speech Recognition) is an evaluation leaderboard that stresses ASR models
|
| 74 |
-
|
|
|
|
| 75 |
|
| 76 |
-
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
* **
|
| 81 |
-
* **
|
| 82 |
-
* **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
Every submission is evaluated on the **same private held‑out set**, using the **same reference transcripts** and the
|
| 85 |
**same text normalization pipeline** (lowercasing + whitespace/punctuation normalization, following the Whisper
|
| 86 |
convention). That is why scores across models are directly comparable.
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
## What you see on the leaderboard
|
| 89 |
|
| 90 |
| Column | Meaning |
|
| 91 |
|---|---|
|
| 92 |
-
| **
|
| 93 |
-
| **Anechoic** | WER on close‑mic
|
| 94 |
-
| **
|
| 95 |
-
| **
|
| 96 |
-
| **
|
| 97 |
-
| **
|
|
|
|
| 98 |
| **RTF** | *Audio seconds ÷ inference seconds*. Higher is faster; >1 means faster than real time. |
|
| 99 |
-
| **
|
| 100 |
|
| 101 |
Timing depends on the hardware this Space runs on, so treat RTF as a **comparison between submissions on this
|
| 102 |
leaderboard**, not as an absolute hardware number.
|
|
@@ -156,8 +179,7 @@ WER = (S + I + D) / N = (1 + 0 + 1) / 6 ≈ 0.33
|
|
| 156 |
### Speed & memory
|
| 157 |
|
| 158 |
* **RTF** (real‑time factor) is the primary speed metric: higher = faster model for the same audio.
|
| 159 |
-
* **
|
| 160 |
-
Smaller models are rewarded on that axis.
|
| 161 |
|
| 162 |
## Privacy and integrity
|
| 163 |
|
|
|
|
| 38 |
"<h1>FFASR</h1>"
|
| 39 |
"<div class='ffasr-subtitle'>Far‑Field Automatic Speech Recognition — a multi‑condition leaderboard</div>"
|
| 40 |
"<div class='ffasr-badges'>"
|
| 41 |
+
"<span class='ffasr-badge'>Far-field score</span>"
|
| 42 |
+
"<span class='ffasr-badge'>Lab vs realistic</span>"
|
| 43 |
+
"<span class='ffasr-badge'>Moving sources</span>"
|
|
|
|
|
|
|
| 44 |
"<span class='ffasr-badge'>WER · RTF</span>"
|
| 45 |
"</div>"
|
| 46 |
"</div>"
|
| 47 |
)
|
| 48 |
|
| 49 |
INTRODUCTION_TEXT = (
|
| 50 |
+
"**FFASR** benchmarks speech‑recognition models on audio closer to **far‑field** deployment — noisy rooms, "
|
| 51 |
+
"reverberation, and adverse SNRs — not just studio‑dry speech. Every model runs on the **same held‑out set** "
|
| 52 |
+
"and the **same text normalization**, so numbers are directly comparable.\n\n"
|
| 53 |
+
"Each submission reports **WER** across six scenario columns (**anechoic speech**, **lab measured**, "
|
| 54 |
+
"**lab simulated**, **realistic high / low SNR**, **moving sources**) plus **RTF** and **parameter count**. "
|
| 55 |
+
"**Moving sources** fills when that evaluation split is available (otherwise shown as NA). Models are ranked "
|
| 56 |
+
"by a far‑field‑weighted **Score (out of 100)** — **higher is better** — derived from per‑scenario WER vs "
|
| 57 |
+
"the rest of the leaderboard.\n\n"
|
| 58 |
"Paste a Hugging Face model id in the **Submit** tab — scoring runs server‑side and your model never sees "
|
| 59 |
+
"the evaluation audio directly. Use the **Analysis** tab for ranked scores, heatmaps, radar robustness, and "
|
| 60 |
+
"color‑coded speed‑vs‑accuracy plots."
|
| 61 |
)
|
| 62 |
|
| 63 |
CITATION_TEXT = """@misc{ffasr_leaderboard_2026,
|
|
|
|
| 70 |
ABOUT_TEXT = """
|
| 71 |
## About FFASR
|
| 72 |
|
| 73 |
+
**FFASR** (Far‑Field Automatic Speech Recognition) is an evaluation leaderboard that stresses ASR models on audio
|
| 74 |
+
beyond close‑mic studio conditions — realistic acoustics, difficult SNRs, and (when enabled in the eval harness)
|
| 75 |
+
moving sources.
|
| 76 |
|
| 77 |
+
### Scenario columns (WER · lower is better)
|
| 78 |
|
| 79 |
+
The leaderboard surfaces six complementary benchmarks:
|
| 80 |
+
|
| 81 |
+
* **Anechoic speech** — dry close‑mic baseline (included but **down‑weighted** in the headline score so rankings emphasise harder realism).
|
| 82 |
+
* **Lab measured** — controlled recordings / additive artifacts captured under measured lab conditions.
|
| 83 |
+
* **Lab simulated** — simulated room acoustics (historically Treble‑style simulated reverberation).
|
| 84 |
+
* **Realistic cond.: High SNR** — real‑world‑style mixtures at comparatively forgiving SNR.
|
| 85 |
+
* **Realistic cond.: Low SNR** — hard mixtures (low SNR, strong reverb, distortion) weighted heavily toward deployment realism.
|
| 86 |
+
* **Moving sources** — moving talkers / geometry shifts (**NA until that packed split ships** in the evaluation pipeline).
|
| 87 |
+
|
| 88 |
+
WER measures word‑level accuracy; **lower WER is always better** for each scenario column individually.
|
| 89 |
|
| 90 |
Every submission is evaluated on the **same private held‑out set**, using the **same reference transcripts** and the
|
| 91 |
**same text normalization pipeline** (lowercasing + whitespace/punctuation normalization, following the Whisper
|
| 92 |
convention). That is why scores across models are directly comparable.
|
| 93 |
|
| 94 |
+
### Far-field score (out of 100 · higher is better)
|
| 95 |
+
|
| 96 |
+
Instead of a flat average across every scenario, the headline **Score (out of 100)** combines scenarios with weights tuned for far‑field impact:
|
| 97 |
+
|
| 98 |
+
| Scenario bucket | Role in the score |
|
| 99 |
+
|---|---|
|
| 100 |
+
| Anechoic speech | Sanity baseline — contributes only a little so rankings aren’t dominated by easy speech |
|
| 101 |
+
| Lab measured / simulated | Important diagnostics — modest weights versus tougher realism |
|
| 102 |
+
| Realistic high SNR | Medium weight |
|
| 103 |
+
| Realistic low SNR | High weight |
|
| 104 |
+
| Moving sources | Highest planned weight once populated |
|
| 105 |
+
|
| 106 |
+
Technically, each populated scenario contributes an inverted **log₁₀(WER)** quality score relative to every other model on the leaderboard, then weighted components are summed and scaled to **0–100**. Missing scenarios drop out automatically with weights **renormalised** over what remains.
|
| 107 |
+
|
| 108 |
+
You can **narrow which scenarios influence Score** by toggling benchmark columns on the Leaderboard tab — only checked scenarios participate in the recomputed headline score.
|
| 109 |
+
|
| 110 |
## What you see on the leaderboard
|
| 111 |
|
| 112 |
| Column | Meaning |
|
| 113 |
|---|---|
|
| 114 |
+
| **Score (out of 100)** | Far‑field weighted composite ranking (**higher is better**). |
|
| 115 |
+
| **Anechoic speech** | WER on dry close‑mic speech. |
|
| 116 |
+
| **Lab measured** | WER on measured lab / controlled noisy conditions. |
|
| 117 |
+
| **Lab simulated** | WER on simulated reverberation / lab simulations. |
|
| 118 |
+
| **Realistic cond.: High SNR** | WER on realistic high‑SNR mixtures. |
|
| 119 |
+
| **Realistic cond.: Low SNR** | WER on realistic low‑SNR / adversarial mixtures. |
|
| 120 |
+
| **Moving sources** | WER when the moving‑source split is available (**NA** otherwise). |
|
| 121 |
| **RTF** | *Audio seconds ÷ inference seconds*. Higher is faster; >1 means faster than real time. |
|
| 122 |
+
| **# PARAMs** | Trainable parameters (millions). |
|
| 123 |
|
| 124 |
Timing depends on the hardware this Space runs on, so treat RTF as a **comparison between submissions on this
|
| 125 |
leaderboard**, not as an absolute hardware number.
|
|
|
|
| 179 |
### Speed & memory
|
| 180 |
|
| 181 |
* **RTF** (real‑time factor) is the primary speed metric: higher = faster model for the same audio.
|
| 182 |
+
* **Compactness** on the robustness radar prefers recorded parameter counts (millions) and falls back to Hub weight‑file sizes when needed — smaller models extend farther on that spoke.
|
|
|
|
| 183 |
|
| 184 |
## Privacy and integrity
|
| 185 |
|
evaluation/orchestrator.py
CHANGED
|
@@ -409,7 +409,7 @@ def run_evaluation(
|
|
| 409 |
``progress_cb(samples_done_across_all_conditions, samples_total_across_all_conditions, current_condition_key)``
|
| 410 |
periodically during the run.
|
| 411 |
|
| 412 |
-
Returns wer_clean, wer_noisy, wer_reverberant, num_samples, model_id, eval_family, plus timing.
|
| 413 |
"""
|
| 414 |
apply_cpu_thread_settings_once()
|
| 415 |
|
|
|
|
| 409 |
``progress_cb(samples_done_across_all_conditions, samples_total_across_all_conditions, current_condition_key)``
|
| 410 |
periodically during the run.
|
| 411 |
|
| 412 |
+
Returns wer_clean, wer_noisy, wer_reverberant, wer_real, wer_difficult, num_samples, model_id, eval_family, plus timing.
|
| 413 |
"""
|
| 414 |
apply_cpu_thread_settings_once()
|
| 415 |
|
init.py
CHANGED
|
@@ -35,6 +35,7 @@ __all__ = [
|
|
| 35 |
"list_audio_files",
|
| 36 |
"load_audio_file",
|
| 37 |
"load_transcript_file",
|
|
|
|
| 38 |
]
|
| 39 |
|
| 40 |
csv_lock = threading.Lock()
|
|
@@ -54,6 +55,34 @@ CSV_FIELDS = [
|
|
| 54 |
"submitted_at",
|
| 55 |
]
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
def leaderboard_row_from_eval_result(
|
| 59 |
result: dict,
|
|
@@ -64,9 +93,19 @@ def leaderboard_row_from_eval_result(
|
|
| 64 |
row = {k: "" for k in CSV_FIELDS}
|
| 65 |
row["model_id"] = str(result.get("model_id", ""))
|
| 66 |
row["eval_family"] = str(result.get("eval_family", ""))
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
row["num_samples"] = str(result.get("num_samples", ""))
|
| 71 |
for k in ("eval_audio_seconds", "eval_wall_time_s", "eval_rtf", "num_params"):
|
| 72 |
if k in result and result[k] is not None and result[k] != "":
|
|
@@ -136,6 +175,7 @@ def load_raw_results() -> list[dict]:
|
|
| 136 |
if not row.get("eval_family"):
|
| 137 |
row["eval_family"] = "—"
|
| 138 |
row.setdefault("submission_notes", "")
|
|
|
|
| 139 |
return results
|
| 140 |
return []
|
| 141 |
except Exception:
|
|
@@ -169,6 +209,8 @@ def load_results() -> pd.DataFrame:
|
|
| 169 |
those still live in the CSV (for moderators and analytics) but are hidden from the
|
| 170 |
public table.
|
| 171 |
"""
|
|
|
|
|
|
|
| 172 |
raw_rows = load_raw_results()
|
| 173 |
|
| 174 |
if not raw_rows:
|
|
@@ -176,13 +218,22 @@ def load_results() -> pd.DataFrame:
|
|
| 176 |
|
| 177 |
raw_df = pd.DataFrame(raw_rows)
|
| 178 |
|
| 179 |
-
# Coerce live-condition columns to numeric so
|
| 180 |
live_cols = [c for c in LIVE_SCENARIO_KEYS if c in raw_df.columns]
|
| 181 |
for c in live_cols:
|
| 182 |
raw_df[c] = pd.to_numeric(raw_df[c], errors="coerce")
|
| 183 |
|
| 184 |
-
|
| 185 |
-
raw_df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
def _num_or_na(series, rnd=4):
|
| 188 |
return series.apply(
|
|
@@ -191,17 +242,18 @@ def load_results() -> pd.DataFrame:
|
|
| 191 |
|
| 192 |
display_df = pd.DataFrame()
|
| 193 |
display_df[AutoEvalColumn.model.name] = raw_df["model_id"].apply(make_clickable_model)
|
| 194 |
-
display_df[AutoEvalColumn.
|
| 195 |
-
lambda x: round(float(x),
|
| 196 |
)
|
| 197 |
|
| 198 |
# Map: scenario CSV key -> display column name (defined in AutoEvalColumn).
|
| 199 |
scenario_col_map = {
|
| 200 |
-
"
|
| 201 |
-
"
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
-
"
|
|
|
|
| 205 |
}
|
| 206 |
for csv_key, display_name in scenario_col_map.items():
|
| 207 |
if csv_key in raw_df.columns:
|
|
|
|
| 35 |
"list_audio_files",
|
| 36 |
"load_audio_file",
|
| 37 |
"load_transcript_file",
|
| 38 |
+
"normalize_legacy_csv_row",
|
| 39 |
]
|
| 40 |
|
| 41 |
csv_lock = threading.Lock()
|
|
|
|
| 55 |
"submitted_at",
|
| 56 |
]
|
| 57 |
|
| 58 |
+
# Legacy CSV / evaluation wire-format keys -> canonical ``SCENARIO_KEYS`` column names.
|
| 59 |
+
_LEGACY_WER_TO_CANONICAL: tuple[tuple[str, str], ...] = (
|
| 60 |
+
("wer_clean", "wer_anechoic_speech"),
|
| 61 |
+
("wer_noisy", "wer_lab_measured"),
|
| 62 |
+
("wer_reverberant", "wer_lab_simulated"),
|
| 63 |
+
("wer_real", "wer_realistic_high_snr"),
|
| 64 |
+
("wer_difficult", "wer_realistic_low_snr"),
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _cell_empty(v) -> bool:
|
| 69 |
+
return v is None or (isinstance(v, str) and v.strip() == "")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def normalize_legacy_csv_row(row: dict) -> dict:
|
| 73 |
+
"""Fill canonical scenario columns from legacy leaderboard CSV keys when needed."""
|
| 74 |
+
for old, new in _LEGACY_WER_TO_CANONICAL:
|
| 75 |
+
ov = row.get(old)
|
| 76 |
+
nv = row.get(new)
|
| 77 |
+
if _cell_empty(nv) and not _cell_empty(ov):
|
| 78 |
+
row[new] = str(ov).strip() if not isinstance(ov, str) else ov.strip()
|
| 79 |
+
# Older CSVs used ``wer_high_snr`` as a standalone column.
|
| 80 |
+
hn = row.get("wer_high_snr")
|
| 81 |
+
rh = row.get("wer_realistic_high_snr")
|
| 82 |
+
if _cell_empty(rh) and not _cell_empty(hn):
|
| 83 |
+
row["wer_realistic_high_snr"] = str(hn).strip() if isinstance(hn, str) else str(hn)
|
| 84 |
+
return row
|
| 85 |
+
|
| 86 |
|
| 87 |
def leaderboard_row_from_eval_result(
|
| 88 |
result: dict,
|
|
|
|
| 93 |
row = {k: "" for k in CSV_FIELDS}
|
| 94 |
row["model_id"] = str(result.get("model_id", ""))
|
| 95 |
row["eval_family"] = str(result.get("eval_family", ""))
|
| 96 |
+
# Evaluation pipeline still emits legacy wer_* keys on the wire.
|
| 97 |
+
_EVAL_WIRE_TO_CANONICAL = {
|
| 98 |
+
"wer_anechoic_speech": "wer_clean",
|
| 99 |
+
"wer_lab_measured": "wer_noisy",
|
| 100 |
+
"wer_lab_simulated": "wer_reverberant",
|
| 101 |
+
"wer_realistic_high_snr": "wer_real",
|
| 102 |
+
"wer_realistic_low_snr": "wer_difficult",
|
| 103 |
+
"wer_moving_sources": "wer_moving_sources",
|
| 104 |
+
}
|
| 105 |
+
for canon_key, wire_key in _EVAL_WIRE_TO_CANONICAL.items():
|
| 106 |
+
v = result.get(wire_key)
|
| 107 |
+
if v is not None and v != "":
|
| 108 |
+
row[canon_key] = str(v)
|
| 109 |
row["num_samples"] = str(result.get("num_samples", ""))
|
| 110 |
for k in ("eval_audio_seconds", "eval_wall_time_s", "eval_rtf", "num_params"):
|
| 111 |
if k in result and result[k] is not None and result[k] != "":
|
|
|
|
| 175 |
if not row.get("eval_family"):
|
| 176 |
row["eval_family"] = "—"
|
| 177 |
row.setdefault("submission_notes", "")
|
| 178 |
+
normalize_legacy_csv_row(row)
|
| 179 |
return results
|
| 180 |
return []
|
| 181 |
except Exception:
|
|
|
|
| 209 |
those still live in the CSV (for moderators and analytics) but are hidden from the
|
| 210 |
public table.
|
| 211 |
"""
|
| 212 |
+
import analytics
|
| 213 |
+
|
| 214 |
raw_rows = load_raw_results()
|
| 215 |
|
| 216 |
if not raw_rows:
|
|
|
|
| 218 |
|
| 219 |
raw_df = pd.DataFrame(raw_rows)
|
| 220 |
|
| 221 |
+
# Coerce live-condition columns to numeric so scoring / sorting behave sensibly.
|
| 222 |
live_cols = [c for c in LIVE_SCENARIO_KEYS if c in raw_df.columns]
|
| 223 |
for c in live_cols:
|
| 224 |
raw_df[c] = pd.to_numeric(raw_df[c], errors="coerce")
|
| 225 |
|
| 226 |
+
bounds = analytics.compute_wer_log_bounds(raw_rows, LIVE_SCENARIO_KEYS)
|
| 227 |
+
raw_df["_ff_score"] = [
|
| 228 |
+
analytics.far_field_score_for_row(dict(r), bounds) for r in raw_rows
|
| 229 |
+
]
|
| 230 |
+
# Tie-breaker: lower mean WER among populated scenarios is better when scores tie.
|
| 231 |
+
raw_df["_mean_wer"] = raw_df[live_cols].mean(axis=1, skipna=True) if live_cols else pd.NA
|
| 232 |
+
raw_df = raw_df.sort_values(
|
| 233 |
+
by=["_ff_score", "_mean_wer"],
|
| 234 |
+
ascending=[False, True],
|
| 235 |
+
na_position="last",
|
| 236 |
+
).reset_index(drop=True)
|
| 237 |
|
| 238 |
def _num_or_na(series, rnd=4):
|
| 239 |
return series.apply(
|
|
|
|
| 242 |
|
| 243 |
display_df = pd.DataFrame()
|
| 244 |
display_df[AutoEvalColumn.model.name] = raw_df["model_id"].apply(make_clickable_model)
|
| 245 |
+
display_df[AutoEvalColumn.ff_score.name] = raw_df["_ff_score"].apply(
|
| 246 |
+
lambda x: round(float(x), 2) if pd.notna(x) else "NA"
|
| 247 |
)
|
| 248 |
|
| 249 |
# Map: scenario CSV key -> display column name (defined in AutoEvalColumn).
|
| 250 |
scenario_col_map = {
|
| 251 |
+
"wer_anechoic_speech": AutoEvalColumn.wer_anechoic.name,
|
| 252 |
+
"wer_lab_measured": AutoEvalColumn.wer_lab_measured.name,
|
| 253 |
+
"wer_lab_simulated": AutoEvalColumn.wer_lab_simulated.name,
|
| 254 |
+
"wer_realistic_high_snr": AutoEvalColumn.wer_realistic_high_snr.name,
|
| 255 |
+
"wer_realistic_low_snr": AutoEvalColumn.wer_realistic_low_snr.name,
|
| 256 |
+
"wer_moving_sources": AutoEvalColumn.wer_moving_sources.name,
|
| 257 |
}
|
| 258 |
for csv_key, display_name in scenario_col_map.items():
|
| 259 |
if csv_key in raw_df.columns:
|
job_queue.py
CHANGED
|
@@ -275,20 +275,12 @@ def _progress_update(job_id: str, done: int, total: int, condition: str) -> None
|
|
| 275 |
j.progress_condition = condition or ""
|
| 276 |
|
| 277 |
|
| 278 |
-
def
|
| 279 |
-
"""
|
| 280 |
-
from
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
raw = row.get(key, "")
|
| 285 |
-
if raw == "" or raw is None:
|
| 286 |
-
continue
|
| 287 |
-
try:
|
| 288 |
-
vals.append(float(raw))
|
| 289 |
-
except (TypeError, ValueError):
|
| 290 |
-
continue
|
| 291 |
-
return sum(vals) / len(vals) if vals else float("inf")
|
| 292 |
|
| 293 |
|
| 294 |
def _evaluate_single_job(job_id: str, mid: str, fid: str) -> dict:
|
|
@@ -436,7 +428,7 @@ def _worker_loop() -> None:
|
|
| 436 |
submission_notes=notes,
|
| 437 |
)
|
| 438 |
)
|
| 439 |
-
|
| 440 |
save_raw_results(rows)
|
| 441 |
|
| 442 |
with _jobs_lock:
|
|
|
|
| 275 |
j.progress_condition = condition or ""
|
| 276 |
|
| 277 |
|
| 278 |
+
def _leaderboard_sort_rows_inplace(rows: list[dict]) -> None:
|
| 279 |
+
"""Sort leaderboard CSV rows by far-field score (descending)."""
|
| 280 |
+
from analytics import compute_far_field_score_map
|
| 281 |
+
|
| 282 |
+
scores = compute_far_field_score_map(rows)
|
| 283 |
+
rows.sort(key=lambda r: (-scores.get(r.get("model_id", ""), 0.0), r.get("model_id") or ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
|
| 286 |
def _evaluate_single_job(job_id: str, mid: str, fid: str) -> dict:
|
|
|
|
| 428 |
submission_notes=notes,
|
| 429 |
)
|
| 430 |
)
|
| 431 |
+
_leaderboard_sort_rows_inplace(rows)
|
| 432 |
save_raw_results(rows)
|
| 433 |
|
| 434 |
with _jobs_lock:
|
metrics_config.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
Scenario metrics: CSV column keys, labels, and grouping for leaderboard + analytics.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
|
@@ -28,68 +28,52 @@ class ScenarioMetric:
|
|
| 28 |
# Order defines CSV column order for scenario WER columns.
|
| 29 |
SCENARIO_METRICS: tuple[ScenarioMetric, ...] = (
|
| 30 |
ScenarioMetric(
|
| 31 |
-
key="
|
| 32 |
-
label="Anechoic (close-mic, dry)",
|
| 33 |
-
short="Anechoic",
|
| 34 |
-
description="Close-mic / anechoic dry speech —
|
| 35 |
group="Core benchmarks",
|
| 36 |
status="live",
|
| 37 |
),
|
| 38 |
ScenarioMetric(
|
| 39 |
-
key="
|
| 40 |
-
label="
|
| 41 |
-
short="
|
| 42 |
-
description="
|
| 43 |
group="Core benchmarks",
|
| 44 |
status="live",
|
| 45 |
),
|
| 46 |
ScenarioMetric(
|
| 47 |
-
key="
|
| 48 |
-
label="
|
| 49 |
-
short="
|
| 50 |
-
description="
|
| 51 |
group="Core benchmarks",
|
| 52 |
status="live",
|
| 53 |
),
|
| 54 |
ScenarioMetric(
|
| 55 |
-
key="
|
| 56 |
-
label="
|
| 57 |
-
short="
|
| 58 |
-
description="
|
| 59 |
-
group="
|
| 60 |
status="live",
|
| 61 |
),
|
| 62 |
ScenarioMetric(
|
| 63 |
-
key="
|
| 64 |
-
label="
|
| 65 |
-
short="
|
| 66 |
-
description="
|
| 67 |
-
group="
|
| 68 |
status="live",
|
| 69 |
),
|
| 70 |
ScenarioMetric(
|
| 71 |
-
key="
|
| 72 |
-
label="
|
| 73 |
-
short="
|
| 74 |
-
description="
|
| 75 |
-
group="
|
| 76 |
-
status="
|
| 77 |
-
),
|
| 78 |
-
ScenarioMetric(
|
| 79 |
-
key="wer_distant",
|
| 80 |
-
label="Distant talker",
|
| 81 |
-
short="Distant",
|
| 82 |
-
description="Far-field / low direct-path energy (reserved).",
|
| 83 |
-
group="Acoustic conditions",
|
| 84 |
-
status="planned",
|
| 85 |
-
),
|
| 86 |
-
ScenarioMetric(
|
| 87 |
-
key="wer_highly_reverberant",
|
| 88 |
-
label="Highly reverberant",
|
| 89 |
-
short="High reverb",
|
| 90 |
-
description="Strong late reflections / long RT60 (reserved).",
|
| 91 |
-
group="Acoustic conditions",
|
| 92 |
-
status="planned",
|
| 93 |
),
|
| 94 |
)
|
| 95 |
|
|
|
|
| 1 |
"""
|
| 2 |
Scenario metrics: CSV column keys, labels, and grouping for leaderboard + analytics.
|
| 3 |
|
| 4 |
+
Canonical scenario keys are far-field oriented. Legacy keys (wer_clean, …) are migrated
|
| 5 |
+
on CSV load in ``init.normalize_legacy_csv_row``.
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
|
|
|
| 28 |
# Order defines CSV column order for scenario WER columns.
|
| 29 |
SCENARIO_METRICS: tuple[ScenarioMetric, ...] = (
|
| 30 |
ScenarioMetric(
|
| 31 |
+
key="wer_anechoic_speech",
|
| 32 |
+
label="Anechoic speech (close-mic, dry)",
|
| 33 |
+
short="Anechoic speech",
|
| 34 |
+
description="Close-mic / anechoic dry speech — easy baseline (downweighted for far-field ranking).",
|
| 35 |
group="Core benchmarks",
|
| 36 |
status="live",
|
| 37 |
),
|
| 38 |
ScenarioMetric(
|
| 39 |
+
key="wer_lab_measured",
|
| 40 |
+
label="Lab measured",
|
| 41 |
+
short="Lab measured",
|
| 42 |
+
description="Controlled lab recordings with measured acoustics / additive noise.",
|
| 43 |
group="Core benchmarks",
|
| 44 |
status="live",
|
| 45 |
),
|
| 46 |
ScenarioMetric(
|
| 47 |
+
key="wer_lab_simulated",
|
| 48 |
+
label="Lab simulated",
|
| 49 |
+
short="Lab simulated",
|
| 50 |
+
description="Simulated acoustics (e.g. simulated room / Treble-style reverb).",
|
| 51 |
group="Core benchmarks",
|
| 52 |
status="live",
|
| 53 |
),
|
| 54 |
ScenarioMetric(
|
| 55 |
+
key="wer_realistic_high_snr",
|
| 56 |
+
label="Realistic conditions — High SNR",
|
| 57 |
+
short="Realistic · High SNR",
|
| 58 |
+
description="Realistic far-field / room conditions at relatively high SNR.",
|
| 59 |
+
group="Realistic conditions",
|
| 60 |
status="live",
|
| 61 |
),
|
| 62 |
ScenarioMetric(
|
| 63 |
+
key="wer_realistic_low_snr",
|
| 64 |
+
label="Realistic conditions — Low SNR",
|
| 65 |
+
short="Realistic · Low SNR",
|
| 66 |
+
description="Hard realistic mixtures (low SNR, strong reverb, distortion).",
|
| 67 |
+
group="Realistic conditions",
|
| 68 |
status="live",
|
| 69 |
),
|
| 70 |
ScenarioMetric(
|
| 71 |
+
key="wer_moving_sources",
|
| 72 |
+
label="Moving sources",
|
| 73 |
+
short="Moving sources",
|
| 74 |
+
description="Moving talker / varying geometry (populated when the eval split is available).",
|
| 75 |
+
group="Realistic conditions",
|
| 76 |
+
status="live",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
),
|
| 78 |
)
|
| 79 |
|
scripts/run_eval.py
CHANGED
|
@@ -43,6 +43,8 @@ def main() -> None:
|
|
| 43 |
print("wer_clean:", out["wer_clean"])
|
| 44 |
print("wer_noisy:", out["wer_noisy"])
|
| 45 |
print("wer_reverberant:", out["wer_reverberant"])
|
|
|
|
|
|
|
| 46 |
print("num_samples:", out["num_samples"])
|
| 47 |
|
| 48 |
|
|
|
|
| 43 |
print("wer_clean:", out["wer_clean"])
|
| 44 |
print("wer_noisy:", out["wer_noisy"])
|
| 45 |
print("wer_reverberant:", out["wer_reverberant"])
|
| 46 |
+
print("wer_real:", out.get("wer_real"))
|
| 47 |
+
print("wer_difficult:", out.get("wer_difficult"))
|
| 48 |
print("num_samples:", out["num_samples"])
|
| 49 |
|
| 50 |
|
scripts/run_eval_and_publish.py
CHANGED
|
@@ -685,33 +685,24 @@ def _write_csv(path: str, fields: list[str], rows: list[dict]) -> None:
|
|
| 685 |
|
| 686 |
|
| 687 |
def _append_leaderboard_local(csv_path: str, result: dict, notes: str) -> dict:
|
| 688 |
-
from
|
| 689 |
-
from
|
| 690 |
|
| 691 |
rows = _read_csv(csv_path)
|
| 692 |
for row in rows:
|
| 693 |
for k in CSV_FIELDS:
|
| 694 |
row.setdefault(k, "")
|
|
|
|
| 695 |
if any(r.get("model_id") == result["model_id"] for r in rows):
|
| 696 |
raise RuntimeError(
|
| 697 |
f"Model {result['model_id']!r} already in {csv_path}; refusing to duplicate."
|
| 698 |
)
|
| 699 |
new_row = leaderboard_row_from_eval_result(result, _now_iso(), submission_notes=notes)
|
|
|
|
| 700 |
rows.append(new_row)
|
| 701 |
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
for k in LIVE_SCENARIO_KEYS:
|
| 705 |
-
v = r.get(k, "")
|
| 706 |
-
if v == "" or v is None:
|
| 707 |
-
continue
|
| 708 |
-
try:
|
| 709 |
-
vals.append(float(v))
|
| 710 |
-
except Exception:
|
| 711 |
-
continue
|
| 712 |
-
return sum(vals) / len(vals) if vals else float("inf")
|
| 713 |
-
|
| 714 |
-
rows.sort(key=_avg)
|
| 715 |
_write_csv(csv_path, CSV_FIELDS, rows)
|
| 716 |
return new_row
|
| 717 |
|
|
@@ -954,9 +945,11 @@ def main() -> int:
|
|
| 954 |
print(f"device: {device_str}")
|
| 955 |
print(f"dtype: {args.dtype}")
|
| 956 |
print(f"batch_size: {args.batch_size}")
|
| 957 |
-
print(f"wer_clean: {result.get('wer_clean')}")
|
| 958 |
-
print(f"wer_noisy:
|
| 959 |
-
print(f"wer_reverberant:
|
|
|
|
|
|
|
| 960 |
print(f"num_samples: {result.get('num_samples')}")
|
| 961 |
print(f"eval_wall_time_s: {result.get('eval_wall_time_s')}")
|
| 962 |
print(f"eval_rtf: {result.get('eval_rtf')}")
|
|
|
|
| 685 |
|
| 686 |
|
| 687 |
def _append_leaderboard_local(csv_path: str, result: dict, notes: str) -> dict:
|
| 688 |
+
from analytics import compute_far_field_score_map
|
| 689 |
+
from init import CSV_FIELDS, leaderboard_row_from_eval_result, normalize_legacy_csv_row
|
| 690 |
|
| 691 |
rows = _read_csv(csv_path)
|
| 692 |
for row in rows:
|
| 693 |
for k in CSV_FIELDS:
|
| 694 |
row.setdefault(k, "")
|
| 695 |
+
normalize_legacy_csv_row(row)
|
| 696 |
if any(r.get("model_id") == result["model_id"] for r in rows):
|
| 697 |
raise RuntimeError(
|
| 698 |
f"Model {result['model_id']!r} already in {csv_path}; refusing to duplicate."
|
| 699 |
)
|
| 700 |
new_row = leaderboard_row_from_eval_result(result, _now_iso(), submission_notes=notes)
|
| 701 |
+
normalize_legacy_csv_row(new_row)
|
| 702 |
rows.append(new_row)
|
| 703 |
|
| 704 |
+
scores = compute_far_field_score_map(rows)
|
| 705 |
+
rows.sort(key=lambda r: (-scores.get(r.get("model_id", ""), 0.0), r.get("model_id") or ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
_write_csv(csv_path, CSV_FIELDS, rows)
|
| 707 |
return new_row
|
| 708 |
|
|
|
|
| 945 |
print(f"device: {device_str}")
|
| 946 |
print(f"dtype: {args.dtype}")
|
| 947 |
print(f"batch_size: {args.batch_size}")
|
| 948 |
+
print(f"wer_clean (→ anechoic CSV): {result.get('wer_clean')}")
|
| 949 |
+
print(f"wer_noisy (→ lab measured CSV): {result.get('wer_noisy')}")
|
| 950 |
+
print(f"wer_reverberant (→ lab sim CSV): {result.get('wer_reverberant')}")
|
| 951 |
+
print(f"wer_real (→ realistic hi SNR CSV): {result.get('wer_real')}")
|
| 952 |
+
print(f"wer_difficult (→ realistic lo CSV): {result.get('wer_difficult')}")
|
| 953 |
print(f"num_samples: {result.get('num_samples')}")
|
| 954 |
print(f"eval_wall_time_s: {result.get('eval_wall_time_s')}")
|
| 955 |
print(f"eval_rtf: {result.get('eval_rtf')}")
|
utils_display.py
CHANGED
|
@@ -16,27 +16,39 @@ def fields(raw_class):
|
|
| 16 |
@dataclass(frozen=True)
|
| 17 |
class AutoEvalColumn:
|
| 18 |
model = ColumnContent("Model", "markdown")
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
eval_rtf = ColumnContent("RTF", "number")
|
| 26 |
-
params_m = ColumnContent("
|
| 27 |
|
| 28 |
|
| 29 |
# Display names of the per-condition WER benchmark columns. Used to:
|
| 30 |
-
# * compute
|
| 31 |
# * sanity-check column visibility logic.
|
| 32 |
SCENARIO_DISPLAY_COLS: tuple[str, ...] = (
|
| 33 |
-
AutoEvalColumn.
|
| 34 |
-
AutoEvalColumn.
|
| 35 |
-
AutoEvalColumn.
|
| 36 |
-
AutoEvalColumn.
|
| 37 |
-
AutoEvalColumn.
|
|
|
|
| 38 |
)
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
# Custom URL mappings for models not on HuggingFace or with special pages
|
| 42 |
_CUSTOM_LINKS = {
|
|
@@ -86,3 +98,17 @@ def styled_warning(warn):
|
|
| 86 |
|
| 87 |
def styled_message(message):
|
| 88 |
return f"<p style='color: green; font-size: 20px; text-align: center;'>{message}</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
@dataclass(frozen=True)
|
| 17 |
class AutoEvalColumn:
|
| 18 |
model = ColumnContent("Model", "markdown")
|
| 19 |
+
ff_score = ColumnContent("Score (out of 100)", "number")
|
| 20 |
+
wer_anechoic = ColumnContent("Anechoic speech", "number")
|
| 21 |
+
wer_lab_measured = ColumnContent("Lab measured", "number")
|
| 22 |
+
wer_lab_simulated = ColumnContent("Lab simulated", "number")
|
| 23 |
+
wer_realistic_high_snr = ColumnContent("Realistic cond.: High SNR", "number")
|
| 24 |
+
wer_realistic_low_snr = ColumnContent("Realistic cond.: Low SNR", "number")
|
| 25 |
+
wer_moving_sources = ColumnContent("Moving sources", "number")
|
| 26 |
eval_rtf = ColumnContent("RTF", "number")
|
| 27 |
+
params_m = ColumnContent("# PARAMs", "number")
|
| 28 |
|
| 29 |
|
| 30 |
# Display names of the per-condition WER benchmark columns. Used to:
|
| 31 |
+
# * compute far-field Score from the visible benchmark columns in the leaderboard filter,
|
| 32 |
# * sanity-check column visibility logic.
|
| 33 |
SCENARIO_DISPLAY_COLS: tuple[str, ...] = (
|
| 34 |
+
AutoEvalColumn.wer_anechoic.name,
|
| 35 |
+
AutoEvalColumn.wer_lab_measured.name,
|
| 36 |
+
AutoEvalColumn.wer_lab_simulated.name,
|
| 37 |
+
AutoEvalColumn.wer_realistic_high_snr.name,
|
| 38 |
+
AutoEvalColumn.wer_realistic_low_snr.name,
|
| 39 |
+
AutoEvalColumn.wer_moving_sources.name,
|
| 40 |
)
|
| 41 |
|
| 42 |
+
# Leaderboard display header -> canonical CSV / analytics metric key.
|
| 43 |
+
SCENARIO_DISPLAY_TO_KEY: dict[str, str] = {
|
| 44 |
+
AutoEvalColumn.wer_anechoic.name: "wer_anechoic_speech",
|
| 45 |
+
AutoEvalColumn.wer_lab_measured.name: "wer_lab_measured",
|
| 46 |
+
AutoEvalColumn.wer_lab_simulated.name: "wer_lab_simulated",
|
| 47 |
+
AutoEvalColumn.wer_realistic_high_snr.name: "wer_realistic_high_snr",
|
| 48 |
+
AutoEvalColumn.wer_realistic_low_snr.name: "wer_realistic_low_snr",
|
| 49 |
+
AutoEvalColumn.wer_moving_sources.name: "wer_moving_sources",
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
|
| 53 |
# Custom URL mappings for models not on HuggingFace or with special pages
|
| 54 |
_CUSTOM_LINKS = {
|
|
|
|
| 98 |
|
| 99 |
def styled_message(message):
|
| 100 |
return f"<p style='color: green; font-size: 20px; text-align: center;'>{message}</p>"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def model_id_from_leaderboard_cell(cell) -> str:
|
| 104 |
+
"""Recover Hugging Face model id from the markdown Model column."""
|
| 105 |
+
if not isinstance(cell, str):
|
| 106 |
+
return ""
|
| 107 |
+
cell = cell.strip()
|
| 108 |
+
if "</a>" in cell:
|
| 109 |
+
import re
|
| 110 |
+
|
| 111 |
+
m = re.search(r">([^<]+)</a>\s*$", cell)
|
| 112 |
+
if m:
|
| 113 |
+
return m.group(1).strip()
|
| 114 |
+
return cell
|