whojavumusic commited on
Commit
395e3c6
·
1 Parent(s): aba3a90

selected dataset eval

Browse files
app.py CHANGED
@@ -521,7 +521,16 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
521
  "When <code>FFASR_REMOTE_JOBS=1</code>, use **Open Hub Job logs** while a job runs. "
522
  "The bucket <code>results/remote_artifacts/</code> folder only gets a JSON file **after** a successful run.\n\n"
523
  "**Retry** re-queues failed, done, or queued jobs (not while running). "
524
- "A successful re-run **replaces** the leaderboard row for that model if it already exists."
 
 
 
 
 
 
 
 
 
525
  )
526
  gr.Markdown("### Current job progress")
527
  mod_progress = gr.HTML(
@@ -667,30 +676,35 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
667
  *data,
668
  )
669
 
670
- def _mod_approve(secret, jid, unlocked, run_custom):
671
  if not unlocked:
672
  return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>")
673
  if not jid or not str(jid).strip():
674
  return _mod_bundle("<p style='color:red'>Select a job.</p>")
675
-
676
- def _approve(j, s):
677
- return job_queue.approve_job(j, s, run_custom_script=bool(run_custom))
678
-
679
- ok, msg = _approve(str(jid).strip(), secret)
 
680
  color = "green" if ok else "red"
681
  return _mod_bundle(f"<p style='color:{color}'>{msg}</p>")
682
 
683
  def _mod_reject(secret, jid, unlocked):
684
  return _mod_run(job_queue.reject_job, secret, jid, "Select a job.", unlocked)
685
 
686
- def _mod_retry(secret, jid, unlocked):
687
- return _mod_run(
688
- job_queue.retry_failed_job,
 
 
 
 
689
  secret,
690
- jid,
691
- "Select a job in Retry / remove.",
692
- unlocked,
693
  )
 
 
694
 
695
  def _mod_remove(secret, jid, unlocked):
696
  return _mod_run(
@@ -745,7 +759,13 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
745
  )
746
  approve_btn.click(
747
  fn=_mod_approve,
748
- inputs=[mod_secret, job_pick, mod_unlocked, mod_run_custom_script],
 
 
 
 
 
 
749
  outputs=_MOD_OUTS,
750
  )
751
  reject_btn.click(
@@ -755,7 +775,7 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
755
  )
756
  retry_job_btn.click(
757
  fn=_mod_retry,
758
- inputs=[mod_secret, mod_action_job, mod_unlocked],
759
  outputs=_MOD_OUTS,
760
  )
761
  remove_job_btn.click(
 
521
  "When <code>FFASR_REMOTE_JOBS=1</code>, use **Open Hub Job logs** while a job runs. "
522
  "The bucket <code>results/remote_artifacts/</code> folder only gets a JSON file **after** a successful run.\n\n"
523
  "**Retry** re-queues failed, done, or queued jobs (not while running). "
524
+ "Use **Datasets to evaluate** to run only selected packed splits; "
525
+ "other leaderboard WER columns are left unchanged on success. "
526
+ "A full re-run (all datasets selected) **replaces** the row when the model is already listed."
527
+ )
528
+ from benchmark.dataset import CONDITION_UI_CHOICES
529
+
530
+ mod_eval_conditions = gr.CheckboxGroup(
531
+ choices=[(lbl, key) for lbl, key in CONDITION_UI_CHOICES],
532
+ value=[key for _, key in CONDITION_UI_CHOICES],
533
+ label="Datasets to evaluate (uncheck to skip; all selected = full benchmark)",
534
  )
535
  gr.Markdown("### Current job progress")
536
  mod_progress = gr.HTML(
 
676
  *data,
677
  )
678
 
679
+ def _mod_approve(secret, jid, unlocked, run_custom, eval_conds):
680
  if not unlocked:
681
  return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>")
682
  if not jid or not str(jid).strip():
683
  return _mod_bundle("<p style='color:red'>Select a job.</p>")
684
+ ok, msg = job_queue.approve_job(
685
+ str(jid).strip(),
686
+ secret,
687
+ run_custom_script=bool(run_custom),
688
+ eval_conditions=list(eval_conds or []),
689
+ )
690
  color = "green" if ok else "red"
691
  return _mod_bundle(f"<p style='color:{color}'>{msg}</p>")
692
 
693
  def _mod_reject(secret, jid, unlocked):
694
  return _mod_run(job_queue.reject_job, secret, jid, "Select a job.", unlocked)
695
 
696
+ def _mod_retry(secret, jid, unlocked, eval_conds):
697
+ if not unlocked:
698
+ return _mod_bundle("<p style='color:red'>Unlock moderator tools first.</p>")
699
+ if not jid or not str(jid).strip():
700
+ return _mod_bundle("<p style='color:red'>Select a job in Retry / remove.</p>")
701
+ ok, msg = job_queue.retry_failed_job(
702
+ str(jid).strip(),
703
  secret,
704
+ eval_conditions=list(eval_conds or []),
 
 
705
  )
706
+ color = "green" if ok else "red"
707
+ return _mod_bundle(f"<p style='color:{color}'>{msg}</p>")
708
 
709
  def _mod_remove(secret, jid, unlocked):
710
  return _mod_run(
 
759
  )
760
  approve_btn.click(
761
  fn=_mod_approve,
762
+ inputs=[
763
+ mod_secret,
764
+ job_pick,
765
+ mod_unlocked,
766
+ mod_run_custom_script,
767
+ mod_eval_conditions,
768
+ ],
769
  outputs=_MOD_OUTS,
770
  )
771
  reject_btn.click(
 
775
  )
776
  retry_job_btn.click(
777
  fn=_mod_retry,
778
+ inputs=[mod_secret, mod_action_job, mod_unlocked, mod_eval_conditions],
779
  outputs=_MOD_OUTS,
780
  )
781
  remove_job_btn.click(
benchmark/__init__.py CHANGED
@@ -1,17 +1,25 @@
1
  """Shared evaluation data loading and metrics (WER over packed tensors)."""
2
 
3
  from .dataset import (
4
- PACKED_FILES,
5
  CONDITION_WER_SUFFIX,
 
 
6
  evaluate_condition_wer,
 
7
  load_packed_condition,
 
8
  wer_result_key,
9
  )
10
 
11
  __all__ = [
12
- "PACKED_FILES",
13
  "CONDITION_WER_SUFFIX",
 
 
14
  "evaluate_condition_wer",
 
15
  "load_packed_condition",
 
16
  "wer_result_key",
17
  ]
 
1
  """Shared evaluation data loading and metrics (WER over packed tensors)."""
2
 
3
  from .dataset import (
4
+ CONDITION_UI_CHOICES,
5
  CONDITION_WER_SUFFIX,
6
+ DEFAULT_CONDITION_KEYS,
7
+ PACKED_FILES,
8
  evaluate_condition_wer,
9
+ is_full_condition_set,
10
  load_packed_condition,
11
+ resolve_condition_keys,
12
  wer_result_key,
13
  )
14
 
15
  __all__ = [
16
+ "CONDITION_UI_CHOICES",
17
  "CONDITION_WER_SUFFIX",
18
+ "DEFAULT_CONDITION_KEYS",
19
+ "PACKED_FILES",
20
  "evaluate_condition_wer",
21
+ "is_full_condition_set",
22
  "load_packed_condition",
23
+ "resolve_condition_keys",
24
  "wer_result_key",
25
  ]
benchmark/dataset.py CHANGED
@@ -37,6 +37,31 @@ def wer_result_key(condition_key: str) -> str:
37
  return f"wer_{suffix}"
38
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  _dataset_cache: dict[str, dict] = {}
41
 
42
 
 
37
  return f"wer_{suffix}"
38
 
39
 
40
+ # Gradio checkbox labels (display, condition_key).
41
+ CONDITION_UI_CHOICES: tuple[tuple[str, str], ...] = (
42
+ ("Dry Speech", "clean"),
43
+ ("Lab Measured", "measured"),
44
+ ("Lab Simulated", "sim"),
45
+ ("High SNR", "high"),
46
+ ("Mid SNR", "mid"),
47
+ ("Low SNR", "low"),
48
+ )
49
+
50
+ DEFAULT_CONDITION_KEYS: tuple[str, ...] = tuple(PACKED_FILES.keys())
51
+
52
+
53
+ def resolve_condition_keys(keys: list[str] | None) -> tuple[str, ...]:
54
+ """Return validated condition keys; ``None`` or empty means all packed splits."""
55
+ if not keys:
56
+ return DEFAULT_CONDITION_KEYS
57
+ valid = [k for k in keys if k in PACKED_FILES]
58
+ return tuple(valid) if valid else DEFAULT_CONDITION_KEYS
59
+
60
+
61
+ def is_full_condition_set(keys: list[str] | None) -> bool:
62
+ return set(resolve_condition_keys(keys)) == set(PACKED_FILES.keys())
63
+
64
+
65
  _dataset_cache: dict[str, dict] = {}
66
 
67
 
evaluation/orchestrator.py CHANGED
@@ -10,10 +10,11 @@ from typing import Any
10
  import torch
11
 
12
  from benchmark.dataset import (
13
- PACKED_FILES,
14
  accumulate_predictions_for_slice,
15
  evaluate_condition_wer_timed,
 
16
  load_packed_condition,
 
17
  wer_result_key,
18
  )
19
  from backends.registry import build_transcriber, resolve_label
@@ -110,13 +111,15 @@ def _run_evaluation_core(
110
  family_id: str = "auto",
111
  progress_cb: Callable[[int, int, str], None] | None = None,
112
  custom_script: str = "",
 
113
  ) -> dict:
114
  device_str, device_int = resolve_eval_devices()
 
115
 
116
  # Precompute totals so the progress bar has a known denominator from the start.
117
  condition_totals: dict[str, int] = {}
118
  grand_total = 0
119
- for ck in PACKED_FILES:
120
  try:
121
  condition_totals[ck] = len(load_packed_condition(ck))
122
  except Exception:
@@ -146,7 +149,7 @@ def _run_evaluation_core(
146
  total_infer_s = 0.0
147
  done_so_far = 0
148
 
149
- for condition_key in PACKED_FILES:
150
  condition_base = done_so_far
151
 
152
  def _sample_cb(ck: str, i: int, _n: int, _base: int = condition_base) -> None:
@@ -166,6 +169,8 @@ def _run_evaluation_core(
166
  total_infer_s += infer_s
167
  done_so_far += count
168
 
 
 
169
  results["num_samples"] = total_samples
170
  results["eval_audio_seconds"] = round(total_audio_s, 3)
171
  results["eval_wall_time_s"] = round(total_infer_s, 3)
@@ -194,6 +199,7 @@ def _run_evaluation_core_segmented_local(
194
  *,
195
  segment_samples: int,
196
  custom_script: str = "",
 
197
  ) -> dict:
198
  """
199
  Same semantics as ``_run_evaluation_core_segmented`` but without ``spaces.GPU``:
@@ -201,9 +207,10 @@ def _run_evaluation_core_segmented_local(
201
  """
202
  from jiwer import wer as compute_wer
203
 
 
204
  condition_totals: dict[str, int] = {}
205
  grand_total = 0
206
- for ck in PACKED_FILES:
207
  try:
208
  condition_totals[ck] = len(load_packed_condition(ck))
209
  except Exception:
@@ -227,7 +234,7 @@ def _run_evaluation_core_segmented_local(
227
  done_so_far = 0
228
  first_num_params: int | None = None
229
 
230
- for condition_key in PACKED_FILES:
231
  data = load_packed_condition(condition_key)
232
  n_items = len(data)
233
  all_p: list[str] = []
@@ -270,6 +277,8 @@ def _run_evaluation_core_segmented_local(
270
  total_samples = max(total_samples, len(all_p))
271
 
272
  results["num_params"] = first_num_params or 0
 
 
273
  results["num_samples"] = total_samples
274
  results["eval_audio_seconds"] = round(total_audio_s, 3)
275
  results["eval_wall_time_s"] = round(total_infer_s, 3)
@@ -295,6 +304,7 @@ def _run_evaluation_core_segmented(
295
  gpu_duration_s: int,
296
  gpu_size: str,
297
  custom_script: str = "",
 
298
  ) -> dict:
299
  """
300
  Long evaluations: each slice of ``segment_samples`` runs under its own ``spaces.GPU``
@@ -304,9 +314,10 @@ def _run_evaluation_core_segmented(
304
 
305
  import spaces
306
 
 
307
  condition_totals: dict[str, int] = {}
308
  grand_total = 0
309
- for ck in PACKED_FILES:
310
  try:
311
  condition_totals[ck] = len(load_packed_condition(ck))
312
  except Exception:
@@ -332,7 +343,7 @@ def _run_evaluation_core_segmented(
332
  done_so_far = 0
333
  first_num_params: int | None = None
334
 
335
- for condition_key in PACKED_FILES:
336
  data = load_packed_condition(condition_key)
337
  n_items = len(data)
338
  all_p: list[str] = []
@@ -375,6 +386,8 @@ def _run_evaluation_core_segmented(
375
  total_samples = max(total_samples, len(all_p))
376
 
377
  results["num_params"] = first_num_params or 0
 
 
378
  results["num_samples"] = total_samples
379
  results["eval_audio_seconds"] = round(total_audio_s, 3)
380
  results["eval_wall_time_s"] = round(total_infer_s, 3)
@@ -410,6 +423,7 @@ def run_evaluation(
410
  family_id: str = "auto",
411
  progress_cb: Callable[[int, int, str], None] | None = None,
412
  custom_script: str = "",
 
413
  ) -> dict:
414
  """
415
  Evaluate `model_id` on all packed conditions using the selected family backend.
@@ -455,12 +469,14 @@ def run_evaluation(
455
  progress_cb=progress_cb,
456
  segment_samples=seg,
457
  custom_script=custom_script,
 
458
  )
459
  return _run_evaluation_core(
460
  model_id,
461
  family_id=family_id,
462
  progress_cb=progress_cb,
463
  custom_script=custom_script,
 
464
  )
465
 
466
  eff = _effective_gpu_duration_s()
@@ -475,6 +491,7 @@ def run_evaluation(
475
  family_id=family_id,
476
  progress_cb=progress_cb,
477
  custom_script=custom_script,
 
478
  )
479
 
480
  return _run_evaluation_core_segmented(
@@ -485,4 +502,5 @@ def run_evaluation(
485
  gpu_duration_s=eff,
486
  gpu_size=size,
487
  custom_script=custom_script,
 
488
  )
 
10
  import torch
11
 
12
  from benchmark.dataset import (
 
13
  accumulate_predictions_for_slice,
14
  evaluate_condition_wer_timed,
15
+ is_full_condition_set,
16
  load_packed_condition,
17
+ resolve_condition_keys,
18
  wer_result_key,
19
  )
20
  from backends.registry import build_transcriber, resolve_label
 
111
  family_id: str = "auto",
112
  progress_cb: Callable[[int, int, str], None] | None = None,
113
  custom_script: str = "",
114
+ condition_keys: list[str] | None = None,
115
  ) -> dict:
116
  device_str, device_int = resolve_eval_devices()
117
+ keys = resolve_condition_keys(condition_keys)
118
 
119
  # Precompute totals so the progress bar has a known denominator from the start.
120
  condition_totals: dict[str, int] = {}
121
  grand_total = 0
122
+ for ck in keys:
123
  try:
124
  condition_totals[ck] = len(load_packed_condition(ck))
125
  except Exception:
 
149
  total_infer_s = 0.0
150
  done_so_far = 0
151
 
152
+ for condition_key in keys:
153
  condition_base = done_so_far
154
 
155
  def _sample_cb(ck: str, i: int, _n: int, _base: int = condition_base) -> None:
 
169
  total_infer_s += infer_s
170
  done_so_far += count
171
 
172
+ results["eval_conditions"] = list(keys)
173
+ results["eval_full_run"] = is_full_condition_set(condition_keys)
174
  results["num_samples"] = total_samples
175
  results["eval_audio_seconds"] = round(total_audio_s, 3)
176
  results["eval_wall_time_s"] = round(total_infer_s, 3)
 
199
  *,
200
  segment_samples: int,
201
  custom_script: str = "",
202
+ condition_keys: list[str] | None = None,
203
  ) -> dict:
204
  """
205
  Same semantics as ``_run_evaluation_core_segmented`` but without ``spaces.GPU``:
 
207
  """
208
  from jiwer import wer as compute_wer
209
 
210
+ keys = resolve_condition_keys(condition_keys)
211
  condition_totals: dict[str, int] = {}
212
  grand_total = 0
213
+ for ck in keys:
214
  try:
215
  condition_totals[ck] = len(load_packed_condition(ck))
216
  except Exception:
 
234
  done_so_far = 0
235
  first_num_params: int | None = None
236
 
237
+ for condition_key in keys:
238
  data = load_packed_condition(condition_key)
239
  n_items = len(data)
240
  all_p: list[str] = []
 
277
  total_samples = max(total_samples, len(all_p))
278
 
279
  results["num_params"] = first_num_params or 0
280
+ results["eval_conditions"] = list(keys)
281
+ results["eval_full_run"] = is_full_condition_set(condition_keys)
282
  results["num_samples"] = total_samples
283
  results["eval_audio_seconds"] = round(total_audio_s, 3)
284
  results["eval_wall_time_s"] = round(total_infer_s, 3)
 
304
  gpu_duration_s: int,
305
  gpu_size: str,
306
  custom_script: str = "",
307
+ condition_keys: list[str] | None = None,
308
  ) -> dict:
309
  """
310
  Long evaluations: each slice of ``segment_samples`` runs under its own ``spaces.GPU``
 
314
 
315
  import spaces
316
 
317
+ keys = resolve_condition_keys(condition_keys)
318
  condition_totals: dict[str, int] = {}
319
  grand_total = 0
320
+ for ck in keys:
321
  try:
322
  condition_totals[ck] = len(load_packed_condition(ck))
323
  except Exception:
 
343
  done_so_far = 0
344
  first_num_params: int | None = None
345
 
346
+ for condition_key in keys:
347
  data = load_packed_condition(condition_key)
348
  n_items = len(data)
349
  all_p: list[str] = []
 
386
  total_samples = max(total_samples, len(all_p))
387
 
388
  results["num_params"] = first_num_params or 0
389
+ results["eval_conditions"] = list(keys)
390
+ results["eval_full_run"] = is_full_condition_set(condition_keys)
391
  results["num_samples"] = total_samples
392
  results["eval_audio_seconds"] = round(total_audio_s, 3)
393
  results["eval_wall_time_s"] = round(total_infer_s, 3)
 
423
  family_id: str = "auto",
424
  progress_cb: Callable[[int, int, str], None] | None = None,
425
  custom_script: str = "",
426
+ condition_keys: list[str] | None = None,
427
  ) -> dict:
428
  """
429
  Evaluate `model_id` on all packed conditions using the selected family backend.
 
469
  progress_cb=progress_cb,
470
  segment_samples=seg,
471
  custom_script=custom_script,
472
+ condition_keys=condition_keys,
473
  )
474
  return _run_evaluation_core(
475
  model_id,
476
  family_id=family_id,
477
  progress_cb=progress_cb,
478
  custom_script=custom_script,
479
+ condition_keys=condition_keys,
480
  )
481
 
482
  eff = _effective_gpu_duration_s()
 
491
  family_id=family_id,
492
  progress_cb=progress_cb,
493
  custom_script=custom_script,
494
+ condition_keys=condition_keys,
495
  )
496
 
497
  return _run_evaluation_core_segmented(
 
502
  gpu_duration_s=eff,
503
  gpu_size=size,
504
  custom_script=custom_script,
505
+ condition_keys=condition_keys,
506
  )
init.py CHANGED
@@ -139,6 +139,63 @@ def leaderboard_row_from_eval_result(
139
  return row
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  # ---------------------------------------------------------------------------
143
  # Storage helpers
144
  # ---------------------------------------------------------------------------
 
139
  return row
140
 
141
 
142
+ def merge_leaderboard_row_from_eval_result(
143
+ result: dict,
144
+ submitted_at_iso: str,
145
+ submission_notes: str = "",
146
+ ) -> None:
147
+ """
148
+ Merge evaluation output into an existing leaderboard row (partial re-eval).
149
+
150
+ Only scenario WER columns and ``eval_family`` / ``num_params`` present in
151
+ ``result`` are overwritten; other metrics and timing fields are kept.
152
+ Creates a new row if the model is not on the leaderboard yet.
153
+ """
154
+ from metrics_config import SCENARIO_KEYS
155
+
156
+ model_id = str(result.get("model_id", "")).strip()
157
+ if not model_id:
158
+ raise ValueError("merge_leaderboard_row_from_eval_result requires model_id")
159
+
160
+ rows = load_raw_results()
161
+ match_idx: int | None = None
162
+ for i, r in enumerate(rows):
163
+ if (r.get("model_id") or "").strip() == model_id:
164
+ match_idx = i
165
+ break
166
+
167
+ patch = leaderboard_row_from_eval_result(
168
+ result, submitted_at_iso, submission_notes=submission_notes
169
+ )
170
+
171
+ if match_idx is None:
172
+ normalize_legacy_csv_row(patch)
173
+ rows.append(patch)
174
+ else:
175
+ row = dict(rows[match_idx])
176
+ for k in SCENARIO_KEYS:
177
+ v = patch.get(k)
178
+ if v is not None and str(v).strip() != "":
179
+ row[k] = v
180
+ if patch.get("eval_family"):
181
+ row["eval_family"] = patch["eval_family"]
182
+ if patch.get("num_params"):
183
+ row["num_params"] = patch["num_params"]
184
+ if (submission_notes or "").strip():
185
+ row["submission_notes"] = patch["submission_notes"]
186
+ normalize_legacy_csv_row(row)
187
+ rows[match_idx] = row
188
+
189
+ from analytics import compute_far_field_score_map
190
+
191
+ scores = compute_far_field_score_map(rows)
192
+ rows.sort(
193
+ key=lambda r: (-scores.get((r.get("model_id") or "").strip(), 0.0), r.get("model_id") or "")
194
+ )
195
+ save_raw_results(rows)
196
+ invalidate_results_cache()
197
+
198
+
199
  # ---------------------------------------------------------------------------
200
  # Storage helpers
201
  # ---------------------------------------------------------------------------
job_queue.py CHANGED
@@ -74,6 +74,8 @@ class Job:
74
  remote_artifact_path: str | None = None
75
  # Set by moderator retry: skip duplicate guard and replace CSV row on success.
76
  replace_leaderboard: bool = False
 
 
77
 
78
 
79
  _jobs: dict[str, Job] = {}
@@ -110,6 +112,7 @@ _JOBS_CSV_FIELDS = [
110
  "run_custom_script",
111
  "hf_remote_job_id",
112
  "remote_artifact_path",
 
113
  ]
114
 
115
 
@@ -258,6 +261,9 @@ def _persist_jobs() -> None:
258
  "run_custom_script": _bool_to_csv(j.run_custom_script),
259
  "hf_remote_job_id": (j.hf_remote_job_id or "").strip(),
260
  "remote_artifact_path": (j.remote_artifact_path or "").strip(),
 
 
 
261
  }
262
  )
263
  rows.sort(key=lambda r: r["created_at"])
@@ -329,6 +335,7 @@ def _load_persisted_jobs_once() -> None:
329
  fid = (row.get("family_id") or "").strip()
330
  hf_rid = (row.get("hf_remote_job_id") or "").strip() or None
331
  art_path = (row.get("remote_artifact_path") or "").strip() or None
 
332
  if not mid:
333
  continue
334
 
@@ -357,6 +364,7 @@ def _load_persisted_jobs_once() -> None:
357
  run_custom_script=run_custom,
358
  hf_remote_job_id=hf_rid,
359
  remote_artifact_path=art_path,
 
360
  )
361
  _jobs[jid] = job
362
 
@@ -488,15 +496,23 @@ def _merge_eval_result_to_leaderboard(
488
  submission_notes: str,
489
  *,
490
  replace_existing: bool = False,
 
491
  ) -> None:
492
  from init import (
493
  invalidate_results_cache,
494
  leaderboard_row_from_eval_result,
495
  load_raw_results,
 
496
  normalize_legacy_csv_row,
497
  save_raw_results,
498
  )
499
 
 
 
 
 
 
 
500
  model_id = str(result.get("model_id", "")).strip()
501
  rows = load_raw_results()
502
  if replace_existing and model_id:
@@ -514,6 +530,55 @@ def _merge_eval_result_to_leaderboard(
514
  invalidate_results_cache()
515
 
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  def _load_artifact_json_from_bucket(artifact_path: str) -> dict:
518
  """Download and parse a remote eval JSON artifact from the Hub bucket."""
519
  from storage import STORAGE_BACKEND, download_bucket_file
@@ -727,6 +792,7 @@ def _remote_dispatch_job(job_id: str, mid: str, fid: str, jobs_token: str) -> st
727
  extra_req = (j_dispatch.extra_requirements or "") if j_dispatch else ""
728
  custom_script = (j_dispatch.custom_script or "") if j_dispatch else ""
729
  run_custom = bool(j_dispatch and j_dispatch.run_custom_script)
 
730
 
731
  info = remote_jobs.submit_eval_job(
732
  model_id=mid,
@@ -737,6 +803,7 @@ def _remote_dispatch_job(job_id: str, mid: str, fid: str, jobs_token: str) -> st
737
  extra_requirements=extra_req,
738
  custom_script=custom_script,
739
  run_custom_script=run_custom,
 
740
  )
741
  hf_id = info.id
742
  with _jobs_lock:
@@ -776,7 +843,8 @@ def _remote_start_queued_job(job_id: str, jobs_token: str) -> None:
776
  with _jobs_lock:
777
  j_chk = _jobs.get(job_id)
778
  replace_lb = bool(j_chk and j_chk.replace_leaderboard)
779
- if not replace_lb and any(r["model_id"] == mid for r in rows):
 
780
  _fail_job(job_id, "Model already on leaderboard (skipped duplicate race).")
781
  return
782
 
@@ -836,8 +904,13 @@ def _remote_tick_job(job_id: str, jobs_token: str) -> None:
836
  with _jobs_lock:
837
  j_done = _jobs.get(job_id)
838
  replace_lb = bool(j_done and j_done.replace_leaderboard)
 
839
  _merge_eval_result_to_leaderboard(
840
- result, _now_iso(), notes, replace_existing=replace_lb
 
 
 
 
841
  )
842
  with _jobs_lock:
843
  j_clr = _jobs.get(job_id)
@@ -1026,7 +1099,11 @@ def enqueue(
1026
 
1027
 
1028
  def approve_job(
1029
- job_id: str, secret: str, *, run_custom_script: bool = False
 
 
 
 
1030
  ) -> tuple[bool, str]:
1031
  """Move a pending job into the execution queue (moderator only)."""
1032
  if not moderation_active():
@@ -1037,6 +1114,11 @@ def approve_job(
1037
  if not job_id:
1038
  return False, "Enter a job ID."
1039
 
 
 
 
 
 
1040
  with _jobs_lock:
1041
  job = _jobs.get(job_id)
1042
  if not job or job.status != JobStatus.pending_moderation:
@@ -1049,6 +1131,8 @@ def approve_job(
1049
  job.run_custom_script = bool(run_custom_script)
1050
  else:
1051
  job.run_custom_script = False
 
 
1052
  _touch(job)
1053
 
1054
  _work_queue.put(job_id)
@@ -1056,13 +1140,16 @@ def approve_job(
1056
  _ensure_worker()
1057
  max_n = remote_max_concurrent_jobs()
1058
  custom_note = ""
 
1059
  with _jobs_lock:
1060
  j = _jobs.get(job_id)
1061
  if j and j.run_custom_script and (j.custom_script or "").strip():
1062
  custom_note = " Custom script will run on the Hub Job."
 
 
1063
  return True, (
1064
  f"Approved job {job_id}. Up to {max_n} Hub Jobs may run in parallel; "
1065
- f"this job starts when a slot is free.{custom_note}"
1066
  )
1067
 
1068
 
@@ -1121,7 +1208,12 @@ def _job_can_retry(status: JobStatus) -> bool:
1121
  return status in (JobStatus.failed, JobStatus.done, JobStatus.queued)
1122
 
1123
 
1124
- def retry_failed_job(job_id: str, secret: str) -> tuple[bool, str]:
 
 
 
 
 
1125
  """Re-queue a job for another evaluation run (moderator only).
1126
 
1127
  Allowed for failed, done, and queued jobs. Successful re-runs replace the
@@ -1134,6 +1226,11 @@ def retry_failed_job(job_id: str, secret: str) -> tuple[bool, str]:
1134
  if not job_id:
1135
  return False, "Select a job."
1136
 
 
 
 
 
 
1137
  with _jobs_lock:
1138
  job = _jobs.get(job_id)
1139
  if not job:
@@ -1146,7 +1243,6 @@ def retry_failed_job(job_id: str, secret: str) -> tuple[bool, str]:
1146
  )
1147
  if _work_queue.qsize() >= _MAX_QUEUE_BACKLOG:
1148
  return False, "Execution queue is full; try again later."
1149
-
1150
  was_queued = job.status == JobStatus.queued
1151
  job.status = JobStatus.queued
1152
  job.error = None
@@ -1156,17 +1252,27 @@ def retry_failed_job(job_id: str, secret: str) -> tuple[bool, str]:
1156
  job.progress_done = 0
1157
  job.progress_total = 0
1158
  job.progress_condition = ""
1159
- job.replace_leaderboard = True
 
1160
  _touch(job)
1161
 
1162
  if not was_queued:
1163
  _work_queue.put(job_id)
1164
  _persist_jobs()
1165
  _ensure_worker()
 
 
 
 
 
 
 
 
 
 
1166
  return (
1167
  True,
1168
- f"Re-queued job {job_id}; it will run after jobs ahead of it. "
1169
- "If the model is already on the leaderboard, the row will be replaced on success.",
1170
  )
1171
 
1172
 
 
74
  remote_artifact_path: str | None = None
75
  # Set by moderator retry: skip duplicate guard and replace CSV row on success.
76
  replace_leaderboard: bool = False
77
+ # Subset of PACKED_FILES keys; None means evaluate all conditions.
78
+ eval_conditions: tuple[str, ...] | None = None
79
 
80
 
81
  _jobs: dict[str, Job] = {}
 
112
  "run_custom_script",
113
  "hf_remote_job_id",
114
  "remote_artifact_path",
115
+ "eval_conditions",
116
  ]
117
 
118
 
 
261
  "run_custom_script": _bool_to_csv(j.run_custom_script),
262
  "hf_remote_job_id": (j.hf_remote_job_id or "").strip(),
263
  "remote_artifact_path": (j.remote_artifact_path or "").strip(),
264
+ "eval_conditions": ",".join(j.eval_conditions)
265
+ if j.eval_conditions
266
+ else "",
267
  }
268
  )
269
  rows.sort(key=lambda r: r["created_at"])
 
335
  fid = (row.get("family_id") or "").strip()
336
  hf_rid = (row.get("hf_remote_job_id") or "").strip() or None
337
  art_path = (row.get("remote_artifact_path") or "").strip() or None
338
+ eval_conds = _parse_eval_conditions_csv(row.get("eval_conditions") or "")
339
  if not mid:
340
  continue
341
 
 
364
  run_custom_script=run_custom,
365
  hf_remote_job_id=hf_rid,
366
  remote_artifact_path=art_path,
367
+ eval_conditions=eval_conds,
368
  )
369
  _jobs[jid] = job
370
 
 
496
  submission_notes: str,
497
  *,
498
  replace_existing: bool = False,
499
+ merge_partial: bool = False,
500
  ) -> None:
501
  from init import (
502
  invalidate_results_cache,
503
  leaderboard_row_from_eval_result,
504
  load_raw_results,
505
+ merge_leaderboard_row_from_eval_result,
506
  normalize_legacy_csv_row,
507
  save_raw_results,
508
  )
509
 
510
+ if merge_partial:
511
+ merge_leaderboard_row_from_eval_result(
512
+ result, submitted_at_iso, submission_notes=submission_notes
513
+ )
514
+ return
515
+
516
  model_id = str(result.get("model_id", "")).strip()
517
  rows = load_raw_results()
518
  if replace_existing and model_id:
 
530
  invalidate_results_cache()
531
 
532
 
533
+ def _parse_eval_conditions_csv(raw: str) -> tuple[str, ...] | None:
534
+ from benchmark.dataset import resolve_condition_keys
535
+
536
+ parts = [p.strip() for p in (raw or "").split(",") if p.strip()]
537
+ if not parts:
538
+ return None
539
+ keys = resolve_condition_keys(parts)
540
+ from benchmark.dataset import PACKED_FILES
541
+
542
+ if set(keys) == set(PACKED_FILES.keys()):
543
+ return None
544
+ return keys
545
+
546
+
547
+ def _encode_eval_conditions_csv(keys: tuple[str, ...] | None) -> str:
548
+ if not keys:
549
+ return ""
550
+ return ",".join(keys)
551
+
552
+
553
+ def normalize_moderator_eval_conditions(
554
+ selected: list[str] | None,
555
+ ) -> tuple[str, ...] | None:
556
+ """``None`` = all packed conditions; otherwise a validated non-empty subset."""
557
+ from benchmark.dataset import PACKED_FILES, resolve_condition_keys
558
+
559
+ if not selected:
560
+ return None
561
+ keys = resolve_condition_keys(selected)
562
+ if not keys:
563
+ raise ValueError("Select at least one dataset to evaluate.")
564
+ if set(keys) == set(PACKED_FILES.keys()):
565
+ return None
566
+ return keys
567
+
568
+
569
+ def eval_condition_checkbox_defaults() -> tuple[list[str], list[str]]:
570
+ """(labels, values) for Gradio CheckboxGroup defaults (all selected)."""
571
+ from benchmark.dataset import CONDITION_UI_CHOICES
572
+
573
+ labels = [lbl for lbl, _ in CONDITION_UI_CHOICES]
574
+ values = [key for _, key in CONDITION_UI_CHOICES]
575
+ return labels, values
576
+
577
+
578
+ def _job_is_partial_eval(job: Job | None) -> bool:
579
+ return bool(job and job.eval_conditions)
580
+
581
+
582
  def _load_artifact_json_from_bucket(artifact_path: str) -> dict:
583
  """Download and parse a remote eval JSON artifact from the Hub bucket."""
584
  from storage import STORAGE_BACKEND, download_bucket_file
 
792
  extra_req = (j_dispatch.extra_requirements or "") if j_dispatch else ""
793
  custom_script = (j_dispatch.custom_script or "") if j_dispatch else ""
794
  run_custom = bool(j_dispatch and j_dispatch.run_custom_script)
795
+ eval_conds = j_dispatch.eval_conditions if j_dispatch else None
796
 
797
  info = remote_jobs.submit_eval_job(
798
  model_id=mid,
 
803
  extra_requirements=extra_req,
804
  custom_script=custom_script,
805
  run_custom_script=run_custom,
806
+ eval_conditions=eval_conds,
807
  )
808
  hf_id = info.id
809
  with _jobs_lock:
 
843
  with _jobs_lock:
844
  j_chk = _jobs.get(job_id)
845
  replace_lb = bool(j_chk and j_chk.replace_leaderboard)
846
+ partial = _job_is_partial_eval(j_chk)
847
+ if not replace_lb and not partial and any(r["model_id"] == mid for r in rows):
848
  _fail_job(job_id, "Model already on leaderboard (skipped duplicate race).")
849
  return
850
 
 
904
  with _jobs_lock:
905
  j_done = _jobs.get(job_id)
906
  replace_lb = bool(j_done and j_done.replace_leaderboard)
907
+ partial = _job_is_partial_eval(j_done)
908
  _merge_eval_result_to_leaderboard(
909
+ result,
910
+ _now_iso(),
911
+ notes,
912
+ replace_existing=replace_lb and not partial,
913
+ merge_partial=partial,
914
  )
915
  with _jobs_lock:
916
  j_clr = _jobs.get(job_id)
 
1099
 
1100
 
1101
  def approve_job(
1102
+ job_id: str,
1103
+ secret: str,
1104
+ *,
1105
+ run_custom_script: bool = False,
1106
+ eval_conditions: list[str] | None = None,
1107
  ) -> tuple[bool, str]:
1108
  """Move a pending job into the execution queue (moderator only)."""
1109
  if not moderation_active():
 
1114
  if not job_id:
1115
  return False, "Enter a job ID."
1116
 
1117
+ try:
1118
+ conds = normalize_moderator_eval_conditions(eval_conditions)
1119
+ except ValueError as e:
1120
+ return False, str(e)
1121
+
1122
  with _jobs_lock:
1123
  job = _jobs.get(job_id)
1124
  if not job or job.status != JobStatus.pending_moderation:
 
1131
  job.run_custom_script = bool(run_custom_script)
1132
  else:
1133
  job.run_custom_script = False
1134
+ job.eval_conditions = conds
1135
+ job.replace_leaderboard = False
1136
  _touch(job)
1137
 
1138
  _work_queue.put(job_id)
 
1140
  _ensure_worker()
1141
  max_n = remote_max_concurrent_jobs()
1142
  custom_note = ""
1143
+ cond_note = ""
1144
  with _jobs_lock:
1145
  j = _jobs.get(job_id)
1146
  if j and j.run_custom_script and (j.custom_script or "").strip():
1147
  custom_note = " Custom script will run on the Hub Job."
1148
+ if j and j.eval_conditions:
1149
+ cond_note = f" Evaluating: {', '.join(j.eval_conditions)} (others unchanged on success)."
1150
  return True, (
1151
  f"Approved job {job_id}. Up to {max_n} Hub Jobs may run in parallel; "
1152
+ f"this job starts when a slot is free.{custom_note}{cond_note}"
1153
  )
1154
 
1155
 
 
1208
  return status in (JobStatus.failed, JobStatus.done, JobStatus.queued)
1209
 
1210
 
1211
+ def retry_failed_job(
1212
+ job_id: str,
1213
+ secret: str,
1214
+ *,
1215
+ eval_conditions: list[str] | None = None,
1216
+ ) -> tuple[bool, str]:
1217
  """Re-queue a job for another evaluation run (moderator only).
1218
 
1219
  Allowed for failed, done, and queued jobs. Successful re-runs replace the
 
1226
  if not job_id:
1227
  return False, "Select a job."
1228
 
1229
+ try:
1230
+ conds = normalize_moderator_eval_conditions(eval_conditions)
1231
+ except ValueError as e:
1232
+ return False, str(e)
1233
+
1234
  with _jobs_lock:
1235
  job = _jobs.get(job_id)
1236
  if not job:
 
1243
  )
1244
  if _work_queue.qsize() >= _MAX_QUEUE_BACKLOG:
1245
  return False, "Execution queue is full; try again later."
 
1246
  was_queued = job.status == JobStatus.queued
1247
  job.status = JobStatus.queued
1248
  job.error = None
 
1252
  job.progress_done = 0
1253
  job.progress_total = 0
1254
  job.progress_condition = ""
1255
+ job.eval_conditions = conds
1256
+ job.replace_leaderboard = conds is None
1257
  _touch(job)
1258
 
1259
  if not was_queued:
1260
  _work_queue.put(job_id)
1261
  _persist_jobs()
1262
  _ensure_worker()
1263
+ cond_note = ""
1264
+ with _jobs_lock:
1265
+ j = _jobs.get(job_id)
1266
+ if j and j.eval_conditions:
1267
+ cond_note = (
1268
+ f" Only {', '.join(j.eval_conditions)} will run; "
1269
+ "existing leaderboard WER columns for other datasets are kept."
1270
+ )
1271
+ elif j and j.replace_leaderboard:
1272
+ cond_note = " Full re-eval; the leaderboard row will be replaced on success."
1273
  return (
1274
  True,
1275
+ f"Re-queued job {job_id}; it will run after jobs ahead of it.{cond_note}"
 
1276
  )
1277
 
1278
 
remote_jobs.py CHANGED
@@ -153,6 +153,7 @@ def submit_eval_job(
153
  extra_requirements: str = "",
154
  custom_script: str = "",
155
  run_custom_script: bool = False,
 
156
  ) -> JobInfo:
157
  """Start a Hub UV Job; returns initial ``JobInfo`` (may already be RUNNING)."""
158
  namespace = os.environ.get("FFASR_REMOTE_JOB_NAMESPACE", "").strip() or None
@@ -225,6 +226,10 @@ def submit_eval_job(
225
  env["FFASR_CUSTOM_SCRIPT_B64"] = base64.b64encode(
226
  custom_script.strip().encode("utf-8")
227
  ).decode("ascii")
 
 
 
 
228
  secrets = {"HF_TOKEN": bucket_tok}
229
 
230
  api = HfApi(token=jobs_tok)
 
153
  extra_requirements: str = "",
154
  custom_script: str = "",
155
  run_custom_script: bool = False,
156
+ eval_conditions: tuple[str, ...] | None = None,
157
  ) -> JobInfo:
158
  """Start a Hub UV Job; returns initial ``JobInfo`` (may already be RUNNING)."""
159
  namespace = os.environ.get("FFASR_REMOTE_JOB_NAMESPACE", "").strip() or None
 
226
  env["FFASR_CUSTOM_SCRIPT_B64"] = base64.b64encode(
227
  custom_script.strip().encode("utf-8")
228
  ).decode("ascii")
229
+ if eval_conditions:
230
+ import json
231
+
232
+ env["FFASR_EVAL_CONDITIONS"] = json.dumps(list(eval_conditions))
233
  secrets = {"HF_TOKEN": bucket_tok}
234
 
235
  api = HfApi(token=jobs_tok)
scripts/run_hf_remote_job_uv.py CHANGED
@@ -119,11 +119,16 @@ def main() -> int:
119
  custom_script = (
120
  base64.b64decode(custom_b64).decode("utf-8") if custom_b64 else ""
121
  )
 
 
 
 
122
  result = run_evaluation(
123
  model_id,
124
  family_id=family_id,
125
  progress_cb=None,
126
  custom_script=custom_script,
 
127
  )
128
  artifact = build_artifact(
129
  model_id=model_id,
 
119
  custom_script = (
120
  base64.b64decode(custom_b64).decode("utf-8") if custom_b64 else ""
121
  )
122
+ cond_raw = os.environ.get("FFASR_EVAL_CONDITIONS", "").strip()
123
+ condition_keys = None
124
+ if cond_raw:
125
+ condition_keys = json.loads(cond_raw)
126
  result = run_evaluation(
127
  model_id,
128
  family_id=family_id,
129
  progress_cb=None,
130
  custom_script=custom_script,
131
+ condition_keys=condition_keys,
132
  )
133
  artifact = build_artifact(
134
  model_id=model_id,