|
38 | 38 | from __future__ import annotations |
39 | 39 |
|
40 | 40 | import argparse |
| 41 | +import contextlib |
41 | 42 | import fnmatch |
42 | 43 | import sys |
43 | 44 | import time |
@@ -804,6 +805,88 @@ def _generate_audio_feature_extraction(case: TestCase, json_path: Path, device: |
804 | 805 | ) |
805 | 806 |
|
806 | 807 |
|
| 808 | +def _generate_ctc_asr(case: TestCase, json_path: Path, device: str) -> None: |
| 809 | + """Generate golden data for CTC-based ASR (Wav2Vec2ForCTC / MMS). |
| 810 | +
|
| 811 | + The model output is per-frame logits over a vocabulary; we save the |
| 812 | + top-K over the final frame's logit vector (matching the existing |
| 813 | + audio-feature-extraction pattern), and when L5 is requested we also |
| 814 | + save the CTC-greedy-decoded transcript as a token-id sequence so the |
| 815 | + end-to-end test can compare against the runtime's greedy decode. |
| 816 | +
|
| 817 | + MMS specifically requires picking a target language adapter via |
| 818 | + ``processor.tokenizer.set_target_lang(lang)`` and |
| 819 | + ``model.load_adapter(lang)`` before the forward pass. The language is |
| 820 | + read from ``case.generation_params['lang']`` (default ``"eng"``). |
| 821 | + """ |
| 822 | + import librosa |
| 823 | + import torch |
| 824 | + import transformers |
| 825 | + |
| 826 | + from mobius._testing.golden import save_generation_json, save_golden_ref |
| 827 | + |
| 828 | + lang = case.generation_params.get("lang", "eng") |
| 829 | + |
| 830 | + processor = transformers.AutoProcessor.from_pretrained( |
| 831 | + case.model_id, trust_remote_code=case.trust_remote_code, target_lang=lang |
| 832 | + ) |
| 833 | + model = transformers.Wav2Vec2ForCTC.from_pretrained( |
| 834 | + case.model_id, |
| 835 | + torch_dtype=torch.float32, |
| 836 | + device_map=device, |
| 837 | + trust_remote_code=case.trust_remote_code, |
| 838 | + target_lang=lang, |
| 839 | + ignore_mismatched_sizes=True, # MMS lm_head shape changes per language |
| 840 | + ) |
| 841 | + # For MMS, switching languages also requires loading the per-language adapter. |
| 842 | + # Non-MMS Wav2Vec2ForCTC checkpoints don't have language adapters; |
| 843 | + # the missing-adapter case is expected and harmless there. |
| 844 | + if hasattr(model, "load_adapter"): |
| 845 | + with contextlib.suppress(ValueError, KeyError, OSError): |
| 846 | + model.load_adapter(lang) |
| 847 | + model.eval() |
| 848 | + |
| 849 | + audio_path = Path("testdata") / case.audio[0] |
| 850 | + audio_array, sample_rate = librosa.load(str(audio_path), sr=16000) |
| 851 | + processed = processor(audio_array, sampling_rate=sample_rate, return_tensors="pt").to( |
| 852 | + next(model.parameters()).device |
| 853 | + ) |
| 854 | + |
| 855 | + with torch.no_grad(): |
| 856 | + outputs = model(**processed) |
| 857 | + |
| 858 | + # CTC logits: (batch, num_frames, vocab_size). Use last frame for top-K. |
| 859 | + logits = outputs.logits[0] # (num_frames, vocab_size) |
| 860 | + last_logits = logits[-1].cpu().numpy() |
| 861 | + golden = _extract_logits_golden(last_logits) |
| 862 | + |
| 863 | + save_golden_ref( |
| 864 | + json_path, |
| 865 | + top1_id=golden["top1_id"], |
| 866 | + top2_id=golden["top2_id"], |
| 867 | + top10_ids=golden["top10_ids"], |
| 868 | + top10_logits=golden["top10_logits"], |
| 869 | + logits_summary=golden["logits_summary"], |
| 870 | + input_ids=np.array([[0]], dtype=np.int64), # placeholder |
| 871 | + ) |
| 872 | + |
| 873 | + if "L5" in case.level: |
| 874 | + # CTC greedy decode: argmax over vocab per frame, then collapse |
| 875 | + # repeats and remove blanks. Save the post-collapse token IDs (and |
| 876 | + # the decoded text for human inspection) into the standard |
| 877 | + # ``*_generation.json`` sidecar. |
| 878 | + predicted_ids = torch.argmax(logits, dim=-1).cpu().numpy() |
| 879 | + transcript = processor.batch_decode(predicted_ids[np.newaxis, :])[0] |
| 880 | + gen_path = json_path.with_name(json_path.stem + "_generation.json") |
| 881 | + save_generation_json( |
| 882 | + gen_path, |
| 883 | + model_id=case.model_id, |
| 884 | + prompt=str(audio_path), |
| 885 | + generated_tokens=predicted_ids.tolist(), |
| 886 | + generated_text=transcript, |
| 887 | + ) |
| 888 | + |
| 889 | + |
807 | 890 | def _generate_image_classification(case: TestCase, json_path: Path, device: str) -> None: |
808 | 891 | """Generate golden data for image classification (ViT, CLIP, etc.). |
809 | 892 |
|
@@ -1186,6 +1269,7 @@ def _generate_phi4mm_multimodal(case: TestCase, json_path: Path, device: str) -> |
1186 | 1269 | "speech-to-text": _generate_speech_to_text, |
1187 | 1270 | "speech-language": _generate_speech_language, |
1188 | 1271 | "audio-feature-extraction": _generate_audio_feature_extraction, |
| 1272 | + "ctc-asr": _generate_ctc_asr, |
1189 | 1273 | # Vision tasks that produce last_hidden_state — reuse image classification. |
1190 | 1274 | "depth-estimation": _generate_image_classification, |
1191 | 1275 | "image-segmentation": _generate_image_classification, |
|
0 commit comments