Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit ·
a65ea12
1
Parent(s): 57e0d5a
custom eval update
Browse files- app.py +38 -14
- backends/custom_eval.py +96 -0
- backends/universal.py +10 -1
- evaluation/orchestrator.py +45 -4
- job_queue.py +18 -1
- remote_jobs.py +7 -21
- scripts/run_hf_remote_job_uv.py +11 -1
app.py
CHANGED
|
@@ -178,6 +178,15 @@ def submit_model(
|
|
| 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,
|
|
@@ -226,18 +235,24 @@ def submit_model(
|
|
| 226 |
return styled_warning(err)
|
| 227 |
|
| 228 |
if awaiting_mod:
|
| 229 |
-
return
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
| 234 |
)
|
| 235 |
|
| 236 |
-
return
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
| 241 |
)
|
| 242 |
|
| 243 |
|
|
@@ -424,13 +439,22 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 424 |
max_length=8000,
|
| 425 |
)
|
| 426 |
script_input = gr.Code(
|
| 427 |
-
label="Optional custom
|
| 428 |
language="python",
|
| 429 |
lines=12,
|
| 430 |
)
|
| 431 |
gr.Markdown(
|
| 432 |
-
"
|
| 433 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
)
|
| 435 |
with gr.Row():
|
| 436 |
is_gated_input = gr.Checkbox(label="This is a gated repo", value=False)
|
|
@@ -508,7 +532,7 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
|
|
| 508 |
value=None,
|
| 509 |
)
|
| 510 |
mod_run_custom_script = gr.Checkbox(
|
| 511 |
-
label="
|
| 512 |
value=False,
|
| 513 |
)
|
| 514 |
with gr.Row():
|
|
|
|
| 178 |
if not on_hub:
|
| 179 |
return styled_error(f"Model '{model_id}' {err_msg}")
|
| 180 |
|
| 181 |
+
script_hint = ""
|
| 182 |
+
if (custom_script or "").strip():
|
| 183 |
+
if not job_queue.custom_script_defines_evaluate(custom_script):
|
| 184 |
+
script_hint = (
|
| 185 |
+
"<p style='color:orange'><strong>Note:</strong> Your custom script should define "
|
| 186 |
+
"<code>evaluate(file: pathlib.Path) -> str</code> at the top level. "
|
| 187 |
+
"It will be called once per sample during evaluation.</p>"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
try:
|
| 191 |
job_id, position, err, awaiting_mod = job_queue.enqueue(
|
| 192 |
model_id,
|
|
|
|
| 235 |
return styled_warning(err)
|
| 236 |
|
| 237 |
if awaiting_mod:
|
| 238 |
+
return (
|
| 239 |
+
script_hint
|
| 240 |
+
+ styled_message(
|
| 241 |
+
f"Request <code>{job_id}</code> recorded for <strong>{model_id}</strong>. "
|
| 242 |
+
f"<strong>A moderator must approve</strong> it before evaluation starts "
|
| 243 |
+
f"(see the <strong>Moderate</strong> tab). "
|
| 244 |
+
f"Approx. backlog awaiting approval: <strong>{position}</strong>."
|
| 245 |
+
)
|
| 246 |
)
|
| 247 |
|
| 248 |
+
return (
|
| 249 |
+
script_hint
|
| 250 |
+
+ styled_message(
|
| 251 |
+
f"Queued job <code>{job_id}</code> for <strong>{model_id}</strong>. "
|
| 252 |
+
f"Approx. position in queue: <strong>{position}</strong>. "
|
| 253 |
+
f"Evaluation runs in the background; refresh the <strong>Leaderboard</strong> tab "
|
| 254 |
+
f"after a few minutes to see WER when the job finishes."
|
| 255 |
+
)
|
| 256 |
)
|
| 257 |
|
| 258 |
|
|
|
|
| 439 |
max_length=8000,
|
| 440 |
)
|
| 441 |
script_input = gr.Code(
|
| 442 |
+
label="Optional custom evaluator (Python)",
|
| 443 |
language="python",
|
| 444 |
lines=12,
|
| 445 |
)
|
| 446 |
gr.Markdown(
|
| 447 |
+
"Define a function that transcribes one WAV file:\n\n"
|
| 448 |
+
"```python\n"
|
| 449 |
+
"from pathlib import Path\n\n"
|
| 450 |
+
"def evaluate(file: Path) -> str:\n"
|
| 451 |
+
" # return the transcription for this audio file\n"
|
| 452 |
+
" ...\n"
|
| 453 |
+
"```\n\n"
|
| 454 |
+
"It is called **once per audio sample** inside the existing eval loop. "
|
| 455 |
+
"Put any extra Python dependencies in the requirements box above. "
|
| 456 |
+
"Custom evaluators are **not** run on this Space; they run on a Hub Job "
|
| 457 |
+
"**only** after a moderator approves them."
|
| 458 |
)
|
| 459 |
with gr.Row():
|
| 460 |
is_gated_input = gr.Checkbox(label="This is a gated repo", value=False)
|
|
|
|
| 532 |
value=None,
|
| 533 |
)
|
| 534 |
mod_run_custom_script = gr.Checkbox(
|
| 535 |
+
label="Use submitter's custom evaluate() function (if provided)",
|
| 536 |
value=False,
|
| 537 |
)
|
| 538 |
with gr.Row():
|
backends/custom_eval.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Load a user-submitted custom evaluator script and wrap it as a transcriber callable.
|
| 3 |
+
|
| 4 |
+
The script must define::
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
def evaluate(file: Path) -> str:
|
| 8 |
+
...
|
| 9 |
+
|
| 10 |
+
Each packed benchmark sample is written to a temporary WAV file and passed to
|
| 11 |
+
``evaluate`` once per sample inside the existing eval loop.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import importlib.util
|
| 17 |
+
import os
|
| 18 |
+
import shutil
|
| 19 |
+
import tempfile
|
| 20 |
+
from collections.abc import Callable
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def build_transcriber_from_custom_script(
|
| 25 |
+
script_text: str,
|
| 26 |
+
) -> tuple[Callable[..., str], Callable[[], None]]:
|
| 27 |
+
"""
|
| 28 |
+
Load ``script_text`` as a module and return ``(transcribe, cleanup)``.
|
| 29 |
+
|
| 30 |
+
``transcribe(audio_np, sampling_rate)`` writes a temp WAV and calls
|
| 31 |
+
``evaluate(path)`` on the loaded module.
|
| 32 |
+
"""
|
| 33 |
+
import soundfile as sf
|
| 34 |
+
|
| 35 |
+
script_text = (script_text or "").strip()
|
| 36 |
+
if not script_text:
|
| 37 |
+
raise RuntimeError("Custom script is empty.")
|
| 38 |
+
|
| 39 |
+
fd, script_path = tempfile.mkstemp(prefix="ffasr_custom_eval_", suffix=".py")
|
| 40 |
+
try:
|
| 41 |
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
| 42 |
+
f.write(script_text)
|
| 43 |
+
except Exception:
|
| 44 |
+
try:
|
| 45 |
+
os.unlink(script_path)
|
| 46 |
+
except FileNotFoundError:
|
| 47 |
+
pass
|
| 48 |
+
raise
|
| 49 |
+
|
| 50 |
+
spec = importlib.util.spec_from_file_location("ffasr_user_eval", script_path)
|
| 51 |
+
if spec is None or spec.loader is None:
|
| 52 |
+
try:
|
| 53 |
+
os.unlink(script_path)
|
| 54 |
+
except FileNotFoundError:
|
| 55 |
+
pass
|
| 56 |
+
raise RuntimeError("Could not load custom evaluator script.")
|
| 57 |
+
|
| 58 |
+
mod = importlib.util.module_from_spec(spec)
|
| 59 |
+
spec.loader.exec_module(mod)
|
| 60 |
+
|
| 61 |
+
evaluate_fn = getattr(mod, "evaluate", None)
|
| 62 |
+
if not callable(evaluate_fn):
|
| 63 |
+
try:
|
| 64 |
+
os.unlink(script_path)
|
| 65 |
+
except FileNotFoundError:
|
| 66 |
+
pass
|
| 67 |
+
raise RuntimeError(
|
| 68 |
+
"Custom script must define `evaluate(file: pathlib.Path) -> str`."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
wav_dir = tempfile.mkdtemp(prefix="ffasr_custom_wav_")
|
| 72 |
+
counter = {"i": 0}
|
| 73 |
+
|
| 74 |
+
def transcribe(audio_np, sampling_rate: int) -> str:
|
| 75 |
+
counter["i"] += 1
|
| 76 |
+
wav_path = Path(wav_dir) / f"sample_{counter['i']:08d}.wav"
|
| 77 |
+
sf.write(str(wav_path), audio_np, int(sampling_rate), subtype="PCM_16")
|
| 78 |
+
try:
|
| 79 |
+
text = evaluate_fn(wav_path)
|
| 80 |
+
finally:
|
| 81 |
+
try:
|
| 82 |
+
wav_path.unlink()
|
| 83 |
+
except FileNotFoundError:
|
| 84 |
+
pass
|
| 85 |
+
return str(text or "")
|
| 86 |
+
|
| 87 |
+
transcribe._num_params = 0 # type: ignore[attr-defined]
|
| 88 |
+
|
| 89 |
+
def cleanup() -> None:
|
| 90 |
+
shutil.rmtree(wav_dir, ignore_errors=True)
|
| 91 |
+
try:
|
| 92 |
+
os.unlink(script_path)
|
| 93 |
+
except FileNotFoundError:
|
| 94 |
+
pass
|
| 95 |
+
|
| 96 |
+
return transcribe, cleanup
|
backends/universal.py
CHANGED
|
@@ -230,6 +230,15 @@ def _normalize_input_features_layout(model, batch: dict) -> dict:
|
|
| 230 |
return batch
|
| 231 |
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
def _generate(model, batch: dict, max_new_tokens: int, language: str):
|
| 234 |
"""Try language-aware generate first (Whisper path); fall back to plain generate."""
|
| 235 |
batch = _normalize_input_features_layout(model, batch)
|
|
@@ -240,7 +249,7 @@ def _generate(model, batch: dict, max_new_tokens: int, language: str):
|
|
| 240 |
# Cohere's remote generate() path can leave decoder_attention_mask as None,
|
| 241 |
# then HF generation tries `decoder_attention_mask.new_ones(...)` and crashes.
|
| 242 |
# Passing an explicit one-token decoder mask keeps generation state valid.
|
| 243 |
-
src =
|
| 244 |
if src is not None and hasattr(src, "shape"):
|
| 245 |
try:
|
| 246 |
batch = dict(batch)
|
|
|
|
| 230 |
return batch
|
| 231 |
|
| 232 |
|
| 233 |
+
def _encoder_tensor_from_batch(batch: dict):
|
| 234 |
+
"""Return encoder inputs without using ``or`` on tensors (ambiguous bool)."""
|
| 235 |
+
if batch.get("input_features") is not None:
|
| 236 |
+
return batch["input_features"]
|
| 237 |
+
if batch.get("input_values") is not None:
|
| 238 |
+
return batch["input_values"]
|
| 239 |
+
return None
|
| 240 |
+
|
| 241 |
+
|
| 242 |
def _generate(model, batch: dict, max_new_tokens: int, language: str):
|
| 243 |
"""Try language-aware generate first (Whisper path); fall back to plain generate."""
|
| 244 |
batch = _normalize_input_features_layout(model, batch)
|
|
|
|
| 249 |
# Cohere's remote generate() path can leave decoder_attention_mask as None,
|
| 250 |
# then HF generation tries `decoder_attention_mask.new_ones(...)` and crashes.
|
| 251 |
# Passing an explicit one-token decoder mask keeps generation state valid.
|
| 252 |
+
src = _encoder_tensor_from_batch(batch)
|
| 253 |
if src is not None and hasattr(src, "shape"):
|
| 254 |
try:
|
| 255 |
batch = dict(batch)
|
evaluation/orchestrator.py
CHANGED
|
@@ -57,6 +57,21 @@ def _samples_per_gpu_segment() -> int:
|
|
| 57 |
return max(0, v)
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
def _zerogpu_run_segment(payload: dict[str, Any]) -> dict[str, Any]:
|
| 61 |
"""Single ZeroGPU segment: build model, transcribe ``[start:end)`` for one condition."""
|
| 62 |
model_id = str(payload["model_id"])
|
|
@@ -64,9 +79,12 @@ def _zerogpu_run_segment(payload: dict[str, Any]) -> dict[str, Any]:
|
|
| 64 |
condition_key = str(payload["condition_key"])
|
| 65 |
start = int(payload["start"])
|
| 66 |
end = int(payload["end"])
|
|
|
|
| 67 |
|
| 68 |
device_str, device_int = resolve_eval_devices()
|
| 69 |
-
transcribe, cleanup =
|
|
|
|
|
|
|
| 70 |
try:
|
| 71 |
num_params = int(getattr(transcribe, "_num_params", 0) or 0)
|
| 72 |
preds, refs, audio_s, infer_s, n_done = accumulate_predictions_for_slice(
|
|
@@ -90,6 +108,7 @@ def _run_evaluation_core(
|
|
| 90 |
model_id: str,
|
| 91 |
family_id: str = "auto",
|
| 92 |
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
|
|
| 93 |
) -> dict:
|
| 94 |
device_str, device_int = resolve_eval_devices()
|
| 95 |
|
|
@@ -109,7 +128,9 @@ def _run_evaluation_core(
|
|
| 109 |
except Exception:
|
| 110 |
pass
|
| 111 |
|
| 112 |
-
transcribe, cleanup =
|
|
|
|
|
|
|
| 113 |
try:
|
| 114 |
# Backends attach `transcribe._num_params` via `_model_utils.attach_params`; defaults to 0
|
| 115 |
# if the backend couldn't introspect a module.
|
|
@@ -171,6 +192,7 @@ def _run_evaluation_core_segmented_local(
|
|
| 171 |
progress_cb: Callable[[int, int, str], None] | None = None,
|
| 172 |
*,
|
| 173 |
segment_samples: int,
|
|
|
|
| 174 |
) -> dict:
|
| 175 |
"""
|
| 176 |
Same semantics as ``_run_evaluation_core_segmented`` but without ``spaces.GPU``:
|
|
@@ -224,6 +246,7 @@ def _run_evaluation_core_segmented_local(
|
|
| 224 |
"condition_key": condition_key,
|
| 225 |
"start": start,
|
| 226 |
"end": end,
|
|
|
|
| 227 |
}
|
| 228 |
out = _zerogpu_run_segment(payload)
|
| 229 |
if first_num_params is None:
|
|
@@ -270,6 +293,7 @@ def _run_evaluation_core_segmented(
|
|
| 270 |
segment_samples: int,
|
| 271 |
gpu_duration_s: int,
|
| 272 |
gpu_size: str,
|
|
|
|
| 273 |
) -> dict:
|
| 274 |
"""
|
| 275 |
Long evaluations: each slice of ``segment_samples`` runs under its own ``spaces.GPU``
|
|
@@ -327,6 +351,7 @@ def _run_evaluation_core_segmented(
|
|
| 327 |
"condition_key": condition_key,
|
| 328 |
"start": start,
|
| 329 |
"end": end,
|
|
|
|
| 330 |
}
|
| 331 |
out = wrapped_seg(payload)
|
| 332 |
if first_num_params is None:
|
|
@@ -383,6 +408,7 @@ def run_evaluation(
|
|
| 383 |
model_id: str,
|
| 384 |
family_id: str = "auto",
|
| 385 |
progress_cb: Callable[[int, int, str], None] | None = None,
|
|
|
|
| 386 |
) -> dict:
|
| 387 |
"""
|
| 388 |
Evaluate `model_id` on all packed conditions using the selected family backend.
|
|
@@ -410,6 +436,9 @@ def run_evaluation(
|
|
| 410 |
``progress_cb(samples_done_across_all_conditions, samples_total_across_all_conditions, current_condition_key)``
|
| 411 |
periodically during the run.
|
| 412 |
|
|
|
|
|
|
|
|
|
|
| 413 |
Returns wer_clean, wer_noisy, wer_reverberant, wer_real, wer_difficult, num_samples, model_id, eval_family, plus timing.
|
| 414 |
"""
|
| 415 |
apply_cpu_thread_settings_once()
|
|
@@ -423,8 +452,14 @@ def run_evaluation(
|
|
| 423 |
family_id=family_id,
|
| 424 |
progress_cb=progress_cb,
|
| 425 |
segment_samples=seg,
|
|
|
|
| 426 |
)
|
| 427 |
-
return _run_evaluation_core(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
eff = _effective_gpu_duration_s()
|
| 430 |
size = os.environ.get("FFASR_ZEROGPU_GPU_SIZE", "large").strip().lower()
|
|
@@ -433,7 +468,12 @@ def run_evaluation(
|
|
| 433 |
|
| 434 |
if seg <= 0:
|
| 435 |
wrapped = _spaces_gpu_wrapped(eff, size)
|
| 436 |
-
return wrapped(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
return _run_evaluation_core_segmented(
|
| 439 |
model_id,
|
|
@@ -442,4 +482,5 @@ def run_evaluation(
|
|
| 442 |
segment_samples=seg,
|
| 443 |
gpu_duration_s=eff,
|
| 444 |
gpu_size=size,
|
|
|
|
| 445 |
)
|
|
|
|
| 57 |
return max(0, v)
|
| 58 |
|
| 59 |
|
| 60 |
+
def _build_transcriber(
|
| 61 |
+
family_id: str,
|
| 62 |
+
model_id: str,
|
| 63 |
+
device_str: str,
|
| 64 |
+
device_int: int,
|
| 65 |
+
custom_script: str = "",
|
| 66 |
+
) -> tuple[Callable[..., str], Callable[[], None]]:
|
| 67 |
+
"""Family backend transcriber, or user ``evaluate(Path)`` script when provided."""
|
| 68 |
+
if (custom_script or "").strip():
|
| 69 |
+
from backends.custom_eval import build_transcriber_from_custom_script
|
| 70 |
+
|
| 71 |
+
return build_transcriber_from_custom_script(custom_script)
|
| 72 |
+
return build_transcriber(family_id, model_id, device_str, device_int)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
def _zerogpu_run_segment(payload: dict[str, Any]) -> dict[str, Any]:
|
| 76 |
"""Single ZeroGPU segment: build model, transcribe ``[start:end)`` for one condition."""
|
| 77 |
model_id = str(payload["model_id"])
|
|
|
|
| 79 |
condition_key = str(payload["condition_key"])
|
| 80 |
start = int(payload["start"])
|
| 81 |
end = int(payload["end"])
|
| 82 |
+
custom_script = str(payload.get("custom_script") or "")
|
| 83 |
|
| 84 |
device_str, device_int = resolve_eval_devices()
|
| 85 |
+
transcribe, cleanup = _build_transcriber(
|
| 86 |
+
family_id, model_id, device_str, device_int, custom_script=custom_script
|
| 87 |
+
)
|
| 88 |
try:
|
| 89 |
num_params = int(getattr(transcribe, "_num_params", 0) or 0)
|
| 90 |
preds, refs, audio_s, infer_s, n_done = accumulate_predictions_for_slice(
|
|
|
|
| 108 |
model_id: str,
|
| 109 |
family_id: str = "auto",
|
| 110 |
progress_cb: Callable[[int, int, str], None] | None = None,
|
| 111 |
+
custom_script: str = "",
|
| 112 |
) -> dict:
|
| 113 |
device_str, device_int = resolve_eval_devices()
|
| 114 |
|
|
|
|
| 128 |
except Exception:
|
| 129 |
pass
|
| 130 |
|
| 131 |
+
transcribe, cleanup = _build_transcriber(
|
| 132 |
+
family_id, model_id, device_str, device_int, custom_script=custom_script
|
| 133 |
+
)
|
| 134 |
try:
|
| 135 |
# Backends attach `transcribe._num_params` via `_model_utils.attach_params`; defaults to 0
|
| 136 |
# if the backend couldn't introspect a module.
|
|
|
|
| 192 |
progress_cb: Callable[[int, int, str], None] | None = None,
|
| 193 |
*,
|
| 194 |
segment_samples: int,
|
| 195 |
+
custom_script: str = "",
|
| 196 |
) -> dict:
|
| 197 |
"""
|
| 198 |
Same semantics as ``_run_evaluation_core_segmented`` but without ``spaces.GPU``:
|
|
|
|
| 246 |
"condition_key": condition_key,
|
| 247 |
"start": start,
|
| 248 |
"end": end,
|
| 249 |
+
"custom_script": custom_script,
|
| 250 |
}
|
| 251 |
out = _zerogpu_run_segment(payload)
|
| 252 |
if first_num_params is None:
|
|
|
|
| 293 |
segment_samples: int,
|
| 294 |
gpu_duration_s: int,
|
| 295 |
gpu_size: str,
|
| 296 |
+
custom_script: str = "",
|
| 297 |
) -> dict:
|
| 298 |
"""
|
| 299 |
Long evaluations: each slice of ``segment_samples`` runs under its own ``spaces.GPU``
|
|
|
|
| 351 |
"condition_key": condition_key,
|
| 352 |
"start": start,
|
| 353 |
"end": end,
|
| 354 |
+
"custom_script": custom_script,
|
| 355 |
}
|
| 356 |
out = wrapped_seg(payload)
|
| 357 |
if first_num_params is None:
|
|
|
|
| 408 |
model_id: str,
|
| 409 |
family_id: str = "auto",
|
| 410 |
progress_cb: Callable[[int, int, str], None] | None = None,
|
| 411 |
+
custom_script: str = "",
|
| 412 |
) -> dict:
|
| 413 |
"""
|
| 414 |
Evaluate `model_id` on all packed conditions using the selected family backend.
|
|
|
|
| 436 |
``progress_cb(samples_done_across_all_conditions, samples_total_across_all_conditions, current_condition_key)``
|
| 437 |
periodically during the run.
|
| 438 |
|
| 439 |
+
When ``custom_script`` is non-empty, it must define ``evaluate(file: pathlib.Path) -> str``;
|
| 440 |
+
that function is called once per packed sample (via a temp WAV) instead of the family backend.
|
| 441 |
+
|
| 442 |
Returns wer_clean, wer_noisy, wer_reverberant, wer_real, wer_difficult, num_samples, model_id, eval_family, plus timing.
|
| 443 |
"""
|
| 444 |
apply_cpu_thread_settings_once()
|
|
|
|
| 452 |
family_id=family_id,
|
| 453 |
progress_cb=progress_cb,
|
| 454 |
segment_samples=seg,
|
| 455 |
+
custom_script=custom_script,
|
| 456 |
)
|
| 457 |
+
return _run_evaluation_core(
|
| 458 |
+
model_id,
|
| 459 |
+
family_id=family_id,
|
| 460 |
+
progress_cb=progress_cb,
|
| 461 |
+
custom_script=custom_script,
|
| 462 |
+
)
|
| 463 |
|
| 464 |
eff = _effective_gpu_duration_s()
|
| 465 |
size = os.environ.get("FFASR_ZEROGPU_GPU_SIZE", "large").strip().lower()
|
|
|
|
| 468 |
|
| 469 |
if seg <= 0:
|
| 470 |
wrapped = _spaces_gpu_wrapped(eff, size)
|
| 471 |
+
return wrapped(
|
| 472 |
+
model_id,
|
| 473 |
+
family_id=family_id,
|
| 474 |
+
progress_cb=progress_cb,
|
| 475 |
+
custom_script=custom_script,
|
| 476 |
+
)
|
| 477 |
|
| 478 |
return _run_evaluation_core_segmented(
|
| 479 |
model_id,
|
|
|
|
| 482 |
segment_samples=seg,
|
| 483 |
gpu_duration_s=eff,
|
| 484 |
gpu_size=size,
|
| 485 |
+
custom_script=custom_script,
|
| 486 |
)
|
job_queue.py
CHANGED
|
@@ -138,6 +138,23 @@ def sanitize_custom_script(text: str) -> str:
|
|
| 138 |
return s
|
| 139 |
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
def _bool_to_csv(flag: bool) -> str:
|
| 142 |
return "1" if flag else "0"
|
| 143 |
|
|
@@ -1262,7 +1279,7 @@ def next_up_html(limit: int = 5) -> str:
|
|
| 1262 |
return (
|
| 1263 |
"<div class='next-up-panel' style='font-size:0.9em'>"
|
| 1264 |
f"<p><strong>Next models to evaluate</strong> ({len(queued)} shown):</p>"
|
| 1265 |
-
f"<ol style='margin:0.25rem 0 0 1.1rem'>{items}</ol></
|
| 1266 |
)
|
| 1267 |
|
| 1268 |
|
|
|
|
| 138 |
return s
|
| 139 |
|
| 140 |
|
| 141 |
+
def custom_script_defines_evaluate(text: str) -> bool:
|
| 142 |
+
"""Best-effort check that the script defines a top-level ``evaluate`` function."""
|
| 143 |
+
s = (text or "").strip()
|
| 144 |
+
if not s:
|
| 145 |
+
return True
|
| 146 |
+
try:
|
| 147 |
+
import ast
|
| 148 |
+
|
| 149 |
+
tree = ast.parse(s)
|
| 150 |
+
except SyntaxError:
|
| 151 |
+
return False
|
| 152 |
+
return any(
|
| 153 |
+
isinstance(node, ast.FunctionDef) and node.name == "evaluate"
|
| 154 |
+
for node in tree.body
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
def _bool_to_csv(flag: bool) -> str:
|
| 159 |
return "1" if flag else "0"
|
| 160 |
|
|
|
|
| 1279 |
return (
|
| 1280 |
"<div class='next-up-panel' style='font-size:0.9em'>"
|
| 1281 |
f"<p><strong>Next models to evaluate</strong> ({len(queued)} shown):</p>"
|
| 1282 |
+
f"<ol style='margin:0.25rem 0 0 1.1rem'>{items}</ol></div>"
|
| 1283 |
)
|
| 1284 |
|
| 1285 |
|
remote_jobs.py
CHANGED
|
@@ -187,21 +187,7 @@ def submit_eval_job(
|
|
| 187 |
)
|
| 188 |
|
| 189 |
script = _worker_script_path()
|
| 190 |
-
|
| 191 |
-
if run_custom_script and (custom_script or "").strip():
|
| 192 |
-
import tempfile
|
| 193 |
-
|
| 194 |
-
fd, custom_script_path = tempfile.mkstemp(
|
| 195 |
-
prefix=f"ffasr_custom_{space_job_id}_", suffix=".py"
|
| 196 |
-
)
|
| 197 |
-
try:
|
| 198 |
-
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
| 199 |
-
f.write(custom_script.strip())
|
| 200 |
-
except Exception:
|
| 201 |
-
os.unlink(custom_script_path)
|
| 202 |
-
raise
|
| 203 |
-
script = custom_script_path
|
| 204 |
-
elif not os.path.isfile(script):
|
| 205 |
raise RuntimeError(f"Remote worker script not found: {script}")
|
| 206 |
|
| 207 |
deps = _select_deps(model_id, family_id)
|
|
@@ -233,6 +219,12 @@ def submit_eval_job(
|
|
| 233 |
"FFASR_REMOTE_EVAL_GIT_BRANCH": eval_branch,
|
| 234 |
"HF_HUB_DISABLE_PROGRESS_BARS": "1",
|
| 235 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
secrets = {"HF_TOKEN": bucket_tok}
|
| 237 |
|
| 238 |
api = HfApi(token=jobs_tok)
|
|
@@ -257,12 +249,6 @@ def submit_eval_job(
|
|
| 257 |
if last_err is not None:
|
| 258 |
raise last_err
|
| 259 |
raise RuntimeError("submit_eval_job: unreachable")
|
| 260 |
-
finally:
|
| 261 |
-
if custom_script_path and os.path.isfile(custom_script_path):
|
| 262 |
-
try:
|
| 263 |
-
os.unlink(custom_script_path)
|
| 264 |
-
except Exception:
|
| 265 |
-
pass
|
| 266 |
|
| 267 |
|
| 268 |
def inspect_job_once(hf_job_id: str, *, token: str | None = None) -> JobInfo:
|
|
|
|
| 187 |
)
|
| 188 |
|
| 189 |
script = _worker_script_path()
|
| 190 |
+
if not os.path.isfile(script):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
raise RuntimeError(f"Remote worker script not found: {script}")
|
| 192 |
|
| 193 |
deps = _select_deps(model_id, family_id)
|
|
|
|
| 219 |
"FFASR_REMOTE_EVAL_GIT_BRANCH": eval_branch,
|
| 220 |
"HF_HUB_DISABLE_PROGRESS_BARS": "1",
|
| 221 |
}
|
| 222 |
+
if run_custom_script and (custom_script or "").strip():
|
| 223 |
+
import base64
|
| 224 |
+
|
| 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)
|
|
|
|
| 249 |
if last_err is not None:
|
| 250 |
raise last_err
|
| 251 |
raise RuntimeError("submit_eval_job: unreachable")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
|
| 254 |
def inspect_job_once(hf_job_id: str, *, token: str | None = None) -> JobInfo:
|
scripts/run_hf_remote_job_uv.py
CHANGED
|
@@ -16,6 +16,7 @@ Environment (set by ``remote_jobs.submit_eval_job``):
|
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
|
|
|
| 19 |
import io
|
| 20 |
import json
|
| 21 |
import os
|
|
@@ -113,7 +114,16 @@ def main() -> int:
|
|
| 113 |
from storage import HF_BUCKET_ID, upload_to_bucket
|
| 114 |
|
| 115 |
apply_cpu_thread_settings_once()
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
artifact = build_artifact(
|
| 118 |
model_id=model_id,
|
| 119 |
family_id=family_id,
|
|
|
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
+
import base64
|
| 20 |
import io
|
| 21 |
import json
|
| 22 |
import os
|
|
|
|
| 114 |
from storage import HF_BUCKET_ID, upload_to_bucket
|
| 115 |
|
| 116 |
apply_cpu_thread_settings_once()
|
| 117 |
+
custom_b64 = os.environ.get("FFASR_CUSTOM_SCRIPT_B64", "").strip()
|
| 118 |
+
custom_script = (
|
| 119 |
+
base64.b64decode(custom_b64).decode("utf-8") if custom_b64 else ""
|
| 120 |
+
)
|
| 121 |
+
result = run_evaluation(
|
| 122 |
+
model_id,
|
| 123 |
+
family_id=family_id,
|
| 124 |
+
progress_cb=None,
|
| 125 |
+
custom_script=custom_script,
|
| 126 |
+
)
|
| 127 |
artifact = build_artifact(
|
| 128 |
model_id=model_id,
|
| 129 |
family_id=family_id,
|