2828)
2929from tabvision .eval .manifest import ManifestValidation , validate_manifest
3030from tabvision .eval .metrics import (
31+ ChordAccuracyResult ,
3132 EventF1Result ,
3233 TabF1Result ,
34+ chord_instance_accuracy ,
3335 event_f1 ,
3436 tab_f1 ,
3537)
3638from tabvision .eval .parsers import get_parser
37- from tabvision .types import GuitarConfig , SessionConfig , TabEvent
39+ from tabvision .types import AudioBackend , GuitarConfig , SessionConfig , TabEvent
3840
3941Predictor = Callable [[Path , SessionConfig ], list [TabEvent ]]
4042"""``(media_path, session) -> list[TabEvent]``. The composite-eval harness
@@ -53,6 +55,7 @@ class ClipEvalResult:
5355 onset : EventF1Result
5456 pitch : EventF1Result
5557 tab : TabF1Result
58+ chord : ChordAccuracyResult
5659 errors : ErrorDecomposition
5760
5861
@@ -66,6 +69,7 @@ class TierReport:
6669 onset_f1 : BootstrapResult
6770 pitch_f1 : BootstrapResult
6871 tab_f1 : BootstrapResult
72+ chord_accuracy : BootstrapResult
6973 errors : ErrorDecomposition # summed across clips in this tier
7074
7175
@@ -170,6 +174,7 @@ def run_composite_eval(
170174 predicted , gold , match_pitch = True , onset_tolerance_s = onset_tolerance_s
171175 ),
172176 tab = tab_f1 (predicted , gold , onset_tolerance_s = onset_tolerance_s ),
177+ chord = chord_instance_accuracy (predicted , gold ),
173178 errors = decompose_errors (predicted , gold , onset_tolerance_s = onset_tolerance_s ),
174179 )
175180 )
@@ -206,13 +211,15 @@ def _aggregate_per_tier(
206211 onset_f1s = [r .onset .f1 for r in results ]
207212 pitch_f1s = [r .pitch .f1 for r in results ]
208213 tab_f1s = [r .tab .f1 for r in results ]
214+ chord_accs = [r .chord .accuracy for r in results ]
209215 reports [tier ] = TierReport (
210216 tier = tier ,
211217 n_clips = len (results ),
212218 n_gold_total = sum (r .n_gold for r in results ),
213219 onset_f1 = bootstrap_ci (onset_f1s , n_bootstrap = bootstrap_n , seed = bootstrap_seed ),
214220 pitch_f1 = bootstrap_ci (pitch_f1s , n_bootstrap = bootstrap_n , seed = bootstrap_seed ),
215221 tab_f1 = bootstrap_ci (tab_f1s , n_bootstrap = bootstrap_n , seed = bootstrap_seed ),
222+ chord_accuracy = bootstrap_ci (chord_accs , n_bootstrap = bootstrap_n , seed = bootstrap_seed ),
216223 errors = aggregate_decompositions (r .errors for r in results ),
217224 )
218225 return reports
@@ -251,16 +258,20 @@ def _session_from_clip(clip: dict[str, object]) -> SessionConfig:
251258
252259
253260DEFAULT_TIER_TARGETS : Mapping [str , float ] = {
254- "clean_acoustic_single_line" : 0.85 ,
255- "clean_acoustic_strummed" : 0.90 ,
256- "clean_electric" : 0.87 ,
257- "distorted_electric" : 0.80 ,
261+ "clean_acoustic_single_line" : 0.45 ,
262+ "clean_acoustic_strummed" : 0.60 ,
263+ "clean_electric" : 0.90 ,
264+ "distorted_electric" : 0.82 ,
258265}
259- """Per-tier Tab F1 acceptance targets from SPEC §1.4.1.
260-
261- These are the v1 acceptance bar locked in by the 2026-05-13 design plan
262- §0 D2. The original SPEC §1.4 numbers (0.94 / 0.86 / 0.90 / 0.82) are
263- the v1.1 / portfolio stretch reference, not used here.
266+ """Per-tier Tab F1 acceptance targets.
267+
268+ Acoustic tiers use the v1 honest audio-only gates from SPEC §1.4.1
269+ (2026-06-02): single-line >= 0.45, strummed >= 0.60. Single-line is
270+ information-limited from audio (string/fret ambiguity), so the original
271+ 0.94 is the v1.1 video-assisted reference, not a v1 gate. Electric tiers
272+ are deferred to v2; their numbers here are the SPEC §1.4 stretch reference
273+ and do not gate the acoustic v1 acceptance (they are "missing" in an
274+ acoustic-only run).
264275"""
265276
266277
@@ -312,6 +323,28 @@ def format_baseline_markdown(
312323 )
313324 lines .append ("" )
314325
326+ lines .append ("## Chord-instance accuracy" )
327+ lines .append ("" )
328+ lines .append (
329+ "Whole-fingering recovery per chord cluster. The >= 0.85 bar is a v1.1 "
330+ "video-assisted target; audio-only is string-resolution-limited, like "
331+ "single-line Tab F1 (SPEC §1.4.1)."
332+ )
333+ lines .append ("" )
334+ lines .append ("| Tier | Clips | Chord acc mean | Lower-95 |" )
335+ lines .append ("|---|---:|---:|---:|" )
336+ for tier in targets :
337+ tier_report = report .tiers .get (tier )
338+ if tier_report is None :
339+ lines .append (f"| { tier } | 0 | — | — |" )
340+ continue
341+ lines .append (
342+ f"| { tier } | { tier_report .n_clips } | "
343+ f"{ tier_report .chord_accuracy .statistic :.4f} | "
344+ f"{ tier_report .chord_accuracy .lower :.4f} |"
345+ )
346+ lines .append ("" )
347+
315348 lines .append ("## Per-source breakdown" )
316349 lines .append ("" )
317350 lines .append ("| Tier | Source | Clips | Tab F1 mean | Onset F1 mean | Pitch F1 mean |" )
@@ -410,11 +443,22 @@ def make_run_pipeline_predictor(
410443 Imports ``run_pipeline`` lazily so the composite-eval CLI's --help
411444 works without the audio-highres extras installed.
412445 """
446+ from tabvision .audio .backend import make as make_audio_backend # noqa: PLC0415
413447 from tabvision .pipeline import run_pipeline # noqa: PLC0415
414448
449+ # Build the audio backend ONCE and reuse it across every clip. The highres
450+ # backend caches its model on first transcribe; rebuilding it per clip (the
451+ # old behaviour) reloaded the ~0.5 GB checkpoint every clip — ~10x slower,
452+ # and the accumulation exhausted memory partway through a 60-clip run.
453+ # "auto" routes per session, so it can't be prebuilt; it falls back per-clip.
454+ shared_backend : AudioBackend | None = (
455+ None if audio_backend_name == "auto" else make_audio_backend (audio_backend_name )
456+ )
457+
415458 def predictor (media_path : Path , session : SessionConfig ) -> list [TabEvent ]:
416459 return run_pipeline (
417460 str (media_path ),
461+ audio_backend = shared_backend ,
418462 audio_backend_name = audio_backend_name ,
419463 position_prior = position_prior ,
420464 melodic_prior_enabled = melodic_prior_enabled ,
0 commit comments