Skip to content

feat(rhythm): surfacing-only tempo-octave evidence at extremes (PR-G3) - #212

Merged
slittycode merged 2 commits into
mainfrom
accuracy/genre-tempo-octave-evidence
Jul 13, 2026
Merged

feat(rhythm): surfacing-only tempo-octave evidence at extremes (PR-G3)#212
slittycode merged 2 commits into
mainfrom
accuracy/genre-tempo-octave-evidence

Conversation

@slittycode

Copy link
Copy Markdown
Owner

What

PR-G3 of the genre-generalization program (stacked on #211, which stacks on #210): a new full-mode-only Phase 1 field, bpmOctaveEvidence, that surfaces how well simple-ratio alternatives (1:2, 2:3, 1:1, 3:2, 2:1, clamped to 40–220 BPM) explain the track's pulse. Surfacing-only: it never overrides bpm (PURPOSE.md invariant #1). Promotion of any correction into the shipped value stays behind the pre-registered GiantSteps tempo gate named in plans/genre-generalization-program.md.

Design

Candidates are scored against two inter-onset-interval streams (IOIs are local, so near-miss candidates can't hide behind grid drift):

  1. Low-band (30–150 Hz kick) — integer beat multiples only, fit divided by the median fitted multiple, so the pulse the kick actually articulates outranks one it merely aggregates (kick-on-every-beat scores 1.0 at the true tempo, 0.5 at half tempo).
  2. Full-band (hat/snare grid) — additionally admits the 8th-note 0.5 multiple. This is what pins the notated tempo when the kick only marks bars: mid-build I found kick-only evidence would prefer half tempo on a correctly-shipped halftime clip (kick every 4 beats reads as a slow pulse on its own); the hat stream fixes it, and test_correct_halftime_shipping_is_not_contradicted locks the case in.

Abstains (null) on sparse/beatless material — the ambient corpus clip confirms.

Measured on the synthetic corpus

Case Shipped bpm Evidence preferred Verdict
grid_4_4_174 86.9 (halved) 173.8 (2:1, dominance 2.0) recovered
grid_4_4_190 94.9 (halved) 189.8 (2:1, dominance 2.0) recovered
halftime_174 117.0 (2:3 error) 175.5 (3:2) recovered
halftime_140 139.9 (correct) 139.9 (1:1, dominance 2.5) no false alarm
10 other rhythm clips correct 1:1 no false alarms
ambient_beatless_70 null honest abstention

Promoted as an active tempoOctave:preferredBpm check (tolerance 2.0) on all 14 rhythm-kind clips: 13 gate green; grid_7_8_140 is registered informational — its 142.6 reading is a 7/8 meter smear, not an octave error, so no simple ratio lands on 140 (PR-G4's problem).

Contract chain (tripwires #3/#4)

analyze.py full-only key → JSON_SCHEMA.md root inventory + full-only list + field row → test_analyze.py full-only docstring → src/types/measurement.ts (BpmOctaveEvidence) → backendPhase1Client.ts reconstructor → phase1FullPayload.ts fixture → golden re-baselined deliberately (UPDATE_PHASE1_GOLDEN=1; picks up the new key plus a one-rounding-step lufsShortTermMax nudge that the golden's tolerance design explicitly absorbs).

Validation

  1. Backend: full unittest discover green (includes 9 new scorer tests in test_bpm_octave_evidence.py).
  2. Synthetic eval: 38/38 clips, 107 active checks passed / 0 failed / 16 informational.
  3. CI-mirror fundamentals gate (default manifest): green.
  4. UI npm run verify: 51 smoke passed (parity + citation-contract tests re-armed by the new golden).

Synthetic-corpus truth only; the real-audio octave-error rate lands when the GiantSteps tempo eval (staged locally) is re-run against this field — that run is the promotion gate's evidence, not this PR's.

Note: CI only arms on PRs based on main — full local gates documented above; rebase after the stack below it merges.

🤖 Generated with Claude Code

@slittycode
slittycode force-pushed the accuracy/genre-corpus-expansion branch from fc0fc92 to deaa72a Compare July 13, 2026 05:52
@slittycode
slittycode force-pushed the accuracy/genre-tempo-octave-evidence branch from 31f71d6 to 2d33463 Compare July 13, 2026 05:52
Base automatically changed from accuracy/genre-corpus-expansion to main 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>
@slittycode
slittycode force-pushed the accuracy/genre-tempo-octave-evidence branch from 2d33463 to 944a0d1 Compare July 13, 2026 05:59
…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
@slittycode
slittycode merged commit e9a7e5f into main Jul 13, 2026
5 checks passed
@slittycode
slittycode deleted the accuracy/genre-tempo-octave-evidence branch July 13, 2026 06:37
slittycode added a commit that referenced this pull request Jul 13, 2026
…R-G4

# Conflicts:
#	apps/backend/scripts/build_synthetic_corpus.py
#	apps/backend/tests/fixtures/fundamentals_eval_manifest.synthetic.json
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