"""Mini-sweep: does chunk_len == audio_len fix Path 2 for short audios? If yes → Path 2 is OK as long as audio fits in exactly one chunk. The bug is in the multi-chunk merging or pad/split logic. If no → Path 2 has a structural problem with single-chunk inputs independent of length matching. Confirms "different inference pipeline" theory. """ import copy import json import os import subprocess import tempfile import time import wave import torch from omegaconf import OmegaConf from nemo.collections.asr.models import ASRModel from nemo.collections.asr.parts.utils.streaming_utils import FrameBatchMultiTaskAED from nemo.collections.asr.parts.utils.transcribe_utils import get_buffered_pred_feat_multitaskAED AUDIO_DIR = "/sweep/threshold_audios" RESULTS_PATH = "/sweep/chunklen_results.jsonl" def transcode(src): dst = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) dst.close() subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", src, "-ac", "1", "-ar", "16000", "-f", "wav", dst.name], check=True) return dst.name def wav_dur(p): with wave.open(p, "rb") as w: return w.getnframes() / float(w.getframerate()) def write_manifest(wav, dur): f = tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) json.dump({"audio_filepath": wav, "duration": dur, "taskname": "asr", "source_lang": "de", "target_lang": "de", "pnc": "yes"}, f) f.write("\n") f.close() return f.name def run_path2(model, cfg, wav, dur, chunk_len): manifest = write_manifest(wav, dur) feature_stride = cfg.preprocessor["window_stride"] model_stride = feature_stride * 8 fa = FrameBatchMultiTaskAED(asr_model=model, frame_len=chunk_len, total_buffer=chunk_len, batch_size=8) t0 = time.monotonic() with torch.amp.autocast("cuda", enabled=True, dtype=torch.bfloat16): with torch.no_grad(): hyps = get_buffered_pred_feat_multitaskAED( fa, cfg.preprocessor, model_stride, model.device, manifest=manifest, filepaths=None, timestamps=False, ) elapsed = time.monotonic() - t0 os.unlink(manifest) return (getattr(hyps[0], "text", None) or str(hyps[0])).strip(), elapsed def run_path1(model, wav): t0 = time.monotonic() with torch.amp.autocast("cuda", enabled=True, dtype=torch.bfloat16): with torch.no_grad(): hyps = model.transcribe([wav], source_lang="de", target_lang="de", pnc="yes", batch_size=4) elapsed = time.monotonic() - t0 return (getattr(hyps[0], "text", None) or str(hyps[0])).strip(), elapsed def main(): open(RESULTS_PATH, "w").close() print("loading model bf16", flush=True) model = ASRModel.from_pretrained("nvidia/canary-1b-v2").to("cuda").to(torch.bfloat16) model.eval() cfg = copy.deepcopy(model._cfg) OmegaConf.set_struct(cfg.preprocessor, False) cfg.preprocessor.dither = 0.0 cfg.preprocessor.pad_to = 0 OmegaConf.set_struct(cfg.preprocessor, True) print("model loaded", flush=True) audios = ["trim_20s.m4a", "trim_30s.m4a"] chunk_lens = { "trim_20s.m4a": [21.0, 25.0, 40.0], # exact, slightly larger, default "trim_30s.m4a": [31.0, 35.0, 40.0], } def record(d): with open(RESULTS_PATH, "a") as f: f.write(json.dumps(d, ensure_ascii=False) + "\n") for name in audios: src = os.path.join(AUDIO_DIR, name) wav = transcode(src) dur = wav_dur(wav) print(f"\n=== {name} ({dur:.1f}s) ===", flush=True) # baseline path1 text, t = run_path1(model, wav) print(f"path1: {len(text)} chars in {t:.1f}s", flush=True) record({"audio": name, "duration": dur, "path": "path1", "chunk_len": None, "chars": len(text), "duration_infer": round(t, 2), "text": text}) for cl in chunk_lens[name]: text, t = run_path2(model, cfg, wav, dur, cl) print(f"path2 chunk_len={cl}: {len(text)} chars in {t:.1f}s", flush=True) record({"audio": name, "duration": dur, "path": "path2", "chunk_len": cl, "chars": len(text), "duration_infer": round(t, 2), "text": text}) print("done", flush=True) if __name__ == "__main__": main()