Download scripts/evaluate_coherence_dial.py from build-small-hackathon/glossolalia-dial: direct link, hf CLI and curl.
- Browser
- Download file 5.69 kB
-
https://huggingface.co/spaces/build-small-hackathon/glossolalia-dial/resolve/main/scripts/evaluate_coherence_dial.py
- Command line
-
hf download hf://spaces/build-small-hackathon/glossolalia-dial/scripts/evaluate_coherence_dial.py
-
curl -L -o evaluate_coherence_dial.py https://huggingface.co/spaces/build-small-hackathon/glossolalia-dial/resolve/main/scripts/evaluate_coherence_dial.py
5.69 kB
| """Coherence Dial validation harness. | |
| Per swept clip: | |
| - Whisper-WER vs the ORIGINAL typed sentence (should RISE as dial level rises) | |
| - Resemblyzer cosine vs the level-0 same-voice/same-seed reference clip | |
| (should stay >= 0.85 across all levels — voice preserved) | |
| Aggregate: | |
| - mean WER per level (and per voice) | |
| - mean cosine per level (and per voice) | |
| - Spearman( level, mean_WER ) --- gate >= +0.80 | |
| - min cosine across levels --- gate >= 0.85 | |
| Verdict: PASS / PARTIAL / FAIL with a per-level breakdown JSON report. | |
| Hallucination guard: when Whisper's avg_logprob falls below the threshold the transcription is | |
| junk (model invented text) — we floor WER to 1.0 in that case so glossolalia doesn't get a | |
| spuriously LOW WER because the model dreamed coherent words from noise. | |
| """ | |
| import argparse | |
| import json | |
| import math | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| def load_manifest(path: Path): | |
| rows = json.loads(path.read_text()) | |
| if isinstance(rows, dict) and "clips" in rows: | |
| rows = rows["clips"] | |
| return rows | |
| def whisper_wer(wav_path, ref_text, model, no_speech_threshold=0.8, logprob_threshold=-1.5): | |
| import jiwer | |
| out = model.transcribe(str(wav_path), no_speech_threshold=no_speech_threshold, | |
| logprob_threshold=logprob_threshold, condition_on_previous_text=False, | |
| language="en", fp16=False) | |
| hyp = (out.get("text") or "").strip() | |
| seg_avg_logprob = np.mean([s.get("avg_logprob", -10) for s in out.get("segments", [])]) if out.get("segments") else -10 | |
| seg_no_speech = np.mean([s.get("no_speech_prob", 1.0) for s in out.get("segments", [])]) if out.get("segments") else 1.0 | |
| if not hyp or seg_avg_logprob < logprob_threshold or seg_no_speech > no_speech_threshold: | |
| return 1.0, hyp, float(seg_avg_logprob), float(seg_no_speech) | |
| wer = jiwer.wer(ref_text.lower(), hyp.lower()) | |
| return float(min(wer, 1.0)), hyp, float(seg_avg_logprob), float(seg_no_speech) | |
| def resemblyzer_embed(wav_path, encoder): | |
| from resemblyzer import preprocess_wav | |
| wav = preprocess_wav(str(wav_path)) | |
| return encoder.embed_utterance(wav) | |
| def main(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--manifest", required=True, help="sweep manifest JSON (from sweep_dial.py)") | |
| p.add_argument("--out", default="sweep/eval_report.json") | |
| p.add_argument("--whisper", default="base.en") | |
| p.add_argument("--spearman-gate", type=float, default=0.80) | |
| p.add_argument("--cosine-gate", type=float, default=0.85) | |
| args = p.parse_args() | |
| rows = load_manifest(Path(args.manifest)) | |
| if not rows: | |
| print("empty manifest", file=sys.stderr); sys.exit(1) | |
| import whisper | |
| from resemblyzer import VoiceEncoder | |
| print(f"loading whisper:{args.whisper} + resemblyzer", file=sys.stderr) | |
| asr = whisper.load_model(args.whisper) | |
| enc = VoiceEncoder() | |
| # index lv0 reference per (voice, seed, sentence) | |
| ref_by = {} | |
| for r in rows: | |
| if r["level"] == 0: | |
| ref_by[(r["voice"], r["seed"], r["sentence"])] = r["wav"] | |
| per_clip = [] | |
| for i, r in enumerate(rows): | |
| wav = r["wav"]; ref = r["sentence"] | |
| wer, hyp, lp, nsp = whisper_wer(wav, ref, asr) | |
| cos = 1.0 | |
| if r["level"] != 0: | |
| ref_wav = ref_by.get((r["voice"], r["seed"], r["sentence"])) | |
| if ref_wav and Path(ref_wav).exists(): | |
| e_clip = resemblyzer_embed(wav, enc) | |
| e_ref = resemblyzer_embed(ref_wav, enc) | |
| cos = float(np.dot(e_clip, e_ref) / ((np.linalg.norm(e_clip) * np.linalg.norm(e_ref)) + 1e-9)) | |
| per_clip.append({**r, "wer": wer, "hyp": hyp, "logprob": lp, "no_speech": nsp, "cos_vs_lv0": cos}) | |
| if (i + 1) % 20 == 0: | |
| print(f" {i + 1}/{len(rows)}", file=sys.stderr) | |
| # aggregate per level | |
| levels = sorted({r["level"] for r in per_clip}) | |
| per_level = {} | |
| for lv in levels: | |
| sub = [r for r in per_clip if r["level"] == lv] | |
| per_level[lv] = { | |
| "n": len(sub), | |
| "mean_wer": float(np.mean([r["wer"] for r in sub])), | |
| "mean_cos_vs_lv0": float(np.mean([r["cos_vs_lv0"] for r in sub])), | |
| } | |
| xs = np.array(levels, dtype=float) | |
| ys = np.array([per_level[lv]["mean_wer"] for lv in levels]) | |
| cs = np.array([per_level[lv]["mean_cos_vs_lv0"] for lv in levels]) | |
| from scipy.stats import spearmanr | |
| sp = float(spearmanr(xs, ys).correlation) if len(xs) >= 3 else float("nan") | |
| min_cos = float(cs[xs > 0].min()) if (xs > 0).any() else 1.0 | |
| verdict = ("PASS" if (sp >= args.spearman_gate and min_cos >= args.cosine_gate) | |
| else "PARTIAL" if (sp >= 0.5 or min_cos >= 0.75) else "FAIL") | |
| report = { | |
| "manifest": args.manifest, "n_clips": len(per_clip), | |
| "per_level": per_level, "spearman_wer_vs_level": sp, | |
| "min_cos_vs_lv0_across_levels": min_cos, | |
| "gates": {"spearman": args.spearman_gate, "cosine": args.cosine_gate}, | |
| "verdict": verdict, | |
| } | |
| Path(args.out).parent.mkdir(parents=True, exist_ok=True) | |
| Path(args.out).write_text(json.dumps({**report, "per_clip": per_clip}, indent=2)) | |
| print("\n===== COHERENCE DIAL EVAL =====") | |
| for lv in levels: | |
| r = per_level[lv] | |
| print(f" level {lv}: WER={r['mean_wer']:.3f} cos_vs_lv0={r['mean_cos_vs_lv0']:.3f} (n={r['n']})") | |
| print(f"\nSpearman(WER, level) = {sp:+.3f} (gate >= +{args.spearman_gate:.2f})") | |
| print(f"min cos vs lv0 = {min_cos:.3f} (gate >= {args.cosine_gate:.2f})") | |
| print(f"VERDICT: {verdict}") | |
| print(f"full report -> {args.out}") | |
| if __name__ == "__main__": | |
| main() | |