Skip to content

feat(rhythm): 16th-grid shuffle detection in the swing measurement (PR-G5) - #214

Merged
slittycode merged 7 commits into
mainfrom
accuracy/genre-swing-16th
Jul 13, 2026
Merged

feat(rhythm): 16th-grid shuffle detection in the swing measurement (PR-G5)#214
slittycode merged 7 commits into
mainfrom
accuracy/genre-swing-16th

Conversation

@slittycode

Copy link
Copy Markdown
Owner

What

PR-G5 of the genre-generalization program (stacked on #213): the swing measurement learns to see the 16th-note grid — UKG/2-step shuffle — which was previously invisible (compute_swing_detail kept only 8th-scale IOIs, 0.30–0.70 beats, and hardwired gridResolution: "8th"; the shuffle corpus clip measured swingDetail: None).

Design

The 8th grid keeps absolute priority and its outputs are byte-identical: swung-8th material returns exactly what it did before, and so does straight material. Only when the 8th grid reads straight or absent is the 16th grid probed, with the same long/short-split logic at half scale (straight center 0.25 beats, split thresholds 0.265/0.235). A 16th result only ships when a genuine long/short alternation exists (≥2 of each) — straight 16ths cluster at 0.25 and cannot fabricate a shuffle; sparse material still abstains with None.

Measured results — an honest split between detection and value

On shuffle16_130_62 (four-on-floor kicks + 62%-shuffled 16th hats):

  1. Detection now gates: swingDetail reads {gridResolution: "16th", direction: "swung", swingConfidence: 0.992} where it previously read None. A new swing:gridResolution check (additive harness capability, expected.swingGrid) promotes this to an active gate.
  2. The ratio value stays a knownGap, with a diagnosis: measured 55.0 vs truth 62.0. The detected IOI clusters are tight but compressed exactly one onset-frame per side — onset detection quantizes to 11.6 ms hops (512 samples), and one frame ≈ 2.7 swing points at 16th scale/130 BPM. Verified NOT to be a fixture artifact: re-rendering with a 50 ms closed hat instead of the 120 ms hat produces byte-identical medians. Correcting the value without a gate would be the confidently-wrong pattern this repo's history warns about (the fuzzy-rewrite lesson), so the value fix is a future, separately gated onset-timing refinement, and the gap entry documents the mechanism.

Regression guard: the five existing 8th-grid swing clips (50–66%) and every other rhythm clip are unchanged — 7 unit tests in test_swing_detail.py cover 16th recovery at 58/62/66 from clean IOIs (exact), 8th-priority, straight-16th no-fabrication, and the original 8th cases.

Validation

  1. Backend: full unittest discover — 1361 tests, OK (3 skipped) (exit codes captured directly).
  2. Synthetic eval: 38/38 clips, 114 active passed / 0 failed / 10 informational.
  3. CI-mirror fundamentals gate: 15/15, allPassed.
  4. UI npm run verify: 51 smoke passed (no UI-side changes in this PR — gridResolution and swingPercent are existing fields).

🤖 Generated with Claude Code

slittycode and others added 6 commits July 13, 2026 05:59
…acy PR-G3)

Adds bpmOctaveEvidence (full-mode only): simple-ratio candidates
(1:2, 2:3, 1:1, 3:2, 2:1 of the shipped bpm, clamped 40-220) scored
against two inter-onset streams — low-band kick articulation (integer
multiples, fit divided by the median multiple) plus the full-band
hat/snare grid (admits the 8th-note 0.5 multiple), which pins the
notated tempo when the kick only marks bars. Never overrides bpm
(invariant #1); abstains (null) on sparse/beatless material.

Measured on the corpus: recovers all three broken octave cases
(174 -> 173.8, 190 -> 189.8, halftime-174 -> 175.5 via 3:2) with zero
false alarms on the 10 correctly-shipped rhythm clips; the beatless
ambient clip abstains. Promoted as an active tempoOctave:preferredBpm
gate on 14 rhythm clips (13 pass; grid_7_8_140 stays informational —
its bpm is meter-smeared, not octave-wrong: PR-G4 territory).

Both sides landed: JSON_SCHEMA.md + full-only docstring, measurement.ts
types, backendPhase1Client reconstructor, phase1FullPayload fixture;
golden deliberately re-baselined (bpmOctaveEvidence key + one
rounding-step lufsShortTermMax nudge within designed tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o-fixture key set

Two omissions from PR-G3 that CI caught on the rebased branch:
- server_phase1._build_phase1 did not forward bpmOctaveEvidence to the
  frontend (test_phase1_golden.BuildPhase1KeySupersetTests caught it)
- EXPECTED_TOP_LEVEL_KEYS in test_audio_fixture.py did not include it
  (AudioFixtureSmokeTest.test_output_contains_all_expected_keys caught it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCtxwnhn8nzHBjzeehPtf8
…(accuracy PR-G4)

The onset-count accent stream is loudness-blind: a kick on every beat
yields identical counts at every bar position regardless of which beat
is accented — measured on the odd-meter corpus clips as 3/4 edging 4/4
by 0.8%, shipping 4/4 at confidence 0.03. analyze_time_signature now
folds the per-beat low-band loudness (beat_data, hoisted above the
meter call) at each candidate bar length as a second accent stream:
max-over-phase dominance, clamped at 10, neutral under 4 bars, harmonic
folds collapsed by echo PROMINENCE (0.262 true-3/4 vs 0.000 true-6/8
measured; raw echo/peak is ~0.72 for both). Decision runs on the
count x loudness product with a 15% override margin when loudness
contributed; count-only callers (including --fast) keep the 20% margin
byte-identical.

Burn-down: 3/4, 6/8, 7/8 meter AND downbeats (F1 0.875 each) flip from
knownGaps to gating checks; all ten 4/4 rhythm clips (incl. broken-kick
2-step/halftime/breakbeat) unchanged. Remaining gaps are all
tempo-family. timeSignatureCandidates entries gain an additive
loudnessDominance key, landed on both sides; golden passes without
re-baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…R-G4

# Conflicts:
#	apps/backend/scripts/build_synthetic_corpus.py
#	apps/backend/tests/fixtures/fundamentals_eval_manifest.synthetic.json
…racy PR-G5)

compute_swing_detail probes the 16th grid when the 8th grid reads
straight or absent: same long/short-split logic at half scale (straight
center 0.25, thresholds 0.265/0.235). UKG/2-step shuffle lives on the
16ths and was previously invisible. 8th-swung material keeps priority
and is byte-identical; straight material cannot fabricate a shuffle (no
split at 0.25); sparse material still abstains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…me-resolution gap (PR-G5)

New swing:gridResolution harness check (expected.swingGrid) promotes
shuffle detection to an active gate on shuffle16_130_62 — swingDetail
now reads swung/16th/conf 0.992 where it read None. The ratio value
stays a knownGap with its measured diagnosis: onset detection
quantizes to 11.6ms hops with a one-frame-per-side inward bias,
compressing 62% shuffle to a 55% read (verified not a fixture
artifact: a 50ms closed hat renders byte-identical medians). Value
correction without a gate would repeat the confidently-wrong pattern;
it stays a future, separately gated onset-timing refinement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from accuracy/genre-meter-downbeats to main July 13, 2026 09:39
@slittycode
slittycode merged commit 0553dcf into main Jul 13, 2026
4 checks passed
@slittycode
slittycode deleted the accuracy/genre-swing-16th branch July 13, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants