Skip to content

Commit e376488

Browse files
committed
Fix AppTek long-form audio evals
1 parent bbbc066 commit e376488

18 files changed

Lines changed: 235 additions & 39 deletions

File tree

nemo_skills/dataset/apptek-callcenter-dialogues/prepare.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,15 @@
5757

5858
def _metadata_paths(data_dir: Path, accent_codes: Iterable[str]) -> list[Path]:
5959
"""Return expected metadata paths for selected accents."""
60-
return [data_dir / accent / "metadata.jsonl" for accent in accent_codes]
60+
return [_metadata_path(data_dir, accent) for accent in accent_codes]
61+
62+
63+
def _metadata_path(data_dir: Path, accent_code: str) -> Path:
64+
"""Return the metadata path, supporting both old and current HF layouts."""
65+
direct_path = data_dir / accent_code / "metadata.jsonl"
66+
if direct_path.exists():
67+
return direct_path
68+
return data_dir / "test" / accent_code / "metadata.jsonl"
6169

6270

6371
def _metadata_complete(data_dir: Path, accent_codes: Iterable[str]) -> bool:
@@ -68,7 +76,7 @@ def _metadata_complete(data_dir: Path, accent_codes: Iterable[str]) -> bool:
6876
def _iter_metadata_rows(data_dir: Path, accent_codes: Iterable[str]) -> Iterable[tuple[str, dict]]:
6977
"""Yield ``(accent_code, row)`` pairs from downloaded metadata files."""
7078
for accent_code in accent_codes:
71-
metadata_path = data_dir / accent_code / "metadata.jsonl"
79+
metadata_path = _metadata_path(data_dir, accent_code)
7280
with metadata_path.open(encoding="utf-8") as fin:
7381
for line in fin:
7482
line = line.strip()
@@ -81,7 +89,10 @@ def _resolve_audio_path(data_dir: Path, accent_code: str, file_name: str) -> Pat
8189
direct_path = data_dir / file_name
8290
if direct_path.exists():
8391
return direct_path
84-
return data_dir / accent_code / file_name
92+
old_layout_path = data_dir / accent_code / file_name
93+
if old_layout_path.exists():
94+
return old_layout_path
95+
return data_dir / "test" / accent_code / file_name
8596

8697

8798
def _audio_complete(data_dir: Path, accent_codes: Iterable[str]) -> bool:
@@ -103,9 +114,9 @@ def _allow_patterns(accent_codes: Iterable[str], with_audio: bool) -> list[str]:
103114
"""
104115
patterns = ["README.md", "score.py", "word_mappings.py"]
105116
for accent_code in accent_codes:
106-
patterns.append(f"{accent_code}/metadata.jsonl")
117+
patterns.append(f"test/{accent_code}/metadata.jsonl")
107118
if with_audio:
108-
patterns.append(f"{accent_code}/audio/*.wav")
119+
patterns.append(f"test/{accent_code}/audio/*.wav")
109120
return patterns
110121

111122

nemo_skills/dataset/audiobench/nonjudge/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@
2626
# Evaluation settings
2727
EVAL_ARGS = "++eval_type=audio ++eval_config.normalization_mode=audiobench"
2828

29-
# Generation settings - OpenAI format for audio-language models
30-
GENERATION_ARGS = "++prompt_format=openai ++enable_audio=true"
29+
# Generation settings - OpenAI format for audio-language models.
30+
# Soft-fail keeps one over-limit or malformed audio request from aborting an
31+
# entire chunk; audio metrics count the empty generation as incorrect.
32+
GENERATION_ARGS = "++prompt_format=openai ++enable_audio=true ++server.enable_soft_fail=true"

nemo_skills/dataset/audiobench/prepare.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"""
2626

2727
import argparse
28+
import copy
2829
import json
2930
import os
3031
import shutil
@@ -155,12 +156,14 @@ def create_manifest_entry(
155156
Manifest entry dict with proper format for nemo-skills
156157
"""
157158
instruction = sample.get("instruction", sample.get("text", "Process the audio"))
159+
if isinstance(instruction, dict):
160+
instruction = instruction.get("text") or "Process the audio"
158161
reference = sample.get("reference", sample.get("answer", ""))
159162
task_type = sample.get("task_type", "unknown")
160163

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}"
164+
# Paths are resolved relative to the manifest directory by the inference code.
165+
# The combined category manifest lives at audiobench/{category}/test.jsonl.
166+
audio_rel_path = f"audio/{dataset_name}/{audio_filename}"
164167

165168
# Create audio metadata (both singular and plural forms for compatibility)
166169
audio_metadata = {"path": audio_rel_path, "duration": duration}
@@ -203,6 +206,28 @@ def create_manifest_entry(
203206
return entry
204207

205208

209+
def make_dataset_manifest_entry(entry: Dict) -> Dict:
210+
"""Adjust category-relative audio paths for per-dataset manifests."""
211+
entry = copy.deepcopy(entry)
212+
213+
def rewrite(path: str) -> str:
214+
return f"../{path}" if path.startswith("audio/") else path
215+
216+
if isinstance(entry.get("audio_path"), list):
217+
entry["audio_path"] = [rewrite(path) for path in entry["audio_path"]]
218+
elif isinstance(entry.get("audio_path"), str):
219+
entry["audio_path"] = rewrite(entry["audio_path"])
220+
221+
for message in entry.get("messages", []):
222+
if "audio" in message and "path" in message["audio"]:
223+
message["audio"]["path"] = rewrite(message["audio"]["path"])
224+
for audio in message.get("audios", []):
225+
if "path" in audio:
226+
audio["path"] = rewrite(audio["path"])
227+
228+
return entry
229+
230+
206231
def process_dataset(
207232
dataset_name: str,
208233
output_dir: Path,
@@ -473,7 +498,7 @@ def process_dataset(
473498
manifest_path = dataset_dir / f"{split}.jsonl"
474499
with open(manifest_path, "w", encoding="utf-8") as f:
475500
for entry in manifest_entries:
476-
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
501+
f.write(json.dumps(make_dataset_manifest_entry(entry), ensure_ascii=False) + "\n")
477502

478503
print(f"✓ Saved {successful} samples to {manifest_path}")
479504
if failed > 0:
@@ -566,6 +591,7 @@ def main():
566591

567592
total_samples = 0
568593
total_datasets = 0
594+
combined_entries = {"judge": [], "nonjudge": []}
569595

570596
for name in target_datasets:
571597
# Normalize dataset name: allow passing without _test suffix
@@ -576,10 +602,10 @@ def main():
576602
dataset_name = f"{dataset_name}_test"
577603

578604
# Determine category for logging
579-
category = "judge" if name in JUDGE_DATASETS else "nonjudge"
605+
category = "judge" if dataset_name in JUDGE_DATASETS else "nonjudge"
580606

581607
try:
582-
num_samples, _ = process_dataset(
608+
num_samples, manifest_entries = process_dataset(
583609
dataset_name=dataset_name,
584610
output_dir=output_dir,
585611
save_audio=args.save_audio,
@@ -588,11 +614,21 @@ def main():
588614
)
589615
total_samples += num_samples
590616
total_datasets += 1
617+
combined_entries[category].extend(manifest_entries)
591618
print(f"✓ Completed {dataset_name}: {num_samples} samples")
592619
except Exception as e:
593620
print(f"✗ Failed {dataset_name}: {e}")
594621
continue
595622

623+
for category, entries in combined_entries.items():
624+
if not entries:
625+
continue
626+
combined_manifest = output_dir / category / f"{args.split}.jsonl"
627+
with open(combined_manifest, "w", encoding="utf-8") as f:
628+
for entry in entries:
629+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
630+
print(f"✓ Saved combined {category} manifest with {len(entries)} samples to {combined_manifest}")
631+
596632
print("\n" + "=" * 60)
597633
print("AudioBench Preparation Summary")
598634
print("=" * 60)

nemo_skills/dataset/mmau-pro/closed_form/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
# limitations under the License.
1414
METRICS_TYPE = "mmau_pro_closed_form"
1515
SCORE_MODULE = "nemo_skills.evaluation.metrics.mmau_pro_metrics"
16-
GENERATION_ARGS = "++prompt_format=openai ++enable_audio=true"
17-
EVAL_ARGS = "++eval_type=mmau-pro"
16+
GENERATION_ARGS = "++prompt_format=openai ++enable_audio=true ++server.enable_soft_fail=true"
1817

1918
# NVEmbed judge configuration for closed-form evaluation
2019
JUDGE_PIPELINE_ARGS = {

nemo_skills/dataset/mmau-pro/prepare.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ def format_entry(entry, with_audio=False):
8686
audio_path = entry["audio_path"]
8787

8888
if isinstance(audio_path, list) and audio_path:
89-
user_message["audios"] = [{"path": path, "duration": 10.0} for path in audio_path]
89+
formatted_entry["audio_path"] = [f"../{path}" for path in audio_path]
90+
user_message["audios"] = [{"path": path, "duration": 10.0} for path in formatted_entry["audio_path"]]
9091
elif isinstance(audio_path, str):
91-
user_message["audio"] = {"path": audio_path, "duration": 10.0}
92+
formatted_entry["audio_path"] = f"../{audio_path}"
93+
user_message["audio"] = {"path": formatted_entry["audio_path"], "duration": 10.0}
9294

9395
formatted_entry["messages"] = [user_message]
9496
return formatted_entry

nemo_skills/evaluation/evaluator/audio.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,12 +764,28 @@ def eval_audio(cfg):
764764
asyncio.run(evaluator.eval_full())
765765

766766

767+
def coerce_text_field(value: Any) -> str:
768+
"""Convert loose AudioBench text/reference fields to plain text."""
769+
if value is None:
770+
return ""
771+
if isinstance(value, str):
772+
return value.strip()
773+
if isinstance(value, dict):
774+
for key in ("text", "expected_answer", "answer", "transcript", "reference"):
775+
if key in value and value[key] is not None:
776+
return coerce_text_field(value[key])
777+
return " ".join(coerce_text_field(item) for item in value.values() if item is not None).strip()
778+
if isinstance(value, (list, tuple)):
779+
return " ".join(coerce_text_field(item) for item in value if item is not None).strip()
780+
return str(value).strip()
781+
782+
767783
def evaluate_sample(sample: dict[str, Any], config: AudioEvaluatorConfig) -> dict[str, Any]:
768784
"""Evaluate single sample based on task_type. Returns dict of updates to merge."""
769785
updates = {}
770786
task_type = sample.get("task_type", "unknown")
771-
generation = sample["generation"].strip()
772-
expected_answer = sample.get("expected_answer", "").strip()
787+
generation = coerce_text_field(sample.get("generation", ""))
788+
expected_answer = coerce_text_field(sample.get("expected_answer", ""))
773789

774790
# Extract ASR text from generation
775791
# E.g Qwen ASR uses <asr_text> tags to indicate the ASR text

nemo_skills/evaluation/evaluator/mmau_pro.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def eval_mmau_pro(cfg):
5656

5757
def evaluate_instruction_following_sample(sample: dict[str, Any]) -> dict[str, Any]:
5858
"""Evaluate a single instruction following sample."""
59+
if sample is None:
60+
LOG.warning("Encountered null MMAU-Pro sample; marking it incorrect")
61+
return {"is_correct": False, "error": "null_sample"}
62+
5963
sample = sample.copy()
6064
generation = sample.get("generation", "").strip()
6165

nemo_skills/evaluation/evaluator/nvembed_judge.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def install_packages():
4242
"""Install required packages for NVEmbed evaluation."""
4343
LOG.info("Installing required packages...")
4444
subprocess.run(
45-
["pip", "install", "-q", "datasets", "einops", "transformers==4.42.4", "tqdm"],
45+
["pip", "install", "-q", "numpy<2", "datasets", "einops", "transformers==4.42.4", "tqdm"],
4646
check=True,
4747
capture_output=True,
4848
text=True,
@@ -124,11 +124,18 @@ def evaluate_sample_with_nvembed(sample: dict[str, Any], model_name: str = "nvid
124124
choices = sample.get("choices", [])
125125
expected_answer = sample.get("expected_answer", "")
126126

127-
# Fail fast if data is malformed - this indicates a pipeline error
127+
# Empty model outputs are valid failed predictions. Keep malformed-data
128+
# checks strict, but do not let one blank generation abort the whole eval.
128129
if not generation:
129-
raise ValueError(
130-
f"Sample missing generation field or has empty generation. Sample ID: {sample.get('id', 'unknown')}"
130+
sample.update(
131+
{
132+
"nvembed_matched_choice": "",
133+
"nvembed_confidence": 0.0,
134+
"is_correct": False,
135+
"nvembed_error": "empty_generation",
136+
}
131137
)
138+
return sample
132139

133140
if not choices:
134141
raise ValueError(

nemo_skills/inference/generate.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -912,11 +912,27 @@ def restore_async_order(self):
912912
with open(self.cfg.output_file + "-async", "rt", encoding="utf-8") as fin:
913913
generations = [json.loads(line) for line in fin]
914914

915-
ordered_generations = [None] * len(generations)
915+
if not generations:
916+
ordered_generations = []
917+
else:
918+
max_position = max(gen_dict[self.cfg.async_position_key] for gen_dict in generations)
919+
ordered_generations = [None] * (max_position + 1)
916920
for gen_dict in generations:
917921
async_pos = gen_dict.pop(self.cfg.async_position_key)
922+
if async_pos < 0:
923+
raise ValueError(f"Invalid async output position {async_pos}")
918924
ordered_generations[async_pos] = gen_dict
919925

926+
missing_positions = [idx for idx, gen_dict in enumerate(ordered_generations) if gen_dict is None]
927+
if missing_positions:
928+
preview = ", ".join(map(str, missing_positions[:20]))
929+
if len(missing_positions) > 20:
930+
preview += ", ..."
931+
raise RuntimeError(
932+
f"Missing async outputs for {len(missing_positions)} positions: {preview}. "
933+
"Refusing to write null rows to the merged output."
934+
)
935+
920936
with open(self.cfg.output_file, "wt", encoding="utf-8") as fout:
921937
for gen_dict in ordered_generations:
922938
fout.write(json.dumps(gen_dict) + "\n")

nemo_skills/inference/model/vllm_multimodal.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,14 @@ def content_text_to_list(self, message: dict) -> dict:
321321
content = result["content"]
322322
if isinstance(content, str):
323323
result["content"] = [{"type": "text", "text": content}]
324+
elif isinstance(content, dict):
325+
if content.get("type") == "text" and "text" in content:
326+
result["content"] = [content]
327+
elif "text" in content:
328+
text = content["text"]
329+
result["content"] = [] if text is None else [{"type": "text", "text": str(text)}]
330+
else:
331+
raise TypeError(f"Unexpected content dict keys: {sorted(content)}")
324332
elif not isinstance(content, list):
325333
raise TypeError(f"Unexpected content type: {type(content)}")
326334

@@ -433,6 +441,14 @@ async def _generate_with_chunking(
433441
content = msg_copy["content"]
434442
if isinstance(content, str):
435443
text_content = [{"type": "text", "text": content}]
444+
elif isinstance(content, dict):
445+
if content.get("type") == "text" and "text" in content:
446+
text_content = [content]
447+
elif "text" in content:
448+
text = content["text"]
449+
text_content = [] if text is None else [{"type": "text", "text": str(text)}]
450+
else:
451+
raise TypeError(f"Unexpected content dict keys: {sorted(content)}")
436452
else:
437453
text_content = content
438454

0 commit comments

Comments
 (0)