From 9908d8393bbc60a53d4249837368a045e54d5166 Mon Sep 17 00:00:00 2001 From: Rob Morgan Date: Fri, 7 Aug 2026 09:28:13 +0800 Subject: [PATCH] feat: honest low-confidence grid display (ROADMAP Stage 10) Three layers make an untrustworthy grid visibly untrustworthy: - Library: when BOTH rigid-adoption gates decline a fit (phase_lock below 0.3 AND tracked-beat corroboration below 0.6), the estimators genuinely disagree - positive evidence the surviving tracked grid's phase is suspect on quantized material - so refine_grid_rigid caps the reported confidence at 0.5. The raw metric scores internal consistency, not ground truth: Somebody To Love measured beat F 0.31 with confidence 0.845. Verified on corpus: somebody 0.772 -> 0.500, adopted tracks unchanged. Ramps/live material unaffected (declined by the sanity floor, or adopted). - Desktop: GridMarks carries low_confidence (threshold 0.6, documented against the measured 0.79-0.94 healthy-corpus floor); beat/downbeat ticks dim to 40% in the zoomed and overview painters and the counter row shows a 'grid: low confidence' hint. - Annotation: somebody-to-love marked phase_indecisive with the 2026-08-07 measurements recorded. Also: desktop/Cargo.toml gains an empty [workspace] table so desktop cargo commands work in git worktrees (cargo otherwise walks past the worktree root and claims the outer repo's workspace; behavior in the main checkout is unchanged). Co-Authored-By: Claude Fable 5 --- .../somebody-to-love-extended-mix.json | 2 + desktop/Cargo.toml | 7 ++ desktop/src/waveform/counter.rs | 10 +++ desktop/src/waveform/mod.rs | 69 +++++++++++++++++++ desktop/src/waveform/overview.rs | 16 +++-- desktop/src/waveform/zoomed.rs | 19 +++-- src/analysis/rigid_grid.rs | 26 ++++++- 7 files changed, 137 insertions(+), 12 deletions(-) diff --git a/benchmarks/annotations/somebody-to-love-extended-mix.json b/benchmarks/annotations/somebody-to-love-extended-mix.json index ceb4ae2..a5e0249 100644 --- a/benchmarks/annotations/somebody-to-love-extended-mix.json +++ b/benchmarks/annotations/somebody-to-love-extended-mix.json @@ -979,6 +979,8 @@ "fitted_phase_secs": 0.0035, "generator": "annotate_rigid_grid (rigid grid at manifest BPM, kick-band phase fit)", "manifest_bpm": 128.0, + "note": "2026-08-07: genuinely phase-indecisive. The rigid fit lands on the exact manifest BPM but phase_lock measures 0.181 (below the 0.3 adoption gate) AND tracked-beat corroboration is only 0.28 (below CORROBORATION_MIN_AGREEMENT = 0.6, src/analysis/rigid_grid.rs) - the two independent estimators (kick-band phase circle vs full-band DP tracker) genuinely disagree, so the rigid grid is correctly not adopted. See LEARNINGS.md 'Stage 10 - Tracked-Beat Corroboration for Rigid Grids'.", + "phase_indecisive": true, "phase_lock": 0.165, "verified_by_ear": false } diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index caed2b7..ae259ab 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,3 +1,10 @@ +# Standalone crate: explicitly not part of any enclosing workspace. The +# repo root excludes "desktop", but that path-based exclude fails to match +# when this checkout is nested inside another (e.g. a git worktree under +# .claude/worktrees/), so cargo would otherwise walk up and claim the +# outer repo's workspace. +[workspace] + [package] name = "timestretch-desktop" version = "0.3.0" diff --git a/desktop/src/waveform/counter.rs b/desktop/src/waveform/counter.rs index f59dfb9..8c8ca9f 100644 --- a/desktop/src/waveform/counter.rs +++ b/desktop/src/waveform/counter.rs @@ -46,4 +46,14 @@ pub fn paint_beat_counter(ui: &mut egui::Ui, marks: &GridMarks, position_frames: ); } } + + // Honest-display hint: the waveform ticks are drawn dimmed for this + // grid, and this says why. + if marks.low_confidence() { + ui.label( + egui::RichText::new("grid: low confidence") + .small() + .color(palette::TEXT_DIM), + ); + } } diff --git a/desktop/src/waveform/mod.rs b/desktop/src/waveform/mod.rs index 9aa3a79..30de95f 100644 --- a/desktop/src/waveform/mod.rs +++ b/desktop/src/waveform/mod.rs @@ -52,6 +52,38 @@ pub(crate) mod palette { pub const PLAYED_TINT: Color32 = Color32::from_rgb(110, 110, 118); } +/// Grid-confidence threshold below which the deck presents the grid as +/// tentative: beat/downbeat ticks draw dimmed and the counter row shows a +/// "grid: low confidence" hint. +/// +/// What `timestretch::BeatGrid::confidence` means: +/// - Tracked (DP) grids score 0.4 * tempogram path salience + 0.3 * +/// beat-level onset support + 0.3 * interval regularity +/// (`grid_confidence`, src/analysis/beat.rs:600-631). +/// - Rigid-adopted grids keep at least that: confidence = +/// `grid.confidence.max(fit.phase_lock)` (src/analysis/rigid_grid.rs:298), +/// so rigid adoption never lowers the reading. +/// +/// Corpus evidence (benchmarks/baselines/bpm_accuracy_baseline_latest.json): +/// every real-music grid across the 16-track corpus — rigid-adopted or +/// tracked — reports confidence 0.79-0.94. 0.6 sits with clear margin below +/// that cluster, so a healthy grid never dims, while grids whose own +/// evidence collapses (weak periodicity or poor beat-level onset support: +/// ambient, rubato, speech-heavy material) fall through the salience and +/// support terms of the formula and flag. +/// +/// Honest limit: this flag cannot catch a wandering DP grid on quantized +/// material — somebody-to-love-extended-mix measures confidence 0.845 with +/// beat F 0.31 in the same baseline, because the metric scores internal +/// consistency, not ground truth. That failure class is handled upstream by +/// rigid-grid adoption and tracked-beat corroboration +/// (src/analysis/rigid_grid.rs:45-61), not by this display threshold. +pub(crate) const LOW_CONFIDENCE_THRESHOLD: f32 = 0.6; + +/// Gamma-space multiplier applied to tick colors on a low-confidence grid +/// (~40% alpha versions of the palette colors). +pub(crate) const LOW_CONFIDENCE_TICK_DIM: f32 = 0.4; + /// Beats in a bar for the counter/phrase math. The Stage 10 grid carries a /// 4/4 prior; bars with other beat counts wrap modulo 4 for display. const BEATS_PER_BAR: usize = 4; @@ -73,6 +105,10 @@ pub struct GridMarks { beat_in_bar: Vec, /// Median beat interval in frames (0.0 when fewer than 2 beats). median_beat_frames: f64, + /// Whether the detector's grid confidence fell below + /// [`LOW_CONFIDENCE_THRESHOLD`]; painters dim their ticks and the + /// counter row shows a hint. + low_confidence: bool, } impl GridMarks { @@ -83,6 +119,7 @@ impl GridMarks { bar_of: Vec::new(), beat_in_bar: Vec::new(), median_beat_frames: 0.0, + low_confidence: false, } } @@ -133,6 +170,7 @@ impl GridMarks { bar_of, beat_in_bar, median_beat_frames, + low_confidence: grid.confidence < LOW_CONFIDENCE_THRESHOLD, } } @@ -145,6 +183,12 @@ impl GridMarks { self.frames.len() >= 2 } + /// Whether the detector reported this grid below + /// [`LOW_CONFIDENCE_THRESHOLD`] (ticks dim, counter hints). + pub fn low_confidence(&self) -> bool { + self.low_confidence + } + pub fn frame(&self, i: usize) -> f64 { self.frames[i] } @@ -412,6 +456,31 @@ mod tests { assert_eq!(phrase_beats, vec![0, 64]); } + #[test] + fn low_confidence_flag_tracks_grid_confidence() { + let mut grid = timestretch::BeatGrid::empty(100); + grid.beats = (0..16).map(|i| i as f64 * 100.0).collect(); + grid.downbeats = vec![0, 4, 8, 12]; + grid.bpm = 60.0; + + // Corpus-healthy reading (tracked or rigid-adopted): not flagged. + grid.confidence = 0.85; + assert!(!GridMarks::from_grid(&grid).low_confidence()); + + // Collapsed evidence (weak periodicity / onset support): flagged. + grid.confidence = 0.3; + assert!(GridMarks::from_grid(&grid).low_confidence()); + + // Exactly at the threshold: not low — the gate is strict-less-than. + grid.confidence = LOW_CONFIDENCE_THRESHOLD; + assert!(!GridMarks::from_grid(&grid).low_confidence()); + } + + #[test] + fn empty_marks_are_not_flagged_low_confidence() { + assert!(!GridMarks::empty().low_confidence()); + } + #[test] fn median_interval_ignores_outliers() { let mut grid = timestretch::BeatGrid::empty(100); diff --git a/desktop/src/waveform/overview.rs b/desktop/src/waveform/overview.rs index 0a9e552..4de9db2 100644 --- a/desktop/src/waveform/overview.rs +++ b/desktop/src/waveform/overview.rs @@ -107,7 +107,16 @@ pub fn paint_overview(ui: &mut egui::Ui, params: OverviewParams<'_>) -> Option 0 { + let (bar_color, phrase_color) = if params.marks.low_confidence() { + ( + palette::TICK_BEAT.gamma_multiply(super::LOW_CONFIDENCE_TICK_DIM), + palette::TICK_PHRASE.gamma_multiply(super::LOW_CONFIDENCE_TICK_DIM), + ) + } else { + (palette::TICK_BEAT, palette::TICK_PHRASE) + }; let plan = overlay_plan( rect.width(), params.marks.len(), @@ -128,12 +137,9 @@ pub fn paint_overview(ui: &mut egui::Ui, params: OverviewParams<'_>) -> Option ( .count(); let agreement = hits as f64 / grid.beats.len() as f64; if agreement < CORROBORATION_MIN_AGREEMENT { + // Both adoption gates failed: the kick-band fit found the + // exact tempo but its phase is indecisive AND the tracked + // beats do not corroborate it — the two independent + // estimators genuinely disagree, which is positive evidence + // the surviving tracked grid's PHASE is untrustworthy on + // quantized material (corpus: Somebody To Love, beat F 0.31 + // yet raw confidence 0.845 — the confidence metric scores + // internal consistency, not ground truth). Cap the reported + // confidence so hosts can show an honest low-confidence + // grid; ramps and live material are unaffected (their fits + // are declined by the sanity floor or never reach here). + let mut grid = grid; + grid.confidence = grid.confidence.min(PHASE_UNTRUSTED_CONFIDENCE_CAP); return (grid, false); } } @@ -593,11 +612,16 @@ mod tests { // the tracked beats visit every phase of the rigid grid. *b += period * i as f64 / n; } - let (_grid, adopted) = refine_grid_rigid(&samples, SR, drifting); + let (grid, adopted) = refine_grid_rigid(&samples, SR, drifting); assert!( !adopted, "drifting tracked beats must not corroborate a rigid fit" ); + assert!( + grid.confidence <= PHASE_UNTRUSTED_CONFIDENCE_CAP, + "estimator disagreement must cap reported confidence, got {}", + grid.confidence + ); } #[test]