Skip to content

Commit 4bf283e

Browse files
committed
Standardize audio manifest container paths
Add shared helpers (build_container_audio_path, get_container_audio_root) and thread --audio-prefix through the audio benchmark prepare scripts so prepared JSONL manifests use in-container paths rooted at /data (overridable via --audio-prefix or NEMO_SKILLS_AUDIO_ROOT) instead of hardcoded /dataset paths. Document the prepare/eval mount contract and add tests covering the path helpers and per-benchmark manifest paths. Signed-off-by: Dongji Gao <dongjig@nvidia.com>
1 parent da85a88 commit 4bf283e

12 files changed

Lines changed: 693 additions & 69 deletions

File tree

docs/evaluation/speech-audio.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,40 @@ MMAU-Pro (Multimodal Audio Understanding - Pro) is a comprehensive benchmark for
3737

3838
These benchmarks require audio files for meaningful evaluation. **Audio files are downloaded by default** to ensure proper evaluation.
3939

40+
### Audio path convention
41+
42+
Prepared audio manifests should write audio paths using the in-container audio
43+
root, not the host filesystem path. The default in-container root is `/data`.
44+
Override it with `--audio-prefix` when a different mount point is needed; if
45+
`--audio-prefix` is omitted, prepare scripts fall back to
46+
`NEMO_SKILLS_AUDIO_ROOT` and then `/data`.
47+
48+
`--audio-prefix` is the global in-container audio root. Do not include the
49+
benchmark name in it; the prepare script appends the benchmark directory.
50+
For example, use `--audio-prefix /data`, not
51+
`--audio-prefix /data/contextasr-bench`.
52+
53+
For example, preparing data with `--audio-prefix /data` writes manifest paths
54+
like:
55+
56+
```text
57+
/data/asr-leaderboard/...
58+
/data/contextasr-bench/...
59+
```
60+
61+
At evaluation time, mount the host prepared-data directory to the same
62+
in-container root:
63+
64+
```bash
65+
ns eval ... --data_dir=/lustre/.../skills_data --mount-paths=/lustre/.../skills_data:/data
66+
```
67+
4068
### Data Preparation
4169

4270
To prepare the dataset with audio files:
4371

4472
```bash
45-
ns prepare_data asr-leaderboard --data_dir=/path/to/data --cluster=<cluster>
73+
ns prepare_data asr-leaderboard --data_dir=/path/to/data --cluster=<cluster> --audio-prefix=/data
4674
```
4775

4876
Prepare specific datasets only:
@@ -74,7 +102,7 @@ eval(
74102
model="/workspace/checkpoint",
75103
server_entrypoint="/workspace/megatron-lm/server.py",
76104
server_container="/path/to/container.sqsh",
77-
data_dir="/dataset",
105+
data_dir="/data",
78106
installation_command="pip install -r requirements/audio.txt",
79107
server_args="--inference-max-requests 1 --model-config /workspace/checkpoint/config.yaml",
80108
)
@@ -98,7 +126,7 @@ eval(benchmarks="asr-leaderboard", split="librispeech_clean", ...)
98126
--model=/workspace/path/to/checkpoint \
99127
--server_entrypoint=/workspace/megatron-lm/server.py \
100128
--server_container=/path/to/container.sqsh \
101-
--data_dir=/dataset \
129+
--data_dir=/data \
102130
--installation_command="pip install -r requirements/audio.txt"
103131
```
104132

@@ -120,7 +148,7 @@ eval(
120148
model="/workspace/checkpoint",
121149
server_entrypoint="/workspace/megatron-lm/server.py",
122150
server_container="/path/to/container.sqsh",
123-
data_dir="/dataset",
151+
data_dir="/data",
124152
installation_command="pip install sacrebleu",
125153
server_args="--inference-max-requests 1 --model-config /workspace/checkpoint/config.yaml",
126154
)
@@ -150,7 +178,7 @@ eval(benchmarks="mmau-pro.closed_form", ...)
150178
--model=/workspace/path/to/checkpoint \
151179
--server_entrypoint=/workspace/megatron-lm/server.py \
152180
--server_container=/path/to/container.sqsh \
153-
--data_dir=/dataset \
181+
--data_dir=/data \
154182
--installation_command="pip install sacrebleu"
155183
```
156184

@@ -534,7 +562,7 @@ used directly. If the file is missing, data is downloaded there automatically.
534562
To use a custom audio path prefix (e.g., for container mount points):
535563

536564
```bash
537-
ns prepare_data contextasr-bench --data_dir=/path/to/ContextASR-Bench --audio-prefix /data/contextasr
565+
ns prepare_data contextasr-bench --data_dir=/path/to/ContextASR-Bench --audio-prefix /data
538566
```
539567

540568
### Running ContextASR-Bench Evaluation

nemo_skills/dataset/asr-leaderboard/prepare.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
same data source used by the official leaderboard, ensuring apples-to-apples
2020
WER comparison.
2121
22-
Audio paths in JSONL: /dataset/asr-leaderboard/data/{dataset}/{sample_id}.flac
22+
Audio paths in JSONL: {audio_prefix}/asr-leaderboard/data/{dataset}/{sample_id}.flac
2323
2424
Usage:
2525
ns prepare_data asr-leaderboard
@@ -36,6 +36,12 @@
3636
from datasets import Audio, load_dataset
3737
from tqdm import tqdm
3838

39+
from nemo_skills.dataset.utils import (
40+
DEFAULT_CONTAINER_AUDIO_ROOT,
41+
build_container_audio_path,
42+
get_container_audio_root,
43+
)
44+
3945
HF_REPO = "hf-audio/esb-datasets-test-only-sorted"
4046
SYSTEM_MESSAGE = "You are a helpful assistant. /no_think"
4147
AUDIO_SAMPLE_RATE = 16000
@@ -69,7 +75,9 @@ def extract_audio(audio_info):
6975
return None, None
7076

7177

72-
def format_entry(entry, dataset_name, audio_dir, text_field, id_field, with_audio):
78+
def format_entry(
79+
entry, dataset_name, audio_dir, text_field, id_field, with_audio, audio_root=DEFAULT_CONTAINER_AUDIO_ROOT
80+
):
7381
"""Format a dataset entry into JSONL and optionally save the audio file."""
7482
text = entry[text_field].strip()
7583
if not text:
@@ -87,7 +95,11 @@ def format_entry(entry, dataset_name, audio_dir, text_field, id_field, with_audi
8795
sf.write(str(audio_dir / audio_filename), audio_array, sampling_rate)
8896

8997
user_message = {"role": "user", "content": "Transcribe the following audio."}
90-
audio_meta = {"path": f"/dataset/asr-leaderboard/data/{dataset_name}/{audio_filename}"}
98+
audio_meta = {
99+
"path": build_container_audio_path(
100+
"asr-leaderboard", "data", dataset_name, audio_filename, audio_prefix=audio_root
101+
)
102+
}
91103
if duration is not None:
92104
audio_meta["duration"] = float(duration)
93105
user_message["audio"] = audio_meta
@@ -105,7 +117,7 @@ def format_entry(entry, dataset_name, audio_dir, text_field, id_field, with_audi
105117
return formatted
106118

107119

108-
def prepare_dataset(dataset_name, output_dir, with_audio=True):
120+
def prepare_dataset(dataset_name, output_dir, with_audio=True, audio_root=DEFAULT_CONTAINER_AUDIO_ROOT):
109121
"""Download, decode, and write a single ASR dataset to JSONL + audio files."""
110122
if dataset_name not in DATASET_CONFIGS:
111123
raise ValueError(f"Unknown dataset: {dataset_name}. Available: {list(DATASET_CONFIGS.keys())}")
@@ -127,7 +139,9 @@ def prepare_dataset(dataset_name, output_dir, with_audio=True):
127139
count = 0
128140
with open(output_file, "w", encoding="utf-8") as fout:
129141
for entry in tqdm(dataset, desc=dataset_name):
130-
formatted = format_entry(entry, dataset_name, audio_dir, text_field, id_field, with_audio)
142+
formatted = format_entry(
143+
entry, dataset_name, audio_dir, text_field, id_field, with_audio, audio_root=audio_root
144+
)
131145
if formatted is None:
132146
continue
133147
fout.write(json.dumps(formatted) + "\n")
@@ -146,26 +160,48 @@ def main():
146160
choices=list(DATASET_CONFIGS.keys()) + ["all"],
147161
help="Datasets to prepare (default: all)",
148162
)
163+
parser.add_argument(
164+
"--data_dir",
165+
type=str,
166+
default=None,
167+
help=(
168+
"Output directory. If provided, outputs go under <data_dir>/asr-leaderboard. "
169+
"If omitted, writes into this package's dataset directory."
170+
),
171+
)
149172
parser.add_argument(
150173
"--no-audio",
151174
action="store_true",
152175
help="Skip saving audio files (JSONL still includes audio paths)",
153176
)
177+
parser.add_argument(
178+
"--audio-prefix",
179+
type=str,
180+
default=None,
181+
help="In-container audio root written into JSONL paths. Defaults to $NEMO_SKILLS_AUDIO_ROOT or /data.",
182+
)
154183
args = parser.parse_args()
155184

156-
data_dir = Path("/dataset/asr-leaderboard")
157-
output_dir = data_dir if data_dir.exists() else Path(__file__).parent
185+
if args.data_dir:
186+
output_dir = Path(args.data_dir) / "asr-leaderboard"
187+
else:
188+
output_dir = Path(__file__).parent
158189
output_dir.mkdir(parents=True, exist_ok=True)
159190

160191
with_audio = not args.no_audio
161-
if not with_audio:
192+
audio_root = get_container_audio_root(args.audio_prefix)
193+
194+
if args.no_audio:
162195
print("Running without saving audio files.")
196+
else:
197+
print("Running with audio. Saving to data/{dataset}/")
198+
print(f"Audio paths in JSONL will use: {audio_root}/asr-leaderboard/data/...")
163199

164200
datasets_to_prepare = list(DATASET_CONFIGS.keys()) if "all" in args.datasets else args.datasets
165201

166202
total_samples = 0
167203
for dataset_name in datasets_to_prepare:
168-
total_samples += prepare_dataset(dataset_name, output_dir, with_audio=with_audio)
204+
total_samples += prepare_dataset(dataset_name, output_dir, with_audio=with_audio, audio_root=audio_root)
169205

170206
combined_file = output_dir / "test.jsonl"
171207
print(f"\nCreating combined file: {combined_file}")

nemo_skills/dataset/audiobench/prepare.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
import soundfile as sf
3636
from tqdm import tqdm
3737

38+
from nemo_skills.dataset.utils import (
39+
DEFAULT_CONTAINER_AUDIO_ROOT,
40+
build_container_audio_path,
41+
get_container_audio_root,
42+
)
43+
3844
# AudioBench datasets categorized by evaluation type
3945
JUDGE_DATASETS = [
4046
"alpaca_audio_test",
@@ -140,6 +146,7 @@ def create_manifest_entry(
140146
dataset_name: str,
141147
sample_id: int,
142148
category: str,
149+
audio_root: str = DEFAULT_CONTAINER_AUDIO_ROOT,
143150
) -> Dict:
144151
"""Create a nemo-skills compatible manifest entry.
145152
@@ -158,9 +165,9 @@ def create_manifest_entry(
158165
reference = sample.get("reference", sample.get("answer", ""))
159166
task_type = sample.get("task_type", "unknown")
160167

161-
# Create absolute audio path with /data/ prefix for cluster deployment
162-
# Format: /data/audiobench/{category}/audio/{dataset_name}/{filename}
163-
audio_rel_path = f"/data/audiobench/{category}/audio/{dataset_name}/{audio_filename}"
168+
audio_rel_path = build_container_audio_path(
169+
"audiobench", category, "audio", dataset_name, audio_filename, audio_prefix=audio_root
170+
)
164171

165172
# Create audio metadata (both singular and plural forms for compatibility)
166173
audio_metadata = {"path": audio_rel_path, "duration": duration}
@@ -209,6 +216,7 @@ def process_dataset(
209216
save_audio: bool = True,
210217
split: str = "test",
211218
max_samples: int = -1,
219+
audio_root: str = DEFAULT_CONTAINER_AUDIO_ROOT,
212220
) -> tuple[int, List[Dict]]:
213221
"""Process a single AudioBench dataset.
214222
@@ -459,6 +467,7 @@ def process_dataset(
459467
dataset_name=dataset_name,
460468
sample_id=idx,
461469
category=category,
470+
audio_root=audio_root,
462471
)
463472

464473
manifest_entries.append(entry)
@@ -519,6 +528,12 @@ def main():
519528
default=-1,
520529
help="Maximum number of samples to process per dataset (-1 for all)",
521530
)
531+
parser.add_argument(
532+
"--audio-prefix",
533+
type=str,
534+
default=None,
535+
help="In-container audio root written into JSONL paths. Defaults to $NEMO_SKILLS_AUDIO_ROOT or /data.",
536+
)
522537
parser.set_defaults(save_audio=True)
523538

524539
args = parser.parse_args()
@@ -531,6 +546,7 @@ def main():
531546
output_dir = Path(__file__).parent
532547

533548
output_dir.mkdir(parents=True, exist_ok=True)
549+
audio_root = get_container_audio_root(args.audio_prefix)
534550

535551
print("\n" + "=" * 60)
536552
print("AudioBench Dataset Preparation")
@@ -539,6 +555,7 @@ def main():
539555
print(f"Output directory: {output_dir}")
540556
print(f"Save audio files: {args.save_audio}")
541557
print(f"Split: {args.split}")
558+
print(f"Audio paths in JSONL will use: {audio_root}/audiobench/...")
542559
print("=" * 60 + "\n")
543560

544561
# Determine which datasets to process
@@ -585,6 +602,7 @@ def main():
585602
save_audio=args.save_audio,
586603
split=args.split,
587604
max_samples=args.max_samples,
605+
audio_root=audio_root,
588606
)
589607
total_samples += num_samples
590608
total_datasets += 1

nemo_skills/dataset/contextasr-bench/prepare.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
import tarfile
4040
from pathlib import Path
4141

42+
from nemo_skills.dataset.utils import build_container_audio_path
43+
44+
BENCHMARK_NAME = "contextasr-bench"
4245
HF_REPO_ID = "MrSupW/ContextASR-Bench"
4346
JSONL_FILENAME = "ContextASR-Speech_English.jsonl"
4447
AUDIO_TAR_PREFIX = "audio/ContextASR-Speech/English/ContextASR-Speech_English"
@@ -142,6 +145,11 @@ def build_messages(prompt_text, audio_path, duration):
142145
]
143146

144147

148+
def resolve_audio_prefix(audio_prefix: str | None = None) -> str:
149+
"""Return the in-container ContextASR audio prefix used in JSONL paths."""
150+
return build_container_audio_path(BENCHMARK_NAME, audio_prefix=audio_prefix)
151+
152+
145153
def format_entry(sample, mode, audio_prefix):
146154
"""Format a single dataset sample into a JSONL record for a given mode."""
147155
audio_path = f"{audio_prefix}/{sample['audio']}"
@@ -191,8 +199,9 @@ def main():
191199
default=None,
192200
help=(
193201
"Override audio path prefix written into JSONL files. "
194-
"Defaults to the data_dir value. Useful for container mount points "
195-
"(e.g., --audio-prefix /data/contextasr-bench)."
202+
"This is the global in-container audio root; the script appends "
203+
f"{BENCHMARK_NAME}/. Defaults to $NEMO_SKILLS_AUDIO_ROOT or /data "
204+
f"(e.g., --audio-prefix /data writes /data/{BENCHMARK_NAME}/...)."
196205
),
197206
)
198207
parser.add_argument(
@@ -220,8 +229,7 @@ def main():
220229
print(f"Data not found at {data_dir}. Downloading there...")
221230
download_dataset(data_dir)
222231

223-
audio_prefix = args.audio_prefix if args.audio_prefix else str(data_dir)
224-
audio_prefix = audio_prefix.rstrip("/")
232+
audio_prefix = resolve_audio_prefix(args.audio_prefix)
225233

226234
jsonl_path = data_dir / JSONL_FILENAME
227235

@@ -236,14 +244,15 @@ def main():
236244
print(f"Loaded {len(samples)} samples")
237245

238246
if not args.no_audio:
239-
sample_audio = Path(audio_prefix) / samples[0]["audio"]
247+
sample_audio = data_dir / samples[0]["audio"]
240248
if not sample_audio.exists():
241249
print(
242250
f"WARNING: Sample audio file not found at {sample_audio}. "
243-
f"Audio paths may need adjustment via --audio-prefix."
251+
f"Audio paths may need adjustment via --data_dir."
244252
)
245253
else:
246254
print(f"Audio files verified (sample check: {sample_audio})")
255+
print(f"Audio paths in JSONL will use: {audio_prefix}/...")
247256

248257
modes = {
249258
"contextless": output_dir / "contextless" / "test.jsonl",

0 commit comments

Comments
 (0)