6b7a1cea49
This commit introduces the `doctate-canary` service and associated sweep experiments. The service provides access to the `nvidia/canary-1b-v2` ASR model via a native FastAPI API. It includes deployment scripts, Docker configurations, and detailed documentation on installation, API usage, and environment variables. The sweep experiments aim to thoroughly evaluate the Canary model's performance under various configurations. This includes testing different inference pipelines (single-shot vs. buffered), decoding strategies (greedy vs. beam search), and parameter tuning (chunk length, overlap, batch size, precision). The goal is to reproduce previous findings and identify optimal settings. The commit also includes: - Utility scripts for audio transcoding and manipulation. - Comprehensive logging and result collection mechanisms for the sweep runs. - Detailed analysis of `dur=0` occurrences and word confidence, concluding they are not reliable indicators of hallucination. - Documentation on the interaction between decoding parameters, especially `return_hypotheses`, and the availability of confidence scores.
216 lines
7.5 KiB
Python
216 lines
7.5 KiB
Python
"""TDT probe — can timestamps_asr_model handle audio standalone, including >40 s?
|
|
|
|
Background:
|
|
EncDecMultiTaskModel.timestamps_asr_model is a Token-Duration-Transducer
|
|
(RNN-T variant) with hybrid CTC head, FastConformer encoder enc_hidden=1024.
|
|
Bundled inside Canary 1B v2 specifically to provide forced-alignment
|
|
timestamps for the AED head, but it is itself a complete ASR model.
|
|
|
|
Open question: does the relative positional encoding lift the 40 s wall that
|
|
AED hits, or does TDT inherit the same training-time max_duration limit?
|
|
|
|
Test set:
|
|
- 20 s : sanity check, well within both models
|
|
- 80 s : full source — Canary path1 truncates here, baseline expectation
|
|
- 160 s : concat(80, 80), no re-encode — pushes 4x past TDT training horizon
|
|
|
|
Per case:
|
|
- TDT.transcribe(timestamps=False)
|
|
- TDT.transcribe(timestamps=True) — also verify timestamp shape
|
|
- Canary path1 — reference, will truncate >40 s
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import wave
|
|
|
|
import torch
|
|
|
|
from nemo.collections.asr.models import EncDecMultiTaskModel
|
|
|
|
|
|
AUDIO_DIR = "/sweep/threshold_audios"
|
|
RESULTS_PATH = "/sweep/probe_tdt.jsonl"
|
|
LOG_PATH = "/sweep/probe_tdt.log"
|
|
|
|
|
|
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, duration_secs: float | None = None) -> str:
|
|
dst = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
dst.close()
|
|
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-i", src,
|
|
"-ac", "1", "-ar", "16000"]
|
|
if duration_secs is not None:
|
|
cmd += ["-t", str(duration_secs)]
|
|
cmd += ["-f", "wav", dst.name]
|
|
subprocess.run(cmd, check=True)
|
|
return dst.name
|
|
|
|
|
|
def concat_wavs(wavs: list[str], repeats: int = 1) -> str:
|
|
list_file = tempfile.NamedTemporaryFile(suffix=".txt", mode="w", delete=False)
|
|
for w in wavs * repeats:
|
|
list_file.write(f"file '{w}'\n")
|
|
list_file.close()
|
|
dst = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
dst.close()
|
|
subprocess.run(["ffmpeg", "-y", "-loglevel", "error",
|
|
"-f", "concat", "-safe", "0", "-i", list_file.name,
|
|
"-c", "copy", dst.name], check=True)
|
|
os.unlink(list_file.name)
|
|
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.0
|
|
|
|
|
|
def run_tdt(tdt, wav: str, timestamps: bool) -> dict:
|
|
torch.cuda.reset_peak_memory_stats()
|
|
t0 = time.monotonic()
|
|
try:
|
|
with torch.amp.autocast("cuda", enabled=True, dtype=torch.bfloat16):
|
|
with torch.no_grad():
|
|
hyps = tdt.transcribe([wav], timestamps=timestamps)
|
|
elapsed = time.monotonic() - t0
|
|
h = hyps[0]
|
|
text = (getattr(h, "text", None) or "").strip()
|
|
ts = getattr(h, "timestamp", None)
|
|
ts_summary = None
|
|
if isinstance(ts, dict):
|
|
ts_summary = {k: (len(v) if hasattr(v, "__len__") else str(type(v).__name__))
|
|
for k, v in ts.items()}
|
|
return {"ok": True, "text": text, "chars": len(text),
|
|
"duration_infer": round(elapsed, 2),
|
|
"vram_peak_mb": round(vram_mb(), 0),
|
|
"timestamp_summary": ts_summary}
|
|
except Exception as e:
|
|
return {"ok": False, "error": f"{type(e).__name__}: {e}",
|
|
"duration_infer": round(time.monotonic() - t0, 2)}
|
|
|
|
|
|
def run_canary(model, wav: str) -> dict:
|
|
torch.cuda.reset_peak_memory_stats()
|
|
t0 = time.monotonic()
|
|
try:
|
|
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
|
|
text = (getattr(hyps[0], "text", None) or str(hyps[0])).strip()
|
|
return {"ok": True, "text": text, "chars": len(text),
|
|
"duration_infer": round(elapsed, 2),
|
|
"vram_peak_mb": round(vram_mb(), 0)}
|
|
except Exception as e:
|
|
return {"ok": False, "error": f"{type(e).__name__}: {e}",
|
|
"duration_infer": round(time.monotonic() - t0, 2)}
|
|
|
|
|
|
def find_source() -> str | None:
|
|
for c in ("trim_full.m4a", "full.m4a", "source.m4a"):
|
|
p = os.path.join(AUDIO_DIR, c)
|
|
if os.path.exists(p):
|
|
return p
|
|
if not os.path.isdir(AUDIO_DIR):
|
|
return None
|
|
m4as = [f for f in os.listdir(AUDIO_DIR) if f.endswith(".m4a")]
|
|
if not m4as:
|
|
return None
|
|
m4as.sort(key=lambda f: os.path.getsize(os.path.join(AUDIO_DIR, f)), reverse=True)
|
|
return os.path.join(AUDIO_DIR, m4as[0])
|
|
|
|
|
|
def main():
|
|
open(RESULTS_PATH, "w").close()
|
|
open(LOG_PATH, "w").close()
|
|
|
|
log("loading EncDecMultiTaskModel (Canary 1B v2)")
|
|
model = EncDecMultiTaskModel.from_pretrained("nvidia/canary-1b-v2")
|
|
model = model.to("cuda").to(torch.bfloat16)
|
|
model.eval()
|
|
log(f" vram after load: {vram_mb():.0f} MB")
|
|
|
|
tdt = getattr(model, "timestamps_asr_model", None)
|
|
if tdt is None:
|
|
log("FATAL: model has no timestamps_asr_model attribute")
|
|
return
|
|
log(f" tdt class: {type(tdt).__name__}")
|
|
log(f" tdt device: {next(tdt.parameters()).device}, dtype: {next(tdt.parameters()).dtype}")
|
|
|
|
src = find_source()
|
|
if src is None:
|
|
log(f"FATAL: no source audio found in {AUDIO_DIR}")
|
|
return
|
|
log(f"source audio: {src}")
|
|
|
|
wav_full = transcode(src)
|
|
dur_full = wav_duration(wav_full)
|
|
log(f" full wav: {dur_full:.1f} s")
|
|
|
|
wav_20 = transcode(src, duration_secs=20.0)
|
|
log(f" 20 s wav: {wav_duration(wav_20):.1f} s")
|
|
|
|
wav_80 = transcode(src, duration_secs=80.0) if dur_full >= 80 else wav_full
|
|
log(f" 80 s wav: {wav_duration(wav_80):.1f} s")
|
|
|
|
wav_160 = concat_wavs([wav_80], repeats=2)
|
|
log(f" 160 s concat wav: {wav_duration(wav_160):.1f} s")
|
|
|
|
cases = [("20s", wav_20), ("80s", wav_80), ("160s_concat", wav_160)]
|
|
|
|
def record(d: dict) -> None:
|
|
with open(RESULTS_PATH, "a") as f:
|
|
f.write(json.dumps(d, ensure_ascii=False) + "\n")
|
|
|
|
for label, wav in cases:
|
|
dur = wav_duration(wav)
|
|
log(f"\n=== case {label} ({dur:.1f} s) ===")
|
|
|
|
log(" TDT timestamps=False")
|
|
r = run_tdt(tdt, wav, timestamps=False)
|
|
record({"case": label, "duration_audio": dur, "model": "tdt",
|
|
"timestamps": False, **r})
|
|
if r["ok"]:
|
|
log(f" -> {r['chars']} chars in {r['duration_infer']} s, "
|
|
f"{r['vram_peak_mb']} MB")
|
|
else:
|
|
log(f" FAIL: {r['error']}")
|
|
|
|
log(" TDT timestamps=True")
|
|
r = run_tdt(tdt, wav, timestamps=True)
|
|
record({"case": label, "duration_audio": dur, "model": "tdt",
|
|
"timestamps": True, **r})
|
|
if r["ok"]:
|
|
log(f" -> {r['chars']} chars, ts_summary={r['timestamp_summary']}")
|
|
else:
|
|
log(f" FAIL: {r['error']}")
|
|
|
|
log(" Canary path1 (reference)")
|
|
r = run_canary(model, wav)
|
|
record({"case": label, "duration_audio": dur, "model": "canary_path1",
|
|
**r})
|
|
if r["ok"]:
|
|
log(f" -> {r['chars']} chars in {r['duration_infer']} s")
|
|
else:
|
|
log(f" FAIL: {r['error']}")
|
|
|
|
log("done")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|