Commit 791a0d78 by PLN (Algolia)

fix(foundry): verify-rerank the loop finder against true post-snap seam + robust sparse seam

Problem — the finder's rank didn't reflect true seam quality. On a real
end-to-end run over the Super freak dub kit (Soul Sugar meets Dub Shepherds),
several top-scored 2-bar windows CLICKED at the wrap after export while cleaner
windows ranked below them. Root cause: analyze_stem scores the seam on the RAW
candidate window, but export_take zero-crossing-snaps both boundaries (B1) AND
slices at the 3-decimal-rounded start_s/end_s — and both transforms move the wrap.
A beat-exact window measuring seam 0.79 read 0.07 once rounded, and that rounded,
snapped slice is what actually ships. So the finder's seam proxy diverged from the
grader (grade.py), which measures the window that exists on disk — the ground truth.

Approach — a verify-rerank pass (loops._verify_rerank) after candidate generation,
before dedup/top_n. It re-measures the ~3×top_n survivors' seam on the TRUE
post-snap window — the exact zc-snapped, rounded-bounds slice export writes —
reusing grade.seam_score (DRY, no duplicated DSP). The proxy seam term is swapped
for the true seam in the composite (same weights), and any candidate whose true
seam falls below VERIFY_SEAM_FLOOR is hard-demoted so a click can never top the
list. Gated behind a new analyze_stem(..., verify=True) kwarg (default on; False
reproduces the pre-#19 raw-proxy ranking for autotune baselines). Public signatures
(analyze_stem, find_takes, weights dict) unchanged — autotune.py/server.py intact.

Also fixed a sparse-material seam false-positive in grade.seam_score: the wrap
curvature was normalized by the MEAN |2nd-diff|, which collapses to ~0 on sparse
dub percussion (mostly silence + a few hits), blowing the click ratio up (129× on
a genuinely clean loop). Now normalized by the 90th-percentile |2nd-diff|,
amplitude-floored — 2–3× on the same clean loop.

Validation (drums+other stems, bars 1/2/4, shared 132.5 BPM grid,
export-faithful grades):
  drums top-3  BEFORE  C 0.65 / C 0.63 / A 0.80   (clicks @93.08s, @24.06s)
               AFTER   S 0.98 / S 0.95 / A 0.81
  other top-3  BEFORE  B 0.72 / B 0.72 / B 0.70   (bad favorite @56.74s clicks)
               AFTER   S 1.00 / S 0.98 / S 0.99
The known-good drums 4-bar @107.21 (S) and other regions surface; the clicking
other @56.74 is demoted out of the top entirely.

Tests: +2 verify-rerank regression tests (a proxy-clean but discontinuous wrap that
survives the snap is demoted below a seamless one; verify=False leaves the proxy
untouched) and +1 sparse-percussion test (a clean sparse loop is NOT flagged
clicking). Suite 57 → 60 green.
parent 94711a72
...@@ -27,6 +27,21 @@ Task IDs map to the session task board. Design: memory `project_foundry`. ...@@ -27,6 +27,21 @@ Task IDs map to the session task board. Design: memory `project_foundry`.
with the lift numbers. with the lift numbers.
Run: `python3 autotune.py --stems <glob> --n 24 --max-stems 4`. Run: `python3 autotune.py --stems <glob> --n 24 --max-stems 4`.
Objective so far = mean full-rubric grade × coverage. Objective so far = mean full-rubric grade × coverage.
**VERIFY-RERANK now BUILT & validated end-to-end on the Super freak dub kit.**
The finder scored the seam on the RAW window, but export zc-snaps (B1) — and the
snap (plus the 3-dp start/end rounding export slices at) changes the wrap. Several
proxy-seam≈1.0 windows CLICKED after the snap. Fix: `loops._verify_rerank` re-scores
the ~3×top_n survivors' seam on the TRUE post-snap window (reusing `grade.seam_score`,
DRY), swaps proxy→true seam in the composite, and hard-demotes true-seam <
`VERIFY_SEAM_FLOOR`. New kwarg `analyze_stem(..., verify=True)` (default on; False
reproduces the pre-#19 raw-proxy ranking for autotune baselines). Also fixed the
sparse-material seam false-positive (`grade.seam_score`): the mean-|2nd-diff|
denominator collapsed to ≈0 on sparse dub drums (129× click ratio on a clean loop);
now 90th-percentile, amplitude-floored (2–3× on the same loop). Validation
(drums+other, bars 1/2/4, BPM 132.5, export-faithful grades):
drums top-3 BEFORE C/C/A → AFTER **S/S/A** (clicking @93.08 & @24.06 demoted)
other top-3 BEFORE B/B/B → AFTER **S/S/S** (bad favorite @56.74 demoted out)
+2 rerank regression tests + 1 sparse-percussion test; suite 57→60 green.
## 🔴 Open ## 🔴 Open
- [ ] **#20 — Batch-explore many sources** (Foundry over the full corpus + merge). - [ ] **#20 — Batch-explore many sources** (Foundry over the full corpus + merge).
......
...@@ -123,7 +123,17 @@ def seam_score(mono: np.ndarray) -> tuple[float, float]: ...@@ -123,7 +123,17 @@ def seam_score(mono: np.ndarray) -> tuple[float, float]:
return 1.0, -60.0 return 1.0, -60.0
# curvature at the wrap: the sequence is … x[-2], x[-1], x[0], x[1] … # curvature at the wrap: the sequence is … x[-2], x[-1], x[0], x[1] …
jerk = abs(float(mono[0]) - 2.0 * float(mono[-1]) + float(mono[-2])) jerk = abs(float(mono[0]) - 2.0 * float(mono[-1]) + float(mono[-2]))
typical = float(np.mean(np.abs(np.diff(mono, n=2)))) + _EPS # typical |2nd diff| # Reference = the loop's TYPICAL curvature. The mean |2nd diff| collapses to ≈0
# on sparse material (dub drums: mostly silence, a few hits) — any real wrap
# curvature then reads as a huge click (a genuinely clean sparse loop measured
# 129× the mean, a false positive). The 90th percentile of |2nd diff| tracks the
# signal's active-region curvature and stays finite on sparse input (2–3× on the
# same loop). Floor it to a fraction of peak amplitude so a near-silent slice
# can't make the denominator vanish either.
d2 = np.abs(np.diff(mono, n=2))
peak = float(np.max(np.abs(mono))) + _EPS
typical = max(float(np.percentile(d2, 90)) if d2.size else 0.0,
1e-3 * peak) + _EPS # 90th-pct curvature, amplitude-floored
click_db = _db(jerk / typical) click_db = _db(jerk / typical)
clean, click = THRESH["seam_db_clean"], THRESH["seam_db_click"] clean, click = THRESH["seam_db_clean"], THRESH["seam_db_click"]
# ≤ clean dB (jerk ≈ natural curvature) ⇒ 1.0 ; ≥ click dB (audible) ⇒ 0 # ≤ clean dB (jerk ≈ natural curvature) ⇒ 1.0 ; ≥ click dB (audible) ⇒ 0
......
...@@ -42,6 +42,15 @@ XFADE_MS = 20.0 # B3 (3 ms for drums, applied at export) ...@@ -42,6 +42,15 @@ XFADE_MS = 20.0 # B3 (3 ms for drums, applied at export)
# §3 composite weights (provisional; calibrate with #7) # §3 composite weights (provisional; calibrate with #7)
W = {"struct": 0.4, "novel": 0.2, "seam": 0.3, "zc": 0.1, "tempo_penalty": 0.2} W = {"struct": 0.4, "novel": 0.2, "seam": 0.3, "zc": 0.1, "tempo_penalty": 0.2}
# verify-rerank (task #19): the finder scores the seam on the RAW window, but the
# exported loop is zero-crossing-snapped (B1) — and the snap changes the wrap. On the
# Super freak dub kit several proxy-seam≈1.0 windows CLICKED after the snap (post-snap
# grade C, click_db 15–20) while genuinely seamless windows ranked below them. The
# verify pass re-measures the TRUE post-snap seam on surviving candidates and folds it
# into the rank so the finder's order matches the grader (grade.py) ground truth.
VERIFY_SEAM_FLOOR = 0.15 # true post-snap seam below this ⇒ hard-demote a click
VERIFY_SURVIVOR_MULT = 3 # verify ~3× top_n survivors, then cut to top_n
class LoopCandidate(BaseModel): class LoopCandidate(BaseModel):
"""One scored window within a single stem.""" """One scored window within a single stem."""
...@@ -147,8 +156,31 @@ def _structural(ssm: np.ndarray, s: int, e: int) -> float: ...@@ -147,8 +156,31 @@ def _structural(ssm: np.ndarray, s: int, e: int) -> float:
return float(block.max(axis=1).mean()) if block.size else 0.0 return float(block.max(axis=1).mean()) if block.size else 0.0
def _snapped_window(y: np.ndarray, s_smp: int, e_smp: int, sr: int) -> np.ndarray:
"""The exact slice export_take would write: both boundaries zc-snapped (B1)."""
a = _snap_zc(y, s_smp, sr)
b = _snap_zc(y, e_smp, sr)
if b <= a: # degenerate snap → fall back to raw bounds
a, b = s_smp, e_smp
return y[a:b]
def _true_seam(y: np.ndarray, s_smp: int, e_smp: int, sr: int) -> float:
"""TRUE post-snap seam quality (0..1) of a window, via the grader's seam scorer.
Reuses grade.seam_score (DRY — no duplicated DSP) on the zc-snapped slice that will
actually be exported, so the finder's seam term is the same quantity the grader
reports rather than a raw-window proxy that the snap invalidates.
"""
snapped = _snapped_window(y, s_smp, e_smp, sr)
if snapped.size < 3:
return 1.0
seam, _ = G.seam_score(snapped)
return seam
def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8, def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8,
grid=None, weights=None) -> list[LoopCandidate]: grid=None, weights=None, verify=True) -> list[LoopCandidate]:
"""Rank loop candidates within one stem. """Rank loop candidates within one stem.
`grid` = a shared (peaks, times, bpm) beat grid (e.g. from the drums stem). Passing `grid` = a shared (peaks, times, bpm) beat grid (e.g. from the drums stem). Passing
...@@ -158,6 +190,13 @@ def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8, ...@@ -158,6 +190,13 @@ def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8,
`weights` = optional override of the §3 composite weights W (struct/novel/seam/zc/ `weights` = optional override of the §3 composite weights W (struct/novel/seam/zc/
tempo_penalty); None ⇒ module defaults. The autotune harness sweeps these. tempo_penalty); None ⇒ module defaults. The autotune harness sweeps these.
`verify` (default True) = run the verify-rerank pass: after candidate generation the
seam term of the ~3× top_n survivors is REPLACED by the true post-snap seam (measured
on the zero-crossing-snapped window that export actually writes), and candidates whose
true seam falls below VERIFY_SEAM_FLOOR are hard-demoted. This closes the divergence
where a proxy-seam≈1.0 window clicked after the B1 snap (see module note). Set False
to reproduce the pre-#19 raw-proxy ranking (autotune baselines).
""" """
w = W if weights is None else {**W, **weights} w = W if weights is None else {**W, **weights}
peaks, times, bpm = grid if grid is not None else beat_grid(y, sr) peaks, times, bpm = grid if grid is not None else beat_grid(y, sr)
...@@ -196,9 +235,40 @@ def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8, ...@@ -196,9 +235,40 @@ def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8,
bpm=round(bmean, 1), tempo_unstable=unstable, bpm=round(bmean, 1), tempo_unstable=unstable,
score=round(max(0.0, score), 4), structural=round(struct, 4), score=round(max(0.0, score), 4), structural=round(struct, 4),
novelty=round(novel, 4), seam=round(seam, 4), zc=round(zc, 4))) novelty=round(novel, 4), seam=round(seam, 4), zc=round(zc, 4)))
if verify:
cands = _verify_rerank(cands, y, sr, w, top_n)
return _dedup(cands, top_n) return _dedup(cands, top_n)
def _verify_rerank(cands: list[LoopCandidate], y: np.ndarray, sr: int,
w: dict, top_n: int) -> list[LoopCandidate]:
"""Re-score the top survivors' seam on the true post-snap window (see analyze_stem).
Cheap: only ~VERIFY_SURVIVOR_MULT×top_n highest-proxy candidates are re-measured, each
on a small already-in-memory slice. For each, the proxy seam term is swapped for the
true post-snap seam (rebuilding the composite with the same weights), and a candidate
whose true seam < VERIFY_SEAM_FLOOR is hard-demoted below all verified-clean ones so a
click can never top the list. Unverified candidates keep their proxy score.
Bounds come from the candidate's ROUNDED start_s/end_s — the exact values export_take
slices at — so verify measures the window that ships, not the sub-sample-exact one
(the 3-dp rounding alone can shift the wrap onto a click: a beat-exact 0.79 seam read
0.07 once rounded, and that rounded slice is what gets written).
"""
survivors = sorted(cands, key=lambda x: -x.score)[:VERIFY_SURVIVOR_MULT * top_n]
for c in survivors:
s_smp, e_smp = int(c.start_s * sr), int(c.end_s * sr)
true_seam = _true_seam(y, s_smp, e_smp, sr)
# rebuild the composite swapping proxy seam → true seam (same weights)
base = c.score - w["seam"] * c.seam + w["seam"] * true_seam
if true_seam < VERIFY_SEAM_FLOOR: # audible wrap click after snap
base -= 1.0 # hard-demote below any clean candidate
c.seam = round(true_seam, 4)
c.score = round(max(0.0, base) if true_seam >= VERIFY_SEAM_FLOOR else base, 4)
return cands
def analyze_chops(y: np.ndarray, sr: int, *, top_n=12, grid=None, def analyze_chops(y: np.ndarray, sr: int, *, top_n=12, grid=None,
min_len_s=0.25, max_len_s=2.0) -> list[LoopCandidate]: min_len_s=0.25, max_len_s=2.0) -> list[LoopCandidate]:
"""Onset-driven SUB-BAR candidates — how PLN actually chops vocal/sample sources. """Onset-driven SUB-BAR candidates — how PLN actually chops vocal/sample sources.
......
...@@ -102,6 +102,76 @@ def test_analyze_chops_finds_subbar_slices_between_onsets(): ...@@ -102,6 +102,76 @@ def test_analyze_chops_finds_subbar_slices_between_onsets():
assert 0.05 <= (c.end_s - c.start_s) <= 2.0 assert 0.05 <= (c.end_s - c.start_s) <= 2.0
def test_verify_rerank_demotes_clicking_wrap_below_seamless():
# #19 regression: two candidates with EQUALLY good proxy features (structural,
# novelty, proxy seam), but one wraps seamlessly and the other wraps on a
# discontinuity that SURVIVES the zero-crossing snap. The verify pass measures the
# true post-snap seam and must rank the seamless one above the clicking one — even
# though before verify they scored identically. This is exactly the divergence seen
# on the Super freak dub kit (proxy-seam≈1.0 windows that clicked after B1 snap).
sr = 22050
rng = np.random.default_rng(1)
T = np.arange(int(8.0 * sr)) / sr
# continuous percussive texture so the zc-snap has real neighbours to snap to
y = (0.3 * np.sin(2 * np.pi * 110 * T)
+ 0.1 * rng.standard_normal(len(T))).astype(np.float32)
# window B ([4,6) s) gets a sustained cliff over its last 40 ms — wider than the
# ±10 ms snap window, so zc-snap cannot escape it: its wrap genuinely clicks.
cliff = int(0.04 * sr)
y[int(6.0 * sr) - cliff:int(6.0 * sr)] += 3.0
cA = L.LoopCandidate(start_s=1.0, end_s=3.0, bars=2, bpm=120, # seamless
tempo_unstable=False, score=0.700, structural=0.6,
novelty=0.6, seam=1.0, zc=0.8)
cB = L.LoopCandidate(start_s=4.0, end_s=6.0, bars=2, bpm=120, # clicks at wrap
tempo_unstable=False, score=0.700, structural=0.6,
novelty=0.6, seam=1.0, zc=0.8) # SAME proxy seam
out = L._verify_rerank([cA, cB], y, sr, L.W, top_n=8)
a = next(c for c in out if c.start_s == 1.0)
b = next(c for c in out if c.start_s == 4.0)
assert a.seam > b.seam, "clicking window's verified seam must drop below the clean one"
assert a.score > b.score, "seamless window must outrank the clicking one after verify"
assert b.seam < L.VERIFY_SEAM_FLOOR, "the click should fall below the demote floor"
assert b.score < a.score - 0.5, "a sub-floor click is hard-demoted well below clean"
def test_verify_off_reproduces_raw_proxy_ranking():
# verify=False must leave the proxy seam untouched (autotune-baseline reproducibility)
sr = 22050
t = np.arange(sr * 8) / sr
y = (0.3 * np.sin(2 * np.pi * 110 * t)).astype(np.float32)
times = np.arange(0, 8, 0.5)
peaks = (times * sr / 512).astype(int)
bpm = np.full(len(times), 120.0)
raw = L.analyze_stem(y, sr, bars=(2,), top_n=8, grid=(peaks, times, bpm), verify=False)
# proxy seam is measured on the un-snapped window; just assert it ran and produced
# candidates with seam populated (not mutated by a verify pass)
assert raw and all(0.0 <= c.seam <= 1.0 for c in raw)
def test_sparse_percussion_clean_loop_not_flagged_clicking():
# finding-2 regression: a clean, SPARSE percussion loop (mostly silence + a few
# hits, wrapping at zero) must NOT be flagged as clicking. The old mean-|2nd-diff|
# denominator collapsed to ≈0 on sparse material and blew the click ratio up
# (129× on a genuinely clean loop); the 90th-percentile denominator keeps it sane.
from engine import grade as G
sr = 22050
y = np.zeros(sr, dtype=np.float32) # 1 s, silent baseline
# four short decaying hits (sparse dub kick/rim feel), all starting & ending at 0
for onset in (0.0, 0.25, 0.5, 0.75):
i = int(onset * sr)
L_hit = int(0.06 * sr)
env = np.exp(-np.linspace(0, 8, L_hit)).astype(np.float32)
y[i:i + L_hit] += (env * np.sin(2 * np.pi * 90 * np.arange(L_hit) / sr)).astype(np.float32)
# window wraps in silence (x[0]=x[-1]=x[-2]=0) → curvature at wrap is ~0 = seamless
score, click_db = G.seam_score(y)
assert score > 0.6, f"clean sparse loop wrongly flagged: seam={score:.2f} click_db={click_db:.1f}"
assert click_db < G.THRESH["seam_db_click"], "click_db should sit below the click threshold"
g = G.grade_array(y, sr)
assert "seam-click" not in g.flags
def test_beat_grid_recovers_tempo_not_its_octave(): def test_beat_grid_recovers_tempo_not_its_octave():
# 120 bpm click train; the fix must report ~120, NOT ~240 # 120 bpm click train; the fix must report ~120, NOT ~240
sr = 22050 sr = 22050
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment