Skip to content

Commit bbbc066

Browse files
committed
Merge remote-tracking branch 'origin/main' into speech/apptek-callcenter-dialogues
2 parents 4cc10e0 + 5435e1d commit bbbc066

26 files changed

Lines changed: 1423 additions & 312 deletions

File tree

docs/evaluation/speech-audio.md

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -663,15 +663,15 @@ Per-domain breakdowns are included automatically based on the `domain_label` fie
663663

664664
CoVoST 2 is a large-scale multilingual corpus for speech recognition (ASR) and speech translation (AST), built on Common Voice audio with translation references from Facebook's [CoVoST v2](https://github.com/facebookresearch/covost) release.
665665

666-
**Tasks:** ASR (monolingual transcription) and AST (X→en / en→X translation)
666+
**Subtasks:** `covost2.asr` (monolingual transcription) and `covost2.st` (X→en / en→X translation)
667667

668668
**Splits:** `validation`, `test`
669669

670670
For non-alphabetic scripts (`zh-CN`, `ja`), evaluation reports Character Error Rate (CER) instead of Word Error Rate (WER); the choice is made per-sample via the `use_cer` flag set during data preparation.
671671

672672
### Dataset Location
673673

674-
- Benchmark is defined in [`nemo_skills/dataset/covost2/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/covost2/__init__.py)
674+
- Benchmark group is defined in [`nemo_skills/dataset/covost2/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/covost2/__init__.py); per-subtask config lives in `covost2/asr/__init__.py` and `covost2/st/__init__.py`.
675675
- Original benchmark source is hosted on [GitHub](https://github.com/facebookresearch/covost)
676676

677677
### Preparing CoVoST 2 Data
@@ -687,73 +687,63 @@ Unlike most other benchmarks on this page, **CoVoST 2 does not auto-download aud
687687

688688
and the corresponding `validated.tsv` (columns: `path, split, lang, sentence`).
689689

690-
The `--languages` flag selects which CoVoST 2 languages are prepared. For ASR it filters the source-language audio that is transcribed; for AST every valid X→en / en→X pair touching the listed languages is included. Omit it to prepare all 21 supported languages.
690+
A single `prepare_data covost2` run produces both subtasks at once — `covost2/asr/{split}.jsonl` and `covost2/st/{split}.jsonl`. There is no `--task` flag; pass the parent group name (`covost2`), not the dotted subtask names.
691691

692-
=== "ASR"
692+
The `--languages` flag selects which CoVoST 2 languages are prepared. For ASR it filters the source-language audio that is transcribed; for ST every valid X→en / en→X pair touching the listed languages is included. Omit it to prepare all 21 supported languages.
693693

694-
```bash
695-
ns prepare_data covost2 \
696-
--data_dir /path/to/data \
697-
--cluster <cluster_name> \
698-
--task ASR \
699-
--languages de fr \
700-
--split test \
701-
--cv_data_dir /workspace/datasets/covost2 \
702-
--validated_tsv /workspace/datasets/covost2/validated.tsv
703-
```
704-
705-
=== "AST"
694+
```bash
695+
ns prepare_data covost2 \
696+
--data_dir /path/to/data \
697+
--cluster <cluster_name> \
698+
--languages de fr es \
699+
--split test \
700+
--cv_data_dir /workspace/datasets/covost2 \
701+
--validated_tsv /workspace/datasets/covost2/validated.tsv
702+
```
706703

707-
```bash
708-
ns prepare_data covost2 \
709-
--data_dir /path/to/data \
710-
--cluster <cluster_name> \
711-
--task AST \
712-
--languages de fr es \
713-
--split test \
714-
--cv_data_dir /workspace/datasets/covost2 \
715-
--validated_tsv /workspace/datasets/covost2/validated.tsv
716-
```
704+
Output:
717705

718-
Each `--task` produces a separate manifest: `{split}-asr.jsonl` or `{split}-ast.jsonl` (e.g. `test-asr.jsonl`).
706+
```
707+
<data_dir>/covost2/
708+
asr/test.jsonl # one record per (lang, audio) for transcription
709+
st/test.jsonl # one record per (src→tgt, audio) for translation
710+
audio/<lang>/<split>/...wav
711+
```
719712

720713
## FLEURS
721714

722-
[FLEURS](https://huggingface.co/datasets/google/fleurs) (Few-shot Learning Evaluation of Universal Representations of Speech) is Google's multilingual speech benchmark covering 102 locales. It supports both ASR and AST.
715+
[FLEURS](https://huggingface.co/datasets/google/fleurs) (Few-shot Learning Evaluation of Universal Representations of Speech) is Google's multilingual speech benchmark covering 102 locales.
716+
717+
**Subtasks:** `fleurs.asr` (monolingual transcription) and `fleurs.st` (`en_us` → locale and locale → `en_us` translation)
723718

724719
**Splits:** `train`, `dev`, `test`
725720

726721
CER (rather than WER) is used for these locales: `cmn_hans_cn`, `yue_hant_hk`, `ja_jp`, `th_th`, `lo_la`, `my_mm`, `km_kh`, `ko_kr`, `vi_vn`.
727722

728723
### Dataset Location
729724

730-
- Benchmark is defined in [`nemo_skills/dataset/fleurs/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/fleurs/__init__.py)
725+
- Benchmark group is defined in [`nemo_skills/dataset/fleurs/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/fleurs/__init__.py); per-subtask config lives in `fleurs/asr/__init__.py` and `fleurs/st/__init__.py`.
731726
- Original dataset is hosted on [HuggingFace](https://huggingface.co/datasets/google/fleurs)
732727

733728
### Preparing FLEURS Data
734729

735-
Audio is downloaded automatically from HuggingFace. As with CoVoST 2, `--task` produces `{split}-asr.jsonl` or `{split}-ast.jsonl`.
730+
Audio is downloaded automatically from HuggingFace. A single `prepare_data fleurs` run produces both subtasks at once — `fleurs/asr/{split}.jsonl` and `fleurs/st/{split}.jsonl`. There is no `--task` flag; pass the parent group name (`fleurs`), not the dotted subtask names.
736731

737-
The `--languages` flag selects which FLEURS locales are prepared. For ASR it filters the source-language audio that is transcribed; for AST every (`en_us` → locale) and (locale → `en_us`) pair across the listed locales is included. Omit it to prepare all 102 locales.
732+
The `--languages` flag selects which FLEURS locales are prepared. ASR records are emitted for the listed locales. ST records are emitted for every (`en_us` → locale) and (locale → `en_us`) pair touching the listed locales — even if `en_us` is not itself in `--languages`, since it is the pivot. Omit `--languages` to prepare all 102 locales.
738733

739-
=== "ASR"
740-
741-
```bash
742-
ns prepare_data fleurs \
743-
--data_dir /path/to/data \
744-
--cluster <cluster_name> \
745-
--task ASR \
746-
--languages en_us de_de fr_fr \
747-
--split test
748-
```
734+
```bash
735+
ns prepare_data fleurs \
736+
--data_dir /path/to/data \
737+
--cluster <cluster_name> \
738+
--languages en_us de_de fr_fr es_419 it_it ja_jp \
739+
--split test
740+
```
749741

750-
=== "AST"
742+
Output:
751743

752-
```bash
753-
ns prepare_data fleurs \
754-
--data_dir /path/to/data \
755-
--cluster <cluster_name> \
756-
--task AST \
757-
--languages en_us de_de fr_fr es_419 it_it ja_jp \
758-
--split test
759-
```
744+
```
745+
<data_dir>/fleurs/
746+
asr/test.jsonl # one record per (locale, audio) for transcription
747+
st/test.jsonl # one record per (src→tgt, audio) for translation
748+
audio/<locale>/...wav
749+
```

nemo_skills/dataset/asr-leaderboard/prepare.py

Lines changed: 57 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,121 +16,122 @@
1616
1717
Downloads and formats datasets from the official HF Open ASR Leaderboard ESB
1818
test-only sorted dataset (hf-audio/esb-datasets-test-only-sorted). This is the
19-
same data source used by the official leaderboard and the offline NeMo eval
20-
pipeline, ensuring apples-to-apples WER comparison.
19+
same data source used by the official leaderboard, ensuring apples-to-apples
20+
WER comparison.
2121
2222
Audio paths in JSONL: /dataset/asr-leaderboard/data/{dataset}/{sample_id}.flac
2323
2424
Usage:
2525
ns prepare_data asr-leaderboard
2626
ns prepare_data asr-leaderboard --datasets librispeech_clean ami
27-
ns prepare_data asr-leaderboard --datasets earnings22
28-
ns prepare_data asr-leaderboard --no-audio # skip saving audio files
27+
ns prepare_data asr-leaderboard --no-audio
2928
"""
3029

3130
import argparse
3231
import json
3332
from pathlib import Path
3433

34+
import numpy as np
3535
import soundfile as sf
36-
from datasets import load_dataset
36+
from datasets import Audio, load_dataset
3737
from tqdm import tqdm
3838

39+
HF_REPO = "hf-audio/esb-datasets-test-only-sorted"
3940
SYSTEM_MESSAGE = "You are a helpful assistant. /no_think"
40-
MIN_AUDIO_DURATION = 0.1 # Skip audio shorter than this (causes mel spectrogram errors)
41+
AUDIO_SAMPLE_RATE = 16000
4142

42-
# (hf_repo, config, split, text_field, id_field)
43+
# (config, split, text_field, id_field)
4344
DATASET_CONFIGS = {
44-
"librispeech_clean": ("hf-audio/esb-datasets-test-only-sorted", "librispeech", "test.clean", "text", "id"),
45-
"librispeech_other": ("hf-audio/esb-datasets-test-only-sorted", "librispeech", "test.other", "text", "id"),
46-
"voxpopuli": ("hf-audio/esb-datasets-test-only-sorted", "voxpopuli", "test", "text", "id"),
47-
"tedlium": ("hf-audio/esb-datasets-test-only-sorted", "tedlium", "test", "text", "id"),
48-
"gigaspeech": ("hf-audio/esb-datasets-test-only-sorted", "gigaspeech", "test", "text", "id"),
49-
"spgispeech": ("hf-audio/esb-datasets-test-only-sorted", "spgispeech", "test", "text", "id"),
50-
"earnings22": ("hf-audio/esb-datasets-test-only-sorted", "earnings22", "test", "text", "id"),
51-
"ami": ("hf-audio/esb-datasets-test-only-sorted", "ami", "test", "text", "id"),
45+
"librispeech_clean": ("librispeech", "test.clean", "text", "id"),
46+
"librispeech_other": ("librispeech", "test.other", "text", "id"),
47+
"voxpopuli": ("voxpopuli", "test", "text", "id"),
48+
"tedlium": ("tedlium", "test", "text", "id"),
49+
"gigaspeech": ("gigaspeech", "test", "text", "id"),
50+
"spgispeech": ("spgispeech", "test", "text", "id"),
51+
"earnings22": ("earnings22", "test", "text", "id"),
52+
"ami": ("ami", "test", "text", "id"),
5253
}
5354

5455

55-
def save_audio_and_format_entry(
56-
entry, dataset_name, audio_dir, sample_idx, text_field="text", id_field="id", with_audio=True
57-
):
58-
"""Format a dataset entry and optionally save audio file."""
59-
text = entry[text_field].strip()
56+
def extract_audio(audio_info):
57+
"""Extract audio array and sampling rate from a HF dataset audio entry.
6058
61-
system_message = {"role": "system", "content": SYSTEM_MESSAGE}
62-
user_message = {"role": "user", "content": "Transcribe the following audio."}
59+
Handles both the legacy dict format ({"array": ..., "sampling_rate": ...})
60+
and the newer AudioDecoder object from torchcodec-based datasets library.
61+
"""
62+
if audio_info is None:
63+
return None, None
64+
try:
65+
audio_array = np.array(audio_info["array"])
66+
sampling_rate = int(audio_info["sampling_rate"])
67+
return audio_array, sampling_rate
68+
except (KeyError, TypeError, IndexError):
69+
return None, None
70+
71+
72+
def format_entry(entry, dataset_name, audio_dir, text_field, id_field, with_audio):
73+
"""Format a dataset entry into JSONL and optionally save the audio file."""
74+
text = entry[text_field].strip()
75+
if not text:
76+
return None
6377

6478
sample_id = str(entry[id_field]).replace("/", "_")
6579
audio_filename = f"{Path(sample_id).stem}.flac"
6680

67-
audio_info = entry.get("audio", {})
81+
audio_array, sampling_rate = extract_audio(entry.get("audio"))
6882
duration = None
69-
if isinstance(audio_info, dict) and "array" in audio_info and "sampling_rate" in audio_info:
70-
audio_array = audio_info["array"]
71-
sampling_rate = audio_info["sampling_rate"]
72-
duration = len(audio_array) / sampling_rate
73-
74-
if duration < MIN_AUDIO_DURATION:
75-
return None
7683

84+
if audio_array is not None and sampling_rate is not None:
85+
duration = len(audio_array) / sampling_rate
7786
if with_audio:
7887
sf.write(str(audio_dir / audio_filename), audio_array, sampling_rate)
7988

89+
user_message = {"role": "user", "content": "Transcribe the following audio."}
8090
audio_meta = {"path": f"/dataset/asr-leaderboard/data/{dataset_name}/{audio_filename}"}
8191
if duration is not None:
8292
audio_meta["duration"] = float(duration)
8393
user_message["audio"] = audio_meta
8494

85-
formatted_entry = {
95+
formatted = {
8696
"task_type": "ASR",
8797
"expected_answer": text,
88-
"messages": [system_message, user_message],
98+
"messages": [{"role": "system", "content": SYSTEM_MESSAGE}, user_message],
8999
"subset_for_metrics": dataset_name,
100+
"id": entry[id_field],
90101
}
91-
92-
formatted_entry["id"] = entry[id_field]
93102
if "speaker_id" in entry:
94-
formatted_entry["speaker_id"] = entry["speaker_id"]
103+
formatted["speaker_id"] = entry["speaker_id"]
95104

96-
return formatted_entry
105+
return formatted
97106

98107

99108
def prepare_dataset(dataset_name, output_dir, with_audio=True):
100-
"""Prepare a single ASR dataset."""
109+
"""Download, decode, and write a single ASR dataset to JSONL + audio files."""
101110
if dataset_name not in DATASET_CONFIGS:
102111
raise ValueError(f"Unknown dataset: {dataset_name}. Available: {list(DATASET_CONFIGS.keys())}")
103112

104-
hf_repo, hf_config, hf_split, text_field, id_field = DATASET_CONFIGS[dataset_name]
113+
hf_config, hf_split, text_field, id_field = DATASET_CONFIGS[dataset_name]
105114

106-
print(f"Loading {dataset_name} from {hf_repo} (config={hf_config}, split={hf_split})...")
107-
dataset = load_dataset(hf_repo, hf_config, split=hf_split, trust_remote_code=True)
115+
print(f"Loading {dataset_name} from {HF_REPO} (config={hf_config}, split={hf_split})...")
116+
dataset = load_dataset(HF_REPO, hf_config, split=hf_split)
117+
if with_audio and "audio" in dataset.column_names:
118+
dataset = dataset.cast_column("audio", Audio(sampling_rate=AUDIO_SAMPLE_RATE))
108119

109120
output_file = output_dir / f"{dataset_name}.jsonl"
110121
audio_dir = output_dir / "data" / dataset_name
111122

112123
if with_audio:
113124
audio_dir.mkdir(parents=True, exist_ok=True)
114-
print(f"Saving audio files to {audio_dir}")
115125

116126
print(f"Processing {len(dataset)} samples from {dataset_name}...")
117-
118127
count = 0
119-
skipped = 0
120128
with open(output_file, "w", encoding="utf-8") as fout:
121-
for idx, entry in enumerate(tqdm(dataset, desc=dataset_name)):
122-
formatted = save_audio_and_format_entry(
123-
entry, dataset_name, audio_dir, idx, text_field=text_field, id_field=id_field, with_audio=with_audio
124-
)
129+
for entry in tqdm(dataset, desc=dataset_name):
130+
formatted = format_entry(entry, dataset_name, audio_dir, text_field, id_field, with_audio)
125131
if formatted is None:
126-
skipped += 1
127132
continue
128-
if formatted["expected_answer"]:
129-
fout.write(json.dumps(formatted) + "\n")
130-
count += 1
131-
132-
if skipped > 0:
133-
print(f"Skipped {skipped} samples with audio < {MIN_AUDIO_DURATION}s")
133+
fout.write(json.dumps(formatted) + "\n")
134+
count += 1
134135

135136
print(f"Saved {count} samples to {output_file}")
136137
return count
@@ -157,25 +158,19 @@ def main():
157158
output_dir.mkdir(parents=True, exist_ok=True)
158159

159160
with_audio = not args.no_audio
160-
161-
if args.no_audio:
161+
if not with_audio:
162162
print("Running without saving audio files.")
163-
else:
164-
print("Running with audio. Saving to data/{dataset}/")
165163

166164
datasets_to_prepare = list(DATASET_CONFIGS.keys()) if "all" in args.datasets else args.datasets
167165

168166
total_samples = 0
169167
for dataset_name in datasets_to_prepare:
170168
total_samples += prepare_dataset(dataset_name, output_dir, with_audio=with_audio)
171169

172-
# Combine all dataset JSONLs into test.jsonl
173170
combined_file = output_dir / "test.jsonl"
174171
print(f"\nCreating combined file: {combined_file}")
175172

176-
all_jsonl_files = sorted(output_dir.glob("*.jsonl"))
177-
dataset_files = [f for f in all_jsonl_files if f.name != "test.jsonl"]
178-
173+
dataset_files = sorted(f for f in output_dir.glob("*.jsonl") if f.name != "test.jsonl")
179174
combined_count = 0
180175
with open(combined_file, "w", encoding="utf-8") as fout:
181176
for dataset_file in dataset_files:

nemo_skills/dataset/covost2/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
# limitations under the License.
1414

1515
REQUIRES_DATA_DIR = True
16-
METRICS_TYPE = "audio"
17-
EVAL_ARGS = "++eval_type=audio ++eval_config.normalization_mode=multilingual"
18-
GENERATION_ARGS = "++prompt_format=openai ++enable_audio=true"
16+
IS_BENCHMARK_GROUP = True
17+
SCORE_MODULE = "nemo_skills.dataset.fleurs.audio_score"
18+
19+
BENCHMARKS = {
20+
"covost2.asr": {},
21+
"covost2.st": {},
22+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
METRICS_TYPE = "audio"
16+
EVAL_ARGS = "++eval_type=audio ++eval_config.normalization_mode=multilingual"
17+
GENERATION_ARGS = "++prompt_format=openai ++enable_audio=true"

0 commit comments

Comments
 (0)