"""Character error rate (CER) between a reference baseline and a candidate. CER = levenshtein(reference, hypothesis) / len(reference). Used to gate work-reduction changes: keep a change only if CER <= 0.005. Usage: python scripts/cer_divergence.py baseline_outputs/ candidate_outputs/ """ from __future__ import annotations import argparse import json from pathlib import Path def _levenshtein(a: str, b: str) -> int: if a == b: return 0 if not a: return len(b) if not b: return len(a) prev = list(range(len(b) + 1)) for i, ca in enumerate(a, 1): cur = [i] for j, cb in enumerate(b, 1): cost = 0 if ca == cb else 1 cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)) prev = cur return prev[-1] def cer(reference: str, hypothesis: str) -> float: if not reference: return 0.0 if not hypothesis else 1.0 return _levenshtein(reference, hypothesis) / len(reference) def cer_over_dirs(baseline_dir: Path, candidate_dir: Path) -> dict: per_file = {} for ref_path in sorted(baseline_dir.glob("*.txt")): cand_path = candidate_dir / ref_path.name ref = ref_path.read_text(encoding="utf-8") hyp = cand_path.read_text(encoding="utf-8") if cand_path.exists() else "" per_file[ref_path.name] = cer(ref, hyp) mean = sum(per_file.values()) / len(per_file) if per_file else 0.0 return {"mean_cer": mean, "max_cer": max(per_file.values(), default=0.0), "per_file": per_file} def main() -> None: parser = argparse.ArgumentParser(description="CER divergence between two output dirs.") parser.add_argument("baseline_dir", type=Path) parser.add_argument("candidate_dir", type=Path) args = parser.parse_args() print(json.dumps(cer_over_dirs(args.baseline_dir, args.candidate_dir), indent=2)) if __name__ == "__main__": main()