""" API-conducive pipeline: VAD-trim each channel LOCALLY (remove the 80% silence that makes whisper-1 hallucinate), then send only the dense speech to the API. This is the fair test: the local pipeline removes silence inside the model via vad_filter=True; the API has no such knob, so we replicate it as a preprocessing step using the SAME silero VAD (same min_silence_duration_ms=500 as our local run). Saves the compact WAVs and the API transcripts so we can inspect what changed. Usage: set OPENAI_API_KEY=sk-... python pilot_api_vad.py """ import os, json, time, tempfile import numpy as np import soundfile as sf import jiwer from openai import OpenAI from faster_whisper.vad import get_speech_timestamps, collect_chunks, VadOptions from eval_common import normalise DATA = r"d:\Desktop\ai-ml-capstone\data\na_testset" MANIFEST = os.path.join(DATA, "manifest.json") PROBE_SET = os.path.join(DATA, "probe_set.json") OUT_DIR = os.path.join(DATA, "results_api_vad") INITIAL_PROMPT = ( "Banking call center transcript. " "Speakers discuss account numbers, balances, transfers, loans, credit cards, " "PINs, dates, dollar amounts, authentication, and customer service." ) # same VAD config as the local per-channel pipeline (transcribe_channels.py) VAD_OPTS = VadOptions(min_silence_duration_ms=500) with open(MANIFEST, encoding="utf-8") as f: manifest = {m["call_id"]: m for m in json.load(f)} with open(PROBE_SET, encoding="utf-8") as f: probe = json.load(f)["calls"] p = probe[0] cid, accent = p["call_id"], p["accent"] m = manifest[cid] a_wav = os.path.join(DATA, m["agent_wav"]) c_wav = os.path.join(DATA, m["customer_wav"]) api_key = os.environ.get("OPENAI_API_KEY") if not api_key: raise SystemExit("ERROR: OPENAI_API_KEY not set.") client = OpenAI(api_key=api_key) os.makedirs(os.path.join(OUT_DIR, accent), exist_ok=True) def vad_trim(wav_path, label): """Load 16k mono, VAD-trim silence, return compact float32 + stats.""" audio, sr = sf.read(wav_path, dtype="float32") if audio.ndim > 1: audio = audio.mean(axis=1) dur_in = len(audio) / sr ts = get_speech_timestamps(audio, VAD_OPTS, sampling_rate=sr) if not ts: print(f" {label:<10} no speech detected; sending original") return audio, sr, dur_in, dur_in chunks, _ = collect_chunks(audio, ts, sampling_rate=sr) # join speech chunks with 0.15s of silence so words don't mash at seams gap = np.zeros(int(0.15 * sr), dtype="float32") pieces = [] for i, ch in enumerate(chunks): if i: pieces.append(gap) pieces.append(ch) compact = np.concatenate(pieces) dur_out = len(compact) / sr print(f" {label:<10} {dur_in:5.0f}s -> {dur_out:5.0f}s " f"({(1-dur_out/dur_in)*100:.0f}% silence removed, {len(ts)} speech regions)") return compact, sr, dur_in, dur_out def transcribe_api(audio, sr, label): # write compact wav straight to the output dir (no temp -> no cross-drive move) keep = os.path.join(OUT_DIR, accent, f"{cid}_{label}_compact.wav") sf.write(keep, audio, sr, subtype="PCM_16") size_mb = os.path.getsize(keep) / 1e6 t0 = time.time() with open(keep, "rb") as f: resp = client.audio.transcriptions.create( model="whisper-1", file=f, language="en", prompt=INITIAL_PROMPT, response_format="text") elapsed = time.time() - t0 text = (resp if isinstance(resp, str) else str(resp)).strip() print(f" {label:<10} {elapsed:5.1f}s {size_mb:4.1f}MB | {len(text.split())} words") return text, elapsed def acc(ref, hyp): r, h = normalise(ref), normalise(hyp) n = len(r.split()) return (1 - jiwer.wer(r, h)) * 100 if n else 100.0, n print(f"Pilot call : {cid} ({accent} {m['domain']})\n") print("Step 1 - local VAD trim:") a_audio, sr, a_din, a_dout = vad_trim(a_wav, "agent") c_audio, sr, c_din, c_dout = vad_trim(c_wav, "customer") print("\nStep 2 - whisper-1 API on trimmed speech:") agent_text, t_a = transcribe_api(a_audio, sr, "agent") customer_text, t_c = transcribe_api(c_audio, sr, "customer") t_total = t_a + t_c # save transcripts with open(os.path.join(OUT_DIR, accent, cid + ".json"), "w", encoding="utf-8") as f: json.dump({"call_id": cid, "accent": accent, "domain": m["domain"], "model": "whisper-1 + local VAD trim", "agent_text": agent_text, "customer_text": customer_text}, f, indent=2) # ── comparison ──────────────────────────────────────────────────────────────── p1_path = os.path.join(DATA, "results_channels", accent, cid + ".json") with open(p1_path, encoding="utf-8") as f: p1 = json.load(f) small_a = " ".join(w["word"] for w in p1["agent"]) small_c = " ".join(w["word"] for w in p1["customer"]) aa_s, na = acc(m["agent_transcript"], small_a) ac_s, nc = acc(m["customer_transcript"], small_c) aa_v, _ = acc(m["agent_transcript"], agent_text) ac_v, _ = acc(m["customer_transcript"], customer_text) small_overall = (aa_s*na + ac_s*nc) / (na+nc) vad_overall = (aa_v*na + ac_v*nc) / (na+nc) dur = a_din # ~equal channels print(f"\n API wall time: {t_total:.1f}s | projected 12-probe: {12*t_total/60:.0f} min") print(f"\n{'='*66}") print(f" Head-to-head on {cid}") print(f" {'Model':<28} {'Agent':>7} {'Customer':>9} {'Overall':>8}") print(f" {'-'*60}") print(f" {'small.en (local)':<28} {aa_s:>6.1f}% {ac_s:>8.1f}% {small_overall:>7.1f}%") print(f" {'medium.en (local)':<28} {90.7:>6.1f}% {87.5:>8.1f}% {89.6:>7.1f}%") print(f" {'whisper-1 API (naive)':<28} {88.6:>6.1f}% {71.4:>8.1f}% {82.5:>7.1f}%") print(f" {'whisper-1 API (VAD-trim)':<28} {aa_v:>6.1f}% {ac_v:>8.1f}% {vad_overall:>7.1f}%") print(f"{'='*66}") print(f"\n Customer channel: naive 71.4% -> VAD-trim {ac_v:.1f}% ({ac_v-71.4:+.1f})")