Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Add Nemotron 3.5 Transcribe evaluation scripts
#3
by nithinraok - opened
recipes/nemotron_transcribe/evaluate.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FFASR custom evaluator for NVIDIA Nemotron 3.5 Transcribe (BF16).
|
| 2 |
+
|
| 3 |
+
The setup script provides a separate NeMo/vLLM environment. This module starts
|
| 4 |
+
one local server before FFASR calls evaluate() for each sample. No external
|
| 5 |
+
inference service receives benchmark audio.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import atexit
|
| 9 |
+
import base64
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import subprocess
|
| 14 |
+
import time
|
| 15 |
+
import urllib.error
|
| 16 |
+
import urllib.request
|
| 17 |
+
|
| 18 |
+
MODEL_ID = "nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B"
|
| 19 |
+
PROMPT = (
|
| 20 |
+
"Audio transcription. Language: as spoken. Wording: verbatim, including "
|
| 21 |
+
"fillers and repetitions. Formatting: punctuation and capitalization. "
|
| 22 |
+
"Output: transcript only."
|
| 23 |
+
)
|
| 24 |
+
_BASE_URL = "http://127.0.0.1:8000"
|
| 25 |
+
_HTTP = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
| 26 |
+
_SERVER = None
|
| 27 |
+
_SERVER_LOG = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _stop_server():
|
| 31 |
+
if _SERVER is not None and _SERVER.poll() is None:
|
| 32 |
+
_SERVER.terminate()
|
| 33 |
+
try:
|
| 34 |
+
_SERVER.wait(timeout=30)
|
| 35 |
+
except subprocess.TimeoutExpired:
|
| 36 |
+
_SERVER.kill()
|
| 37 |
+
_SERVER.wait()
|
| 38 |
+
if _SERVER_LOG is not None:
|
| 39 |
+
_SERVER_LOG.close()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _check_server():
|
| 43 |
+
with _HTTP.open(_BASE_URL + "/v1/models", timeout=5) as response:
|
| 44 |
+
models = json.load(response).get("data", [])
|
| 45 |
+
if not any(item.get("id") == MODEL_ID for item in models):
|
| 46 |
+
raise RuntimeError("The local server does not serve " + MODEL_ID)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _start_server():
|
| 50 |
+
global _SERVER, _SERVER_LOG
|
| 51 |
+
# Use external mode only for a server already running on this same machine.
|
| 52 |
+
if os.environ.get("NEMOTRON_FFASR_SERVER_MODE") == "external":
|
| 53 |
+
_check_server()
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
executable = os.environ.get("NEMOTRON_FFASR_VLLM")
|
| 57 |
+
checkpoint = os.environ.get("NEMOTRON_FFASR_CHECKPOINT")
|
| 58 |
+
if not executable or not checkpoint:
|
| 59 |
+
raise RuntimeError("Run setup.sh first through FFASR's setup field.")
|
| 60 |
+
try:
|
| 61 |
+
_check_server()
|
| 62 |
+
except (OSError, ValueError, RuntimeError):
|
| 63 |
+
pass
|
| 64 |
+
else:
|
| 65 |
+
raise RuntimeError("Port 8000 already serves this model. Use external mode explicitly.")
|
| 66 |
+
|
| 67 |
+
env = os.environ.copy()
|
| 68 |
+
# Do not expose the parent job's virtual environment to the server.
|
| 69 |
+
env.pop("VIRTUAL_ENV", None)
|
| 70 |
+
env.pop("PYTHONPATH", None)
|
| 71 |
+
env["VLLM_PLUGINS"] = "nemo_speechlm"
|
| 72 |
+
env["VLLM_MAX_AUDIO_DECODE_DURATION_S"] = "3600"
|
| 73 |
+
env["VLLM_MAX_AUDIO_CLIP_FILESIZE_MB"] = "180"
|
| 74 |
+
env["VLLM_USE_V2_MODEL_RUNNER"] = "0"
|
| 75 |
+
command = [
|
| 76 |
+
executable, "serve", checkpoint,
|
| 77 |
+
"--host", "127.0.0.1", "--port", "8000",
|
| 78 |
+
"--served-model-name", MODEL_ID,
|
| 79 |
+
"--trust-remote-code", "--dtype", "bfloat16",
|
| 80 |
+
"--gpu-memory-utilization", "0.94",
|
| 81 |
+
"--max-model-len", "65536", "--max-num-batched-tokens", "4096",
|
| 82 |
+
"--max-num-seqs", "192", "--enable-chunked-prefill",
|
| 83 |
+
"--no-enable-prefix-caching", "--use-replayssm",
|
| 84 |
+
"--mamba-backend", "flashinfer", "--mamba-cache-mode", "none",
|
| 85 |
+
"--mm-processor-cache-gb", "0", "--no-async-scheduling",
|
| 86 |
+
"--limit-mm-per-prompt", json.dumps({"audio": {"count": 6, "length": 120000000}}),
|
| 87 |
+
"--speculative-config", json.dumps({"method": "mtp", "num_speculative_tokens": 8}),
|
| 88 |
+
"--reasoning-parser", "nemotron_v3",
|
| 89 |
+
"--trust-request-mm-kwargs",
|
| 90 |
+
"--middleware", "nemo.collections.speechlm2.vllm.salm.ctc_serving.ctc_timestamp_middleware",
|
| 91 |
+
]
|
| 92 |
+
log_path = Path("/tmp/nemotron_ffasr_vllm.log")
|
| 93 |
+
_SERVER_LOG = log_path.open("w", encoding="utf-8")
|
| 94 |
+
_SERVER = subprocess.Popen(command, env=env, stdout=_SERVER_LOG, stderr=subprocess.STDOUT)
|
| 95 |
+
atexit.register(_stop_server)
|
| 96 |
+
deadline = time.monotonic() + 1800
|
| 97 |
+
while time.monotonic() < deadline:
|
| 98 |
+
if _SERVER.poll() is not None:
|
| 99 |
+
tail = log_path.read_text(errors="replace")[-8000:]
|
| 100 |
+
raise RuntimeError("vLLM stopped during startup:\n" + tail)
|
| 101 |
+
try:
|
| 102 |
+
_check_server()
|
| 103 |
+
return
|
| 104 |
+
except (OSError, ValueError):
|
| 105 |
+
time.sleep(2)
|
| 106 |
+
_stop_server()
|
| 107 |
+
raise TimeoutError("vLLM startup exceeded 30 minutes. See " + str(log_path))
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def evaluate(file: Path) -> str:
|
| 111 |
+
"""Return the raw transcript. FFASR applies its standard normalization."""
|
| 112 |
+
audio = base64.b64encode(Path(file).read_bytes()).decode("ascii")
|
| 113 |
+
payload = {
|
| 114 |
+
"model": MODEL_ID,
|
| 115 |
+
"messages": [{"role": "user", "content": [
|
| 116 |
+
{"type": "text", "text": PROMPT},
|
| 117 |
+
{"type": "audio_url", "audio_url": {"url": "data:audio/wav;base64," + audio}},
|
| 118 |
+
]}],
|
| 119 |
+
"temperature": 0,
|
| 120 |
+
"top_p": 1.0,
|
| 121 |
+
"seed": 0,
|
| 122 |
+
"max_tokens": 16384,
|
| 123 |
+
"chat_template_kwargs": {"enable_thinking": False},
|
| 124 |
+
"skip_special_tokens": False,
|
| 125 |
+
"spaces_between_special_tokens": False,
|
| 126 |
+
}
|
| 127 |
+
request = urllib.request.Request(
|
| 128 |
+
_BASE_URL + "/v1/chat/completions",
|
| 129 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 130 |
+
headers={"Content-Type": "application/json"},
|
| 131 |
+
)
|
| 132 |
+
try:
|
| 133 |
+
with _HTTP.open(request, timeout=600) as response:
|
| 134 |
+
result = json.load(response)
|
| 135 |
+
except urllib.error.HTTPError as exc:
|
| 136 |
+
detail = exc.read().decode("utf-8", errors="replace")[:2000]
|
| 137 |
+
raise RuntimeError(f"vLLM request failed ({exc.code}): {detail}") from exc
|
| 138 |
+
choice = result["choices"][0]
|
| 139 |
+
if choice.get("finish_reason") != "stop":
|
| 140 |
+
raise RuntimeError("Incomplete transcript: " + str(choice.get("finish_reason")))
|
| 141 |
+
text = choice["message"].get("content")
|
| 142 |
+
if not isinstance(text, str):
|
| 143 |
+
raise RuntimeError("The server did not return a transcript string.")
|
| 144 |
+
return text.strip()
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Load the server before FFASR starts sample timing.
|
| 148 |
+
_start_server()
|
recipes/nemotron_transcribe/setup.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
# Paste this file into FFASR's one-time setup field.
|
| 5 |
+
# Run on Linux with one H100 80 GB and sufficient disk space for the BF16 model.
|
| 6 |
+
: "${FFASR_ENV_FILE:?FFASR must provide its setup environment file}"
|
| 7 |
+
task_root=$(mktemp -d /tmp/nemotron_ffasr.XXXXXX)
|
| 8 |
+
task_repo="$task_root/Speech"
|
| 9 |
+
task_venv="$task_repo/.venv"
|
| 10 |
+
task_model="$task_root/checkpoint"
|
| 11 |
+
|
| 12 |
+
git clone https://github.com/NVIDIA-NeMo/Speech.git "$task_repo"
|
| 13 |
+
git -C "$task_repo" checkout 040574f7ffa3bb0ae4b92aa8424ecea454d03c91
|
| 14 |
+
cd "$task_repo"
|
| 15 |
+
uv venv --python 3.13 "$task_venv"
|
| 16 |
+
UV_PROJECT_ENVIRONMENT="$task_venv" uv sync --extra asr --extra vllm --locked --no-install-project
|
| 17 |
+
uv pip install --python "$task_venv/bin/python" --no-deps -e .
|
| 18 |
+
uv pip install --python "$task_venv/bin/python" --no-deps av==19.0.2 opencv-python-headless==5.0.0.93
|
| 19 |
+
"$task_venv/bin/python" -c 'import vllm; assert vllm.__version__ == "0.32.0", vllm.__version__'
|
| 20 |
+
"$task_venv/bin/hf" download nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B \
|
| 21 |
+
--revision ef905575a106fdad904f3b0ce661dccad654607d --local-dir "$task_model"
|
| 22 |
+
|
| 23 |
+
printf 'NEMOTRON_FFASR_VLLM=%s\n' "$task_venv/bin/vllm" >> "$FFASR_ENV_FILE"
|
| 24 |
+
printf 'NEMOTRON_FFASR_CHECKPOINT=%s\n' "$task_model" >> "$FFASR_ENV_FILE"
|
recipes/nemotron_transcribe/validation.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Local validation completed on 28 October 2026.
|
| 2 |
+
PASS: Python syntax.
|
| 3 |
+
PASS: Shell syntax (bash -n setup.sh).
|
| 4 |
+
PASS: Fixed NeMo Speech revision specifies vLLM 0.32.0.
|
| 5 |
+
PASS: Mock server receives the correct model ID, prompt, decoding settings, and WAV bytes.
|
| 6 |
+
PASS: Evaluator returns a transcript string.
|
| 7 |
+
PASS: Evaluator rejects truncated output and missing transcript content.
|
| 8 |
+
PASS: Mock process receives the BF16 and MTP8 server arguments.
|
| 9 |
+
|
| 10 |
+
These checks used mock responses. No model inference was run.
|
| 11 |
+
The Linux dependency installation, GPU memory use, startup time, WER, and speed
|
| 12 |
+
remain untested. The files have not been submitted to FFASR or sent to Slack.
|