"""Threshold sweep — varying audio length around the 40 s Encoder-Window. Compares three transcription paths on artificially trimmed audios: - path1_default: model.transcribe([wav]) — what main.py uses for ≤ 25 s - path1_bs1: model.transcribe([wav], batch_size=1) — NeMo ≥ 2.5 auto-chunking - path2: FrameBatchMultiTaskAED with chunk_len=40 — what main.py uses for > 25 s Audios are 20, 30, 38 and 45 s slices of the 80.9 s source. The 25 s threshold in main.py sits in the middle, so this sweep tells us: - Does Path 1 truncate already at 30/38 s, or only > 40 s? - Does the threshold need to move? """ 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" OUT_DIR = "/sweep" RESULTS_PATH = f"{OUT_DIR}/threshold_results.jsonl" LOG_PATH = f"{OUT_DIR}/threshold_log.txt" def log(msg: str) -> None: line = f"{time.strftime('%H:%M:%S')} {msg}" print(line, flush=True) with open(LOG_PATH, "a") as f: f.write(line + "\n") def transcode(src: str) -> str: 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_duration(p: str) -> float: with wave.open(p, "rb") as w: return w.getnframes() / float(w.getframerate()) def vram_mb() -> float: return torch.cuda.max_memory_allocated() / (1024 * 1024) if torch.cuda.is_available() else 0 def write_manifest(wav_path, duration, pnc="yes"): f = tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) json.dump({"audio_filepath": wav_path, "duration": duration, "taskname": "asr", "source_lang": "de", "target_lang": "de", "pnc": pnc}, f) f.write("\n") f.close() return f.name def run_path1(model, wav, batch_size): torch.cuda.reset_peak_memory_stats() 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=batch_size, ) return (getattr(hyps[0], "text", None) or str(hyps[0])).strip(), \ time.monotonic() - t0, vram_mb() def run_path2(model, model_cfg, wav, dur, chunk_len=40.0): manifest = write_manifest(wav, dur) feature_stride = model_cfg.preprocessor["window_stride"] model_stride_secs = feature_stride * 8 frame_asr = FrameBatchMultiTaskAED( asr_model=model, frame_len=chunk_len, total_buffer=chunk_len, batch_size=8, ) torch.cuda.reset_peak_memory_stats() t0 = time.monotonic() with torch.amp.autocast("cuda", enabled=True, dtype=torch.bfloat16): with torch.no_grad(): hyps = get_buffered_pred_feat_multitaskAED( frame_asr, model_cfg.preprocessor, model_stride_secs, 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, vram_mb() def main(): open(LOG_PATH, "w").close() open(RESULTS_PATH, "w").close() log("loading model bf16") model = ASRModel.from_pretrained("nvidia/canary-1b-v2") model = model.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) log(f"model loaded, vram={vram_mb():.0f}MB") audios = sorted(os.listdir(AUDIO_DIR)) wavs = {} for name in audios: if not name.endswith(".m4a"): continue src = os.path.join(AUDIO_DIR, name) wav = transcode(src) dur = wav_duration(wav) wavs[name] = (wav, dur) log(f" {name} → {dur:.1f}s") def record(d): with open(RESULTS_PATH, "a") as f: f.write(json.dumps(d, ensure_ascii=False) + "\n") for name, (wav, dur) in wavs.items(): # path 1 default (no batch_size override → triggers no auto-chunking # because input is a list, not a single string) log(f"path1_default on {name}") try: text, t, vram = run_path1(model, wav, batch_size=4) record({"audio": name, "duration_audio": dur, "path": "path1_default", "batch_size": 4, "duration_infer": round(t, 2), "vram_peak_mb": round(vram, 0), "chars": len(text), "text": text}) except Exception as e: log(f" FAIL: {e}") record({"audio": name, "path": "path1_default", "error": str(e)}) # path 1 with batch_size=1 (auto-chunking trigger per NeMo docs) log(f"path1_bs1 on {name}") try: text, t, vram = run_path1(model, wav, batch_size=1) record({"audio": name, "duration_audio": dur, "path": "path1_bs1", "batch_size": 1, "duration_infer": round(t, 2), "vram_peak_mb": round(vram, 0), "chars": len(text), "text": text}) except Exception as e: log(f" FAIL: {e}") record({"audio": name, "path": "path1_bs1", "error": str(e)}) # path 2 (always chunked, chunk_len=40) log(f"path2 on {name}") try: text, t, vram = run_path2(model, cfg, wav, dur, chunk_len=40.0) record({"audio": name, "duration_audio": dur, "path": "path2", "chunk_len": 40.0, "batch_size": 8, "duration_infer": round(t, 2), "vram_peak_mb": round(vram, 0), "chars": len(text), "text": text}) except Exception as e: log(f" FAIL: {e}") record({"audio": name, "path": "path2", "error": str(e)}) log("done") if __name__ == "__main__": main()