Add Nemotron 3.5 Transcribe evaluation scripts

#3
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.