Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """Utterance-level paired bootstrap on per-utterance error. | |
| Chains from ``score_seed_tts_eval.py``, which writes ``per_utt_wer.jsonl`` | |
| (one JSON object per utterance with a ``gen_wav`` key and a ``wer`` / ``cer`` | |
| field). Given two such files for two systems evaluated on the *same* prompts, | |
| this reports: | |
| * mean paired difference (system B − system A) | |
| * a bootstrap 95% confidence interval over the per-utterance differences | |
| * a Wilcoxon signed-rank p-value (no normality assumption) | |
| Utterances are aligned by an id derived from the ``gen_wav`` filename stem so | |
| the two systems' rows match even if their output directories differ. | |
| Example:: | |
| python scripts/paired_bootstrap.py \ | |
| --a runs/base4/per_utt_wer.jsonl \ | |
| --b runs/srfd/per_utt_wer.jsonl \ | |
| --metric wer | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, List, Tuple | |
| import numpy as np | |
| def parse_args() -> argparse.Namespace: | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--a", required=True, help="per_utt JSONL for system A (e.g. baseline).") | |
| p.add_argument("--b", required=True, help="per_utt JSONL for system B (e.g. SR-FD).") | |
| p.add_argument("--metric", default="wer", help="Field name to compare (wer or cer).") | |
| p.add_argument("--id_field", default="gen_wav", help="Field used to align utterances.") | |
| p.add_argument("--n_bootstrap", type=int, default=10000) | |
| p.add_argument("--seed", type=int, default=0) | |
| p.add_argument("--out", default="") | |
| return p.parse_args() | |
| def _load(path: str, metric: str, id_field: str) -> Dict[str, float]: | |
| out: Dict[str, float] = {} | |
| for line in Path(path).read_text(encoding="utf-8").splitlines(): | |
| line = line.strip() | |
| if not line: | |
| continue | |
| row = json.loads(line) | |
| if metric not in row or row[metric] is None: | |
| continue | |
| key = Path(str(row[id_field])).stem | |
| out[key] = float(row[metric]) | |
| return out | |
| def bootstrap_ci(diffs: np.ndarray, n: int, seed: int, alpha: float = 0.05) -> Tuple[float, float]: | |
| rng = np.random.default_rng(seed) | |
| means = np.empty(n, dtype=np.float64) | |
| for i in range(n): | |
| idx = rng.integers(0, len(diffs), size=len(diffs)) | |
| means[i] = diffs[idx].mean() | |
| lo, hi = np.percentile(means, [100 * alpha / 2, 100 * (1 - alpha / 2)]) | |
| return float(lo), float(hi) | |
| def wilcoxon_p(diffs: np.ndarray) -> float: | |
| nz = diffs[diffs != 0] | |
| if nz.size < 2: | |
| return float("nan") | |
| try: | |
| from scipy import stats | |
| except Exception: | |
| return float("nan") | |
| return float(stats.wilcoxon(nz, alternative="two-sided", zero_method="zsplit").pvalue) | |
| def main() -> int: | |
| args = parse_args() | |
| a = _load(args.a, args.metric, args.id_field) | |
| b = _load(args.b, args.metric, args.id_field) | |
| common: List[str] = sorted(set(a) & set(b)) | |
| if not common: | |
| raise SystemExit("No overlapping utterance ids between the two files.") | |
| m_a = np.array([a[k] for k in common]) | |
| m_b = np.array([b[k] for k in common]) | |
| diffs = m_b - m_a # negative -> system B better | |
| lo, hi = bootstrap_ci(diffs, n=args.n_bootstrap, seed=args.seed) | |
| pval = wilcoxon_p(diffs) | |
| result = { | |
| "n_prompts": len(common), | |
| "metric": args.metric, | |
| "a_mean": float(m_a.mean()), | |
| "b_mean": float(m_b.mean()), | |
| "diff_mean": float(diffs.mean()), | |
| "diff_median": float(np.median(diffs)), | |
| "bootstrap_ci_95_lo": lo, | |
| "bootstrap_ci_95_hi": hi, | |
| "wilcoxon_pvalue": pval, | |
| "n_better_for_b": int((diffs < 0).sum()), | |
| "n_worse_for_b": int((diffs > 0).sum()), | |
| "n_tie": int((diffs == 0).sum()), | |
| } | |
| print(json.dumps(result, indent=2)) | |
| if args.out: | |
| Path(args.out).parent.mkdir(parents=True, exist_ok=True) | |
| Path(args.out).write_text(json.dumps(result, indent=2), encoding="utf-8") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |