Skip to content

Commit 8bdb28d

Browse files
authored
fix(eval): #M17 spike — paired UTMOS comparison + stable doc refs (#79)
Tiny follow-up to PR #77. Fixes the spike script's UTMOS gate computation to use a sample-matched clean mean (matches what the canonical report tables already publish), exposes both means in the JSON, and swaps a fragile design-doc line-number reference for a stable section heading. Resolves 3 of 5 post-merge Copilot threads on #77 (all duplicates of the same UTMOS-population issue) plus the line-number-drift thread. The 5th thread (torch.hub trust_repo=True) is the 4th recurrence of an already-treated supply-chain concern (pinned + documented in #77 Limitations §8) and is intentionally not re-addressed. No published PR #77 conclusions change — the report tables already used the correct paired comparison.
1 parent 2408757 commit 8bdb28d

2 files changed

Lines changed: 18 additions & 9 deletions

File tree

‎docs/m17_phase_a_validation_report.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# M17 Phase A Validation Report — Whisper + UTMOS on Hebrew Clips
22

33
This report validates the Phase A acceptance criteria from the M17 design doc
4-
(`docs/automated_eval_design.md` lines 418–422) before the E1/E2 module skeletons land.
4+
(`docs/automated_eval_design.md` §"Acceptance Criteria") before the E1/E2 module skeletons land.
55

66
Generated: 2026-05-05. Raw data: `state/spikes/m17_phase_a/results.json` (gitignored). Auto-tables: `state/spikes/m17_phase_a/report_auto.md`. Listening samples: `state/spikes/m17_phase_a/<clip_id>__<degradation>.wav`. SHA-256 prefixes shown in the manifest are first-8-chars only — full hashes in `results.json`.
77

‎scripts/m17_phase_a_validation.py‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,16 +411,21 @@ def write_auto_report(results: dict) -> None:
411411
md.append(f"| {cid} | {score:.3f} |")
412412
md.append("")
413413
md.append(
414-
f"clean mean: **{results['utmos']['clean_mean']:.3f}** (n={len(results['utmos']['clean'])})"
414+
f"clean mean (all): **{results['utmos']['clean_mean']:.3f}** "
415+
f"(n={len(results['utmos']['clean'])}) · "
416+
f"clean mean (degradation sample, n={len(results['utmos']['degradation_sample_clip_ids'])}): "
417+
f"**{results['utmos']['sample_clean_mean']:.3f}**"
415418
)
416419
md.append("")
417420
md.append("## UTMOS — degradation sweep")
418421
md.append("")
419422
md.append("| Degradation | mean UTMOS | clean − deg | direction |")
420423
md.append("|---|---|---|---|")
421-
clean_mean = results["utmos"]["clean_mean"]
424+
# Paired comparison — degraded means cover only the degradation-sample clips,
425+
# so we subtract from the sample-matched clean mean, not the all-clip mean.
426+
sample_clean_mean = results["utmos"]["sample_clean_mean"]
422427
for d in results["utmos"]["degradations"]:
423-
diff = clean_mean - d["mean_utmos"]
428+
diff = sample_clean_mean - d["mean_utmos"]
424429
direction = "↓ as expected" if diff > 0 else "↑ INVERTED"
425430
md.append(
426431
f"| `{d['id']}` ({d['kind']}) | {d['mean_utmos']:.3f} | {diff:+.3f} | {direction} |"
@@ -557,14 +562,16 @@ def main() -> None:
557562
}
558563
)
559564

560-
# Gate: original gate uses the white-noise -10 dB SNR baseline (the
561-
# design's implicit reference). We also report whether ANY degradation in
562-
# the sweep meets the 0.5 separation threshold.
565+
# Gate: paired comparison between the SAME 5 clips clean and degraded.
566+
# `clean_mean` (n=10) is kept in the JSON as a population baseline, but the
567+
# gate must use `sample_clean_mean` to avoid mixing populations — degraded
568+
# means are over `sample_clips` only.
569+
sample_clean_mean = float(np.mean([clean_scores[c.clip_id] for c in sample_clips]))
563570
primary_deg = next(d for d in degradation_results if d["id"] == "wn_snr_-10db")
564-
primary_separation = clean_mean - primary_deg["mean_utmos"]
571+
primary_separation = sample_clean_mean - primary_deg["mean_utmos"]
565572
primary_gate = "PASS" if primary_separation >= UTMOS_SEPARATION_GATE else "FAIL"
566573
any_passes = any(
567-
(clean_mean - d["mean_utmos"]) >= UTMOS_SEPARATION_GATE for d in degradation_results
574+
(sample_clean_mean - d["mean_utmos"]) >= UTMOS_SEPARATION_GATE for d in degradation_results
568575
)
569576
monotonic_in_severity = is_monotonic_with_severity(
570577
[d for d in degradation_results if d["kind"] == "white_noise"]
@@ -599,6 +606,8 @@ def main() -> None:
599606
"utmos": {
600607
"clean": clean_scores,
601608
"clean_mean": clean_mean,
609+
"sample_clean_mean": sample_clean_mean,
610+
"degradation_sample_clip_ids": [c.clip_id for c in sample_clips],
602611
"degradations": degradation_results,
603612
"primary_separation_db_-10_white_noise": primary_separation,
604613
"primary_gate": primary_gate,

0 commit comments

Comments
 (0)