multimodalart HF Staff commited on
Commit
a8e9396
·
verified ·
1 Parent(s): e8a29ab

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/mlk_speech.wav filter=lfs diff=lfs merge=lfs -text
37
+ examples/sample_speech.wav filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -6,7 +6,27 @@ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
  app_file: app.py
9
- short_description: Probe
10
  python_version: "3.10"
11
  startup_duration_timeout: 1h
12
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
  app_file: app.py
9
+ short_description: Audio understanding, speech recognition & translation
10
  python_version: "3.10"
11
  startup_duration_timeout: 1h
12
  ---
13
+
14
+ # Nemotron-Labs-Audex-30B-A3B
15
+
16
+ A Gradio demo for [`nvidia/Nemotron-Labs-Audex-30B-A3B`](https://huggingface.co/nvidia/Nemotron-Labs-Audex-30B-A3B),
17
+ a unified audio-text MoE (30B total / 3B active) built on Nemotron-Cascade-2.
18
+ This demo exposes its **audio understanding, speech recognition, and speech
19
+ translation** capabilities via the official Hugging Face inference path.
20
+
21
+ ## Correct numerics
22
+
23
+ The model is a Nemotron-H hybrid (Mamba2 + attention + MLP). The Mamba layers
24
+ require the compiled CUDA fast path from `mamba-ssm` (`selective_scan_cuda`) and
25
+ `causal-conv1d`; the pure-torch fallback produces degenerate output. This Space
26
+ builds both extensions from source against the runtime's exact toolchain
27
+ (torch 2.11.0+cu130, CUDA 13.0, Python 3.10, sm_120) on first boot and caches
28
+ the resulting wheels back into the repo (`wheels/`) for fast subsequent boots.
29
+
30
+ Runs on ZeroGPU `xlarge` (full 96 GB card) since the bf16 weights are ~65 GB.
31
+
32
+ *Licensed for non-commercial use only (NVIDIA One-Way Noncommercial License).*
app.py CHANGED
@@ -1,42 +1,355 @@
1
- import os, sys, subprocess, platform
 
2
 
3
- # Probe at import so it lands in run logs regardless of UI/SSR state
4
- def _probe():
5
- lines = []
6
- lines.append(f"python: {sys.version}")
7
- lines.append(f"platform: {platform.platform()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  try:
9
- import torch
10
- lines.append(f"torch: {torch.__version__}")
11
- lines.append(f"torch.version.cuda: {torch.version.cuda}")
12
- except Exception as e:
13
- lines.append(f"torch import error: {e!r}")
14
- for cmd in (["nvcc", "--version"], ["which", "nvcc"], ["ls", "/usr/local/cuda/bin"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  try:
16
- out = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
17
- lines.append(f"$ {' '.join(cmd)} (rc={out.returncode})\n{out.stdout}{out.stderr}")
 
 
 
18
  except Exception as e:
19
- lines.append(f"{' '.join(cmd)} error: {e!r}")
20
- # look for nvcc from pip nvidia-cuda-nvcc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  try:
22
- import glob
23
- cands = glob.glob("/usr/local/lib/python3.10/site-packages/nvidia/**/nvcc", recursive=True)
24
- cands += glob.glob("/usr/local/lib/python3.10/site-packages/nvidia/cuda_nvcc/bin/*", recursive=True)
25
- lines.append("pip nvcc candidates: " + str(cands))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  except Exception as e:
27
- lines.append(f"glob err {e!r}")
28
- lines.append("CUDA_HOME=" + str(os.environ.get("CUDA_HOME")))
29
- return "\n".join(lines)
30
 
31
- PROBE = _probe()
32
- print("=========== PROBE START ===========")
33
- print(PROBE)
34
- print("=========== PROBE END ===========", flush=True)
35
 
 
 
 
 
36
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- with gr.Blocks() as demo:
39
- gr.Markdown("# Probe")
40
- gr.Textbox(value=PROBE, label="env", lines=30)
 
 
 
 
 
 
 
 
 
41
 
42
- demo.launch(ssr_mode=False)
 
 
1
+ """
2
+ Nemotron-Labs-Audex-30B-A3B — Audio Understanding / Speech Recognition & Translation demo.
3
 
4
+ This unified audio-text MoE (30B total, 3B active) is a Nemotron-H hybrid
5
+ (Mamba2 + attention + MLP). Correct numerics REQUIRE the compiled CUDA fast
6
+ path from `mamba-ssm` (selective_scan_cuda) and `causal-conv1d`; the pure-torch
7
+ fallback produces degenerate/repeated tokens. We therefore build both extensions
8
+ from source against the Space's exact torch (2.11.0+cu130) / CUDA 13.0 / py3.10
9
+ / sm_120 toolchain, cache the resulting wheels back into this Space repo, and
10
+ reuse them on subsequent boots.
11
+ """
12
+ import os
13
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
14
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
15
+
16
+ import sys
17
+ import glob
18
+ import time
19
+ import subprocess
20
+ from pathlib import Path
21
+
22
+ # Import spaces FIRST (before torch / any CUDA-touching import) so its
23
+ # torch.cuda.* monkey-patch is installed. The kernel build below runs in
24
+ # subprocesses, so it does not initialize CUDA in this process.
25
+ import spaces # noqa: E402
26
+
27
+ APP_DIR = Path(__file__).resolve().parent
28
+ WHEELS_DIR = APP_DIR / "wheels"
29
+ REPO_ID = "hugging-apps/nvidia-nemotron-labs-audex-30b-a3b"
30
+
31
+ # Pinned versions recommended by the model card.
32
+ CAUSAL_CONV1D_SPEC = "causal-conv1d==1.6.2.post1"
33
+ MAMBA_SSM_SPEC = "mamba-ssm==2.3.2.post1"
34
+ # sm_120 (RTX PRO 6000 Blackwell). Build env has no GPU, so arch must be explicit.
35
+ TORCH_ARCH = "12.0"
36
+
37
+
38
+ def _run(cmd, env=None, timeout=3000):
39
+ print(f"[build] $ {' '.join(cmd)}", flush=True)
40
+ p = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=timeout)
41
+ if p.stdout:
42
+ print(p.stdout[-4000:], flush=True)
43
+ if p.returncode != 0:
44
+ print(p.stderr[-8000:], flush=True)
45
+ raise RuntimeError(f"command failed ({p.returncode}): {' '.join(cmd)}")
46
+ else:
47
+ # surface tail of stderr (nvcc warnings etc.) but not as failure
48
+ if p.stderr:
49
+ print(p.stderr[-2000:], flush=True)
50
+ return p
51
+
52
+
53
+ def _installed(mod):
54
  try:
55
+ __import__(mod)
56
+ return True
57
+ except Exception:
58
+ return False
59
+
60
+
61
+ def _pip(*args):
62
+ _run([sys.executable, "-m", "pip"] + list(args))
63
+
64
+
65
+ def _build_env():
66
+ env = dict(os.environ)
67
+ env["MAMBA_FORCE_BUILD"] = "TRUE"
68
+ env["CAUSAL_CONV1D_FORCE_BUILD"] = "TRUE"
69
+ env["TORCH_CUDA_ARCH_LIST"] = TORCH_ARCH
70
+ env["MAX_JOBS"] = env.get("MAX_JOBS", "4")
71
+ # Ensure nvcc is discoverable.
72
+ cuda_home = env.get("CUDA_HOME") or "/cuda-image/usr/local/cuda-13.0"
73
+ env["CUDA_HOME"] = cuda_home
74
+ env["PATH"] = f"{cuda_home}/bin:" + env.get("PATH", "")
75
+ return env
76
+
77
+
78
+ def ensure_kernels():
79
+ """Install compiled causal-conv1d + mamba-ssm. Use cached wheels if present,
80
+ otherwise build from source and cache the wheels back into the repo."""
81
+ if _installed("causal_conv1d") and _installed("selective_scan_cuda"):
82
+ print("[build] kernels already importable", flush=True)
83
+ return
84
+
85
+ WHEELS_DIR.mkdir(exist_ok=True)
86
+ cached = sorted(glob.glob(str(WHEELS_DIR / "*.whl")))
87
+ if cached:
88
+ print(f"[build] installing cached wheels: {cached}", flush=True)
89
  try:
90
+ _pip("install", "--no-deps", "--no-build-isolation", *cached)
91
+ if _installed("causal_conv1d") and _installed("selective_scan_cuda"):
92
+ print("[build] cached wheels installed OK", flush=True)
93
+ return
94
+ print("[build] cached wheels imported incompletely; rebuilding", flush=True)
95
  except Exception as e:
96
+ print(f"[build] cached wheel install failed ({e!r}); rebuilding", flush=True)
97
+
98
+ env = _build_env()
99
+ out_dir = APP_DIR / "_wheelout"
100
+ out_dir.mkdir(exist_ok=True)
101
+
102
+ # Build wheels (no-build-isolation => uses the preinstalled torch).
103
+ t0 = time.time()
104
+ print("[build] building causal-conv1d + mamba-ssm from source (this can take ~20 min)", flush=True)
105
+ _pip("wheel", "--no-build-isolation", "--no-deps", "-w", str(out_dir),
106
+ CAUSAL_CONV1D_SPEC)
107
+ # mamba-ssm needs causal-conv1d importable during its own build; install it first.
108
+ built = sorted(glob.glob(str(out_dir / "causal_conv1d*.whl")))
109
+ _pip("install", "--no-deps", *built)
110
+ _pip("wheel", "--no-build-isolation", "--no-deps", "-w", str(out_dir),
111
+ MAMBA_SSM_SPEC)
112
+ print(f"[build] source build finished in {time.time()-t0:.0f}s", flush=True)
113
+
114
+ all_wheels = sorted(glob.glob(str(out_dir / "*.whl")))
115
+ _pip("install", "--no-deps", *all_wheels)
116
+
117
+ if not (_installed("causal_conv1d") and _installed("selective_scan_cuda")):
118
+ raise RuntimeError("kernel build completed but imports still fail")
119
+ print("[build] kernels built + installed OK", flush=True)
120
+
121
+ # Cache wheels back into the repo for fast subsequent boots.
122
  try:
123
+ for w in all_wheels:
124
+ dst = WHEELS_DIR / Path(w).name
125
+ if not dst.exists():
126
+ import shutil
127
+ shutil.copy(w, dst)
128
+ from huggingface_hub import HfApi
129
+ tok = os.environ.get("HF_TOKEN")
130
+ if tok:
131
+ HfApi(token=tok).upload_folder(
132
+ folder_path=str(WHEELS_DIR),
133
+ path_in_repo="wheels",
134
+ repo_id=REPO_ID,
135
+ repo_type="space",
136
+ commit_message="cache compiled mamba-ssm + causal-conv1d wheels",
137
+ )
138
+ print("[build] cached wheels uploaded to repo", flush=True)
139
+ else:
140
+ print("[build] no HF_TOKEN; skipping wheel cache upload", flush=True)
141
  except Exception as e:
142
+ print(f"[build] wheel cache upload skipped ({e!r})", flush=True)
 
 
143
 
 
 
 
 
144
 
145
+ ensure_kernels()
146
+
147
+ # ---- Now safe to bring in torch / model ----
148
+ import torch
149
  import gradio as gr
150
+ from huggingface_hub import snapshot_download
151
+ from transformers import AutoConfig, AutoFeatureExtractor, AutoModelForCausalLM, AutoTokenizer
152
+
153
+ from audio_utils import (
154
+ IM_END_TOKEN,
155
+ build_attention_mask,
156
+ build_prompt_template,
157
+ expand_sound_placeholder,
158
+ extract_whisper_features,
159
+ load_audio,
160
+ resolve_audio_preprocessor_path,
161
+ split_thinking,
162
+ )
163
+
164
+ MODEL_ID = "nvidia/Nemotron-Labs-Audex-30B-A3B"
165
+ SUBFOLDER = "checkpoint_folder_full"
166
+ SAMPLE_RATE = 16000
167
+
168
+ print("[load] downloading checkpoint…", flush=True)
169
+ LOCAL_DIR = snapshot_download(
170
+ MODEL_ID,
171
+ allow_patterns=[f"{SUBFOLDER}/*"],
172
+ token=os.environ.get("HF_TOKEN"),
173
+ )
174
+ MODEL_PATH = os.path.join(LOCAL_DIR, SUBFOLDER)
175
+ print(f"[load] checkpoint at {MODEL_PATH}", flush=True)
176
+
177
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
178
+ config = AutoConfig.from_pretrained(MODEL_PATH, trust_remote_code=True)
179
+ feature_extractor = AutoFeatureExtractor.from_pretrained(
180
+ resolve_audio_preprocessor_path(MODEL_PATH, config)
181
+ )
182
+ print("[load] tokenizer/config/feature_extractor loaded", flush=True)
183
+
184
+ model = AutoModelForCausalLM.from_pretrained(
185
+ MODEL_PATH,
186
+ trust_remote_code=True,
187
+ torch_dtype=torch.bfloat16,
188
+ low_cpu_mem_usage=True,
189
+ ).eval()
190
+ model = model.to("cuda")
191
+ print("[load] model loaded and moved to cuda (packed by ZeroGPU)", flush=True)
192
+
193
+ # Verify fast path is active (import-time flag inside the remote modeling module).
194
+ try:
195
+ import transformers_modules # noqa
196
+ except Exception:
197
+ pass
198
+
199
+
200
+ def _find_fast_path_flag():
201
+ for name, mod in list(sys.modules.items()):
202
+ if name.endswith("modeling_nemotron_h") and hasattr(mod, "is_fast_path_available"):
203
+ return bool(mod.is_fast_path_available)
204
+ return None
205
+
206
+
207
+ TASK_PROMPTS = {
208
+ "Describe the audio": "Describe the audio in detail.",
209
+ "Transcribe (ASR)": "Transcribe the speech in the input audio.",
210
+ "Translate speech to English": "Translate the speech in the input audio into English.",
211
+ "Answer a question about the audio": "What is being said in this audio, and what is the tone?",
212
+ }
213
+
214
+
215
+ def _estimate(audio, prompt, reasoning, max_new_tokens, *a, **k):
216
+ base = 90
217
+ return int(min(240, base + (int(max_new_tokens) / 1024.0) * 60))
218
+
219
+
220
+ @spaces.GPU(duration=_estimate, size="xlarge")
221
+ def transcribe(audio, prompt, reasoning, max_new_tokens,
222
+ temperature=0.7, top_p=0.9, top_k=0):
223
+ """Run Nemotron-Labs-Audex audio understanding / ASR / translation.
224
+
225
+ Args:
226
+ audio: path to an input audio file (wav/mp3/flac), 16 kHz mono is ideal.
227
+ prompt: natural-language instruction about the audio.
228
+ reasoning: enable the model's <think> reasoning mode.
229
+ max_new_tokens: maximum number of tokens to generate.
230
+ temperature: sampling temperature (>0).
231
+ top_p: nucleus sampling threshold in [0,1].
232
+ top_k: top-k sampling (0 disables).
233
+ Returns:
234
+ (thinking, answer) — the reasoning trace (if any) and the final answer.
235
+ """
236
+ if audio is None:
237
+ return "", "Please provide an audio input."
238
+ if not prompt or not prompt.strip():
239
+ prompt = "Describe the audio in detail."
240
+
241
+ ff = _find_fast_path_flag()
242
+ print(f"[gpu] is_fast_path_available={ff}", flush=True)
243
+
244
+ wav, sr = load_audio(audio, target_sr=SAMPLE_RATE)
245
+ input_features = extract_whisper_features(
246
+ feature_extractor, wav, sample_rate=sr,
247
+ clip_duration=float(getattr(config, "sound_clip_duration", 30.0)),
248
+ )
249
+ num_embeddings = input_features.shape[0] * int(getattr(config, "sound_embedding_size", 750))
250
+ formatted = build_prompt_template(prompt.strip(), reasoning=bool(reasoning),
251
+ prompt_repitition="none")
252
+ expanded = expand_sound_placeholder(formatted, num_embeddings)
253
+ tok = tokenizer(expanded, return_tensors="pt", add_special_tokens=False)
254
+ input_ids = tok.input_ids.to("cuda")
255
+ attention_mask = (tok.attention_mask if "attention_mask" in tok
256
+ else build_attention_mask(input_ids)).to("cuda")
257
+ input_features = input_features.to("cuda")
258
+
259
+ eos_token_id = tokenizer.convert_tokens_to_ids(IM_END_TOKEN)
260
+ if eos_token_id is None or eos_token_id == tokenizer.unk_token_id:
261
+ eos_token_id = getattr(config, "eos_token_id", None)
262
+
263
+ temperature = max(float(temperature), 1e-4)
264
+ top_p = float(top_p)
265
+ top_k = int(top_k)
266
+ do_sample = (temperature != 1.0) or (0.0 < top_p < 1.0) or (top_k > 0)
267
+ gen_kwargs = dict(
268
+ do_sample=do_sample,
269
+ eos_token_id=eos_token_id,
270
+ pad_token_id=tokenizer.pad_token_id or getattr(config, "pad_token_id", 0),
271
+ max_new_tokens=int(max_new_tokens),
272
+ )
273
+ if do_sample:
274
+ gen_kwargs["temperature"] = temperature
275
+ if top_p > 0.0:
276
+ gen_kwargs["top_p"] = top_p
277
+ if top_k > 0:
278
+ gen_kwargs["top_k"] = top_k
279
+
280
+ with torch.inference_mode():
281
+ out = model.generate(
282
+ input_ids=input_ids,
283
+ attention_mask=attention_mask,
284
+ input_features=input_features,
285
+ **gen_kwargs,
286
+ )
287
+ new_tokens = out[0, input_ids.shape[-1]:]
288
+ response = tokenizer.decode(new_tokens, skip_special_tokens=False)
289
+ response = response.split(IM_END_TOKEN, 1)[0].strip()
290
+ thinking, prediction = split_thinking(response)
291
+ return thinking, prediction
292
+
293
+
294
+ def _ui_run(audio, task, custom_prompt, reasoning, max_new_tokens, temperature, top_p):
295
+ prompt = custom_prompt.strip() if custom_prompt and custom_prompt.strip() else TASK_PROMPTS.get(task, "")
296
+ thinking, answer = transcribe(audio, prompt, reasoning, max_new_tokens,
297
+ temperature=temperature, top_p=top_p, top_k=0)
298
+ return answer, thinking
299
+
300
+
301
+ THEME = gr.themes.Citrus()
302
+
303
+ with gr.Blocks(theme=THEME, title="Nemotron-Labs-Audex-30B-A3B") as demo:
304
+ gr.Markdown(
305
+ "# 🎧 Nemotron-Labs-Audex-30B-A3B\n"
306
+ "Unified audio-text MoE (30B total / 3B active) for **audio understanding, "
307
+ "speech recognition, and speech translation**. Runs the official Hugging Face "
308
+ "inference path with compiled `mamba-ssm` + `causal-conv1d` CUDA kernels for "
309
+ "correct numerics.\n\n"
310
+ "*Non-commercial use only (NVIDIA One-Way Noncommercial License).*"
311
+ )
312
+ with gr.Row():
313
+ with gr.Column():
314
+ audio_in = gr.Audio(type="filepath", label="Input audio", sources=["upload", "microphone"])
315
+ task = gr.Radio(
316
+ choices=list(TASK_PROMPTS.keys()),
317
+ value="Transcribe (ASR)",
318
+ label="Task",
319
+ )
320
+ custom_prompt = gr.Textbox(
321
+ label="Custom instruction (optional — overrides Task)",
322
+ placeholder="e.g. Summarize what the speaker is saying.",
323
+ lines=2,
324
+ )
325
+ run_btn = gr.Button("Run", variant="primary")
326
+ with gr.Accordion("Advanced options", open=False):
327
+ reasoning = gr.Checkbox(value=False, label="Enable reasoning (<think>) mode")
328
+ max_new_tokens = gr.Slider(16, 1024, value=256, step=16, label="Max new tokens")
329
+ temperature = gr.Slider(0.1, 1.5, value=0.7, step=0.05, label="Temperature")
330
+ top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p")
331
+ with gr.Column():
332
+ answer_out = gr.Textbox(label="Answer", lines=10)
333
+ thinking_out = gr.Textbox(label="Reasoning trace (if enabled)", lines=6)
334
+
335
+ run_btn.click(
336
+ _ui_run,
337
+ inputs=[audio_in, task, custom_prompt, reasoning, max_new_tokens, temperature, top_p],
338
+ outputs=[answer_out, thinking_out],
339
+ )
340
 
341
+ gr.Examples(
342
+ examples=[
343
+ ["examples/mlk_speech.wav", "Transcribe (ASR)", "", False, 256, 0.7, 0.9],
344
+ ["examples/sample_speech.wav", "Transcribe (ASR)", "", False, 256, 0.7, 0.9],
345
+ ["examples/mlk_speech.wav", "Describe the audio", "", False, 256, 0.7, 0.9],
346
+ ],
347
+ inputs=[audio_in, task, custom_prompt, reasoning, max_new_tokens, temperature, top_p],
348
+ outputs=[answer_out, thinking_out],
349
+ fn=_ui_run,
350
+ cache_examples=False,
351
+ run_on_click=True,
352
+ )
353
 
354
+ if __name__ == "__main__":
355
+ demo.queue(max_size=8).launch(ssr_mode=False, mcp_server=True)
audio_utils.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import math
19
+ import os
20
+ from pathlib import Path
21
+ from typing import Iterable, Optional
22
+
23
+ import numpy as np
24
+ import torch
25
+
26
+
27
+ SOUND_PLACEHOLDER = "<sound>"
28
+ SOUND_TOKEN = "<so_embedding>"
29
+ SOUND_START_TOKEN = "<so_start>"
30
+ SOUND_END_TOKEN = "<so_end>"
31
+ IM_END_TOKEN = "<|im_end|>"
32
+ DEFAULT_SYSTEM_PROMPT = (
33
+ "<|im_start|>system\n"
34
+ "You are a helpful and harmless assistant.\n\n"
35
+ "You are not allowed to use any tools."
36
+ "<|im_end|>\n"
37
+ )
38
+
39
+
40
+ def strip_hf_prefix(path: str) -> str:
41
+ """Convert Megatron-style hf:// paths into local filesystem paths."""
42
+ return path[len("hf://") :] if path.startswith("hf://") else path
43
+
44
+
45
+ def load_audio(audio_path: str, target_sr: int = 16000) -> tuple[np.ndarray, int]:
46
+ import librosa
47
+
48
+ audio_data, sr = librosa.load(audio_path, sr=target_sr, mono=True)
49
+ return normalize_audio(audio_data), sr
50
+
51
+
52
+ def normalize_audio(audio: np.ndarray) -> np.ndarray:
53
+ """Return mono float32 audio in [-1, 1], matching the Megatron eval path."""
54
+ audio = np.asarray(audio)
55
+ if audio.ndim == 2:
56
+ if audio.shape[1] <= 2:
57
+ audio = audio.mean(axis=1)
58
+ elif audio.shape[0] <= 2:
59
+ audio = audio.mean(axis=0)
60
+ else:
61
+ raise ValueError(f"Unsupported audio shape: {audio.shape}")
62
+
63
+ if audio.dtype == np.int16:
64
+ audio = audio.astype(np.float32) / 32768.0
65
+ elif audio.dtype != np.float32:
66
+ audio = audio.astype(np.float32)
67
+
68
+ max_abs = float(np.abs(audio).max()) if audio.size else 0.0
69
+ if max_abs > 1.0:
70
+ audio = audio / max_abs
71
+ return audio.astype(np.float32, copy=False)
72
+
73
+
74
+ def split_audio_into_clips(
75
+ audio: np.ndarray,
76
+ sample_rate: int = 16000,
77
+ clip_duration: float = 30.0,
78
+ ) -> list[np.ndarray]:
79
+ """Split audio into fixed 30s clips; keep a padded final clip for Whisper."""
80
+ audio = normalize_audio(audio)
81
+ clip_samples = int(round(sample_rate * clip_duration))
82
+ if clip_samples <= 0:
83
+ raise ValueError(f"Invalid clip_samples: {clip_samples}")
84
+ if audio.size == 0:
85
+ audio = np.zeros(1, dtype=np.float32)
86
+
87
+ num_clips = max(1, math.ceil(audio.shape[0] / clip_samples))
88
+ clips: list[np.ndarray] = []
89
+ for idx in range(num_clips):
90
+ start = idx * clip_samples
91
+ clip = audio[start : start + clip_samples]
92
+ if clip.shape[0] < clip_samples:
93
+ clip = np.pad(clip, (0, clip_samples - clip.shape[0]))
94
+ clips.append(clip.astype(np.float32, copy=False))
95
+ return clips
96
+
97
+
98
+ def extract_whisper_features(
99
+ feature_extractor,
100
+ audio: np.ndarray,
101
+ sample_rate: int = 16000,
102
+ clip_duration: float = 30.0,
103
+ ) -> torch.Tensor:
104
+ """Return NV-Whisper input features shaped (num_clips, 128, 3000)."""
105
+ clips = split_audio_into_clips(audio, sample_rate=sample_rate, clip_duration=clip_duration)
106
+ features = feature_extractor(
107
+ clips,
108
+ sampling_rate=sample_rate,
109
+ return_tensors="pt",
110
+ padding="max_length",
111
+ return_attention_mask=False,
112
+ )
113
+ input_features = features.input_features
114
+ if input_features.ndim != 3:
115
+ raise ValueError(f"Expected 3D Whisper features, got {tuple(input_features.shape)}")
116
+ return input_features
117
+
118
+
119
+ def parse_conversation(conversation: list[dict]) -> tuple[str, str]:
120
+ human_prompt = ""
121
+ gt_answer = ""
122
+ for turn in conversation:
123
+ if turn["from"] == "human":
124
+ human_prompt = turn["value"].replace("<sound>\n", "").replace("<sound>", "").strip()
125
+ elif turn["from"] == "gpt":
126
+ gt_answer = turn["value"]
127
+ return human_prompt, gt_answer
128
+
129
+
130
+ def build_prompt_template(
131
+ prompt: str,
132
+ reasoning: bool = False,
133
+ prompt_repitition: str = "none",
134
+ ) -> str:
135
+ if prompt_repitition not in {"none", "repetition"}:
136
+ raise ValueError(f"Unknown prompt repetition mode: {prompt_repitition}")
137
+ if prompt_repitition == "repetition":
138
+ prompt = f"{prompt}\n{prompt}"
139
+
140
+ if reasoning:
141
+ return f"<|im_start|>user\n<sound>\n{prompt}<|im_end|>\n<|im_start|>assistant\n<think>\n"
142
+ return f"<|im_start|>user\n<sound>\n{prompt}<|im_end|>\n<|im_start|>assistant\n<think></think>"
143
+
144
+
145
+ def expand_sound_placeholder(prompt: str, num_embeddings: int) -> str:
146
+ if prompt.count(SOUND_PLACEHOLDER) != 1:
147
+ raise ValueError(f"Expected exactly one {SOUND_PLACEHOLDER}, found {prompt.count(SOUND_PLACEHOLDER)}")
148
+ replacement = SOUND_START_TOKEN + (SOUND_TOKEN * num_embeddings) + SOUND_END_TOKEN
149
+ return prompt.replace(SOUND_PLACEHOLDER, replacement)
150
+
151
+
152
+ def build_attention_mask(input_ids: torch.Tensor) -> torch.Tensor:
153
+ return torch.ones_like(input_ids, dtype=torch.long)
154
+
155
+
156
+ def split_thinking(response: str) -> tuple[str, str]:
157
+ if "</think>" not in response:
158
+ return "", response.strip()
159
+ thinking = response.rsplit("</think>", 1)[0].strip() + "</think>"
160
+ prediction = response.rsplit("</think>", 1)[1].strip()
161
+ return thinking, prediction
162
+
163
+
164
+ def save_results_jsonl(results: Iterable[dict], output_path: str) -> None:
165
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
166
+ with open(output_path, "w", encoding="utf-8") as f:
167
+ for result in results:
168
+ f.write(json.dumps(result, ensure_ascii=False) + "\n")
169
+
170
+
171
+ def resolve_audio_preprocessor_path(model_path: str, config) -> str:
172
+ path = getattr(config, "audio_preprocessor_path", None) or "audio_preprocessor"
173
+ candidate = Path(path)
174
+ if not candidate.is_absolute():
175
+ candidate = Path(model_path) / candidate
176
+ return str(candidate)
examples/mlk_speech.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:711eb85a61be58cdaa901b03e28edb8400541dc86015f1aa9aab0a56c2a799f2
3
+ size 416044
examples/sample_speech.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0b1785dba56f22af426ccb25d318f7e103558fd40e1c3ab064b455dba2afae12
3
+ size 333964
requirements.txt CHANGED
@@ -1 +1,12 @@
1
- transformers
 
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers>=4.53.3
2
+ accelerate
3
+ sentencepiece
4
+ librosa
5
+ soundfile
6
+ numpy
7
+ einops
8
+ # build-time deps for compiling mamba-ssm + causal-conv1d from source (no-build-isolation)
9
+ ninja
10
+ packaging
11
+ setuptools
12
+ wheel