Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit ·
c0dd455
1
Parent(s): 6845229
beautification, custom scripts loader, artefacts json to csv
Browse files- README.md +2 -2
- analytics.py +63 -92
- app.py +251 -140
- backends/auto.py +9 -1
- backends/family_resolve.py +61 -0
- backends/registry.py +3 -1
- constants.py +99 -12
- evaluation/remote_artifact.py +18 -0
- family_registry.py +5 -2
- job_queue.py +325 -7
- remote_jobs.py +96 -39
README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
| 2 |
title: FFASR Leaderboard
|
| 3 |
short_description: Far-Field ASR — clean / noisy / reverberant benchmark
|
| 4 |
emoji: 🎙️
|
| 5 |
-
colorFrom:
|
| 6 |
-
colorTo:
|
| 7 |
sdk: gradio
|
| 8 |
sdk_version: 5.29.0
|
| 9 |
app_file: app.py
|
|
|
|
| 2 |
title: FFASR Leaderboard
|
| 3 |
short_description: Far-Field ASR — clean / noisy / reverberant benchmark
|
| 4 |
emoji: 🎙️
|
| 5 |
+
colorFrom: teal
|
| 6 |
+
colorTo: green
|
| 7 |
sdk: gradio
|
| 8 |
sdk_version: 5.29.0
|
| 9 |
app_file: app.py
|
analytics.py
CHANGED
|
@@ -312,53 +312,56 @@ def plot_leaderboard_score_bars(
|
|
| 312 |
return fig
|
| 313 |
|
| 314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
def plot_robustness_radar(
|
| 316 |
df: pd.DataFrame,
|
| 317 |
model_ids: Sequence[str],
|
| 318 |
-
title: str = "Robustness radar (
|
| 319 |
) -> go.Figure:
|
| 320 |
"""
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
Every axis is phrased so farther from the centre = better. WER and model
|
| 324 |
-
size / parameter count are inverted before normalisation; RTF is not.
|
| 325 |
-
Strength is a per-axis score computed across **all** leaderboard rows
|
| 326 |
-
with a log transform and global min/max, so axes are directly comparable
|
| 327 |
-
across models. Missing or invalid raw values map to a low baseline strength for display.
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
the asterisk on the axis label flags that fallback in the chart.
|
| 332 |
"""
|
| 333 |
if df.empty:
|
| 334 |
return _empty_fig("No leaderboard data yet.")
|
| 335 |
|
| 336 |
d = df.copy().reset_index(drop=True)
|
| 337 |
-
|
| 338 |
-
axes: list[tuple[str, np.ndarray, np.ndarray, str, bool, str, str, bool]] = []
|
| 339 |
-
for label, primary, fallback, inverse, raw_disp, raw_unit in _RADAR_AXES:
|
| 340 |
-
col_label = label
|
| 341 |
-
col_name = primary
|
| 342 |
-
using_fallback = False
|
| 343 |
-
col = (
|
| 344 |
-
pd.to_numeric(d[primary], errors="coerce")
|
| 345 |
-
if primary in d.columns
|
| 346 |
-
else pd.Series([np.nan] * len(d))
|
| 347 |
-
)
|
| 348 |
-
if not col.notna().any() and fallback and fallback in d.columns:
|
| 349 |
-
col = pd.to_numeric(d[fallback], errors="coerce")
|
| 350 |
-
col_label = f"{label}*"
|
| 351 |
-
col_name = fallback
|
| 352 |
-
using_fallback = True
|
| 353 |
-
override = _RADAR_FALLBACK_DISPLAY.get(primary)
|
| 354 |
-
if override is not None:
|
| 355 |
-
raw_disp, raw_unit = override
|
| 356 |
-
strength = _log_normalize(col.to_numpy(), inverse=inverse)
|
| 357 |
-
axes.append(
|
| 358 |
-
(col_label, strength, col.to_numpy(), col_name, inverse, raw_disp, raw_unit, using_fallback)
|
| 359 |
-
)
|
| 360 |
-
|
| 361 |
-
labels = [a[0] for a in axes]
|
| 362 |
labels_closed = labels + [labels[0]]
|
| 363 |
|
| 364 |
selected = [m for m in (model_ids or []) if m in set(d["model_id"])]
|
|
@@ -372,21 +375,13 @@ def plot_robustness_radar(
|
|
| 372 |
rows = d.index[d["model_id"] == mid].tolist()
|
| 373 |
if not rows:
|
| 374 |
continue
|
| 375 |
-
|
| 376 |
r_vals: list[float] = []
|
| 377 |
raw_text: list[str] = []
|
| 378 |
-
for
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
r_vals.append(float(max(s, 0.1)))
|
| 383 |
-
else:
|
| 384 |
-
r_vals.append(0.1)
|
| 385 |
-
direction = "lower is better" if inv else "higher is better"
|
| 386 |
-
if not np.isfinite(rv):
|
| 387 |
-
raw_text.append(f"{raw_disp}: N/A ({direction})")
|
| 388 |
-
else:
|
| 389 |
-
raw_text.append(f"{raw_disp}: {rv:.4f}{raw_unit} ({direction})")
|
| 390 |
|
| 391 |
r_closed = r_vals + [r_vals[0]]
|
| 392 |
raw_closed = raw_text + [raw_text[0]]
|
|
@@ -405,7 +400,7 @@ def plot_robustness_radar(
|
|
| 405 |
opacity=1.0,
|
| 406 |
hovertemplate=(
|
| 407 |
f"<b>{short}</b><br>"
|
| 408 |
-
"%{theta}
|
| 409 |
"%{customdata}<extra></extra>"
|
| 410 |
),
|
| 411 |
)
|
|
@@ -415,10 +410,10 @@ def plot_robustness_radar(
|
|
| 415 |
polar=dict(
|
| 416 |
radialaxis=dict(
|
| 417 |
visible=True,
|
| 418 |
-
range=[0
|
| 419 |
showticklabels=True,
|
| 420 |
tickformat=".2f",
|
| 421 |
-
tickvals=[0
|
| 422 |
),
|
| 423 |
angularaxis=dict(direction="clockwise", rotation=90),
|
| 424 |
),
|
|
@@ -696,56 +691,32 @@ def plot_latency_vs_wer(
|
|
| 696 |
"num_params_m": "Parameters (millions)",
|
| 697 |
}.get(x_key, x_key)
|
| 698 |
|
| 699 |
-
xs = d[x_key].to_numpy(dtype=float)
|
| 700 |
-
ys = d[y_metric_key].to_numpy(dtype=float)
|
| 701 |
-
mx_f = float(np.nanmedian(xs))
|
| 702 |
-
my_f = float(np.nanmedian(ys))
|
| 703 |
-
|
| 704 |
-
def _bucket(xv: float, yv: float) -> tuple[str, str]:
|
| 705 |
-
if x_key == "eval_rtf":
|
| 706 |
-
fast = xv >= mx_f
|
| 707 |
-
else:
|
| 708 |
-
fast = xv <= mx_f
|
| 709 |
-
accurate = yv <= my_f
|
| 710 |
-
if fast and accurate:
|
| 711 |
-
return "Fast · accurate", "#22c55e"
|
| 712 |
-
if fast and not accurate:
|
| 713 |
-
return "Fast · higher WER", "#3b82f6"
|
| 714 |
-
if not fast and accurate:
|
| 715 |
-
return "Slower · lower WER", "#f59e0b"
|
| 716 |
-
return "Slower · higher WER", "#ef4444"
|
| 717 |
-
|
| 718 |
-
buckets = [_bucket(float(a), float(b)) for a, b in zip(xs, ys)]
|
| 719 |
-
labels = [b[0] for b in buckets]
|
| 720 |
-
|
| 721 |
fig = go.Figure()
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
if not
|
| 730 |
continue
|
|
|
|
| 731 |
fig.add_trace(
|
| 732 |
go.Scatter(
|
| 733 |
-
x=
|
| 734 |
-
y=
|
| 735 |
mode="markers",
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
legendgroup=lab,
|
| 739 |
marker=dict(size=11, opacity=0.9, color=color, line=dict(width=0.6, color="white")),
|
| 740 |
hovertemplate=(
|
| 741 |
-
f"<b>
|
| 742 |
f"{y_label}: %{{y:.4f}}<extra></extra>"
|
| 743 |
),
|
| 744 |
)
|
| 745 |
)
|
| 746 |
|
| 747 |
-
fig.add_vline(x=mx_f, line_dash="dot", line_color="rgba(0,0,0,0.25)")
|
| 748 |
-
fig.add_hline(y=my_f, line_dash="dot", line_color="rgba(0,0,0,0.25)")
|
| 749 |
ttl = title or f"Speed versus accuracy: {y_label}"
|
| 750 |
fig.update_layout(
|
| 751 |
title=dict(text=ttl, x=0.5, xanchor="center"),
|
|
@@ -753,9 +724,9 @@ def plot_latency_vs_wer(
|
|
| 753 |
yaxis=dict(title=y_label + " (lower is better)", rangemode="tozero"),
|
| 754 |
template=_TEMPLATE,
|
| 755 |
height=_FIG_HEIGHT,
|
| 756 |
-
margin=dict(l=60, r=
|
| 757 |
-
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
| 758 |
showlegend=True,
|
|
|
|
| 759 |
)
|
| 760 |
return fig
|
| 761 |
|
|
|
|
| 312 |
return fig
|
| 313 |
|
| 314 |
|
| 315 |
+
def _radar_absolute_value(row: pd.Series, axis: str) -> tuple[float, str]:
|
| 316 |
+
"""Map a leaderboard row to an absolute 0–1 radar coordinate (higher = better)."""
|
| 317 |
+
if axis == "Dry Speech":
|
| 318 |
+
wer = pd.to_numeric(row.get("wer_anechoic_speech"), errors="coerce")
|
| 319 |
+
if not np.isfinite(wer):
|
| 320 |
+
return 0.0, "Dry speech WER: N/A"
|
| 321 |
+
v = float(max(0.0, min(1.0, 1.0 - float(wer))))
|
| 322 |
+
return v, f"Dry speech WER: {float(wer):.4f} → accuracy {v:.3f}"
|
| 323 |
+
if axis == "Score":
|
| 324 |
+
score = pd.to_numeric(row.get("ff_score"), errors="coerce")
|
| 325 |
+
if not np.isfinite(score):
|
| 326 |
+
return 0.0, "Far-field score: N/A"
|
| 327 |
+
v = float(max(0.0, min(1.0, float(score) / 100.0)))
|
| 328 |
+
return v, f"Far-field score: {float(score):.2f} / 100 → {v:.3f}"
|
| 329 |
+
if axis == "Speed":
|
| 330 |
+
rtf = pd.to_numeric(row.get("eval_rtf"), errors="coerce")
|
| 331 |
+
if not np.isfinite(rtf) or float(rtf) < 0:
|
| 332 |
+
return 0.0, "RTF: N/A"
|
| 333 |
+
r = float(rtf)
|
| 334 |
+
v = float(max(0.0, min(1.0, r / (1.0 + r))))
|
| 335 |
+
return v, f"RTF: {r:.4f}× → speed {v:.3f} (RTF/(1+RTF))"
|
| 336 |
+
if axis == "Compactness":
|
| 337 |
+
pm = pd.to_numeric(row.get("num_params_m"), errors="coerce")
|
| 338 |
+
if not np.isfinite(pm) or float(pm) < 0:
|
| 339 |
+
return 0.0, "Parameters: N/A"
|
| 340 |
+
p = float(pm)
|
| 341 |
+
v = float(max(0.0, min(1.0, 1.0 / (1.0 + p / 1000.0))))
|
| 342 |
+
return v, f"Parameters: {p:.2f} M → compactness {v:.3f}"
|
| 343 |
+
return 0.0, f"{axis}: N/A"
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
_RADAR_ABSOLUTE_AXES: tuple[str, ...] = ("Dry Speech", "Score", "Speed", "Compactness")
|
| 347 |
+
|
| 348 |
+
|
| 349 |
def plot_robustness_radar(
|
| 350 |
df: pd.DataFrame,
|
| 351 |
model_ids: Sequence[str],
|
| 352 |
+
title: str = "Robustness radar (absolute 0–1; outward is better)",
|
| 353 |
) -> go.Figure:
|
| 354 |
"""
|
| 355 |
+
Four-axis radar with **absolute** coordinates in [0, 1] (not relative to other models).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
+
Dry Speech = 1 − WER; Score = ff_score / 100; Speed = RTF / (1 + RTF);
|
| 358 |
+
Compactness = 1 / (1 + num_params_m / 1000). Missing values map to 0.
|
|
|
|
| 359 |
"""
|
| 360 |
if df.empty:
|
| 361 |
return _empty_fig("No leaderboard data yet.")
|
| 362 |
|
| 363 |
d = df.copy().reset_index(drop=True)
|
| 364 |
+
labels = list(_RADAR_ABSOLUTE_AXES)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
labels_closed = labels + [labels[0]]
|
| 366 |
|
| 367 |
selected = [m for m in (model_ids or []) if m in set(d["model_id"])]
|
|
|
|
| 375 |
rows = d.index[d["model_id"] == mid].tolist()
|
| 376 |
if not rows:
|
| 377 |
continue
|
| 378 |
+
row = d.iloc[rows[0]]
|
| 379 |
r_vals: list[float] = []
|
| 380 |
raw_text: list[str] = []
|
| 381 |
+
for ax in labels:
|
| 382 |
+
v, tip = _radar_absolute_value(row, ax)
|
| 383 |
+
r_vals.append(v)
|
| 384 |
+
raw_text.append(tip)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
r_closed = r_vals + [r_vals[0]]
|
| 387 |
raw_closed = raw_text + [raw_text[0]]
|
|
|
|
| 400 |
opacity=1.0,
|
| 401 |
hovertemplate=(
|
| 402 |
f"<b>{short}</b><br>"
|
| 403 |
+
"%{theta}: %{r:.2f}<br>"
|
| 404 |
"%{customdata}<extra></extra>"
|
| 405 |
),
|
| 406 |
)
|
|
|
|
| 410 |
polar=dict(
|
| 411 |
radialaxis=dict(
|
| 412 |
visible=True,
|
| 413 |
+
range=[0, 1],
|
| 414 |
showticklabels=True,
|
| 415 |
tickformat=".2f",
|
| 416 |
+
tickvals=[0, 0.25, 0.5, 0.75, 1.0],
|
| 417 |
),
|
| 418 |
angularaxis=dict(direction="clockwise", rotation=90),
|
| 419 |
),
|
|
|
|
| 691 |
"num_params_m": "Parameters (millions)",
|
| 692 |
}.get(x_key, x_key)
|
| 693 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
fig = go.Figure()
|
| 695 |
+
palette = qualitative.Alphabet * 3
|
| 696 |
+
|
| 697 |
+
for i, (_, row) in enumerate(d.iterrows()):
|
| 698 |
+
mid = str(row["model_id"])
|
| 699 |
+
short = mid.split("/")[-1][:28]
|
| 700 |
+
xv = pd.to_numeric(row[x_key], errors="coerce")
|
| 701 |
+
yv = pd.to_numeric(row[y_metric_key], errors="coerce")
|
| 702 |
+
if not (np.isfinite(xv) and np.isfinite(yv)):
|
| 703 |
continue
|
| 704 |
+
color = palette[i % len(palette)]
|
| 705 |
fig.add_trace(
|
| 706 |
go.Scatter(
|
| 707 |
+
x=[float(xv)],
|
| 708 |
+
y=[float(yv)],
|
| 709 |
mode="markers",
|
| 710 |
+
name=short,
|
| 711 |
+
legendgroup=mid,
|
|
|
|
| 712 |
marker=dict(size=11, opacity=0.9, color=color, line=dict(width=0.6, color="white")),
|
| 713 |
hovertemplate=(
|
| 714 |
+
f"<b>{mid}</b><br>{x_title}: %{{x:.4f}}<br>"
|
| 715 |
f"{y_label}: %{{y:.4f}}<extra></extra>"
|
| 716 |
),
|
| 717 |
)
|
| 718 |
)
|
| 719 |
|
|
|
|
|
|
|
| 720 |
ttl = title or f"Speed versus accuracy: {y_label}"
|
| 721 |
fig.update_layout(
|
| 722 |
title=dict(text=ttl, x=0.5, xanchor="center"),
|
|
|
|
| 724 |
yaxis=dict(title=y_label + " (lower is better)", rangemode="tozero"),
|
| 725 |
template=_TEMPLATE,
|
| 726 |
height=_FIG_HEIGHT,
|
| 727 |
+
margin=dict(l=60, r=120, t=60, b=60),
|
|
|
|
| 728 |
showlegend=True,
|
| 729 |
+
**_plotly_legend_config(),
|
| 730 |
)
|
| 731 |
return fig
|
| 732 |
|
app.py
CHANGED
|
@@ -11,6 +11,7 @@ from constants import (
|
|
| 11 |
INTRODUCTION_TEXT,
|
| 12 |
LEADERBOARD_CSS,
|
| 13 |
tab_label,
|
|
|
|
| 14 |
)
|
| 15 |
import analytics
|
| 16 |
from family_registry import default_family_id
|
|
@@ -46,9 +47,9 @@ MODEL_COL = AutoEvalColumn.model.name # "Model"
|
|
| 46 |
# Empty table at import; real data loaded on demo.load (avoids blocking Space "preparing").
|
| 47 |
original_df = pd.DataFrame(columns=COLS)
|
| 48 |
|
| 49 |
-
|
| 50 |
-
"<div class='
|
| 51 |
-
"<em>Loading
|
| 52 |
)
|
| 53 |
|
| 54 |
# ---------------------------------------------------------------------------
|
|
@@ -126,7 +127,28 @@ def _wer_cell(row: dict, key: str) -> str:
|
|
| 126 |
return str(v).strip()
|
| 127 |
|
| 128 |
|
| 129 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
"""Validate and enqueue evaluation; a background worker runs approved jobs (up to 4 parallel Hub Jobs when remote mode is on).
|
| 131 |
|
| 132 |
The backend always uses the "Auto" family (pipeline → Granite → universal → CTC),
|
|
@@ -156,9 +178,17 @@ def submit_model(model_id: str, submission_notes: str) -> str:
|
|
| 156 |
if not on_hub:
|
| 157 |
return styled_error(f"Model '{model_id}' {err_msg}")
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
if err == "remote_jobs_required":
|
| 163 |
return styled_error(
|
| 164 |
"Hub Jobs are required. Set Space secrets "
|
|
@@ -289,15 +319,7 @@ def _analytics_apply(models, line_keys, top_n, latency_x, latency_y):
|
|
| 289 |
# Gradio App
|
| 290 |
# ---------------------------------------------------------------------------
|
| 291 |
|
| 292 |
-
_theme =
|
| 293 |
-
primary_hue="indigo",
|
| 294 |
-
secondary_hue="blue",
|
| 295 |
-
neutral_hue="slate",
|
| 296 |
-
).set(
|
| 297 |
-
button_primary_background_fill="*primary_500",
|
| 298 |
-
button_primary_background_fill_hover="*primary_400",
|
| 299 |
-
block_radius="12px",
|
| 300 |
-
)
|
| 301 |
|
| 302 |
with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
| 303 |
gr.HTML(BANNER, elem_id="banner")
|
|
@@ -342,7 +364,7 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 342 |
return original_df
|
| 343 |
|
| 344 |
def _on_startup():
|
| 345 |
-
"""Load leaderboard +
|
| 346 |
global original_df
|
| 347 |
from init import invalidate_results_cache
|
| 348 |
|
|
@@ -352,20 +374,19 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 352 |
table = filter_main_table("", toggleable_columns)
|
| 353 |
except Exception as exc:
|
| 354 |
table = original_df
|
| 355 |
-
|
| 356 |
-
"<div class='
|
| 357 |
f"<strong>Startup warning:</strong> {exc}</p></div>"
|
| 358 |
)
|
| 359 |
-
return table,
|
| 360 |
try:
|
| 361 |
job_queue.ensure_worker_started()
|
| 362 |
-
|
| 363 |
except Exception as exc:
|
| 364 |
-
|
| 365 |
-
"<div class='
|
| 366 |
-
f"<strong>Queue unavailable:</strong> {exc}</p></
|
| 367 |
-
|
| 368 |
-
return table, queue_html
|
| 369 |
|
| 370 |
filter_inputs = [search_box, column_checkboxes]
|
| 371 |
search_box.change(filter_main_table, inputs=filter_inputs, outputs=leaderboard_table)
|
|
@@ -396,30 +417,55 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 396 |
)
|
| 397 |
submit_btn = gr.Button("Evaluate", variant="primary", scale=1)
|
| 398 |
|
| 399 |
-
|
| 400 |
-
label="Optional
|
| 401 |
-
placeholder="
|
| 402 |
lines=4,
|
| 403 |
-
max_length=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
)
|
| 405 |
gr.Markdown(
|
| 406 |
-
"
|
| 407 |
-
"
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
)
|
| 410 |
|
| 411 |
status_output = gr.HTML()
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
submit_btn.click(
|
| 418 |
-
fn=
|
| 419 |
-
inputs=[
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
)
|
| 422 |
-
queue_refresh_btn.click(fn=job_queue.status_html, outputs=[queue_panel])
|
| 423 |
|
| 424 |
with gr.TabItem(tab_label("moderate", "Moderate"), elem_id="moderate-tab", id=2):
|
| 425 |
gr.Markdown("## Moderator access")
|
|
@@ -446,40 +492,56 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 446 |
)
|
| 447 |
mod_refresh = gr.Button("Refresh pending list")
|
| 448 |
pending_table = gr.HTML(value=job_queue.moderation_locked_placeholder_html())
|
| 449 |
-
gr.Markdown("### Recent job activity
|
| 450 |
-
mod_recent_jobs = gr.HTML(value=job_queue.moderation_locked_placeholder_html())
|
| 451 |
gr.Markdown(
|
| 452 |
-
"**Retry
|
| 453 |
-
"
|
| 454 |
-
)
|
| 455 |
-
mod_action_job = gr.Dropdown(
|
| 456 |
-
label="Job for retry or remove",
|
| 457 |
-
choices=[("(Unlock required)", "")],
|
| 458 |
-
value=None,
|
| 459 |
)
|
| 460 |
-
|
| 461 |
-
retry_job_btn = gr.Button("Retry selected job", variant="secondary")
|
| 462 |
-
remove_job_btn = gr.Button("Remove from list", variant="stop")
|
| 463 |
job_pick = gr.Dropdown(
|
| 464 |
label="Select a pending job (approve / reject)",
|
| 465 |
choices=[("(Unlock required)", "")],
|
| 466 |
value=None,
|
| 467 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
with gr.Row():
|
| 469 |
approve_btn = gr.Button("Approve selected job", variant="primary")
|
| 470 |
reject_btn = gr.Button("Reject selected job", variant="stop")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
mod_feedback = gr.HTML()
|
| 472 |
|
| 473 |
_MOD_OUTS = [
|
| 474 |
mod_feedback,
|
| 475 |
pending_table,
|
| 476 |
job_pick,
|
| 477 |
-
|
| 478 |
-
mod_action_job,
|
| 479 |
mod_progress,
|
| 480 |
]
|
| 481 |
|
| 482 |
-
def
|
| 483 |
try:
|
| 484 |
return (
|
| 485 |
job_queue.moderation_list_html(),
|
|
@@ -487,11 +549,6 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 487 |
choices=job_queue.pending_job_dropdown_choices(),
|
| 488 |
value=None,
|
| 489 |
),
|
| 490 |
-
job_queue.recent_jobs_html(30, with_heading=False),
|
| 491 |
-
gr.update(
|
| 492 |
-
choices=job_queue.moderation_action_job_choices(),
|
| 493 |
-
value=None,
|
| 494 |
-
),
|
| 495 |
job_queue.progress_html(),
|
| 496 |
)
|
| 497 |
except Exception as e:
|
|
@@ -502,91 +559,150 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 502 |
return (
|
| 503 |
msg,
|
| 504 |
gr.update(choices=[("(Queue unavailable)", "")], value=None),
|
| 505 |
-
job_queue.recent_jobs_html(30, with_heading=False),
|
| 506 |
-
gr.update(choices=[("(Queue unavailable)", "")], value=None),
|
| 507 |
msg,
|
| 508 |
)
|
| 509 |
|
| 510 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
if not unlocked:
|
| 512 |
-
return (
|
| 513 |
-
job_queue.moderation_locked_placeholder_html(),
|
| 514 |
-
gr.update(choices=[("(Unlock required)", "")], value=None),
|
| 515 |
-
job_queue.moderation_locked_placeholder_html(),
|
| 516 |
-
gr.update(choices=[("(Unlock required)", "")], value=None),
|
| 517 |
-
job_queue.moderation_locked_placeholder_html(),
|
| 518 |
-
)
|
| 519 |
ok, msg = job_queue.verify_moderator_secret(secret)
|
| 520 |
if not ok:
|
| 521 |
return (
|
| 522 |
f"<p style='color:red'>{msg}</p>",
|
| 523 |
gr.update(choices=[("(Invalid secret)", "")], value=None),
|
| 524 |
-
|
| 525 |
-
gr.update(choices=[("(Invalid secret)", "")], value=None),
|
| 526 |
job_queue.moderation_locked_placeholder_html(),
|
| 527 |
)
|
| 528 |
-
return _mod_refresh_data()
|
| 529 |
|
| 530 |
-
def _mod_bundle(note_html: str):
|
| 531 |
-
|
| 532 |
-
return
|
| 533 |
|
| 534 |
-
def
|
| 535 |
-
|
| 536 |
-
return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>")
|
| 537 |
-
if not jid or not str(jid).strip():
|
| 538 |
-
return _mod_bundle(f"<p style='color:red'>{empty_msg}</p>")
|
| 539 |
-
ok, msg = fn(jid, secret)
|
| 540 |
color = "green" if ok else "red"
|
| 541 |
-
return _mod_bundle(f"<p style='color:{color}'>{msg}</p>")
|
| 542 |
|
| 543 |
-
def _mod_unlock(secret: str):
|
| 544 |
ok, msg = job_queue.verify_moderator_secret(secret)
|
| 545 |
if not ok:
|
|
|
|
| 546 |
return (
|
| 547 |
False,
|
| 548 |
gr.update(visible=False),
|
| 549 |
f"<p style='color:red'>{msg}</p>",
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
gr.update(choices=[("(Unlock required)", "")], value=None),
|
| 555 |
-
job_queue.moderation_locked_placeholder_html(),
|
| 556 |
-
),
|
| 557 |
)
|
| 558 |
try:
|
| 559 |
-
|
| 560 |
except Exception as e:
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
gr.update(choices=[("(Queue unavailable)", "")], value=None),
|
| 566 |
-
f"<p style='color:orange'><strong>Queue unavailable:</strong> {e}</p>",
|
| 567 |
-
)
|
| 568 |
return (
|
| 569 |
True,
|
| 570 |
gr.update(visible=True),
|
| 571 |
"<p style='color:green'>Moderator tools unlocked for this session.</p>",
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
| 573 |
)
|
| 574 |
|
| 575 |
-
def _mod_approve(secret, jid, unlocked):
|
| 576 |
-
|
|
|
|
|
|
|
|
|
|
| 577 |
|
| 578 |
-
|
| 579 |
-
|
| 580 |
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
|
| 586 |
-
def
|
| 587 |
-
|
| 588 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
|
| 591 |
def _mod_progress_gated(unlocked: bool):
|
| 592 |
if not unlocked:
|
|
@@ -595,47 +711,42 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 595 |
|
| 596 |
mod_unlock_btn.click(
|
| 597 |
fn=_mod_unlock,
|
| 598 |
-
inputs=[mod_secret],
|
| 599 |
outputs=[
|
| 600 |
mod_unlocked,
|
| 601 |
mod_panel,
|
| 602 |
mod_unlock_feedback,
|
| 603 |
pending_table,
|
| 604 |
job_pick,
|
| 605 |
-
|
| 606 |
-
mod_action_job,
|
| 607 |
mod_progress,
|
| 608 |
],
|
| 609 |
)
|
| 610 |
mod_refresh.click(
|
| 611 |
fn=_mod_refresh,
|
| 612 |
-
inputs=[mod_secret, mod_unlocked],
|
| 613 |
-
outputs=[
|
| 614 |
-
pending_table,
|
| 615 |
-
job_pick,
|
| 616 |
-
mod_recent_jobs,
|
| 617 |
-
mod_action_job,
|
| 618 |
-
mod_progress,
|
| 619 |
-
],
|
| 620 |
)
|
| 621 |
approve_btn.click(
|
| 622 |
fn=_mod_approve,
|
| 623 |
-
inputs=[mod_secret, job_pick, mod_unlocked],
|
| 624 |
outputs=_MOD_OUTS,
|
| 625 |
)
|
| 626 |
reject_btn.click(
|
| 627 |
fn=_mod_reject,
|
| 628 |
-
inputs=[mod_secret, job_pick, mod_unlocked],
|
| 629 |
outputs=_MOD_OUTS,
|
| 630 |
)
|
| 631 |
-
|
| 632 |
-
fn=
|
| 633 |
-
inputs=[
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
|
|
|
|
|
|
| 639 |
outputs=_MOD_OUTS,
|
| 640 |
)
|
| 641 |
|
|
@@ -647,10 +758,10 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 647 |
"(weighted harmonic mean of 1/(1+WER); higher is better).\n\n"
|
| 648 |
"**WER heatmap** shows WER by model and scenario for the top N models (sorted by score); lower WER "
|
| 649 |
"corresponds to greener cells.\n\n"
|
| 650 |
-
"**Speed versus accuracy**
|
| 651 |
-
"
|
| 652 |
-
"**Robustness radar**
|
| 653 |
-
"
|
| 654 |
"**WER by scenario** compares raw WER across conditions for the selected top models."
|
| 655 |
)
|
| 656 |
with gr.Row():
|
|
@@ -740,12 +851,12 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 740 |
# Periodic queue refresh (Timer added in Gradio 4.44). Avoid demo.load(..., every=); unsupported on some builds.
|
| 741 |
if hasattr(gr, "Timer"):
|
| 742 |
with gr.Row(visible=False):
|
| 743 |
-
|
| 744 |
_progress_timer = gr.Timer(2)
|
| 745 |
-
|
| 746 |
_progress_timer.tick(fn=_mod_progress_gated, inputs=[mod_unlocked], outputs=[mod_progress])
|
| 747 |
|
| 748 |
-
demo.load(fn=_on_startup, outputs=[leaderboard_table,
|
| 749 |
demo.load(
|
| 750 |
fn=_analytics_initial,
|
| 751 |
outputs=[
|
|
|
|
| 11 |
INTRODUCTION_TEXT,
|
| 12 |
LEADERBOARD_CSS,
|
| 13 |
tab_label,
|
| 14 |
+
treble_gradio_theme,
|
| 15 |
)
|
| 16 |
import analytics
|
| 17 |
from family_registry import default_family_id
|
|
|
|
| 47 |
# Empty table at import; real data loaded on demo.load (avoids blocking Space "preparing").
|
| 48 |
original_df = pd.DataFrame(columns=COLS)
|
| 49 |
|
| 50 |
+
_NEXT_UP_PLACEHOLDER = (
|
| 51 |
+
"<div class='next-up-panel' style='font-size:0.9em;opacity:0.85'>"
|
| 52 |
+
"<em>Loading upcoming evaluations…</em></div>"
|
| 53 |
)
|
| 54 |
|
| 55 |
# ---------------------------------------------------------------------------
|
|
|
|
| 127 |
return str(v).strip()
|
| 128 |
|
| 129 |
|
| 130 |
+
def _model_hub_page_link(model_id: str) -> str:
|
| 131 |
+
mid = (model_id or "").strip().replace(" ", "")
|
| 132 |
+
if not mid or "/" not in mid:
|
| 133 |
+
return (
|
| 134 |
+
"<p style='font-size:0.9em;opacity:0.85'>"
|
| 135 |
+
"Enter a model id (<code>author/name</code>) to open its Hub page.</p>"
|
| 136 |
+
)
|
| 137 |
+
url = f"https://huggingface.co/{mid}"
|
| 138 |
+
return (
|
| 139 |
+
f"<p style='font-size:0.9em'>"
|
| 140 |
+
f"<a href='{url}' target='_blank' rel='noopener noreferrer'>"
|
| 141 |
+
f"Open model page on the Hub</a> — request access if the repo is gated.</p>"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def submit_model(
|
| 146 |
+
model_id: str,
|
| 147 |
+
submission_notes: str,
|
| 148 |
+
extra_requirements: str,
|
| 149 |
+
custom_script: str,
|
| 150 |
+
is_gated: bool,
|
| 151 |
+
) -> str:
|
| 152 |
"""Validate and enqueue evaluation; a background worker runs approved jobs (up to 4 parallel Hub Jobs when remote mode is on).
|
| 153 |
|
| 154 |
The backend always uses the "Auto" family (pipeline → Granite → universal → CTC),
|
|
|
|
| 178 |
if not on_hub:
|
| 179 |
return styled_error(f"Model '{model_id}' {err_msg}")
|
| 180 |
|
| 181 |
+
try:
|
| 182 |
+
job_id, position, err, awaiting_mod = job_queue.enqueue(
|
| 183 |
+
model_id,
|
| 184 |
+
family_id,
|
| 185 |
+
submission_notes=submission_notes or "",
|
| 186 |
+
extra_requirements=extra_requirements or "",
|
| 187 |
+
custom_script=custom_script or "",
|
| 188 |
+
is_gated=bool(is_gated),
|
| 189 |
+
)
|
| 190 |
+
except Exception as exc:
|
| 191 |
+
return styled_warning(f"Could not enqueue submission: {exc}")
|
| 192 |
if err == "remote_jobs_required":
|
| 193 |
return styled_error(
|
| 194 |
"Hub Jobs are required. Set Space secrets "
|
|
|
|
| 319 |
# Gradio App
|
| 320 |
# ---------------------------------------------------------------------------
|
| 321 |
|
| 322 |
+
_theme = treble_gradio_theme()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
| 325 |
gr.HTML(BANNER, elem_id="banner")
|
|
|
|
| 364 |
return original_df
|
| 365 |
|
| 366 |
def _on_startup():
|
| 367 |
+
"""Load leaderboard + next-up list after UI is up (keeps Space prepare step fast)."""
|
| 368 |
global original_df
|
| 369 |
from init import invalidate_results_cache
|
| 370 |
|
|
|
|
| 374 |
table = filter_main_table("", toggleable_columns)
|
| 375 |
except Exception as exc:
|
| 376 |
table = original_df
|
| 377 |
+
next_html = (
|
| 378 |
+
"<div class='next-up-panel'><p style='color:orange'>"
|
| 379 |
f"<strong>Startup warning:</strong> {exc}</p></div>"
|
| 380 |
)
|
| 381 |
+
return table, next_html
|
| 382 |
try:
|
| 383 |
job_queue.ensure_worker_started()
|
| 384 |
+
next_html = job_queue.next_up_html()
|
| 385 |
except Exception as exc:
|
| 386 |
+
next_html = (
|
| 387 |
+
"<div class='next-up-panel'><p style='color:orange'>"
|
| 388 |
+
f"<strong>Queue unavailable:</strong> {exc}</p></motionmotionmotionmotiondiv>")
|
| 389 |
+
return table, next_html
|
|
|
|
| 390 |
|
| 391 |
filter_inputs = [search_box, column_checkboxes]
|
| 392 |
search_box.change(filter_main_table, inputs=filter_inputs, outputs=leaderboard_table)
|
|
|
|
| 417 |
)
|
| 418 |
submit_btn = gr.Button("Evaluate", variant="primary", scale=1)
|
| 419 |
|
| 420 |
+
requirements_input = gr.Textbox(
|
| 421 |
+
label="Optional extra Python requirements (one per line, requirements.txt format)",
|
| 422 |
+
placeholder="transformers @ git+https://github.com/huggingface/transformers.git\nsentencepiece",
|
| 423 |
lines=4,
|
| 424 |
+
max_length=8000,
|
| 425 |
+
)
|
| 426 |
+
script_input = gr.Code(
|
| 427 |
+
label="Optional custom uv-run script (only run if a moderator approves it)",
|
| 428 |
+
language="python",
|
| 429 |
+
lines=12,
|
| 430 |
)
|
| 431 |
gr.Markdown(
|
| 432 |
+
"Custom scripts are **not** executed on this Space. They are stored with your submission "
|
| 433 |
+
"and run on a Hub Job **only** after a moderator explicitly approves using that script."
|
| 434 |
+
)
|
| 435 |
+
with gr.Row():
|
| 436 |
+
is_gated_input = gr.Checkbox(label="This is a gated repo", value=False)
|
| 437 |
+
gated_link = gr.HTML(value=_model_hub_page_link(""))
|
| 438 |
+
model_input.change(fn=_model_hub_page_link, inputs=[model_input], outputs=[gated_link])
|
| 439 |
+
notes_input = gr.Textbox(
|
| 440 |
+
label="Optional free-form notes for moderators",
|
| 441 |
+
placeholder="Repo link, paper, eval caveats, hardware notes…",
|
| 442 |
+
lines=2,
|
| 443 |
+
max_length=4000,
|
| 444 |
)
|
| 445 |
|
| 446 |
status_output = gr.HTML()
|
| 447 |
+
gr.Markdown("### Next models to evaluate")
|
| 448 |
+
next_up_panel = gr.HTML(value=_NEXT_UP_PLACEHOLDER, elem_classes="next-up-panel")
|
| 449 |
+
|
| 450 |
+
def _submit_and_refresh(model_id, notes, reqs, script, gated):
|
| 451 |
+
status = submit_model(model_id, notes, reqs, script, gated)
|
| 452 |
+
try:
|
| 453 |
+
nxt = job_queue.next_up_html()
|
| 454 |
+
except Exception as exc:
|
| 455 |
+
nxt = f"<p style='color:orange'>Could not load queue: {exc}</p>"
|
| 456 |
+
return status, nxt
|
| 457 |
|
| 458 |
submit_btn.click(
|
| 459 |
+
fn=_submit_and_refresh,
|
| 460 |
+
inputs=[
|
| 461 |
+
model_input,
|
| 462 |
+
notes_input,
|
| 463 |
+
requirements_input,
|
| 464 |
+
script_input,
|
| 465 |
+
is_gated_input,
|
| 466 |
+
],
|
| 467 |
+
outputs=[status_output, next_up_panel],
|
| 468 |
)
|
|
|
|
| 469 |
|
| 470 |
with gr.TabItem(tab_label("moderate", "Moderate"), elem_id="moderate-tab", id=2):
|
| 471 |
gr.Markdown("## Moderator access")
|
|
|
|
| 492 |
)
|
| 493 |
mod_refresh = gr.Button("Refresh pending list")
|
| 494 |
pending_table = gr.HTML(value=job_queue.moderation_locked_placeholder_html())
|
| 495 |
+
gr.Markdown("### Recent job activity")
|
|
|
|
| 496 |
gr.Markdown(
|
| 497 |
+
"**Retry** re-queues **failed** jobs only. **Remove** drops a job from this list "
|
| 498 |
+
"(not allowed while **running**)."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
)
|
| 500 |
+
mod_jobs_tick = gr.State(0)
|
|
|
|
|
|
|
| 501 |
job_pick = gr.Dropdown(
|
| 502 |
label="Select a pending job (approve / reject)",
|
| 503 |
choices=[("(Unlock required)", "")],
|
| 504 |
value=None,
|
| 505 |
)
|
| 506 |
+
mod_run_custom_script = gr.Checkbox(
|
| 507 |
+
label="Run submitter's custom script on approval (if provided)",
|
| 508 |
+
value=False,
|
| 509 |
+
)
|
| 510 |
with gr.Row():
|
| 511 |
approve_btn = gr.Button("Approve selected job", variant="primary")
|
| 512 |
reject_btn = gr.Button("Reject selected job", variant="stop")
|
| 513 |
+
gr.Markdown("### Import result from bucket artifact")
|
| 514 |
+
gr.Markdown(
|
| 515 |
+
"If a Hub Job finished but the leaderboard CSV was not updated, paste the artifact "
|
| 516 |
+
"file name from <code>results/remote_artifacts/</code> (e.g. <code>a1b2c3d4.json</code>) "
|
| 517 |
+
"to merge WER/RTF into the CSV. The far-field score is computed automatically."
|
| 518 |
+
)
|
| 519 |
+
mod_artifact_ref = gr.Textbox(
|
| 520 |
+
label="Artifact JSON file name or bucket path",
|
| 521 |
+
placeholder="e.g. a1b2c3d4.json or results/remote_artifacts/a1b2c3d4.json",
|
| 522 |
+
)
|
| 523 |
+
mod_replace_existing = gr.Checkbox(
|
| 524 |
+
label="Replace existing leaderboard row for this model",
|
| 525 |
+
value=False,
|
| 526 |
+
)
|
| 527 |
+
mod_import_notes = gr.Textbox(
|
| 528 |
+
label="Optional submission notes override",
|
| 529 |
+
placeholder="Leave empty to use notes from the matched queue job, if any",
|
| 530 |
+
lines=1,
|
| 531 |
+
max_length=4000,
|
| 532 |
+
)
|
| 533 |
+
import_artifact_btn = gr.Button("Import artifact to CSV", variant="secondary")
|
| 534 |
mod_feedback = gr.HTML()
|
| 535 |
|
| 536 |
_MOD_OUTS = [
|
| 537 |
mod_feedback,
|
| 538 |
pending_table,
|
| 539 |
job_pick,
|
| 540 |
+
mod_jobs_tick,
|
|
|
|
| 541 |
mod_progress,
|
| 542 |
]
|
| 543 |
|
| 544 |
+
def _mod_refresh_core():
|
| 545 |
try:
|
| 546 |
return (
|
| 547 |
job_queue.moderation_list_html(),
|
|
|
|
| 549 |
choices=job_queue.pending_job_dropdown_choices(),
|
| 550 |
value=None,
|
| 551 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
job_queue.progress_html(),
|
| 553 |
)
|
| 554 |
except Exception as e:
|
|
|
|
| 559 |
return (
|
| 560 |
msg,
|
| 561 |
gr.update(choices=[("(Queue unavailable)", "")], value=None),
|
|
|
|
|
|
|
| 562 |
msg,
|
| 563 |
)
|
| 564 |
|
| 565 |
+
def _mod_refresh_data(tick: int):
|
| 566 |
+
pending, pick, progress = _mod_refresh_core()
|
| 567 |
+
return pending, pick, int(tick or 0) + 1, progress
|
| 568 |
+
|
| 569 |
+
def _mod_locked_tuple(tick: int):
|
| 570 |
+
ph = job_queue.moderation_locked_placeholder_html()
|
| 571 |
+
return ph, gr.update(choices=[("(Unlock required)", "")], value=None), int(tick or 0), ph
|
| 572 |
+
|
| 573 |
+
def _mod_refresh(secret: str, unlocked: bool, tick: int):
|
| 574 |
if not unlocked:
|
| 575 |
+
return _mod_locked_tuple(tick)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
ok, msg = job_queue.verify_moderator_secret(secret)
|
| 577 |
if not ok:
|
| 578 |
return (
|
| 579 |
f"<p style='color:red'>{msg}</p>",
|
| 580 |
gr.update(choices=[("(Invalid secret)", "")], value=None),
|
| 581 |
+
int(tick or 0),
|
|
|
|
| 582 |
job_queue.moderation_locked_placeholder_html(),
|
| 583 |
)
|
| 584 |
+
return _mod_refresh_data(tick)
|
| 585 |
|
| 586 |
+
def _mod_bundle(note_html: str, tick: int):
|
| 587 |
+
pending, pick, new_tick, progress = _mod_refresh_data(tick)
|
| 588 |
+
return note_html, pending, pick, new_tick, progress
|
| 589 |
|
| 590 |
+
def _mod_row_action(secret: str, jid: str, tick: int, action_fn):
|
| 591 |
+
ok, msg = action_fn(jid, secret)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
color = "green" if ok else "red"
|
| 593 |
+
return _mod_bundle(f"<p style='color:{color}'>{msg}</p>", tick)
|
| 594 |
|
| 595 |
+
def _mod_unlock(secret: str, tick: int):
|
| 596 |
ok, msg = job_queue.verify_moderator_secret(secret)
|
| 597 |
if not ok:
|
| 598 |
+
ph = job_queue.moderation_locked_placeholder_html()
|
| 599 |
return (
|
| 600 |
False,
|
| 601 |
gr.update(visible=False),
|
| 602 |
f"<p style='color:red'>{msg}</p>",
|
| 603 |
+
ph,
|
| 604 |
+
gr.update(choices=[("(Unlock required)", "")], value=None),
|
| 605 |
+
int(tick or 0),
|
| 606 |
+
ph,
|
|
|
|
|
|
|
|
|
|
| 607 |
)
|
| 608 |
try:
|
| 609 |
+
pending, pick, new_tick, progress = _mod_refresh_data(tick)
|
| 610 |
except Exception as e:
|
| 611 |
+
pending = f"<p style='color:orange'><strong>Queue unavailable:</strong> {e}</p>"
|
| 612 |
+
pick = gr.update(choices=[("(Queue unavailable)", "")], value=None)
|
| 613 |
+
new_tick = int(tick or 0) + 1
|
| 614 |
+
progress = pending
|
|
|
|
|
|
|
|
|
|
| 615 |
return (
|
| 616 |
True,
|
| 617 |
gr.update(visible=True),
|
| 618 |
"<p style='color:green'>Moderator tools unlocked for this session.</p>",
|
| 619 |
+
pending,
|
| 620 |
+
pick,
|
| 621 |
+
new_tick,
|
| 622 |
+
progress,
|
| 623 |
)
|
| 624 |
|
| 625 |
+
def _mod_approve(secret, jid, unlocked, run_custom, tick):
|
| 626 |
+
if not unlocked:
|
| 627 |
+
return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>", tick)
|
| 628 |
+
if not jid or not str(jid).strip():
|
| 629 |
+
return _mod_bundle("<p style='color:red'>Select a job.</p>", tick)
|
| 630 |
|
| 631 |
+
def _approve(j, s):
|
| 632 |
+
return job_queue.approve_job(j, s, run_custom_script=bool(run_custom))
|
| 633 |
|
| 634 |
+
return _mod_row_action(secret, str(jid).strip(), tick, _approve)
|
| 635 |
+
|
| 636 |
+
def _mod_reject(secret, jid, unlocked, tick):
|
| 637 |
+
if not unlocked:
|
| 638 |
+
return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>", tick)
|
| 639 |
+
if not jid or not str(jid).strip():
|
| 640 |
+
return _mod_bundle("<p style='color:red'>Select a job.</p>", tick)
|
| 641 |
+
return _mod_row_action(secret, str(jid).strip(), tick, job_queue.reject_job)
|
| 642 |
|
| 643 |
+
def _mod_import_artifact(
|
| 644 |
+
secret, artifact_ref, replace_existing, import_notes, unlocked, tick
|
| 645 |
+
):
|
| 646 |
+
if not unlocked:
|
| 647 |
+
return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>", tick)
|
| 648 |
+
ok, msg = job_queue.import_artifact_to_leaderboard(
|
| 649 |
+
artifact_ref,
|
| 650 |
+
secret,
|
| 651 |
+
replace_existing=bool(replace_existing),
|
| 652 |
+
submission_notes=import_notes or "",
|
| 653 |
)
|
| 654 |
+
color = "green" if ok else "red"
|
| 655 |
+
return _mod_bundle(f"<p style='color:{color}'>{msg}</p>", tick)
|
| 656 |
+
|
| 657 |
+
@gr.render(inputs=[mod_unlocked, mod_secret, mod_jobs_tick])
|
| 658 |
+
def _render_job_activity_rows(unlocked: bool, secret: str, tick: int):
|
| 659 |
+
if not unlocked:
|
| 660 |
+
gr.Markdown("*Unlock moderator tools to see job activity.*")
|
| 661 |
+
return
|
| 662 |
+
try:
|
| 663 |
+
rows = job_queue.recent_jobs_for_render(30)
|
| 664 |
+
except Exception as e:
|
| 665 |
+
gr.Markdown(f"*Queue unavailable: {e}*")
|
| 666 |
+
return
|
| 667 |
+
if not rows:
|
| 668 |
+
gr.Markdown("*No job history yet.*")
|
| 669 |
+
return
|
| 670 |
+
for row in rows:
|
| 671 |
+
jid = row["id"]
|
| 672 |
+
hub = row.get("hub_link_html") or ""
|
| 673 |
+
hub_bit = f" · {hub}" if hub else ""
|
| 674 |
+
script_bit = " · custom script" if row.get("has_custom_script") else ""
|
| 675 |
+
summary = (
|
| 676 |
+
f"**{row['model_id']}** — `{row['status']}` "
|
| 677 |
+
f"(job `{jid}`, {row['updated_at']}){script_bit}{hub_bit}"
|
| 678 |
+
)
|
| 679 |
+
if row.get("error"):
|
| 680 |
+
summary += f"\n\n`{row['error']}`"
|
| 681 |
+
with gr.Row():
|
| 682 |
+
gr.Markdown(summary, scale=4)
|
| 683 |
+
retry_b = gr.Button(
|
| 684 |
+
"Retry",
|
| 685 |
+
variant="secondary",
|
| 686 |
+
scale=0,
|
| 687 |
+
min_width=90,
|
| 688 |
+
interactive=bool(row.get("can_retry")),
|
| 689 |
+
)
|
| 690 |
+
remove_b = gr.Button(
|
| 691 |
+
"Remove",
|
| 692 |
+
variant="stop",
|
| 693 |
+
scale=0,
|
| 694 |
+
min_width=90,
|
| 695 |
+
interactive=bool(row.get("can_remove")),
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
def _retry_fn(s=secret, j=jid, t=tick):
|
| 699 |
+
return _mod_row_action(s, j, t, job_queue.retry_failed_job)
|
| 700 |
+
|
| 701 |
+
def _remove_fn(s=secret, j=jid, t=tick):
|
| 702 |
+
return _mod_row_action(s, j, t, job_queue.remove_job_entry)
|
| 703 |
+
|
| 704 |
+
retry_b.click(fn=_retry_fn, outputs=_MOD_OUTS)
|
| 705 |
+
remove_b.click(fn=_remove_fn, outputs=_MOD_OUTS)
|
| 706 |
|
| 707 |
def _mod_progress_gated(unlocked: bool):
|
| 708 |
if not unlocked:
|
|
|
|
| 711 |
|
| 712 |
mod_unlock_btn.click(
|
| 713 |
fn=_mod_unlock,
|
| 714 |
+
inputs=[mod_secret, mod_jobs_tick],
|
| 715 |
outputs=[
|
| 716 |
mod_unlocked,
|
| 717 |
mod_panel,
|
| 718 |
mod_unlock_feedback,
|
| 719 |
pending_table,
|
| 720 |
job_pick,
|
| 721 |
+
mod_jobs_tick,
|
|
|
|
| 722 |
mod_progress,
|
| 723 |
],
|
| 724 |
)
|
| 725 |
mod_refresh.click(
|
| 726 |
fn=_mod_refresh,
|
| 727 |
+
inputs=[mod_secret, mod_unlocked, mod_jobs_tick],
|
| 728 |
+
outputs=[pending_table, job_pick, mod_jobs_tick, mod_progress],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
)
|
| 730 |
approve_btn.click(
|
| 731 |
fn=_mod_approve,
|
| 732 |
+
inputs=[mod_secret, job_pick, mod_unlocked, mod_run_custom_script, mod_jobs_tick],
|
| 733 |
outputs=_MOD_OUTS,
|
| 734 |
)
|
| 735 |
reject_btn.click(
|
| 736 |
fn=_mod_reject,
|
| 737 |
+
inputs=[mod_secret, job_pick, mod_unlocked, mod_jobs_tick],
|
| 738 |
outputs=_MOD_OUTS,
|
| 739 |
)
|
| 740 |
+
import_artifact_btn.click(
|
| 741 |
+
fn=_mod_import_artifact,
|
| 742 |
+
inputs=[
|
| 743 |
+
mod_secret,
|
| 744 |
+
mod_artifact_ref,
|
| 745 |
+
mod_replace_existing,
|
| 746 |
+
mod_import_notes,
|
| 747 |
+
mod_unlocked,
|
| 748 |
+
mod_jobs_tick,
|
| 749 |
+
],
|
| 750 |
outputs=_MOD_OUTS,
|
| 751 |
)
|
| 752 |
|
|
|
|
| 758 |
"(weighted harmonic mean of 1/(1+WER); higher is better).\n\n"
|
| 759 |
"**WER heatmap** shows WER by model and scenario for the top N models (sorted by score); lower WER "
|
| 760 |
"corresponds to greener cells.\n\n"
|
| 761 |
+
"**Speed versus accuracy** colors each model by name (toggle in the legend) on an inference or "
|
| 762 |
+
"scale metric versus WER.\n\n"
|
| 763 |
+
"**Robustness radar** uses absolute axes from 0 to 1: **Dry Speech** (1 − WER), **Score** "
|
| 764 |
+
"(far-field / 100), **Speed** (RTF / (1 + RTF)), and **Compactness** (1 / (1 + params_M / 1000)).\n\n"
|
| 765 |
"**WER by scenario** compares raw WER across conditions for the selected top models."
|
| 766 |
)
|
| 767 |
with gr.Row():
|
|
|
|
| 851 |
# Periodic queue refresh (Timer added in Gradio 4.44). Avoid demo.load(..., every=); unsupported on some builds.
|
| 852 |
if hasattr(gr, "Timer"):
|
| 853 |
with gr.Row(visible=False):
|
| 854 |
+
_next_up_timer = gr.Timer(15)
|
| 855 |
_progress_timer = gr.Timer(2)
|
| 856 |
+
_next_up_timer.tick(fn=job_queue.next_up_html, outputs=[next_up_panel])
|
| 857 |
_progress_timer.tick(fn=_mod_progress_gated, inputs=[mod_unlocked], outputs=[mod_progress])
|
| 858 |
|
| 859 |
+
demo.load(fn=_on_startup, outputs=[leaderboard_table, next_up_panel])
|
| 860 |
demo.load(
|
| 861 |
fn=_analytics_initial,
|
| 862 |
outputs=[
|
backends/auto.py
CHANGED
|
@@ -108,8 +108,16 @@ def build_transcriber(
|
|
| 108 |
except Exception as e:
|
| 109 |
_record_error("universal", e)
|
| 110 |
|
|
|
|
| 111 |
try:
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
except Exception as e:
|
| 114 |
_record_error("ctc", e)
|
| 115 |
|
|
|
|
| 108 |
except Exception as e:
|
| 109 |
_record_error("universal", e)
|
| 110 |
|
| 111 |
+
# CTC only applies to wav2vec/hubert-style checkpoints; skip when config says seq2seq.
|
| 112 |
try:
|
| 113 |
+
from .family_resolve import infer_model_type
|
| 114 |
+
|
| 115 |
+
mt = infer_model_type(model_id)
|
| 116 |
+
_ctc_skip_types = frozenset(
|
| 117 |
+
{"cohere_asr", "whisper", "granite_speech", "speech_to_text", "moonshine_streaming"}
|
| 118 |
+
)
|
| 119 |
+
if mt not in _ctc_skip_types:
|
| 120 |
+
return transformers_ctc.build_transcriber(model_id, device_str)
|
| 121 |
except Exception as e:
|
| 122 |
_record_error("ctc", e)
|
| 123 |
|
backends/family_resolve.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Resolve ``family_id='auto'`` using Hub ``config.json`` ``model_type`` when possible.
|
| 3 |
+
|
| 4 |
+
Avoids running the full auto cascade (pipeline → universal → CTC) for models that
|
| 5 |
+
need a specific loader (e.g. Cohere ASR is seq2seq remote code, not CTC).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
# model_type (config.json) -> backend family id
|
| 14 |
+
_MODEL_TYPE_TO_FAMILY: dict[str, str] = {
|
| 15 |
+
"granite_speech": "granite_speech",
|
| 16 |
+
"cohere_asr": "universal",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
# Extra uv/pip packages commonly required by remote-code checkpoints (not always declared on Hub).
|
| 20 |
+
_MODEL_TYPE_EXTRA_DEPS: dict[str, list[str]] = {
|
| 21 |
+
"cohere_asr": ["sentencepiece"],
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def infer_model_type(model_id: str) -> str:
|
| 26 |
+
"""Read ``model_type`` from the Hub ``config.json`` without loading weights."""
|
| 27 |
+
try:
|
| 28 |
+
from huggingface_hub import hf_hub_download
|
| 29 |
+
|
| 30 |
+
token = os.environ.get("HF_TOKEN") or os.environ.get("ds_token")
|
| 31 |
+
path = hf_hub_download(
|
| 32 |
+
repo_id=model_id,
|
| 33 |
+
filename="config.json",
|
| 34 |
+
token=token,
|
| 35 |
+
)
|
| 36 |
+
with open(path, encoding="utf-8") as f:
|
| 37 |
+
cfg = json.load(f)
|
| 38 |
+
return str(cfg.get("model_type", "") or "")
|
| 39 |
+
except Exception:
|
| 40 |
+
return ""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def extra_deps_for_model(model_id: str) -> list[str]:
|
| 44 |
+
"""Optional packages implied by ``model_type`` (for uv job dependency lists)."""
|
| 45 |
+
mt = infer_model_type(model_id)
|
| 46 |
+
return list(_MODEL_TYPE_EXTRA_DEPS.get(mt, []))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def resolve_auto_family(model_id: str, family_id: str) -> str:
|
| 50 |
+
"""
|
| 51 |
+
If ``family_id`` is ``auto``, map known ``model_type`` values to a concrete backend.
|
| 52 |
+
Otherwise normalize aliases and return unchanged.
|
| 53 |
+
"""
|
| 54 |
+
from family_registry import _FAMILY_ALIASES
|
| 55 |
+
|
| 56 |
+
fid = (family_id or "auto").strip().lower()
|
| 57 |
+
fid = _FAMILY_ALIASES.get(fid, fid)
|
| 58 |
+
if fid != "auto":
|
| 59 |
+
return fid
|
| 60 |
+
mt = infer_model_type(model_id)
|
| 61 |
+
return _MODEL_TYPE_TO_FAMILY.get(mt, "auto")
|
backends/registry.py
CHANGED
|
@@ -43,7 +43,9 @@ def build_transcriber(
|
|
| 43 |
universal,
|
| 44 |
)
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
| 47 |
if fid == "auto":
|
| 48 |
return auto.build_transcriber(model_id, device_str, device_int)
|
| 49 |
if fid == "transformers_pipeline":
|
|
|
|
| 43 |
universal,
|
| 44 |
)
|
| 45 |
|
| 46 |
+
from .family_resolve import resolve_auto_family
|
| 47 |
+
|
| 48 |
+
fid = resolve_auto_family(model_id, _FAMILY_ALIASES.get(family_id, family_id))
|
| 49 |
if fid == "auto":
|
| 50 |
return auto.build_transcriber(model_id, device_str, device_int)
|
| 51 |
if fid == "transformers_pipeline":
|
constants.py
CHANGED
|
@@ -197,6 +197,71 @@ AUDIO_CONDITIONS = {
|
|
| 197 |
"reverberant": {"folder": "wavs", "label": "WER Reverberant"},
|
| 198 |
}
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
LEADERBOARD_CSS = """
|
| 201 |
/* Hide Gradio footer logo */
|
| 202 |
footer { display: none !important; }
|
|
@@ -205,7 +270,7 @@ footer { display: none !important; }
|
|
| 205 |
.ffasr-banner {
|
| 206 |
text-align: center;
|
| 207 |
padding: 1.4rem 0 0.8rem 0;
|
| 208 |
-
border-bottom: 1px solid var(--border-color-primary, rgba(
|
| 209 |
margin-bottom: 0.6rem;
|
| 210 |
}
|
| 211 |
.ffasr-banner h1 {
|
|
@@ -214,7 +279,7 @@ footer { display: none !important; }
|
|
| 214 |
margin: 0;
|
| 215 |
font-weight: 800;
|
| 216 |
letter-spacing: -0.02em;
|
| 217 |
-
background: linear-gradient(90deg,
|
| 218 |
-webkit-background-clip: text;
|
| 219 |
background-clip: text;
|
| 220 |
-webkit-text-fill-color: transparent;
|
|
@@ -224,6 +289,7 @@ footer { display: none !important; }
|
|
| 224 |
margin-top: 0.35rem;
|
| 225 |
font-size: 1rem;
|
| 226 |
opacity: 0.72;
|
|
|
|
| 227 |
}
|
| 228 |
.ffasr-badges {
|
| 229 |
margin-top: 0.7rem;
|
|
@@ -236,9 +302,9 @@ footer { display: none !important; }
|
|
| 236 |
font-size: 0.78rem;
|
| 237 |
padding: 0.18rem 0.65rem;
|
| 238 |
border-radius: 999px;
|
| 239 |
-
background: rgba(
|
| 240 |
-
color:
|
| 241 |
-
border: 1px solid rgba(
|
| 242 |
font-weight: 500;
|
| 243 |
letter-spacing: 0.01em;
|
| 244 |
}
|
|
@@ -251,8 +317,9 @@ footer { display: none !important; }
|
|
| 251 |
font-weight: 500 !important;
|
| 252 |
}
|
| 253 |
.tab-buttons button.selected {
|
| 254 |
-
border-bottom: 2px solid
|
| 255 |
-
background: var(--block-background-fill,
|
|
|
|
| 256 |
}
|
| 257 |
|
| 258 |
/* ---- Leaderboard table ---- */
|
|
@@ -274,18 +341,31 @@ footer { display: none !important; }
|
|
| 274 |
table-layout: auto !important;
|
| 275 |
}
|
| 276 |
#leaderboard-table tbody tr:hover td {
|
| 277 |
-
background: rgba(
|
| 278 |
}
|
| 279 |
|
| 280 |
/* ---- Card-like panels ---- */
|
| 281 |
.queue-status,
|
|
|
|
| 282 |
.ffasr-card {
|
| 283 |
padding: 0.8rem 1rem;
|
| 284 |
border-radius: 12px;
|
| 285 |
-
background: var(--block-background-fill,
|
| 286 |
-
border: 1px solid var(--border-color-primary, rgba(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
}
|
| 288 |
-
.queue-status p { margin: 0.15rem 0; }
|
| 289 |
|
| 290 |
/* ---- Sub-status footer line ---- */
|
| 291 |
.ffasr-footnote {
|
|
@@ -293,10 +373,17 @@ footer { display: none !important; }
|
|
| 293 |
font-size: 0.82rem;
|
| 294 |
opacity: 0.65;
|
| 295 |
margin-top: 0.8rem;
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
/* ---- Primary button emphasis ---- */
|
| 299 |
button.primary, .primary button {
|
| 300 |
font-weight: 600 !important;
|
| 301 |
}
|
| 302 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
"reverberant": {"folder": "wavs", "label": "WER Reverberant"},
|
| 198 |
}
|
| 199 |
|
| 200 |
+
# Treble brand palette (dark UI — teal accent instead of purple/indigo)
|
| 201 |
+
TREBLE_TEAL = "#38BFA1"
|
| 202 |
+
TREBLE_TEAL_RGB = "56, 191, 161"
|
| 203 |
+
TREBLE_MINT = "#3DFFA3"
|
| 204 |
+
TREBLE_CYAN = "#2D9BF0"
|
| 205 |
+
TREBLE_BG = "#121212"
|
| 206 |
+
TREBLE_SURFACE = "#1A1A1E"
|
| 207 |
+
TREBLE_BORDER = "rgba(160, 160, 160, 0.18)"
|
| 208 |
+
TREBLE_TEXT_MUTED = "#A0A0A0"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def treble_gradio_theme():
|
| 212 |
+
"""Dark Gradio theme with Treble teal primary (replaces indigo/purple)."""
|
| 213 |
+
import gradio as gr
|
| 214 |
+
|
| 215 |
+
treble_primary = gr.themes.colors.Color(
|
| 216 |
+
name="treble",
|
| 217 |
+
c50="#e8faf6",
|
| 218 |
+
c100="#c5f0e8",
|
| 219 |
+
c200="#9ae4d4",
|
| 220 |
+
c300="#6fd7c0",
|
| 221 |
+
c400="#52cdb0",
|
| 222 |
+
c500=TREBLE_TEAL,
|
| 223 |
+
c600="#2da88a",
|
| 224 |
+
c700="#238a72",
|
| 225 |
+
c800="#1a6c5a",
|
| 226 |
+
c900="#124e43",
|
| 227 |
+
c950="#0a3029",
|
| 228 |
+
)
|
| 229 |
+
return (
|
| 230 |
+
gr.themes.Base(
|
| 231 |
+
primary_hue=treble_primary,
|
| 232 |
+
secondary_hue="cyan",
|
| 233 |
+
neutral_hue="gray",
|
| 234 |
+
)
|
| 235 |
+
.set(
|
| 236 |
+
body_background_fill=TREBLE_BG,
|
| 237 |
+
body_background_fill_dark=TREBLE_BG,
|
| 238 |
+
block_background_fill=TREBLE_SURFACE,
|
| 239 |
+
block_background_fill_dark=TREBLE_SURFACE,
|
| 240 |
+
block_border_color=f"rgba({TREBLE_TEAL_RGB}, 0.14)",
|
| 241 |
+
block_border_color_dark=f"rgba({TREBLE_TEAL_RGB}, 0.14)",
|
| 242 |
+
border_color_primary=f"rgba({TREBLE_TEAL_RGB}, 0.22)",
|
| 243 |
+
border_color_primary_dark=f"rgba({TREBLE_TEAL_RGB}, 0.22)",
|
| 244 |
+
color_accent=TREBLE_TEAL,
|
| 245 |
+
color_accent_soft=f"rgba({TREBLE_TEAL_RGB}, 0.14)",
|
| 246 |
+
link_text_color=TREBLE_TEAL,
|
| 247 |
+
link_text_color_hover=TREBLE_MINT,
|
| 248 |
+
link_text_color_active=TREBLE_CYAN,
|
| 249 |
+
link_text_color_dark=TREBLE_TEAL,
|
| 250 |
+
link_text_color_hover_dark=TREBLE_MINT,
|
| 251 |
+
button_primary_background_fill="*primary_500",
|
| 252 |
+
button_primary_background_fill_hover="*primary_400",
|
| 253 |
+
button_primary_text_color="#0d1412",
|
| 254 |
+
button_primary_text_color_hover="#0d1412",
|
| 255 |
+
button_large_radius="999px",
|
| 256 |
+
button_medium_radius="999px",
|
| 257 |
+
button_small_radius="999px",
|
| 258 |
+
block_radius="12px",
|
| 259 |
+
input_background_fill="#141418",
|
| 260 |
+
input_background_fill_dark="#141418",
|
| 261 |
+
)
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
LEADERBOARD_CSS = """
|
| 266 |
/* Hide Gradio footer logo */
|
| 267 |
footer { display: none !important; }
|
|
|
|
| 270 |
.ffasr-banner {
|
| 271 |
text-align: center;
|
| 272 |
padding: 1.4rem 0 0.8rem 0;
|
| 273 |
+
border-bottom: 1px solid var(--border-color-primary, rgba(160, 160, 160, 0.18));
|
| 274 |
margin-bottom: 0.6rem;
|
| 275 |
}
|
| 276 |
.ffasr-banner h1 {
|
|
|
|
| 279 |
margin: 0;
|
| 280 |
font-weight: 800;
|
| 281 |
letter-spacing: -0.02em;
|
| 282 |
+
background: linear-gradient(90deg, __TREBLE_TEAL__ 0%, __TREBLE_CYAN__ 52%, __TREBLE_MINT__ 100%);
|
| 283 |
-webkit-background-clip: text;
|
| 284 |
background-clip: text;
|
| 285 |
-webkit-text-fill-color: transparent;
|
|
|
|
| 289 |
margin-top: 0.35rem;
|
| 290 |
font-size: 1rem;
|
| 291 |
opacity: 0.72;
|
| 292 |
+
color: __TREBLE_TEXT_MUTED__;
|
| 293 |
}
|
| 294 |
.ffasr-badges {
|
| 295 |
margin-top: 0.7rem;
|
|
|
|
| 302 |
font-size: 0.78rem;
|
| 303 |
padding: 0.18rem 0.65rem;
|
| 304 |
border-radius: 999px;
|
| 305 |
+
background: rgba(__TREBLE_TEAL_RGB__, 0.12);
|
| 306 |
+
color: __TREBLE_TEAL__;
|
| 307 |
+
border: 1px solid rgba(__TREBLE_TEAL_RGB__, 0.32);
|
| 308 |
font-weight: 500;
|
| 309 |
letter-spacing: 0.01em;
|
| 310 |
}
|
|
|
|
| 317 |
font-weight: 500 !important;
|
| 318 |
}
|
| 319 |
.tab-buttons button.selected {
|
| 320 |
+
border-bottom: 2px solid __TREBLE_TEAL__ !important;
|
| 321 |
+
background: var(--block-background-fill, __TREBLE_SURFACE__) !important;
|
| 322 |
+
color: __TREBLE_TEAL__ !important;
|
| 323 |
}
|
| 324 |
|
| 325 |
/* ---- Leaderboard table ---- */
|
|
|
|
| 341 |
table-layout: auto !important;
|
| 342 |
}
|
| 343 |
#leaderboard-table tbody tr:hover td {
|
| 344 |
+
background: rgba(__TREBLE_TEAL_RGB__, 0.08);
|
| 345 |
}
|
| 346 |
|
| 347 |
/* ---- Card-like panels ---- */
|
| 348 |
.queue-status,
|
| 349 |
+
.next-up-panel,
|
| 350 |
.ffasr-card {
|
| 351 |
padding: 0.8rem 1rem;
|
| 352 |
border-radius: 12px;
|
| 353 |
+
background: var(--block-background-fill, __TREBLE_SURFACE__);
|
| 354 |
+
border: 1px solid var(--border-color-primary, rgba(__TREBLE_TEAL_RGB__, 0.15));
|
| 355 |
+
}
|
| 356 |
+
.queue-status p,
|
| 357 |
+
.next-up-panel p { margin: 0.15rem 0; }
|
| 358 |
+
|
| 359 |
+
/* ---- Links (markdown, footnote) ---- */
|
| 360 |
+
.markdown-text a,
|
| 361 |
+
.ffasr-footnote a {
|
| 362 |
+
color: __TREBLE_TEAL__;
|
| 363 |
+
text-decoration-color: rgba(__TREBLE_TEAL_RGB__, 0.45);
|
| 364 |
+
}
|
| 365 |
+
.markdown-text a:hover,
|
| 366 |
+
.ffasr-footnote a:hover {
|
| 367 |
+
color: __TREBLE_MINT__;
|
| 368 |
}
|
|
|
|
| 369 |
|
| 370 |
/* ---- Sub-status footer line ---- */
|
| 371 |
.ffasr-footnote {
|
|
|
|
| 373 |
font-size: 0.82rem;
|
| 374 |
opacity: 0.65;
|
| 375 |
margin-top: 0.8rem;
|
| 376 |
+
color: __TREBLE_TEXT_MUTED__;
|
| 377 |
}
|
| 378 |
|
| 379 |
/* ---- Primary button emphasis ---- */
|
| 380 |
button.primary, .primary button {
|
| 381 |
font-weight: 600 !important;
|
| 382 |
}
|
| 383 |
+
""".replace("__TREBLE_TEAL__", TREBLE_TEAL).replace(
|
| 384 |
+
"__TREBLE_CYAN__", TREBLE_CYAN
|
| 385 |
+
).replace("__TREBLE_MINT__", TREBLE_MINT).replace(
|
| 386 |
+
"__TREBLE_TEAL_RGB__", TREBLE_TEAL_RGB
|
| 387 |
+
).replace("__TREBLE_SURFACE__", TREBLE_SURFACE).replace(
|
| 388 |
+
"__TREBLE_TEXT_MUTED__", TREBLE_TEXT_MUTED
|
| 389 |
+
)
|
evaluation/remote_artifact.py
CHANGED
|
@@ -13,6 +13,24 @@ def default_remote_artifact_path(job_id: str) -> str:
|
|
| 13 |
return f"results/remote_artifacts/{jid}.json"
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def build_artifact(
|
| 17 |
*,
|
| 18 |
model_id: str,
|
|
|
|
| 13 |
return f"results/remote_artifacts/{jid}.json"
|
| 14 |
|
| 15 |
|
| 16 |
+
def normalize_artifact_bucket_path(artifact_ref: str) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Resolve a moderator-entered artifact reference to a bucket path.
|
| 19 |
+
|
| 20 |
+
Accepts ``jobid.json``, ``jobid``, or ``results/remote_artifacts/jobid.json``.
|
| 21 |
+
"""
|
| 22 |
+
ref = (artifact_ref or "").strip().replace("\\", "/").lstrip("/")
|
| 23 |
+
if not ref:
|
| 24 |
+
raise ValueError("Enter a JSON artifact file name or bucket path.")
|
| 25 |
+
if ".." in ref.split("/"):
|
| 26 |
+
raise ValueError("Invalid artifact path.")
|
| 27 |
+
if "/" not in ref:
|
| 28 |
+
ref = f"results/remote_artifacts/{ref}"
|
| 29 |
+
if not ref.lower().endswith(".json"):
|
| 30 |
+
ref = f"{ref}.json"
|
| 31 |
+
return ref
|
| 32 |
+
|
| 33 |
+
|
| 34 |
def build_artifact(
|
| 35 |
*,
|
| 36 |
model_id: str,
|
family_registry.py
CHANGED
|
@@ -7,7 +7,8 @@ from __future__ import annotations
|
|
| 7 |
FAMILY_META: dict[str, tuple[str, str]] = {
|
| 8 |
"auto": (
|
| 9 |
"Auto",
|
| 10 |
-
"
|
|
|
|
| 11 |
),
|
| 12 |
"transformers_pipeline": (
|
| 13 |
"Transformers pipeline",
|
|
@@ -15,7 +16,9 @@ FAMILY_META: dict[str, tuple[str, str]] = {
|
|
| 15 |
),
|
| 16 |
"universal": (
|
| 17 |
"Universal speech-seq2seq",
|
| 18 |
-
"`AutoModelForSpeechSeq2Seq` (or `AutoModel` + `trust_remote_code`) + `generate()` + `decode()`."
|
|
|
|
|
|
|
| 19 |
),
|
| 20 |
"granite_speech": (
|
| 21 |
"IBM Granite speech (chat prompt)",
|
|
|
|
| 7 |
FAMILY_META: dict[str, tuple[str, str]] = {
|
| 8 |
"auto": (
|
| 9 |
"Auto",
|
| 10 |
+
"Reads Hub config.model_type when possible, then: SpeechBrain → Granite → NeMo → Qwen → "
|
| 11 |
+
"pipeline → universal (trust_remote_code) → CTC only if applicable.",
|
| 12 |
),
|
| 13 |
"transformers_pipeline": (
|
| 14 |
"Transformers pipeline",
|
|
|
|
| 16 |
),
|
| 17 |
"universal": (
|
| 18 |
"Universal speech-seq2seq",
|
| 19 |
+
"`AutoModelForSpeechSeq2Seq` (or `AutoModel` + `trust_remote_code`) + `generate()` + `decode()`. "
|
| 20 |
+
"Use for custom Hub code (e.g. Cohere ASR). Extra pip deps: set Space secret "
|
| 21 |
+
"`FFASR_REMOTE_EXTRA_DEPS` (comma-separated) for remote jobs.",
|
| 22 |
),
|
| 23 |
"granite_speech": (
|
| 24 |
"IBM Granite speech (chat prompt)",
|
job_queue.py
CHANGED
|
@@ -62,6 +62,10 @@ class Job:
|
|
| 62 |
error: str | None = None
|
| 63 |
result: dict[str, Any] | None = None
|
| 64 |
submission_notes: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
# Progress (not persisted; only meaningful while running)
|
| 66 |
progress_done: int = 0
|
| 67 |
progress_total: int = 0
|
|
@@ -85,6 +89,9 @@ _MAX_JOBS_TRACKED = 400
|
|
| 85 |
_MAX_QUEUE_BACKLOG = 32
|
| 86 |
_MAX_PENDING_MODERATION = 64
|
| 87 |
_DEFAULT_REMOTE_MAX_CONCURRENT = 4
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
_JOBS_CSV_FIELDS = [
|
| 90 |
"job_id",
|
|
@@ -95,11 +102,70 @@ _JOBS_CSV_FIELDS = [
|
|
| 95 |
"updated_at",
|
| 96 |
"error",
|
| 97 |
"submission_notes",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
"hf_remote_job_id",
|
| 99 |
"remote_artifact_path",
|
| 100 |
]
|
| 101 |
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def _now_iso() -> str:
|
| 104 |
return datetime.now(timezone.utc).isoformat()
|
| 105 |
|
|
@@ -167,6 +233,10 @@ def _persist_jobs() -> None:
|
|
| 167 |
"updated_at": j.updated_at or j.created_at,
|
| 168 |
"error": (j.error or "").replace("\n", " ")[:2000],
|
| 169 |
"submission_notes": (j.submission_notes or "").replace("\n", " ")[:4000],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
"hf_remote_job_id": (j.hf_remote_job_id or "").strip(),
|
| 171 |
"remote_artifact_path": (j.remote_artifact_path or "").strip(),
|
| 172 |
}
|
|
@@ -230,6 +300,10 @@ def _load_persisted_jobs_once() -> None:
|
|
| 230 |
continue
|
| 231 |
err = (row.get("error") or "").strip() or None
|
| 232 |
notes = (row.get("submission_notes") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
created = (row.get("created_at") or _now_iso()).strip()
|
| 234 |
updated = (row.get("updated_at") or created).strip()
|
| 235 |
mid = (row.get("model_id") or "").strip()
|
|
@@ -258,6 +332,10 @@ def _load_persisted_jobs_once() -> None:
|
|
| 258 |
updated_at=updated,
|
| 259 |
error=err,
|
| 260 |
submission_notes=notes,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
hf_remote_job_id=hf_rid,
|
| 262 |
remote_artifact_path=art_path,
|
| 263 |
)
|
|
@@ -400,6 +478,150 @@ def _merge_eval_result_to_leaderboard(
|
|
| 400 |
save_raw_results(rows)
|
| 401 |
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
def _remote_collect_result(job_id: str, hf_id: str, jobs_token: str) -> dict:
|
| 404 |
from evaluation.remote_artifact import extract_result_or_raise
|
| 405 |
from storage import download_bucket_file
|
|
@@ -464,12 +686,21 @@ def _remote_dispatch_job(job_id: str, mid: str, fid: str, jobs_token: str) -> st
|
|
| 464 |
_touch(j3)
|
| 465 |
_persist_jobs()
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
info = remote_jobs.submit_eval_job(
|
| 468 |
model_id=mid,
|
| 469 |
family_id=fid,
|
| 470 |
space_job_id=job_id,
|
| 471 |
artifact_path=artifact_path,
|
| 472 |
token=jobs_token,
|
|
|
|
|
|
|
|
|
|
| 473 |
)
|
| 474 |
hf_id = info.id
|
| 475 |
with _jobs_lock:
|
|
@@ -663,6 +894,10 @@ def enqueue(
|
|
| 663 |
model_id: str,
|
| 664 |
family_id: str,
|
| 665 |
submission_notes: str = "",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
) -> tuple[str, int, str | None, bool]:
|
| 667 |
"""
|
| 668 |
Enqueue an evaluation job.
|
|
@@ -699,6 +934,12 @@ def enqueue(
|
|
| 699 |
job_id = str(uuid.uuid4())[:8]
|
| 700 |
created = _now_iso()
|
| 701 |
notes_clean = (submission_notes or "").strip()[:4000]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
|
| 703 |
awaiting = moderation_active()
|
| 704 |
status = JobStatus.pending_moderation if awaiting else JobStatus.queued
|
|
@@ -711,6 +952,10 @@ def enqueue(
|
|
| 711 |
created_at=created,
|
| 712 |
updated_at=created,
|
| 713 |
submission_notes=notes_clean,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
)
|
| 715 |
|
| 716 |
with _jobs_lock:
|
|
@@ -732,7 +977,9 @@ def enqueue(
|
|
| 732 |
return job_id, position, None, False
|
| 733 |
|
| 734 |
|
| 735 |
-
def approve_job(
|
|
|
|
|
|
|
| 736 |
"""Move a pending job into the execution queue (moderator only)."""
|
| 737 |
if not moderation_active():
|
| 738 |
return False, "Moderation is not active on this Space."
|
|
@@ -750,15 +997,24 @@ def approve_job(job_id: str, secret: str) -> tuple[bool, str]:
|
|
| 750 |
return False, "Execution queue is full; try again in a moment."
|
| 751 |
|
| 752 |
job.status = JobStatus.queued
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
_touch(job)
|
| 754 |
|
| 755 |
_work_queue.put(job_id)
|
| 756 |
_persist_jobs()
|
| 757 |
_ensure_worker()
|
| 758 |
max_n = remote_max_concurrent_jobs()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
return True, (
|
| 760 |
f"Approved job {job_id}. Up to {max_n} Hub Jobs may run in parallel; "
|
| 761 |
-
"this job starts when a slot is free."
|
| 762 |
)
|
| 763 |
|
| 764 |
|
|
@@ -955,22 +1211,84 @@ def moderation_list_html() -> str:
|
|
| 955 |
for j in jobs:
|
| 956 |
note = (j.submission_notes or "").strip()
|
| 957 |
note_cell = _escape_html(note[:200] + ("…" if len(note) > 200 else "")) if note else "N/A"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 958 |
rows.append(
|
| 959 |
f"<tr><td><code>{_escape_html(j.id)}</code></td>"
|
| 960 |
-
f"<td><code>{_escape_html(j.model_id)}</code></td>"
|
| 961 |
f"<td><code>{_escape_html(j.family_id)}</code></td>"
|
|
|
|
| 962 |
f"<td style='max-width:280px;font-size:0.9em'>{note_cell}</td>"
|
| 963 |
f"<td>{_escape_html(j.created_at)}</td></tr>"
|
| 964 |
)
|
| 965 |
body = "".join(rows)
|
| 966 |
return (
|
| 967 |
"<table style='width:100%;border-collapse:collapse;font-size:0.95em'>"
|
| 968 |
-
"<thead><tr><th>Job ID</th><th>Model</th><th>Family</th><th>Notes</th>"
|
| 969 |
"<th>Submitted (UTC)</th></tr></thead>"
|
| 970 |
f"<tbody>{body}</tbody></table>"
|
| 971 |
)
|
| 972 |
|
| 973 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 974 |
def recent_jobs_html(limit: int = 25, *, with_heading: bool = True) -> str:
|
| 975 |
"""Recent jobs with status (for spotting stuck or failed runs)."""
|
| 976 |
with _jobs_lock:
|
|
@@ -1105,15 +1423,15 @@ def progress_html() -> str:
|
|
| 1105 |
|
| 1106 |
if total <= 0:
|
| 1107 |
inner = (
|
| 1108 |
-
"<div style='width:40%;height:100%;background:#
|
| 1109 |
-
"background-image:linear-gradient(90deg,#
|
| 1110 |
"background-size:200% 100%;animation:ffasr-indet 1.6s linear infinite;'></div>"
|
| 1111 |
)
|
| 1112 |
pct_label = "preparing…"
|
| 1113 |
else:
|
| 1114 |
pct = max(0.0, min(100.0, 100.0 * done / total))
|
| 1115 |
inner = (
|
| 1116 |
-
f"<div style='height:100%;width:{pct:.1f}%;background:#
|
| 1117 |
"transition:width 0.4s ease-out'></div>"
|
| 1118 |
)
|
| 1119 |
cond_txt = f"; current condition: <code>{_escape_html(cond)}</code>" if cond else ""
|
|
|
|
| 62 |
error: str | None = None
|
| 63 |
result: dict[str, Any] | None = None
|
| 64 |
submission_notes: str = ""
|
| 65 |
+
extra_requirements: str = ""
|
| 66 |
+
custom_script: str = ""
|
| 67 |
+
is_gated: bool = False
|
| 68 |
+
run_custom_script: bool = False
|
| 69 |
# Progress (not persisted; only meaningful while running)
|
| 70 |
progress_done: int = 0
|
| 71 |
progress_total: int = 0
|
|
|
|
| 89 |
_MAX_QUEUE_BACKLOG = 32
|
| 90 |
_MAX_PENDING_MODERATION = 64
|
| 91 |
_DEFAULT_REMOTE_MAX_CONCURRENT = 4
|
| 92 |
+
_MAX_CUSTOM_SCRIPT_BYTES = 32 * 1024
|
| 93 |
+
_MAX_REQUIREMENT_LINE_LEN = 200
|
| 94 |
+
_MAX_REQUIREMENT_LINES = 50
|
| 95 |
|
| 96 |
_JOBS_CSV_FIELDS = [
|
| 97 |
"job_id",
|
|
|
|
| 102 |
"updated_at",
|
| 103 |
"error",
|
| 104 |
"submission_notes",
|
| 105 |
+
"extra_requirements",
|
| 106 |
+
"custom_script_b64",
|
| 107 |
+
"is_gated",
|
| 108 |
+
"run_custom_script",
|
| 109 |
"hf_remote_job_id",
|
| 110 |
"remote_artifact_path",
|
| 111 |
]
|
| 112 |
|
| 113 |
|
| 114 |
+
def parse_requirements_lines(text: str) -> list[str]:
|
| 115 |
+
"""One package spec per line (requirements.txt style); ignores blanks and # comments."""
|
| 116 |
+
out: list[str] = []
|
| 117 |
+
for raw in (text or "").splitlines():
|
| 118 |
+
line = raw.strip()
|
| 119 |
+
if not line or line.startswith("#"):
|
| 120 |
+
continue
|
| 121 |
+
if len(line) > _MAX_REQUIREMENT_LINE_LEN:
|
| 122 |
+
line = line[:_MAX_REQUIREMENT_LINE_LEN]
|
| 123 |
+
out.append(line)
|
| 124 |
+
if len(out) >= _MAX_REQUIREMENT_LINES:
|
| 125 |
+
break
|
| 126 |
+
return out
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def sanitize_custom_script(text: str) -> str:
|
| 130 |
+
"""Trim and cap custom script body for storage."""
|
| 131 |
+
s = (text or "").strip()
|
| 132 |
+
if not s:
|
| 133 |
+
return ""
|
| 134 |
+
enc = s.encode("utf-8")
|
| 135 |
+
if len(enc) > _MAX_CUSTOM_SCRIPT_BYTES:
|
| 136 |
+
enc = enc[:_MAX_CUSTOM_SCRIPT_BYTES]
|
| 137 |
+
s = enc.decode("utf-8", errors="ignore")
|
| 138 |
+
return s
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _bool_to_csv(flag: bool) -> str:
|
| 142 |
+
return "1" if flag else "0"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _bool_from_csv(raw: str) -> bool:
|
| 146 |
+
return (raw or "").strip().lower() in ("1", "true", "yes", "on")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _encode_script_b64(script: str) -> str:
|
| 150 |
+
if not script:
|
| 151 |
+
return ""
|
| 152 |
+
import base64
|
| 153 |
+
|
| 154 |
+
return base64.b64encode(script.encode("utf-8")).decode("ascii")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _decode_script_b64(raw: str) -> str:
|
| 158 |
+
raw = (raw or "").strip()
|
| 159 |
+
if not raw:
|
| 160 |
+
return ""
|
| 161 |
+
import base64
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
return base64.b64decode(raw.encode("ascii")).decode("utf-8")
|
| 165 |
+
except Exception:
|
| 166 |
+
return ""
|
| 167 |
+
|
| 168 |
+
|
| 169 |
def _now_iso() -> str:
|
| 170 |
return datetime.now(timezone.utc).isoformat()
|
| 171 |
|
|
|
|
| 233 |
"updated_at": j.updated_at or j.created_at,
|
| 234 |
"error": (j.error or "").replace("\n", " ")[:2000],
|
| 235 |
"submission_notes": (j.submission_notes or "").replace("\n", " ")[:4000],
|
| 236 |
+
"extra_requirements": (j.extra_requirements or "").replace("\r\n", "\n")[:8000],
|
| 237 |
+
"custom_script_b64": _encode_script_b64(j.custom_script or ""),
|
| 238 |
+
"is_gated": _bool_to_csv(j.is_gated),
|
| 239 |
+
"run_custom_script": _bool_to_csv(j.run_custom_script),
|
| 240 |
"hf_remote_job_id": (j.hf_remote_job_id or "").strip(),
|
| 241 |
"remote_artifact_path": (j.remote_artifact_path or "").strip(),
|
| 242 |
}
|
|
|
|
| 300 |
continue
|
| 301 |
err = (row.get("error") or "").strip() or None
|
| 302 |
notes = (row.get("submission_notes") or "").strip()
|
| 303 |
+
extra_req = (row.get("extra_requirements") or "").strip()
|
| 304 |
+
script = _decode_script_b64(row.get("custom_script_b64") or "")
|
| 305 |
+
is_gated = _bool_from_csv(row.get("is_gated") or "")
|
| 306 |
+
run_custom = _bool_from_csv(row.get("run_custom_script") or "")
|
| 307 |
created = (row.get("created_at") or _now_iso()).strip()
|
| 308 |
updated = (row.get("updated_at") or created).strip()
|
| 309 |
mid = (row.get("model_id") or "").strip()
|
|
|
|
| 332 |
updated_at=updated,
|
| 333 |
error=err,
|
| 334 |
submission_notes=notes,
|
| 335 |
+
extra_requirements=extra_req,
|
| 336 |
+
custom_script=script,
|
| 337 |
+
is_gated=is_gated,
|
| 338 |
+
run_custom_script=run_custom,
|
| 339 |
hf_remote_job_id=hf_rid,
|
| 340 |
remote_artifact_path=art_path,
|
| 341 |
)
|
|
|
|
| 478 |
save_raw_results(rows)
|
| 479 |
|
| 480 |
|
| 481 |
+
def _load_artifact_json_from_bucket(artifact_path: str) -> dict:
|
| 482 |
+
"""Download and parse a remote eval JSON artifact from the Hub bucket."""
|
| 483 |
+
from storage import STORAGE_BACKEND, download_bucket_file
|
| 484 |
+
|
| 485 |
+
if STORAGE_BACKEND != "hf_bucket":
|
| 486 |
+
raise RuntimeError("Artifact import requires STORAGE_BACKEND='hf_bucket'.")
|
| 487 |
+
local_path = download_bucket_file(artifact_path)
|
| 488 |
+
try:
|
| 489 |
+
with open(local_path, "r", encoding="utf-8") as f:
|
| 490 |
+
return json.load(f)
|
| 491 |
+
finally:
|
| 492 |
+
try:
|
| 493 |
+
os.unlink(local_path)
|
| 494 |
+
except Exception:
|
| 495 |
+
pass
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def _submission_notes_for_artifact_import(
|
| 499 |
+
data: dict, artifact_path: str, override: str
|
| 500 |
+
) -> str:
|
| 501 |
+
notes = (override or "").strip()
|
| 502 |
+
if notes:
|
| 503 |
+
return notes[:4000]
|
| 504 |
+
space_job_id = str(data.get("job_id") or "").strip()
|
| 505 |
+
with _jobs_lock:
|
| 506 |
+
for j in _jobs.values():
|
| 507 |
+
if space_job_id and j.id == space_job_id:
|
| 508 |
+
return (j.submission_notes or "").strip()[:4000]
|
| 509 |
+
if (j.remote_artifact_path or "").strip() == artifact_path:
|
| 510 |
+
return (j.submission_notes or "").strip()[:4000]
|
| 511 |
+
return ""
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def _mark_job_done_for_artifact(
|
| 515 |
+
data: dict, artifact_path: str, result: dict
|
| 516 |
+
) -> str | None:
|
| 517 |
+
"""If a tracked queue job matches, mark it done. Returns matched job id or None."""
|
| 518 |
+
space_job_id = str(data.get("job_id") or "").strip()
|
| 519 |
+
matched: Job | None = None
|
| 520 |
+
with _jobs_lock:
|
| 521 |
+
for j in _jobs.values():
|
| 522 |
+
if space_job_id and j.id == space_job_id:
|
| 523 |
+
matched = j
|
| 524 |
+
break
|
| 525 |
+
if (j.remote_artifact_path or "").strip() == artifact_path:
|
| 526 |
+
matched = j
|
| 527 |
+
break
|
| 528 |
+
if matched is not None:
|
| 529 |
+
matched.status = JobStatus.done
|
| 530 |
+
matched.result = result
|
| 531 |
+
matched.error = None
|
| 532 |
+
if not (matched.remote_artifact_path or "").strip():
|
| 533 |
+
matched.remote_artifact_path = artifact_path
|
| 534 |
+
_touch(matched)
|
| 535 |
+
if matched is not None:
|
| 536 |
+
_persist_jobs()
|
| 537 |
+
return matched.id
|
| 538 |
+
return None
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def import_artifact_to_leaderboard(
|
| 542 |
+
artifact_ref: str,
|
| 543 |
+
secret: str,
|
| 544 |
+
*,
|
| 545 |
+
replace_existing: bool = False,
|
| 546 |
+
submission_notes: str = "",
|
| 547 |
+
) -> tuple[bool, str]:
|
| 548 |
+
"""
|
| 549 |
+
Moderator-only: load a bucket JSON artifact and merge its result into leaderboard.csv.
|
| 550 |
+
|
| 551 |
+
``artifact_ref`` may be a file name (``abc123.json``), job id (``abc123``), or full bucket path.
|
| 552 |
+
"""
|
| 553 |
+
ok, msg = _moderator_secret_ok(secret)
|
| 554 |
+
if not ok:
|
| 555 |
+
return False, msg
|
| 556 |
+
|
| 557 |
+
from evaluation.remote_artifact import extract_result_or_raise, normalize_artifact_bucket_path
|
| 558 |
+
from init import (
|
| 559 |
+
invalidate_results_cache,
|
| 560 |
+
leaderboard_row_from_eval_result,
|
| 561 |
+
load_raw_results,
|
| 562 |
+
normalize_legacy_csv_row,
|
| 563 |
+
save_raw_results,
|
| 564 |
+
)
|
| 565 |
+
import analytics
|
| 566 |
+
|
| 567 |
+
try:
|
| 568 |
+
artifact_path = normalize_artifact_bucket_path(artifact_ref)
|
| 569 |
+
except ValueError as e:
|
| 570 |
+
return False, str(e)
|
| 571 |
+
|
| 572 |
+
try:
|
| 573 |
+
data = _load_artifact_json_from_bucket(artifact_path)
|
| 574 |
+
except Exception as e:
|
| 575 |
+
return False, f"Could not load artifact <code>{_escape_html(artifact_path)}</code>: {e}"
|
| 576 |
+
|
| 577 |
+
try:
|
| 578 |
+
result = extract_result_or_raise(data)
|
| 579 |
+
except Exception as e:
|
| 580 |
+
return False, f"Artifact is not a successful evaluation: {e}"
|
| 581 |
+
|
| 582 |
+
model_id = str(result.get("model_id", "")).strip()
|
| 583 |
+
if not model_id:
|
| 584 |
+
return False, "Artifact result is missing model_id."
|
| 585 |
+
|
| 586 |
+
notes = _submission_notes_for_artifact_import(data, artifact_path, submission_notes)
|
| 587 |
+
rows = load_raw_results()
|
| 588 |
+
existing = [i for i, r in enumerate(rows) if (r.get("model_id") or "").strip() == model_id]
|
| 589 |
+
|
| 590 |
+
if existing and not replace_existing:
|
| 591 |
+
row = dict(rows[existing[0]])
|
| 592 |
+
normalize_legacy_csv_row(row)
|
| 593 |
+
score = analytics.far_field_score_for_row(row)
|
| 594 |
+
score_txt = f"{score:.2f}" if score is not None else "N/A"
|
| 595 |
+
return False, (
|
| 596 |
+
f"Model <code>{_escape_html(model_id)}</code> is already on the leaderboard "
|
| 597 |
+
f"(score {score_txt}). Enable <strong>Replace existing row</strong> to overwrite."
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
if existing and replace_existing:
|
| 601 |
+
for i in sorted(existing, reverse=True):
|
| 602 |
+
rows.pop(i)
|
| 603 |
+
|
| 604 |
+
submitted_at = _now_iso()
|
| 605 |
+
new_row = leaderboard_row_from_eval_result(result, submitted_at, submission_notes=notes)
|
| 606 |
+
normalize_legacy_csv_row(new_row)
|
| 607 |
+
rows.append(new_row)
|
| 608 |
+
_leaderboard_sort_rows_inplace(rows)
|
| 609 |
+
save_raw_results(rows)
|
| 610 |
+
invalidate_results_cache()
|
| 611 |
+
|
| 612 |
+
ff_score = analytics.far_field_score_for_row(new_row)
|
| 613 |
+
score_txt = f"{ff_score:.2f}" if ff_score is not None else "N/A"
|
| 614 |
+
action = "Replaced" if existing else "Added"
|
| 615 |
+
matched_job = _mark_job_done_for_artifact(data, artifact_path, result)
|
| 616 |
+
job_bit = f" Matched queue job <code>{_escape_html(matched_job)}</code> marked done." if matched_job else ""
|
| 617 |
+
|
| 618 |
+
return True, (
|
| 619 |
+
f"{action} <strong>{_escape_html(model_id)}</strong> from "
|
| 620 |
+
f"<code>{_escape_html(artifact_path)}</code>. "
|
| 621 |
+
f"Far-field score: <strong>{score_txt}</strong> / 100.{job_bit}"
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
|
| 625 |
def _remote_collect_result(job_id: str, hf_id: str, jobs_token: str) -> dict:
|
| 626 |
from evaluation.remote_artifact import extract_result_or_raise
|
| 627 |
from storage import download_bucket_file
|
|
|
|
| 686 |
_touch(j3)
|
| 687 |
_persist_jobs()
|
| 688 |
|
| 689 |
+
with _jobs_lock:
|
| 690 |
+
j_dispatch = _jobs.get(job_id)
|
| 691 |
+
extra_req = (j_dispatch.extra_requirements or "") if j_dispatch else ""
|
| 692 |
+
custom_script = (j_dispatch.custom_script or "") if j_dispatch else ""
|
| 693 |
+
run_custom = bool(j_dispatch and j_dispatch.run_custom_script)
|
| 694 |
+
|
| 695 |
info = remote_jobs.submit_eval_job(
|
| 696 |
model_id=mid,
|
| 697 |
family_id=fid,
|
| 698 |
space_job_id=job_id,
|
| 699 |
artifact_path=artifact_path,
|
| 700 |
token=jobs_token,
|
| 701 |
+
extra_requirements=extra_req,
|
| 702 |
+
custom_script=custom_script,
|
| 703 |
+
run_custom_script=run_custom,
|
| 704 |
)
|
| 705 |
hf_id = info.id
|
| 706 |
with _jobs_lock:
|
|
|
|
| 894 |
model_id: str,
|
| 895 |
family_id: str,
|
| 896 |
submission_notes: str = "",
|
| 897 |
+
*,
|
| 898 |
+
extra_requirements: str = "",
|
| 899 |
+
custom_script: str = "",
|
| 900 |
+
is_gated: bool = False,
|
| 901 |
) -> tuple[str, int, str | None, bool]:
|
| 902 |
"""
|
| 903 |
Enqueue an evaluation job.
|
|
|
|
| 934 |
job_id = str(uuid.uuid4())[:8]
|
| 935 |
created = _now_iso()
|
| 936 |
notes_clean = (submission_notes or "").strip()[:4000]
|
| 937 |
+
try:
|
| 938 |
+
req_lines = parse_requirements_lines(extra_requirements)
|
| 939 |
+
extra_req_clean = "\n".join(req_lines)
|
| 940 |
+
script_clean = sanitize_custom_script(custom_script)
|
| 941 |
+
except Exception as e:
|
| 942 |
+
return "", 0, f"Invalid submission fields: {e}", False
|
| 943 |
|
| 944 |
awaiting = moderation_active()
|
| 945 |
status = JobStatus.pending_moderation if awaiting else JobStatus.queued
|
|
|
|
| 952 |
created_at=created,
|
| 953 |
updated_at=created,
|
| 954 |
submission_notes=notes_clean,
|
| 955 |
+
extra_requirements=extra_req_clean,
|
| 956 |
+
custom_script=script_clean,
|
| 957 |
+
is_gated=bool(is_gated),
|
| 958 |
+
run_custom_script=False,
|
| 959 |
)
|
| 960 |
|
| 961 |
with _jobs_lock:
|
|
|
|
| 977 |
return job_id, position, None, False
|
| 978 |
|
| 979 |
|
| 980 |
+
def approve_job(
|
| 981 |
+
job_id: str, secret: str, *, run_custom_script: bool = False
|
| 982 |
+
) -> tuple[bool, str]:
|
| 983 |
"""Move a pending job into the execution queue (moderator only)."""
|
| 984 |
if not moderation_active():
|
| 985 |
return False, "Moderation is not active on this Space."
|
|
|
|
| 997 |
return False, "Execution queue is full; try again in a moment."
|
| 998 |
|
| 999 |
job.status = JobStatus.queued
|
| 1000 |
+
if (job.custom_script or "").strip():
|
| 1001 |
+
job.run_custom_script = bool(run_custom_script)
|
| 1002 |
+
else:
|
| 1003 |
+
job.run_custom_script = False
|
| 1004 |
_touch(job)
|
| 1005 |
|
| 1006 |
_work_queue.put(job_id)
|
| 1007 |
_persist_jobs()
|
| 1008 |
_ensure_worker()
|
| 1009 |
max_n = remote_max_concurrent_jobs()
|
| 1010 |
+
custom_note = ""
|
| 1011 |
+
with _jobs_lock:
|
| 1012 |
+
j = _jobs.get(job_id)
|
| 1013 |
+
if j and j.run_custom_script and (j.custom_script or "").strip():
|
| 1014 |
+
custom_note = " Custom script will run on the Hub Job."
|
| 1015 |
return True, (
|
| 1016 |
f"Approved job {job_id}. Up to {max_n} Hub Jobs may run in parallel; "
|
| 1017 |
+
f"this job starts when a slot is free.{custom_note}"
|
| 1018 |
)
|
| 1019 |
|
| 1020 |
|
|
|
|
| 1211 |
for j in jobs:
|
| 1212 |
note = (j.submission_notes or "").strip()
|
| 1213 |
note_cell = _escape_html(note[:200] + ("…" if len(note) > 200 else "")) if note else "N/A"
|
| 1214 |
+
gated = " <span style='color:#b45309'>[gated]</span>" if j.is_gated else ""
|
| 1215 |
+
req_n = len(parse_requirements_lines(j.extra_requirements or ""))
|
| 1216 |
+
req_bit = f"{req_n} extra req(s)" if req_n else ""
|
| 1217 |
+
script_bit = "custom script" if (j.custom_script or "").strip() else ""
|
| 1218 |
+
extras = ", ".join(x for x in (req_bit, script_bit) if x)
|
| 1219 |
+
extras_cell = _escape_html(extras) if extras else "—"
|
| 1220 |
rows.append(
|
| 1221 |
f"<tr><td><code>{_escape_html(j.id)}</code></td>"
|
| 1222 |
+
f"<td><code>{_escape_html(j.model_id)}</code>{gated}</td>"
|
| 1223 |
f"<td><code>{_escape_html(j.family_id)}</code></td>"
|
| 1224 |
+
f"<td style='max-width:200px;font-size:0.85em'>{extras_cell}</td>"
|
| 1225 |
f"<td style='max-width:280px;font-size:0.9em'>{note_cell}</td>"
|
| 1226 |
f"<td>{_escape_html(j.created_at)}</td></tr>"
|
| 1227 |
)
|
| 1228 |
body = "".join(rows)
|
| 1229 |
return (
|
| 1230 |
"<table style='width:100%;border-collapse:collapse;font-size:0.95em'>"
|
| 1231 |
+
"<thead><tr><th>Job ID</th><th>Model</th><th>Family</th><th>Extras</th><th>Notes</th>"
|
| 1232 |
"<th>Submitted (UTC)</th></tr></thead>"
|
| 1233 |
f"<tbody>{body}</tbody></table>"
|
| 1234 |
)
|
| 1235 |
|
| 1236 |
|
| 1237 |
+
def next_up_html(limit: int = 5) -> str:
|
| 1238 |
+
"""Ordered list of models next in the approved execution queue (Submit tab)."""
|
| 1239 |
+
try:
|
| 1240 |
+
_ensure_worker()
|
| 1241 |
+
except Exception as e:
|
| 1242 |
+
return _worker_unavailable_html(e)
|
| 1243 |
+
with _jobs_lock:
|
| 1244 |
+
queued = [j for j in _jobs.values() if j.status == JobStatus.queued]
|
| 1245 |
+
queued.sort(key=lambda j: j.created_at)
|
| 1246 |
+
queued = queued[: max(1, int(limit))]
|
| 1247 |
+
if not queued:
|
| 1248 |
+
return (
|
| 1249 |
+
"<div class='next-up-panel' style='font-size:0.9em;opacity:0.85'>"
|
| 1250 |
+
"<p><em>No models are waiting in the evaluation queue.</em></p></div>"
|
| 1251 |
+
)
|
| 1252 |
+
items = "".join(
|
| 1253 |
+
f"<li><code>{_escape_html(j.model_id)}</code> "
|
| 1254 |
+
f"<span style='opacity:0.75'>(job {_escape_html(j.id)})</span></li>"
|
| 1255 |
+
for j in queued
|
| 1256 |
+
)
|
| 1257 |
+
return (
|
| 1258 |
+
"<div class='next-up-panel' style='font-size:0.9em'>"
|
| 1259 |
+
f"<p><strong>Next models to evaluate</strong> ({len(queued)} shown):</p>"
|
| 1260 |
+
f"<ol style='margin:0.25rem 0 0 1.1rem'>{items}</ol></div>"
|
| 1261 |
+
).replace("</div>", "</div>")
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def recent_jobs_for_render(limit: int = 30) -> list[dict[str, Any]]:
|
| 1265 |
+
"""Structured recent jobs for Gradio @gr.render rows (Retry / Remove)."""
|
| 1266 |
+
with _jobs_lock:
|
| 1267 |
+
items = list(_jobs.values())
|
| 1268 |
+
items.sort(key=lambda j: j.updated_at or j.created_at, reverse=True)
|
| 1269 |
+
out: list[dict[str, Any]] = []
|
| 1270 |
+
for j in items[: max(1, int(limit))]:
|
| 1271 |
+
err = (j.error or "").strip()
|
| 1272 |
+
try:
|
| 1273 |
+
hub = _hub_job_link_html(j.hf_remote_job_id)
|
| 1274 |
+
except Exception:
|
| 1275 |
+
hub = ""
|
| 1276 |
+
out.append(
|
| 1277 |
+
{
|
| 1278 |
+
"id": j.id,
|
| 1279 |
+
"model_id": j.model_id,
|
| 1280 |
+
"status": j.status.value,
|
| 1281 |
+
"error": err[:200] + ("…" if len(err) > 200 else ""),
|
| 1282 |
+
"hub_link_html": hub,
|
| 1283 |
+
"updated_at": (j.updated_at or j.created_at)[:19],
|
| 1284 |
+
"can_retry": j.status == JobStatus.failed,
|
| 1285 |
+
"can_remove": j.status != JobStatus.running,
|
| 1286 |
+
"has_custom_script": bool((j.custom_script or "").strip()),
|
| 1287 |
+
}
|
| 1288 |
+
)
|
| 1289 |
+
return out
|
| 1290 |
+
|
| 1291 |
+
|
| 1292 |
def recent_jobs_html(limit: int = 25, *, with_heading: bool = True) -> str:
|
| 1293 |
"""Recent jobs with status (for spotting stuck or failed runs)."""
|
| 1294 |
with _jobs_lock:
|
|
|
|
| 1423 |
|
| 1424 |
if total <= 0:
|
| 1425 |
inner = (
|
| 1426 |
+
"<div style='width:40%;height:100%;background:#38BFA1;"
|
| 1427 |
+
"background-image:linear-gradient(90deg,#38BFA1,#3DFFA3,#38BFA1);"
|
| 1428 |
"background-size:200% 100%;animation:ffasr-indet 1.6s linear infinite;'></div>"
|
| 1429 |
)
|
| 1430 |
pct_label = "preparing…"
|
| 1431 |
else:
|
| 1432 |
pct = max(0.0, min(100.0, 100.0 * done / total))
|
| 1433 |
inner = (
|
| 1434 |
+
f"<div style='height:100%;width:{pct:.1f}%;background:#38BFA1;"
|
| 1435 |
"transition:width 0.4s ease-out'></div>"
|
| 1436 |
)
|
| 1437 |
cond_txt = f"; current condition: <code>{_escape_html(cond)}</code>" if cond else ""
|
remote_jobs.py
CHANGED
|
@@ -44,6 +44,7 @@ _TRANSFORMERS_DEPS: list[str] = [
|
|
| 44 |
"transformers @ git+https://github.com/huggingface/transformers.git",
|
| 45 |
"torchcodec",
|
| 46 |
"librosa",
|
|
|
|
| 47 |
]
|
| 48 |
|
| 49 |
_NEMO_DEPS: list[str] = [
|
|
@@ -96,29 +97,49 @@ def _needs_speechbrain_stack(model_id: str) -> bool:
|
|
| 96 |
return m.startswith("speechbrain/") or "speechbrain" in m
|
| 97 |
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def _select_deps(model_id: str, family_id: str) -> list[str]:
|
| 100 |
"""Return uv dependency list for this model/family (core + backend-specific)."""
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
if fid == "speechbrain":
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
|
| 124 |
def submit_eval_job(
|
|
@@ -128,6 +149,9 @@ def submit_eval_job(
|
|
| 128 |
space_job_id: str,
|
| 129 |
artifact_path: str,
|
| 130 |
token: str | None,
|
|
|
|
|
|
|
|
|
|
| 131 |
) -> JobInfo:
|
| 132 |
"""Start a Hub UV Job; returns initial ``JobInfo`` (may already be RUNNING)."""
|
| 133 |
namespace = os.environ.get("FFASR_REMOTE_JOB_NAMESPACE", "").strip() or None
|
|
@@ -162,10 +186,36 @@ def submit_eval_job(
|
|
| 162 |
)
|
| 163 |
|
| 164 |
script = _worker_script_path()
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
raise RuntimeError(f"Remote worker script not found: {script}")
|
| 167 |
|
| 168 |
deps = _select_deps(model_id, family_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
env = {
|
| 171 |
"FFASR_MODEL_ID": model_id,
|
|
@@ -186,25 +236,32 @@ def submit_eval_job(
|
|
| 186 |
|
| 187 |
api = HfApi(token=jobs_tok)
|
| 188 |
last_err: Exception | None = None
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
def inspect_job_once(hf_job_id: str, *, token: str | None = None) -> JobInfo:
|
|
|
|
| 44 |
"transformers @ git+https://github.com/huggingface/transformers.git",
|
| 45 |
"torchcodec",
|
| 46 |
"librosa",
|
| 47 |
+
"sentencepiece",
|
| 48 |
]
|
| 49 |
|
| 50 |
_NEMO_DEPS: list[str] = [
|
|
|
|
| 97 |
return m.startswith("speechbrain/") or "speechbrain" in m
|
| 98 |
|
| 99 |
|
| 100 |
+
def _parse_extra_deps_env() -> list[str]:
|
| 101 |
+
raw = os.environ.get("FFASR_REMOTE_EXTRA_DEPS", "").strip()
|
| 102 |
+
if not raw:
|
| 103 |
+
return []
|
| 104 |
+
return [p.strip() for p in raw.split(",") if p.strip()]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
def _select_deps(model_id: str, family_id: str) -> list[str]:
|
| 108 |
"""Return uv dependency list for this model/family (core + backend-specific)."""
|
| 109 |
+
from backends.family_resolve import extra_deps_for_model, resolve_auto_family
|
| 110 |
+
from family_registry import _FAMILY_ALIASES
|
| 111 |
+
|
| 112 |
+
raw = (family_id or "auto").strip().lower()
|
| 113 |
+
fid = _FAMILY_ALIASES.get(raw, raw)
|
| 114 |
+
fid = resolve_auto_family(model_id, fid)
|
| 115 |
|
| 116 |
if fid == "speechbrain":
|
| 117 |
+
base = [*CORE_DEPS, *_SPEECHBRAIN_DEPS]
|
| 118 |
+
elif fid == "nemo_asr":
|
| 119 |
+
base = [*CORE_DEPS, *_NEMO_DEPS]
|
| 120 |
+
elif fid in ("qwen_asr", "qwen3_asr"):
|
| 121 |
+
base = [*CORE_DEPS, *_QWEN_DEPS]
|
| 122 |
+
elif fid in ("transformers_pipeline", "universal", "granite_speech", "transformers_ctc"):
|
| 123 |
+
base = [*CORE_DEPS, *_TRANSFORMERS_DEPS]
|
| 124 |
+
elif _needs_nemo_stack(model_id):
|
| 125 |
+
base = [*CORE_DEPS, *_NEMO_DEPS]
|
| 126 |
+
elif _needs_qwen_asr_stack(model_id):
|
| 127 |
+
base = [*CORE_DEPS, *_QWEN_DEPS]
|
| 128 |
+
elif _needs_speechbrain_stack(model_id):
|
| 129 |
+
base = [*CORE_DEPS, *_SPEECHBRAIN_DEPS]
|
| 130 |
+
else:
|
| 131 |
+
base = [*CORE_DEPS, *_TRANSFORMERS_DEPS]
|
| 132 |
+
|
| 133 |
+
extras = extra_deps_for_model(model_id)
|
| 134 |
+
env_extras = _parse_extra_deps_env()
|
| 135 |
+
# Preserve order, dedupe.
|
| 136 |
+
seen: set[str] = set()
|
| 137 |
+
out: list[str] = []
|
| 138 |
+
for pkg in [*base, *extras, *env_extras]:
|
| 139 |
+
if pkg not in seen:
|
| 140 |
+
seen.add(pkg)
|
| 141 |
+
out.append(pkg)
|
| 142 |
+
return out
|
| 143 |
|
| 144 |
|
| 145 |
def submit_eval_job(
|
|
|
|
| 149 |
space_job_id: str,
|
| 150 |
artifact_path: str,
|
| 151 |
token: str | None,
|
| 152 |
+
extra_requirements: str = "",
|
| 153 |
+
custom_script: str = "",
|
| 154 |
+
run_custom_script: bool = False,
|
| 155 |
) -> JobInfo:
|
| 156 |
"""Start a Hub UV Job; returns initial ``JobInfo`` (may already be RUNNING)."""
|
| 157 |
namespace = os.environ.get("FFASR_REMOTE_JOB_NAMESPACE", "").strip() or None
|
|
|
|
| 186 |
)
|
| 187 |
|
| 188 |
script = _worker_script_path()
|
| 189 |
+
custom_script_path: str | None = None
|
| 190 |
+
if run_custom_script and (custom_script or "").strip():
|
| 191 |
+
import tempfile
|
| 192 |
+
|
| 193 |
+
fd, custom_script_path = tempfile.mkstemp(
|
| 194 |
+
prefix=f"ffasr_custom_{space_job_id}_", suffix=".py"
|
| 195 |
+
)
|
| 196 |
+
try:
|
| 197 |
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
| 198 |
+
f.write(custom_script.strip())
|
| 199 |
+
except Exception:
|
| 200 |
+
os.unlink(custom_script_path)
|
| 201 |
+
raise
|
| 202 |
+
script = custom_script_path
|
| 203 |
+
elif not os.path.isfile(script):
|
| 204 |
raise RuntimeError(f"Remote worker script not found: {script}")
|
| 205 |
|
| 206 |
deps = _select_deps(model_id, family_id)
|
| 207 |
+
try:
|
| 208 |
+
from job_queue import parse_requirements_lines
|
| 209 |
+
|
| 210 |
+
extra_lines = parse_requirements_lines(extra_requirements)
|
| 211 |
+
except Exception:
|
| 212 |
+
extra_lines = []
|
| 213 |
+
if extra_lines:
|
| 214 |
+
seen = set(deps)
|
| 215 |
+
for pkg in extra_lines:
|
| 216 |
+
if pkg not in seen:
|
| 217 |
+
seen.add(pkg)
|
| 218 |
+
deps.append(pkg)
|
| 219 |
|
| 220 |
env = {
|
| 221 |
"FFASR_MODEL_ID": model_id,
|
|
|
|
| 236 |
|
| 237 |
api = HfApi(token=jobs_tok)
|
| 238 |
last_err: Exception | None = None
|
| 239 |
+
try:
|
| 240 |
+
for attempt in range(3):
|
| 241 |
+
try:
|
| 242 |
+
return api.run_uv_job(
|
| 243 |
+
script=script,
|
| 244 |
+
dependencies=deps,
|
| 245 |
+
env=env,
|
| 246 |
+
secrets=secrets,
|
| 247 |
+
flavor=flavor,
|
| 248 |
+
timeout=timeout,
|
| 249 |
+
namespace=namespace,
|
| 250 |
+
python="3.12",
|
| 251 |
+
token=jobs_tok,
|
| 252 |
+
)
|
| 253 |
+
except Exception as e:
|
| 254 |
+
last_err = e
|
| 255 |
+
time.sleep(2.0 * (2**attempt))
|
| 256 |
+
if last_err is not None:
|
| 257 |
+
raise last_err
|
| 258 |
+
raise RuntimeError("submit_eval_job: unreachable")
|
| 259 |
+
finally:
|
| 260 |
+
if custom_script_path and os.path.isfile(custom_script_path):
|
| 261 |
+
try:
|
| 262 |
+
os.unlink(custom_script_path)
|
| 263 |
+
except Exception:
|
| 264 |
+
pass
|
| 265 |
|
| 266 |
|
| 267 |
def inspect_job_once(hf_job_id: str, *, token: str | None = None) -> JobInfo:
|