ffasr / app.py
Shivam
see email for all
21eb3ea
Raw
History Blame Contribute Delete
67.6 kB
import os
import time
import gradio as gr
import pandas as pd
from constants import (
ABOUT_TEXT,
APP_TITLE,
BANNER,
CITATION_TEXT,
DEFAULT_CUSTOM_EVAL_EXAMPLE,
INTRODUCTION_TEXT,
LEADERBOARD_CSS,
tab_label,
TREBLE_TEAL,
treble_gradio_theme,
)
import examples_renderer
import analytics
from family_registry import default_family_id
import job_queue
from metrics_config import (
HEATMAP_SCENARIO_KEYS,
LIVE_SCENARIO_KEYS,
SCENARIO_METRICS,
resolve_scenario_metric_keys,
)
from recipes.registry import RECIPE_CHOICES, resolve_recipe_id
from init import (
LATEST_VERSION,
is_model_on_hub,
list_leaderboard_versions,
load_raw_results,
load_results,
normalize_legacy_csv_row,
raw_rows_for_version,
)
from utils_display import (
AutoEvalColumn,
SCENARIO_DISPLAY_COLS,
SCENARIO_DISPLAY_TO_KEY,
column_widths_for,
fields,
format_wer_percent,
model_id_from_leaderboard_cell,
styled_error,
styled_message,
styled_warning,
)
# ---------------------------------------------------------------------------
# Column setup
# ---------------------------------------------------------------------------
COLS = [c.name for c in fields(AutoEvalColumn)]
TYPES = [c.type for c in fields(AutoEvalColumn)]
AVG_WER_COL = AutoEvalColumn.avg_wer_core.name # "Avg WER (%)"
MODEL_COL = AutoEvalColumn.model.name # "Model"
ALWAYS_VISIBLE_COLS = (MODEL_COL, AVG_WER_COL)
# Empty table at import; real data loaded on demo.load (avoids blocking Space "preparing").
original_df = pd.DataFrame(columns=COLS)
# Raw CSV rows backing the currently displayed leaderboard version. ``None`` means "live"
# (filter_main_table falls back to load_raw_results()). Set whenever a version is selected
# so the Avg-WER recompute in filter_main_table matches the displayed snapshot.
_active_raw_rows: list[dict] | None = None
_NEXT_UP_PLACEHOLDER = (
"<div class='next-up-panel' style='font-size:0.8em;opacity:0.85'>"
"<em>Loading upcoming evaluations…</em></div>"
)
# ---------------------------------------------------------------------------
# Filter (matches open_asr_leaderboard pattern)
# ---------------------------------------------------------------------------
def filter_main_table(search_query, selected_columns):
filtered_df = original_df.copy()
selected_columns = list(selected_columns or [])
if MODEL_COL not in filtered_df.columns:
return pd.DataFrame(columns=COLS)
# Filter by model name search (case-insensitive, supports comma-separated terms).
if search_query:
terms = [t.strip() for t in search_query.split(",") if t.strip()]
if terms:
mask = filtered_df[MODEL_COL].str.lower().apply(
lambda cell: any(term.lower() in cell for term in terms)
)
filtered_df = filtered_df[mask]
# Hide toggleable columns that the user unchecked. Model + Avg WER stay visible.
all_toggleable = [c for c in COLS if c not in ALWAYS_VISIBLE_COLS]
columns_to_hide = set(all_toggleable) - set(selected_columns)
filtered_df = filtered_df[[c for c in filtered_df.columns if c not in columns_to_hide]]
raw_rows = _active_raw_rows if _active_raw_rows is not None else load_raw_results()
for r in raw_rows:
normalize_legacy_csv_row(r)
raw_by_mid = {r["model_id"]: r for r in raw_rows}
visible_metric_keys = [
SCENARIO_DISPLAY_TO_KEY[c]
for c in SCENARIO_DISPLAY_COLS
if c in selected_columns
]
def _row_avg_wer(rdict: dict) -> float | None:
if not visible_metric_keys:
return None
vals = []
for k in visible_metric_keys:
try:
v = rdict.get(k, "")
if v is None or str(v).strip() == "":
continue
vals.append(float(v))
except (TypeError, ValueError):
continue
if not vals:
return None
return sum(vals) / len(vals)
def _avg_wer_key(row) -> tuple[int, float]:
mid = model_id_from_leaderboard_cell(row[MODEL_COL])
rdict = raw_by_mid.get(mid)
if not rdict:
return (1, float("inf"))
avg = _row_avg_wer(rdict)
if avg is None:
return (1, float("inf"))
return (0, avg)
if AVG_WER_COL in filtered_df.columns:
def _display_avg(row):
mid = model_id_from_leaderboard_cell(row[MODEL_COL])
rdict = raw_by_mid.get(mid)
if not rdict:
return "NA"
avg = _row_avg_wer(rdict)
if avg is None:
return "NA"
return format_wer_percent(avg)
filtered_df[AVG_WER_COL] = filtered_df.apply(_display_avg, axis=1)
filtered_df = filtered_df.assign(_sort=filtered_df.apply(_avg_wer_key, axis=1))
filtered_df = filtered_df.sort_values(by="_sort").drop(columns="_sort")
return filtered_df
def _leaderboard_table_update(search_query, selected_columns):
"""Return a Gradio update with table data and matching column widths."""
df = filter_main_table(search_query, selected_columns)
return gr.update(value=df, column_widths=column_widths_for(list(df.columns)))
def _format_version_recorded_at(ts: str) -> str:
from datetime import datetime
try:
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M UTC")
except Exception:
return str(ts)
def _version_choices() -> list[tuple[str, str]]:
"""(label, value) pairs for the version dropdown; first entry is always 'Latest'."""
choices: list[tuple[str, str]] = [("Latest", LATEST_VERSION)]
try:
for v in list_leaderboard_versions():
label = f"{_format_version_recorded_at(v['recorded_at'])}{v['label']}"
choices.append((label, v["version"]))
except Exception:
pass
return choices
# ---------------------------------------------------------------------------
# Submission handler
# ---------------------------------------------------------------------------
def _wer_cell(row: dict, key: str) -> str:
from utils_display import format_wer_percent
normalize_legacy_csv_row(row)
v = row.get(key, "")
if v is None or (isinstance(v, str) and str(v).strip() == ""):
return "N/A"
pct = format_wer_percent(v)
return f"{pct}%" if pct != "NA" else "N/A"
def _model_hub_page_link(model_id: str) -> str:
mid = (model_id or "").strip().replace(" ", "")
if not mid or "/" not in mid:
return (
"<p style='font-size:0.8em;opacity:0.85'>"
"Enter a model id (<code>author/name</code>) to open its Hub page.</p>"
)
url = f"https://huggingface.co/{mid}"
return (
f"<p style='font-size:0.8em'>"
f"<a href='{url}' target='_blank' rel='noopener noreferrer'>"
f"Open model page on the Hub</a> — request access if the repo is gated.</p>"
)
def submit_model(
model_id: str,
submission_notes: str,
contact_email: str,
extra_requirements: str,
setup_script: str,
custom_script: str,
recipe_id: str,
is_gated: bool,
) -> str:
"""Validate and enqueue evaluation; a background worker runs approved jobs (up to 4 parallel Hub Jobs when remote mode is on).
The backend always uses the "Auto" family (pipeline → Granite → universal → CTC),
which covers every checkpoint we currently support, so there is no per-submission family choice.
"""
model_id = model_id.strip()
if not model_id:
return styled_error("Please enter a model ID.")
family_id = default_family_id()
existing = load_raw_results()
for row in existing:
if row["model_id"] == model_id:
return styled_message(
f"Model '{model_id}' has already been evaluated. "
f"Near Field Speech: {_wer_cell(row, 'wer_anechoic_speech')} | "
f"Lab Measured: {_wer_cell(row, 'wer_lab_measured')} | "
f"Lab Simulated: {_wer_cell(row, 'wer_lab_simulated')} | "
f"High SNR: {_wer_cell(row, 'wer_realistic_high_snr')} | "
f"Mid SNR: {_wer_cell(row, 'wer_realistic_mid_snr')} | "
f"Low SNR: {_wer_cell(row, 'wer_realistic_low_snr')} | "
f"Moving Sources (Low SNR): {_wer_cell(row, 'wer_moving_sources')}"
)
# API / recipe / custom-script models (e.g. zoom/scribe_v1) are not Hub repos, so the
# Hub-existence check would wrongly reject them. Skip it whenever a recipe resolves or a
# custom evaluator is supplied; otherwise validate the model id against the Hub.
resolved_recipe = resolve_recipe_id(model_id, (recipe_id or "").strip() or None)
has_custom_script = bool((custom_script or "").strip())
if not resolved_recipe and not has_custom_script:
on_hub, err_msg = is_model_on_hub(model_id)
if not on_hub:
return styled_error(f"Model '{model_id}' {err_msg}")
if not (contact_email or "").strip():
return styled_error("Please enter a contact email address.")
try:
job_queue.sanitize_contact_email(contact_email)
except ValueError as exc:
msg = str(exc)
if msg == "Contact email is required.":
return styled_error("Please enter a contact email address.")
return styled_error("Please enter a valid email address.")
script_hint = ""
if (custom_script or "").strip():
dep_warn = job_queue.custom_script_deprecated_api_warning(custom_script)
if dep_warn:
script_hint += f"<p style='color:orange'><strong>Note:</strong> {dep_warn}</p>"
argparse_warn = job_queue.custom_script_argparse_warning(custom_script)
if argparse_warn:
script_hint += f"<p style='color:orange'><strong>Note:</strong> {argparse_warn}</p>"
if not job_queue.custom_script_defines_evaluate(custom_script):
script_hint += (
"<p style='color:orange'><strong>Note:</strong> Your custom script should define "
"<code>evaluate(file: pathlib.Path) -&gt; str</code> at the top level. "
"It will be called once per sample during evaluation.</p>"
)
try:
job_id, position, err, awaiting_mod = job_queue.enqueue(
model_id,
family_id,
submission_notes=submission_notes or "",
contact_email=contact_email or "",
extra_requirements=extra_requirements or "",
setup_script=setup_script or "",
custom_script=custom_script or "",
recipe_id=(recipe_id or "").strip(),
is_gated=bool(is_gated),
)
except Exception as exc:
return styled_warning(f"Could not enqueue submission: {exc}")
if err == "remote_jobs_required":
return styled_error(
"Hub Jobs are required. Set Space secrets "
"<code>FFASR_REMOTE_JOBS=1</code>, <code>token_for_ffasr_jobs</code>, "
"<code>HF_TOKEN</code>, and <code>FFASR_REMOTE_EVAL_REPO_URL</code>."
)
if err == "moderation_misconfigured":
return styled_error(
"This Space has moderation enabled but <code>FFASR_MODERATOR_SECRET</code> is not set. "
"Ask the owner to add it under Space Settings → Secrets."
)
if err == "pending_moderation_full":
return styled_warning(
"Too many models are awaiting moderator approval. Please try again later."
)
if err == "already_in_csv":
existing = load_raw_results()
for row in existing:
if row["model_id"] == model_id:
return styled_message(
f"Model '{model_id}' has already been evaluated. "
f"Near Field Speech: {_wer_cell(row, 'wer_anechoic_speech')} | "
f"Lab Measured: {_wer_cell(row, 'wer_lab_measured')} | "
f"Lab Simulated: {_wer_cell(row, 'wer_lab_simulated')} | "
f"High SNR: {_wer_cell(row, 'wer_realistic_high_snr')} | "
f"Mid SNR: {_wer_cell(row, 'wer_realistic_mid_snr')} | "
f"Low SNR: {_wer_cell(row, 'wer_realistic_low_snr')} | "
f"Moving Sources (Low SNR): {_wer_cell(row, 'wer_moving_sources')}"
)
return styled_error("Could not enqueue; please try again.")
if err == "queue_full":
return styled_warning(
"The evaluation queue is full. Please try again later."
)
if err:
return styled_warning(err)
if awaiting_mod:
return (
script_hint
+ styled_message(
f"Request <code>{job_id}</code> recorded for <strong>{model_id}</strong>. "
f"<strong>A moderator must approve</strong> it before evaluation starts "
f"(see the <strong>Moderate</strong> tab). "
f"Approx. backlog awaiting approval: <strong>{position}</strong>."
)
)
return (
script_hint
+ styled_message(
f"Queued job <code>{job_id}</code> for <strong>{model_id}</strong>. "
f"Approx. position in queue: <strong>{position}</strong>. "
f"Evaluation runs in the background; refresh the <strong>Leaderboard</strong> tab "
f"after a few minutes to see WER when the job finishes."
)
)
# ---------------------------------------------------------------------------
# Analysis tab: radar + line + bar charts
# ---------------------------------------------------------------------------
_METRIC_BY_KEY = {m.key: m for m in SCENARIO_METRICS}
_METRIC_CHOICES = [
(
f"{_METRIC_BY_KEY[k].short}: {_METRIC_BY_KEY[k].label}"
+ (" (planned)" if _METRIC_BY_KEY[k].status == "planned" else ""),
k,
)
for k in HEATMAP_SCENARIO_KEYS
if k in _METRIC_BY_KEY
]
def _analytics_initial():
raw = load_raw_results()
df = analytics._raw_to_analytics_df(raw)
ids = df["model_id"].tolist() if not df.empty else []
avail = analytics.available_metric_keys(df)
line_default = [k for k in HEATMAP_SCENARIO_KEYS if k in avail] or list(avail)
tn = 10
fig_intel = analytics.plot_avg_wer_bars(df, top_n=tn)
fig_speed = analytics.plot_speed_bars(df, top_n=tn)
fig_hm = analytics.plot_scenario_heatmap(df, line_default, top_n=tn)
fig_pareto = analytics.plot_pareto_frontier(df)
# fig_radar = analytics.plot_robustness_radar(df, sel)
fig_b = analytics.plot_scenario_bar_summary(df, 8)
return (
gr.update(value=line_default),
fig_intel,
fig_speed,
fig_hm,
fig_pareto,
fig_b,
)
def _analytics_apply(line_keys, top_n):
raw = load_raw_results()
df = analytics._raw_to_analytics_df(raw)
avail = analytics.available_metric_keys(df)
lk = resolve_scenario_metric_keys(line_keys, _METRIC_CHOICES)
if not lk:
lk = [k for k in HEATMAP_SCENARIO_KEYS if k in avail] or list(avail)
tn = int(top_n) if top_n else 10
fig_intel = analytics.plot_avg_wer_bars(df, top_n=tn)
fig_speed = analytics.plot_speed_bars(df, top_n=tn)
fig_hm = analytics.plot_scenario_heatmap(df, lk, top_n=tn)
fig_pareto = analytics.plot_pareto_frontier(df)
# fig_radar = analytics.plot_robustness_radar(df, valid_models)
fig_b = analytics.plot_scenario_bar_summary(df, tn)
return fig_intel, fig_speed, fig_hm, fig_pareto, fig_b
# ---------------------------------------------------------------------------
# Gradio App
# ---------------------------------------------------------------------------
_theme = treble_gradio_theme()
with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
gr.HTML(BANNER, elem_id="banner")
gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
with gr.Tabs(elem_classes="tab-buttons") as tabs:
with gr.TabItem(tab_label("leaderboard", "Leaderboard"), elem_id="od-benchmark-tab-table", id=0):
with gr.Row():
search_box = gr.Textbox(
label="Search models",
placeholder="Filter by model name -- separate multiple terms with commas, e.g. whisper, nvidia",
scale=4,
)
version_dropdown = gr.Dropdown(
choices=_version_choices(),
value=LATEST_VERSION,
label="Version",
scale=2,
min_width=200,
)
leaderboard_update_btn = gr.Button(
"Update", variant="primary", scale=1, min_width=120
)
toggleable_columns = [c for c in COLS if c not in ALWAYS_VISIBLE_COLS]
_default_visible_cols = [
c
for c in toggleable_columns
if c != AutoEvalColumn.wer_lab_measured.name
and c != AutoEvalColumn.wer_lab_simulated.name
and c != AutoEvalColumn.wer_moving_low.name
and c != AutoEvalColumn.wer_moving_mid.name
and c != AutoEvalColumn.wer_moving_high.name
]
column_checkboxes = gr.CheckboxGroup(
choices=toggleable_columns,
value=_default_visible_cols,
label="Select columns to display",
)
leaderboard_table = gr.components.Dataframe(
value=original_df,
datatype=TYPES,
elem_id="leaderboard-table",
interactive=False,
visible=True,
wrap=False,
column_widths=column_widths_for(COLS),
)
gr.Markdown(
"\* Moving-source evaluations are a beta feature.",
elem_classes="markdown-text",
)
def _set_active_version(version):
"""Load the chosen version into the module globals used by filter_main_table."""
global original_df, _active_raw_rows
if version:
_active_raw_rows = raw_rows_for_version(version)
original_df = load_results(version)
else:
_active_raw_rows = None
original_df = load_results()
def _leaderboard_refresh(search, cols, version):
"""Reload results from storage for the selected version, refresh version list."""
from init import invalidate_results_cache
try:
invalidate_results_cache()
choices = _version_choices()
valid = {v for _, v in choices}
if version not in valid:
version = LATEST_VERSION
_set_active_version(version)
return (
_leaderboard_table_update(search, cols),
gr.update(choices=choices, value=version),
)
except Exception:
return gr.update(
value=original_df,
column_widths=column_widths_for(list(original_df.columns)),
), gr.update()
def _on_version_change(version, search, cols):
"""Swap the displayed leaderboard to the selected version, keep filters."""
try:
_set_active_version(version)
return _leaderboard_table_update(search, cols)
except Exception:
return gr.update(
value=original_df,
column_widths=column_widths_for(list(original_df.columns)),
)
def _on_startup():
"""Load leaderboard + next-up list after UI is up (keeps Space prepare step fast)."""
global original_df, _active_raw_rows
from init import invalidate_results_cache
version_update = gr.update()
try:
invalidate_results_cache()
_active_raw_rows = None
original_df = load_results()
table = _leaderboard_table_update("", _default_visible_cols)
version_update = gr.update(choices=_version_choices(), value=LATEST_VERSION)
except Exception as exc:
table = gr.update(
value=original_df,
column_widths=column_widths_for(list(original_df.columns)),
)
next_html = (
"<div class='next-up-panel'><p style='color:orange'>"
f"<strong>Startup warning:</strong> {exc}</p></div>"
)
return table, next_html, version_update
try:
job_queue.ensure_worker_started()
next_html = job_queue.next_up_html()
except Exception as exc:
next_html = (
"<div class='next-up-panel'><p style='color:orange'>"
f"<strong>Queue unavailable:</strong> {exc}</p></div>"
)
return table, next_html, version_update
filter_inputs = [search_box, column_checkboxes]
search_box.change(_leaderboard_table_update, inputs=filter_inputs, outputs=leaderboard_table)
column_checkboxes.change(_leaderboard_table_update, inputs=filter_inputs, outputs=leaderboard_table)
version_dropdown.change(
fn=_on_version_change,
inputs=[version_dropdown, search_box, column_checkboxes],
outputs=leaderboard_table,
)
leaderboard_update_btn.click(
fn=_leaderboard_refresh,
inputs=[search_box, column_checkboxes, version_dropdown],
outputs=[leaderboard_table, version_dropdown],
)
with gr.TabItem(tab_label("submit", "Submit"), elem_id="submit-tab", id=1):
gr.Markdown("## Submit a model for evaluation")
gr.Markdown(
"Paste a Hugging Face model id. The server loads every checkpoint with an automatic backend, so "
"**Whisper, IBM Granite speech, Cohere Transcribe, efficient‑speech / custom Whisper variants, "
"Wav2Vec2 / HuBERT CTC heads, and SpeechBrain ASR**, plus most other ASR stacks on the Hub are "
"supported without a per‑submission choice.\n\n"
"Submissions are **queued**; when Hub Jobs are enabled, up to **four** models evaluate in parallel. "
"Otherwise jobs run **one at a time** on this Space. "
"When moderation is enabled, new requests wait for approval before they run."
)
with gr.Row():
model_input = gr.Textbox(
label="Model ID",
placeholder="e.g. openai/whisper-tiny",
scale=3,
)
submit_btn = gr.Button("Evaluate", variant="primary", scale=1)
status_output = gr.HTML()
recipe_input = gr.Dropdown(
label="Complex model recipe (optional)",
choices=RECIPE_CHOICES,
value="",
info="Pre-fills setup script, evaluate(), and deps for install-heavy stacks (e.g. Mega-ASR).",
)
requirements_input = gr.Textbox(
label="Optional extra Python requirements (one per line, requirements.txt format)",
placeholder="transformers @ git+https://github.com/huggingface/transformers.git\nsentencepiece",
lines=4,
max_length=8000,
)
setup_script_input = gr.Textbox(
label="Optional one-time setup script (shell or Python, runs once per Hub Job)",
placeholder="git clone …\npip install -r requirements.txt\npython scripts/download.py",
lines=6,
max_length=8000,
)
script_input = gr.Code(
label="Optional custom evaluator (Python)",
language="python",
lines=16,
value=DEFAULT_CUSTOM_EVAL_EXAMPLE,
)
gr.Markdown(
"Edit the example below: define **`evaluate(file: Path) -> str`** to transcribe one WAV. "
"Load your model **once** at module level (as in the Cohere example). "
"Use **soundfile** (or numpy) to read each WAV (see the example). "
"Hub Jobs disable torchcodec and fall back to librosa if you use "
"``transformers.audio_utils.load_audio`` on file paths. "
"The function is called **once per audio sample** inside the eval loop. "
"**Model size:** expose your loaded model as a module-level `model` "
"(or set `NUM_PARAMS = <int>`) so the leaderboard can report its "
"parameter count; otherwise size is left blank. "
"Put extra Python dependencies in the requirements box above. "
"Use the **setup script** for git clone / weight download (runs once before evaluation). "
"**No maintainer recipe is needed for a new model:** if your `evaluate()` imports a "
"cloned package (e.g. `from MyASR.model import MyASR`), the worker auto-discovers it "
"under `/tmp`. For full control, export paths from your setup script via "
"`echo \"PYTHONPATH=/tmp/MyASR/src\" >> \"$FFASR_ENV_FILE\"` (also accepts "
"`FFASR_IMPORT_PATHS` and any `KEY=VALUE`). "
"See [Mega-ASR recipe](docs/recipes/mega_asr.md) for an example. "
"Custom evaluators run on a Hub Job **only** after a moderator approves them."
)
with gr.Row():
is_gated_input = gr.Checkbox(label="This is a gated repo", value=False)
gated_link = gr.HTML(value=_model_hub_page_link(""))
model_input.change(fn=_model_hub_page_link, inputs=[model_input], outputs=[gated_link])
notes_input = gr.Textbox(
label="Optional free-form notes for moderators",
placeholder="Repo link, paper, eval caveats, hardware notes…",
lines=2,
max_length=4000,
)
email_input = gr.Textbox(
label="Contact email",
placeholder="you@example.com",
max_length=254,
)
gr.Markdown(
"### Contact\n\n"
"Questions about submissions, gated repos, or evaluation issues? "
"Email [contact@treble.tech](mailto:contact@treble.tech)."
)
gr.Markdown("### Next models to evaluate")
next_up_panel = gr.HTML(value=_NEXT_UP_PLACEHOLDER, elem_classes="next-up-panel")
def _apply_recipe_fields(model_id, recipe_id, reqs, setup, script):
from recipes.registry import apply_recipe_to_submission
extra, setup_out, custom, _rid = apply_recipe_to_submission(
(model_id or "").strip(),
(recipe_id or "").strip() or None,
reqs or "",
setup or "",
script or "",
)
return extra, setup_out, custom
def _submit_and_refresh(model_id, notes, email, reqs, setup, script, recipe, gated):
status = submit_model(model_id, notes, email, reqs, setup, script, recipe, gated)
try:
nxt = job_queue.next_up_html()
except Exception as exc:
nxt = f"<p style='color:orange'>Could not load queue: {exc}</p>"
return status, nxt
recipe_input.change(
fn=_apply_recipe_fields,
inputs=[
model_input,
recipe_input,
requirements_input,
setup_script_input,
script_input,
],
outputs=[requirements_input, setup_script_input, script_input],
)
submit_btn.click(
fn=_submit_and_refresh,
inputs=[
model_input,
notes_input,
email_input,
requirements_input,
setup_script_input,
script_input,
recipe_input,
is_gated_input,
],
outputs=[status_output, next_up_panel],
)
with gr.TabItem(tab_label("moderate", "Moderate"), elem_id="moderate-tab", id=2):
from benchmark.dataset import CONDITION_UI_CHOICES
_MOD_COND_CHOICES = [(lbl, key) for lbl, key in CONDITION_UI_CHOICES]
_MOD_COND_DEFAULT = [key for _, key in CONDITION_UI_CHOICES]
gr.Markdown("## Moderator access")
mod_unlock_feedback = gr.HTML()
mod_secret = gr.Textbox(
label="Moderator secret",
type="password",
placeholder="FFASR_MODERATOR_SECRET",
)
mod_unlock_btn = gr.Button("Unlock moderator tools", variant="primary")
mod_unlocked = gr.State(False)
with gr.Column(visible=False) as mod_panel:
gr.Markdown(
"When <code>FFASR_MODERATION=1</code> is set, new submissions **wait here** until you approve them. "
"Use per-row **Check**, **Approve**, **Retry**, and **Remove** buttons — popups open for "
"submission review, dataset selection, and approval.\n\n"
"When <code>FFASR_REMOTE_JOBS=1</code>, use **Open Hub Job logs** while a job runs. "
"The bucket <code>results/remote_artifacts/</code> folder only gets a JSON file **after** a successful run.\n\n"
"**Retry** re-queues failed, done, or queued jobs (not while running). "
"Uncheck datasets in the retry popup to run only selected packed splits; "
"other leaderboard WER columns are left unchanged on success. "
"A full re-run (all datasets selected) **replaces** the row when the model is already listed."
)
gr.Markdown("### Current job progress")
mod_progress = gr.HTML(
value=job_queue.moderation_locked_placeholder_html(),
elem_classes="ffasr-card",
)
mod_refresh_tick = gr.Number(value=0, visible=False)
mod_refresh = gr.Button("Refresh lists")
mod_feedback = gr.HTML()
mod_check_job_id = gr.State("")
mod_retry_job_id = gr.State("")
mod_approve_job_id = gr.State("")
with gr.Column(visible=False) as check_panel:
check_header = gr.Markdown("### Check submission")
check_notes = gr.Textbox(
label="Submitter notes",
lines=2,
interactive=False,
)
check_recipe = gr.Dropdown(
label="Recipe",
choices=RECIPE_CHOICES,
value="",
)
check_reqs = gr.Textbox(
label="Extra Python requirements (one per line)",
lines=4,
max_length=8000,
)
check_setup = gr.Textbox(
label="One-time setup script (shell or Python)",
lines=6,
max_length=8000,
)
check_script = gr.Code(
label="Custom evaluator (Python)",
language="python",
lines=16,
)
with gr.Row():
check_save_btn = gr.Button("Save", variant="primary")
check_cancel_btn = gr.Button("Cancel")
with gr.Column(visible=False) as retry_panel:
retry_header = gr.Markdown("### Retry job")
retry_eval_conditions = gr.CheckboxGroup(
choices=_MOD_COND_CHOICES,
value=_MOD_COND_DEFAULT,
label="Datasets to evaluate (all selected = full benchmark)",
)
with gr.Row():
retry_confirm_btn = gr.Button(
"Confirm retry", variant="primary"
)
retry_cancel_btn = gr.Button("Cancel")
with gr.Column(visible=False) as approve_panel:
approve_header = gr.Markdown("### Approve job")
approve_run_custom = gr.Checkbox(
label="Use submitter's custom evaluate() function (if provided)",
value=False,
)
approve_eval_conditions = gr.CheckboxGroup(
choices=_MOD_COND_CHOICES,
value=_MOD_COND_DEFAULT,
label="Datasets to evaluate (all selected = full benchmark)",
)
with gr.Row():
approve_confirm_btn = gr.Button("Approve", variant="primary")
approve_cancel_btn = gr.Button("Cancel")
_MOD_POPUP_HIDE = (
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
)
def _mod_bump_tick(tick):
return int(tick or 0) + 1
def _mod_feedback_html(ok: bool, msg: str) -> str:
color = "green" if ok else "red"
return f"<p style='color:{color}'>{msg}</p>"
def _mod_open_check(jid: str, secret: str, unlocked: bool):
hidden = gr.update(visible=False)
if not unlocked:
return (
gr.update(visible=False),
hidden,
hidden,
"",
"### Check submission",
"",
"",
"",
"",
"",
)
job = job_queue.peek_job(str(jid).strip())
if not job:
return (
gr.update(visible=False),
hidden,
hidden,
"",
"### Check submission",
"",
"",
"",
"",
"",
)
return (
gr.update(visible=True),
hidden,
hidden,
job.id,
f"### Check submission: `{job.model_id}`",
job.submission_notes or "",
job.recipe_id or "",
job.extra_requirements or "",
job.setup_script or "",
job.custom_script or "",
)
def _mod_open_retry(jid: str, secret: str, unlocked: bool):
hidden = gr.update(visible=False)
if not unlocked:
return (
hidden,
gr.update(visible=False),
hidden,
"",
"### Retry job",
)
job = job_queue.peek_job(str(jid).strip())
if not job:
return (
hidden,
gr.update(visible=False),
hidden,
"",
"### Retry job",
)
return (
hidden,
gr.update(visible=True),
hidden,
job.id,
f"### Retry: `{job.model_id}` (`{job.id}`)",
)
def _mod_open_approve(jid: str, secret: str, unlocked: bool):
hidden = gr.update(visible=False)
if not unlocked:
return (
hidden,
hidden,
gr.update(visible=False),
"",
"### Approve job",
False,
)
job = job_queue.peek_job(str(jid).strip())
if not job:
return (
hidden,
hidden,
gr.update(visible=False),
"",
"### Approve job",
False,
)
has_script = bool(
(job.custom_script or "").strip()
or (job.setup_script or "").strip()
or (job.recipe_id or "").strip()
)
return (
hidden,
hidden,
gr.update(visible=True),
job.id,
f"### Approve: `{job.model_id}` (`{job.id}`)",
has_script,
)
def _mod_reject_row(jid, secret, unlocked, tick):
if not unlocked:
return (
"<p style='color:red'>Unlock moderator tools first.</p>",
tick,
job_queue.progress_html(),
)
ok, msg = job_queue.reject_job(str(jid).strip(), secret)
return (
_mod_feedback_html(ok, msg),
_mod_bump_tick(tick),
job_queue.progress_html(),
)
def _mod_remove_row(jid, secret, unlocked, tick):
if not unlocked:
return (
"<p style='color:red'>Unlock moderator tools first.</p>",
tick,
job_queue.progress_html(),
)
ok, msg = job_queue.remove_job_entry(str(jid).strip(), secret)
return (
_mod_feedback_html(ok, msg),
_mod_bump_tick(tick),
job_queue.progress_html(),
)
def _mod_save_check(jid, secret, unlocked, tick, recipe, reqs, setup, script):
if not unlocked:
return (
"<p style='color:red'>Unlock moderator tools first.</p>",
tick,
*_MOD_POPUP_HIDE,
)
ok, msg = job_queue.update_job_script_and_requirements(
str(jid).strip(),
secret,
extra_requirements=reqs or "",
setup_script=setup or "",
custom_script=script or "",
recipe_id=(recipe or "").strip(),
)
return (
_mod_feedback_html(ok, msg),
_mod_bump_tick(tick),
*_MOD_POPUP_HIDE,
)
def _mod_confirm_retry(jid, secret, unlocked, tick, eval_conds):
if not unlocked:
return (
"<p style='color:red'>Unlock moderator tools first.</p>",
tick,
*_MOD_POPUP_HIDE,
job_queue.progress_html(),
)
ok, msg = job_queue.retry_failed_job(
str(jid).strip(),
secret,
eval_conditions=list(eval_conds or []),
)
return (
_mod_feedback_html(ok, msg),
_mod_bump_tick(tick),
*_MOD_POPUP_HIDE,
job_queue.progress_html(),
)
def _mod_confirm_approve(
jid, secret, unlocked, tick, run_custom, eval_conds
):
if not unlocked:
return (
"<p style='color:red'>Unlock moderator tools first.</p>",
tick,
*_MOD_POPUP_HIDE,
job_queue.progress_html(),
)
ok, msg = job_queue.approve_job(
str(jid).strip(),
secret,
run_custom_script=bool(run_custom),
eval_conditions=list(eval_conds or []),
)
return (
_mod_feedback_html(ok, msg),
_mod_bump_tick(tick),
*_MOD_POPUP_HIDE,
job_queue.progress_html(),
)
def _mod_cancel_popup(tick):
return tick, *_MOD_POPUP_HIDE
def _mod_retry_all(secret, unlocked, tick):
if not unlocked:
return (
"<p style='color:orange'>Unlock moderator tools first.</p>",
tick,
job_queue.progress_html(),
)
ok, msg = job_queue.retry_all_eligible_jobs(secret)
return (
_mod_feedback_html(ok, msg),
_mod_bump_tick(tick),
job_queue.progress_html(),
)
def _mod_import_artifact(
secret, artifact_ref, replace_existing, import_notes, unlocked, tick
):
if not unlocked:
return (
"<p style='color:red'>Unlock moderator tools first.</p>",
tick,
)
ok, msg = job_queue.import_artifact_to_leaderboard(
artifact_ref,
secret,
replace_existing=bool(replace_existing),
submission_notes=import_notes or "",
)
return (_mod_feedback_html(ok, msg), _mod_bump_tick(tick))
def _mod_refresh_lists(secret, unlocked, tick):
if not unlocked:
return tick, job_queue.moderation_locked_placeholder_html()
ok, msg = job_queue.verify_moderator_secret(secret)
if not ok:
return tick, job_queue.moderation_locked_placeholder_html()
return _mod_bump_tick(tick), job_queue.progress_html()
gr.Markdown("### Pending moderation")
@gr.render(inputs=[mod_refresh_tick])
def _render_pending_jobs(_tick):
try:
jobs = job_queue.pending_jobs_for_render()
except Exception as exc:
gr.Markdown(f"*Queue unavailable: {exc}*")
return
if not jobs:
gr.Markdown("*No jobs awaiting approval.*")
return
for job in jobs:
jid = job["id"]
row_cls = job_queue.job_row_elem_classes(job["status"])
gated = " · **gated**" if job["is_gated"] else ""
script_bit = (
" · custom/setup/recipe"
if job["has_custom_script"]
else ""
)
extras = (
f"{job['req_count']} extra req(s)"
if job["req_count"]
else "no extra reqs"
)
email_bit = (
f" · 📧 `{job['contact_email']}`"
if job.get("contact_email")
else " · 📧 *(none)*"
)
with gr.Row(elem_classes=row_cls):
gr.Markdown(
f"**`{jid}`** · `{job['model_id']}` · "
f"`{job['family_id']}`{gated}{script_bit} · {extras}{email_bit} · "
f"{job['notes_preview'] or '—'} · {job['created_at']}",
elem_classes="ffasr-job-info",
)
check_btn = gr.Button("Check", size="sm")
approve_btn = gr.Button(
"Approve", size="sm", variant="primary"
)
reject_btn = gr.Button("Reject", size="sm", variant="stop")
check_btn.click(
fn=_mod_open_check,
inputs=[gr.State(jid), mod_secret, mod_unlocked],
outputs=[
check_panel,
retry_panel,
approve_panel,
mod_check_job_id,
check_header,
check_notes,
check_recipe,
check_reqs,
check_setup,
check_script,
],
)
approve_btn.click(
fn=_mod_open_approve,
inputs=[gr.State(jid), mod_secret, mod_unlocked],
outputs=[
check_panel,
retry_panel,
approve_panel,
mod_approve_job_id,
approve_header,
approve_run_custom,
],
)
reject_btn.click(
fn=_mod_reject_row,
inputs=[gr.State(jid), mod_secret, mod_unlocked, mod_refresh_tick],
outputs=[mod_feedback, mod_refresh_tick, mod_progress],
)
gr.Markdown("### Recent job activity")
retry_all_btn = gr.Button(
"Retry ALL eligible jobs (full benchmark)",
variant="secondary",
)
gr.Markdown(
"<small><em>Retry ALL</em> re-queues every <strong>failed / done / queued</strong> "
"job against the full dataset set (all packed conditions). Running, dispatching, and "
"pending-moderation jobs are skipped. Successful re-runs replace the existing "
"leaderboard row for each model.</small>"
)
@gr.render(inputs=[mod_refresh_tick])
def _render_recent_jobs(_tick):
try:
jobs = job_queue.recent_jobs_for_render(30)
except Exception as exc:
gr.Markdown(f"*Queue unavailable: {exc}*")
return
if not jobs:
gr.Markdown("*No job history loaded yet.*")
return
for job in jobs:
jid = job["id"]
err = job["error"] or "N/A"
hub = job["hub_link_html"] or ""
row_cls = job_queue.job_row_elem_classes(job["status"])
email_bit = (
f" · 📧 `{job['contact_email']}`"
if job.get("contact_email")
else " · 📧 *(none)*"
)
info_md = (
f"**`{jid}`** · `{job['model_id']}` · "
f"**{job['status']}**{email_bit} · {err} · {job['updated_at']}"
)
if hub:
info_md += f" · {hub}"
with gr.Row(elem_classes=row_cls):
gr.Markdown(
info_md,
elem_classes="ffasr-job-info",
)
check_btn = gr.Button("Check", size="sm")
if job["can_retry"]:
retry_btn = gr.Button(
"Retry", size="sm", variant="secondary"
)
if job["can_remove"]:
remove_btn = gr.Button("Remove", size="sm", variant="stop")
check_btn.click(
fn=_mod_open_check,
inputs=[gr.State(jid), mod_secret, mod_unlocked],
outputs=[
check_panel,
retry_panel,
approve_panel,
mod_check_job_id,
check_header,
check_notes,
check_recipe,
check_reqs,
check_setup,
check_script,
],
)
if job["can_retry"]:
retry_btn.click(
fn=_mod_open_retry,
inputs=[gr.State(jid), mod_secret, mod_unlocked],
outputs=[
check_panel,
retry_panel,
approve_panel,
mod_retry_job_id,
retry_header,
],
)
if job["can_remove"]:
remove_btn.click(
fn=_mod_remove_row,
inputs=[
gr.State(jid),
mod_secret,
mod_unlocked,
mod_refresh_tick,
],
outputs=[mod_feedback, mod_refresh_tick, mod_progress],
)
gr.Markdown("### Import result from bucket artifact")
gr.Markdown(
"If a Hub Job finished but the leaderboard CSV was not updated, paste the artifact "
"file name from <code>results/remote_artifacts/</code> (e.g. <code>a1b2c3d4.json</code>) "
"to merge WER/RTFx into the CSV."
)
mod_artifact_ref = gr.Textbox(
label="Artifact JSON file name or bucket path",
placeholder="e.g. a1b2c3d4.json or results/remote_artifacts/a1b2c3d4.json",
)
mod_replace_existing = gr.Checkbox(
label="Replace existing leaderboard row for this model",
value=False,
)
mod_import_notes = gr.Textbox(
label="Optional submission notes override",
placeholder="Leave empty to use notes from the matched queue job, if any",
lines=1,
max_length=4000,
)
import_artifact_btn = gr.Button("Import artifact to CSV", variant="secondary")
def _mod_unlock(secret: str):
ok, msg = job_queue.verify_moderator_secret(secret)
if not ok:
return (
False,
gr.update(visible=False),
f"<p style='color:red'>{msg}</p>",
0,
)
return (
True,
gr.update(visible=True),
"<p style='color:green'>Moderator tools unlocked for this session.</p>",
1,
)
def _mod_progress_gated(unlocked: bool):
if not unlocked:
return job_queue.moderation_locked_placeholder_html()
return job_queue.progress_html()
mod_unlock_btn.click(
fn=_mod_unlock,
inputs=[mod_secret],
outputs=[mod_unlocked, mod_panel, mod_unlock_feedback, mod_refresh_tick],
)
mod_refresh.click(
fn=_mod_refresh_lists,
inputs=[mod_secret, mod_unlocked, mod_refresh_tick],
outputs=[mod_refresh_tick, mod_progress],
)
check_save_btn.click(
fn=_mod_save_check,
inputs=[
mod_check_job_id,
mod_secret,
mod_unlocked,
mod_refresh_tick,
check_recipe,
check_reqs,
check_setup,
check_script,
],
outputs=[
mod_feedback,
mod_refresh_tick,
check_panel,
retry_panel,
approve_panel,
],
)
check_cancel_btn.click(
fn=_mod_cancel_popup,
inputs=[mod_refresh_tick],
outputs=[mod_refresh_tick, check_panel, retry_panel, approve_panel],
)
retry_confirm_btn.click(
fn=_mod_confirm_retry,
inputs=[
mod_retry_job_id,
mod_secret,
mod_unlocked,
mod_refresh_tick,
retry_eval_conditions,
],
outputs=[
mod_feedback,
mod_refresh_tick,
check_panel,
retry_panel,
approve_panel,
mod_progress,
],
)
retry_cancel_btn.click(
fn=_mod_cancel_popup,
inputs=[mod_refresh_tick],
outputs=[mod_refresh_tick, check_panel, retry_panel, approve_panel],
)
approve_confirm_btn.click(
fn=_mod_confirm_approve,
inputs=[
mod_approve_job_id,
mod_secret,
mod_unlocked,
mod_refresh_tick,
approve_run_custom,
approve_eval_conditions,
],
outputs=[
mod_feedback,
mod_refresh_tick,
check_panel,
retry_panel,
approve_panel,
mod_progress,
],
)
approve_cancel_btn.click(
fn=_mod_cancel_popup,
inputs=[mod_refresh_tick],
outputs=[mod_refresh_tick, check_panel, retry_panel, approve_panel],
)
retry_all_btn.click(
fn=_mod_retry_all,
inputs=[mod_secret, mod_unlocked, mod_refresh_tick],
outputs=[mod_feedback, mod_refresh_tick, mod_progress],
)
import_artifact_btn.click(
fn=_mod_import_artifact,
inputs=[
mod_secret,
mod_artifact_ref,
mod_replace_existing,
mod_import_notes,
mod_unlocked,
mod_refresh_tick,
],
outputs=[mod_feedback, mod_refresh_tick],
)
with gr.TabItem(tab_label("analysis", "Analysis"), elem_id="analysis-tab", id=3) as analysis_tab:
gr.Markdown("## Scenario analysis")
gr.Markdown(
"Charts support standard Plotly interaction (legend, zoom, pan, hover).\n\n"
"**Leaderboard order** is by **Average WER** (mean across checked scenario columns; lower is better).\n\n"
"**Average WER** ranks the top N models by mean WER across live scenarios (lower is better). "
"**Speed** ranks the top N models by RTFx (audio sec / inference sec, higher is better). "
"Bars are colored per company (e.g. all NVIDIA models share one color).\n\n"
"**WER heatmap** shows WER (%) by model and scenario for the top N models (sorted by Average WER); "
"lower WER corresponds to greener cells.\n\n"
"**Pareto Front** plots Average WER on X and RTFx on Y (log scale). Models on the frontier are "
"labeled and connected by the dashed blue line; other models render as faint dots.\n\n"
"**WER by scenario** compares raw WER across conditions for the selected top models."
)
# with gr.Row():
# an_models = gr.Dropdown(
# label="Models on radar (up to 8)",
# choices=[],
# value=None,
# multiselect=True,
# max_choices=8,
# scale=2,
# )
an_topn = gr.Slider(
5,
80,
value=10,
step=1,
label="Top‑N models (WER bars, heatmap & grouped WER)",
scale=1,
)
an_line_metrics = gr.CheckboxGroup(
label="Scenarios shown in the heatmap",
choices=_METRIC_CHOICES,
value=[k for k in HEATMAP_SCENARIO_KEYS if k in LIVE_SCENARIO_KEYS],
)
an_apply = gr.Button("Apply / refresh charts", variant="primary")
gr.Markdown(
"### Pareto Front: Average WER vs RTFx\n\n"
"Models on the Pareto frontier achieve the best trade-off between WER "
"and speed (RTFx). Names are shown for frontier models; hover over "
"other points to see their names."
)
an_plot_pareto = gr.Plot(
label="Pareto Front: Average WER vs RTFx",
elem_id="analysis-pareto-plot",
)
with gr.Row():
an_plot_intelligence = gr.Plot(label="Average WER")
an_plot_speed = gr.Plot(label="Speed")
an_plot_compare = gr.Plot(label="WER heatmap")
# with gr.Row():
# an_plot_radar = gr.Plot(label="Robustness radar")
an_plot_bar = gr.Plot(label="WER by scenario")
an_apply.click(
fn=_analytics_apply,
inputs=[
an_line_metrics,
an_topn,
],
outputs=[
an_plot_intelligence,
an_plot_speed,
an_plot_compare,
an_plot_pareto,
an_plot_bar,
],
)
_analytics_outputs = [
an_line_metrics,
an_plot_intelligence,
an_plot_speed,
an_plot_compare,
an_plot_pareto,
an_plot_bar,
]
_analytics_loaded = gr.State(False)
def _analytics_on_tab_select(loaded: bool):
"""Load charts once the Analysis tab is visible so Plotly gets the real width."""
if loaded:
return (loaded,) + (gr.skip(),) * 6
(
metrics_upd,
fig_intel,
fig_speed,
fig_hm,
fig_pareto,
fig_b,
) = _analytics_initial()
return (
True,
metrics_upd,
fig_intel,
fig_speed,
fig_hm,
fig_pareto,
fig_b,
)
analysis_tab.select(
fn=_analytics_on_tab_select,
inputs=[_analytics_loaded],
outputs=[_analytics_loaded, *_analytics_outputs],
)
with gr.TabItem(tab_label("examples", "Examples"), elem_id="examples-tab", id=4):
examples_renderer.ensure_assets()
_dry = examples_renderer.sample_paths("dry")
_hi = examples_renderer.sample_paths("high_snr")
_mid = examples_renderer.sample_paths("mid_snr")
_lo = examples_renderer.sample_paths("low_snr")
_scene = examples_renderer.scene_image_path()
def _example_audio(path: str | None, label: str):
return gr.Audio(
value=path,
type="filepath",
label=label,
interactive=False,
editable=False,
show_download_button=False,
show_share_button=False,
waveform_options=gr.WaveformOptions(
waveform_color="#5c6670",
waveform_progress_color=TREBLE_TEAL,
show_recording_waveform=True,
),
elem_classes="examples-audio",
)
with gr.Column(elem_classes="examples-scene-wrap"):
gr.Image(
value=_scene,
type="filepath",
label="Treble scene",
interactive=False,
show_label=False,
show_download_button=False,
show_fullscreen_button=False,
height=300,
container=False,
)
gr.Markdown("### Near Field Speech")
with gr.Row():
_example_audio(_dry[0], "Example")
gr.Markdown("### High SNR")
with gr.Row():
_example_audio(_hi[0], "Example")
gr.Markdown("### Mid SNR")
with gr.Row():
_example_audio(_mid[0], "Example")
gr.Markdown("### Low SNR")
with gr.Row():
_example_audio(_lo[0], "Example")
with gr.TabItem(tab_label("about", "About"), elem_id="about-tab", id=5):
gr.Markdown(ABOUT_TEXT, elem_classes="markdown-text")
_footer_html = (
"<div class='ffasr-footnote'>"
"Leaderboard data loads from storage after the page opens · "
"Evaluation runs on Hub Jobs against a held-out set · "
"<a href='https://huggingface.co/' target='_blank' rel='noopener' "
"style='color: inherit; text-decoration: underline; text-decoration-style: dotted;'>"
"Models loaded from the Hugging Face Hub</a>"
"</div>"
)
gr.HTML(_footer_html)
with gr.Row():
with gr.Accordion("Citation", open=False):
gr.Textbox(
value=CITATION_TEXT, lines=7,
label="Copy the BibTeX snippet to cite this source",
elem_id="citation-button",
show_label=True,
)
# Periodic queue refresh (Timer added in Gradio 4.44). Avoid demo.load(..., every=); unsupported on some builds.
if hasattr(gr, "Timer"):
with gr.Row(visible=False):
_next_up_timer = gr.Timer(15)
_progress_timer = gr.Timer(2)
_next_up_timer.tick(fn=job_queue.next_up_html, outputs=[next_up_panel])
_progress_timer.tick(fn=_mod_progress_gated, inputs=[mod_unlocked], outputs=[mod_progress])
demo.load(fn=_on_startup, outputs=[leaderboard_table, next_up_panel, version_dropdown])
def _launch_gradio_demo() -> None:
"""
Avoid OSError: Cannot find empty port in range: 7860-7860.
Hugging Face / tooling sometimes sets ``GRADIO_NUM_PORTS=1``, so only 7860 is tried.
After a Space restart (e.g. post pip install), that port can still be in use briefly.
We drop an overly small ``GRADIO_NUM_PORTS`` so Gradio's default port sweep applies, then
retry with short backoff.
"""
raw_np = os.environ.get("GRADIO_NUM_PORTS", "").strip()
if raw_np.isdigit() and int(raw_np) < 10:
os.environ.pop("GRADIO_NUM_PORTS", None)
server_name = os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0")
last_err: OSError | None = None
for attempt in range(24):
try:
demo.launch(server_name=server_name, server_port=None)
return
except OSError as e:
last_err = e
msg = str(e).lower()
if "cannot find empty port" not in msg and "address already in use" not in msg:
raise
time.sleep(0.45 + attempt * 0.12)
if last_err is not None:
raise last_err
_launch_gradio_demo()