Commit d683d786 by PLN (Algolia)

feat(cuts): cut-lens v2 — better, still not trustworthy, and now we know why

Second attempt at locating crossfaded track boundaries without PLN's ears, with
the three faults of v1 fixed: references taken ADJACENT to the boundary instead
of from a track midpoint (a livecoded track is not stationary), a MEDIAN over
every separating orbit instead of a weighted mean over 1-5 (one liar cannot move
a median), and a CUSUM change-point instead of a threshold plus a 6s sustained-
crossing rule that could not resolve a 4-cycle crossfade (~7.7s at 124 BPM).

It got better: median error 24.0s -> 16.6s, voters 1-5 -> 4-8. It is still not
good enough to ship, and I did not ship it.

The useful part is the SHAPE of the failure. Every single error is negative, and
the accurate boundaries are exactly the ones PLN described as short:

    #3 -21.0  #4 -15.2  #5 -18.0  #6 -18.5  #7 -41.2   (his: 'still the drops')
    #8  -4.4  #9  -2.0  #10 -2.4                        (his: 'perfect starts perfect')

The lens finds where the incoming track FIRST APPEARS; the ear marks where it
TAKES OVER. They differ by the length of the crossfade, which is precisely the
quantity that varies. So the lens is measuring a real thing, just not the one we
asked for — a much better position than 'it is noisy'.

Two ways forward are written into the file, plus an explicit warning not to
paper over it with a constant offset fitted to these eight points: the errors
span 2.0-41.2s, so a fitted constant would be memorisation, not a method.

Prior art checked at PLN's suggestion and worth recording: the OPAL gig log is
useless here (2 track events in 87 minutes — the gig-log 'track' field only
started recording properly in 97b22c0b, AFTER the gig), but
Prod/Montreuil26_master/boundaries_ground_truth.json holds 14 user-confirmed
boundaries from a RELEASED split. That is a second, independent labelled set the
next attempt should validate against before touching OPAL.
parent 73e9d798
#!/usr/bin/env python3
"""cut_lens2 — locate a crossfaded track boundary, validated against the ear.
v1 (cut_lens.py) failed its own validation: median 24 s error against PLN's
ear-verified boundaries. Three diagnosed causes, each fixed here:
v1 v2
-------------------------------------- ------------------------------------
reference from the track MIDPOINT — but reference from the segment region
a livecoded track is not stationary, and ADJACENT to the boundary (but clear
an 8-minute track's middle can be further of it), so "what the outgoing track
from its own head than from its neighbour sounds like HERE" is what we compare
weighted MEAN over 1-5 voting orbits, so MEDIAN over every orbit that has
a single bad orbit moved the answer real separation — one liar cannot
move a median
threshold + 6 s sustained-crossing rule, CUSUM change-point: no threshold at
which cannot resolve a 4-cycle crossfade all, it finds where the series MOST
(~7.7 s at 124 BPM) — the exact span changes regime, which is what a
being measured crossfade is
The measure per orbit is still timbral — across a ParVagues switch the ORBIT SET
barely changes (d1 is a kick before and after), so only the SAMPLE tells you
anything. Activity-based detectors are structurally blind here and both of ours
passed the bad cuts.
VALIDATION IS THE POINT. `--truth` runs LEAVE-ONE-OUT against the ear-verified
boundaries: each known answer is estimated without being used to tune anything,
and the error is reported. If that does not come in tight, the proposals for the
unknown boundaries are worthless and must not be shipped.
python3 cut_lens2.py --spec judge_specs/opal26.json \
--truth judge_specs/opal26_boundaries_ear.json
>>> RESULT, 2026-08-16: BETTER THAN v1, STILL NOT TRUSTWORTHY. <<<
median |err| 24.0s (v1) -> 16.6s (v2); voters 1-5 -> 4-8; worst 41.2s.
Within 2s on only 1 of 8. Its proposals were NOT shipped.
But the failure has a SHAPE, and it is the useful part:
#3 -21.0 #4 -15.2 #5 -18.0 #6 -18.5 #7 -41.2 early/mid set
#8 -4.4 #9 -2.0 #10 -2.4 late set
EVERY error is negative, and the accurate ones are exactly the boundaries PLN
described as short ("perfect starts perfect"), while the wild ones are the ones
he described as long ("this start is still the drops ... piment starts real at
0:21 only").
THE LENS FINDS WHERE THE INCOMING TRACK FIRST APPEARS.
THE EAR MARKS WHERE IT TAKES OVER.
Those differ by the length of the crossfade, which is what varies.
So this is not noise to be tuned away — it is measuring a real and different
quantity. Two ways forward for whoever picks it up:
1. Measure the transition SPAN, not a point: the lens gives the onset, and the
end is where the OUTGOING track's exclusive sounds fall below audibility.
Report [start, end] and take the end as the cut.
2. Cheaper and honest: present PLN the lens's onset plus a candidate end, and
ask him to confirm ONE of two timestamps per boundary. That is seconds of
listening per cut instead of a full pass.
Do NOT paper over it with a constant offset fitted to these eight points; the
errors range 2.0-41.2s and a fitted constant is memorisation, not a method.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from audio_lens import load_window, profile # noqa: E402
SEARCH = 40.0 # +/- seconds around the nominal cut
WIN = 3.0 # profiling window
STEP = 0.5 # slide — finer than v1; a 4-cycle fade is ~8 s
REF_MAX = 40.0 # cap on a reference window
REF_CLEAR = 12.0 # keep references this far from the boundary being measured
MIN_SEP = 0.05 # an orbit must sound this different across the switch to vote
SR = 44100
def mmss(t):
return f"{int(t) // 60}:{int(t) % 60:02d}"
def timbral(a, b):
if a is None or b is None:
return None
ba = np.array([a["bands"][k] for k in sorted(a["bands"])])
bb = np.array([b["bands"][k] for k in sorted(b["bands"])])
band = np.abs(ba - bb).sum() / 200.0
ca, cb = max(a["centroid"], 1.0), max(b["centroid"], 1.0)
cen = min(abs(np.log2(ca / cb)) / 4.0, 1.0)
return float(0.65 * band + 0.35 * cen)
def make_m2s(keeps):
def f(t):
acc = 0.0
for a, b in keeps:
if t < acc + (b - a):
return a + (t - acc)
acc += b - a
return keeps[-1][1]
return f
def ref_window(seg_start, seg_end, boundary, side):
"""A reference span inside one segment, adjacent to the boundary but clear of it."""
if side == "prev":
hi = boundary - REF_CLEAR
lo = max(seg_start + 2.0, hi - REF_MAX)
else:
lo = boundary + REF_CLEAR
hi = min(seg_end - 2.0, lo + REF_MAX)
return (lo, hi) if hi - lo >= 6.0 else None
def cusum_changepoint(series, grid):
"""Where the series most changes regime. No threshold, no tuning.
S_k = sum_{i<k}(x_i - mean(x)); the extremum of S is the point that best
splits the series into two levels. For "sounds like prev" -> "sounds like
next" that IS the handover.
"""
ok = ~np.isnan(series)
if ok.sum() < 8:
return None
x = series.copy()
x[~ok] = np.nanmean(series)
s = np.cumsum(x - x.mean())
# the series runs negative (prev) then positive (next), so S dips then climbs:
# the MINIMUM of S is the switch.
return float(grid[int(np.argmin(s))])
def estimate(stems, m2s, seg_prev, seg_next, verbose=False):
boundary = seg_next["start"]
lo = max(seg_prev["start"] + 3, boundary - SEARCH)
hi = min(seg_next["end"] - 3, boundary + SEARCH)
grid = np.arange(lo, hi - WIN, STEP)
if grid.size < 10:
return None, 0
wp = ref_window(seg_prev["start"], seg_prev["end"], boundary, "prev")
wn = ref_window(seg_next["start"], seg_next["end"], boundary, "next")
if not wp or not wn:
return None, 0
votes = []
for path in stems:
rp = _prof(path, m2s, *wp)
rn = _prof(path, m2s, *wn)
if rp is None or rn is None:
continue
sep = timbral(rp, rn)
if sep is None or sep < MIN_SEP:
continue # same sound both sides: no information
st0 = m2s(float(grid[0]))
try:
sig = load_window(path, st0, st0 + float(grid[-1] - grid[0]) + WIN)
except Exception:
continue
n = int(WIN * SR)
series = []
for t in grid:
a = int((t - grid[0]) * SR)
seg = sig[a:a + n]
p = profile(seg) if seg.size >= n // 2 else None
if p is None or p["rms_db"] < -55:
series.append(np.nan)
continue
series.append(timbral(p, rp) - timbral(p, rn))
cp = cusum_changepoint(np.array(series, dtype=float), grid)
if cp is not None:
votes.append(cp)
if verbose:
print(f" {path.name[-6:]:>6} sep={sep:.2f} -> {mmss(cp)}")
if len(votes) < 3:
return None, len(votes)
return float(np.median(votes)), len(votes) # median: one liar cannot move it
def _prof(path, m2s, t0, t1):
try:
st = m2s(t0)
sig = load_window(path, st, st + min(t1 - t0, REF_MAX))
return profile(sig) if sig.size else None
except Exception:
return None
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--spec", required=True)
ap.add_argument("--truth")
ap.add_argument("--out")
ap.add_argument("-v", "--verbose", action="store_true")
a = ap.parse_args()
spec = json.loads(Path(a.spec).read_text())
segs = json.loads(Path(spec["segments"]).read_text())
m2s = make_m2s(spec["keeps"])
stems = sorted(Path(spec["stemsDir"]).glob(spec.get("stemsGlob", "*.wav")))
truth = {}
if a.truth:
td = json.loads(Path(a.truth).read_text())
for k, v in td.get("verified", {}).items():
if v.get("start") is not None:
truth[int(k)] = float(v["start"])
print(f"cut-lens2 — {len(stems)} orbits, {len(truth)} ear-verified boundaries\n")
print(f"{'#':>2} {'track':<30} {'nominal':>8} {'est':>8} {'shift':>7} {'n':>3} {'truth':>8} {'err':>7}")
errs, out = [], []
for i in range(1, len(segs)):
prev, nxt = segs[i - 1], segs[i]
if a.verbose:
print(f"\n -- into #{nxt['track']} {nxt['title']}")
est, n = estimate(stems, m2s, prev, nxt, a.verbose)
line = f"{nxt['track']:>2} {nxt['title'][:30]:<30} {nxt['start']:>8.1f}"
if est is None:
print(line + f" {'—':>8} {'—':>7} {n:>3}")
continue
line += f" {est:>8.1f} {est - nxt['start']:>+7.1f} {n:>3}"
if nxt["track"] in truth:
err = est - truth[nxt["track"]]
errs.append(abs(err))
line += f" {truth[nxt['track']]:>8.1f} {err:>+7.1f}"
print(line)
out.append({"track": nxt["track"], "title": nxt["title"],
"nominal": nxt["start"], "proposed": round(est, 2), "voters": n})
if errs:
e = np.array(errs)
print(f"\nVALIDATION on {len(e)} ear-verified boundaries:")
print(f" median |err| {np.median(e):5.1f}s mean {e.mean():5.1f}s worst {e.max():5.1f}s")
print(f" within 2s: {(e <= 2).sum()}/{len(e)} within 5s: {(e <= 5).sum()}/{len(e)}")
verdict = "USABLE" if np.median(e) <= 3 and e.max() <= 8 else "NOT TRUSTWORTHY"
print(f" -> {verdict}")
if a.out:
Path(a.out).write_text(json.dumps(out, indent=2))
print(f"\n✓ {a.out}")
return 0
if __name__ == "__main__":
sys.exit(main())
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
"title": "Sunset Forest", "title": "Sunset Forest",
"date": "2026-08-08", "date": "2026-08-08",
"variant": "stream", "variant": "stream",
"liveRoot": "/home/pln/Work/Sound/Tidal", "liveRoot": "/home/pln/Work/Sound/Tidal",
"clipsDir": "/home/pln/Work/Sound/Prod/Opal26_master/heads_v4", "clipsDir": "/home/pln/Work/Sound/Prod/Opal26_master/heads_v4",
"tracksDir": "/home/pln/Work/Sound/Prod/Opal26_master/tracks_v4_streaming", "tracksDir": "/home/pln/Work/Sound/Prod/Opal26_master/tracks_v4_streaming",
...@@ -19,13 +18,20 @@ ...@@ -19,13 +18,20 @@
"stemsGlob": "*.wav", "stemsGlob": "*.wav",
"stemmap": "/home/pln/Work/Sound/Tidal/armada/tide-table/stemmap_opal26.json", "stemmap": "/home/pln/Work/Sound/Tidal/armada/tide-table/stemmap_opal26.json",
"tracksJson": "/home/pln/Work/Web/www/content/lives/2026/opal-festival-2026/tracks.json", "tracksJson": "/home/pln/Work/Web/www/content/lives/2026/opal-festival-2026/tracks.json",
"audioBase": "/audio/heads", "audioBase": "/audio/heads",
"fullAudioBase": "/audio/full", "fullAudioBase": "/audio/full",
"keeps": [
"keeps": [[6.0, 4540.0], [4569.9, 4822.27]], [
6.0,
4540.0
],
[
4569.9,
4822.27
]
],
"binS": 2.0, "binS": 2.0,
"calibration": "clip time = master time; master → stem via keeps [[6.0,4540.0],[4569.9,4822.27]] (sums to 4786.370 s = the master to the millisecond)", "calibration": "clip time = master time; master → stem via keeps [[6.0,4540.0],[4569.9,4822.27]] (sums to 4786.370 s = the master to the millisecond)",
"note": "v4 = the premix pass that trimmed d4, which owned 76-91% of the low-frequency bed floor. Per-track loudness deliberately keeps the live arc: tracks range roughly -12.5 to -18.2 LUFS rather than being flattened to a single number." "note": "v4 = the premix pass that trimmed d4, which owned 76-91% of the low-frequency bed floor. Per-track loudness deliberately keeps the live arc: tracks range roughly -12.5 to -18.2 LUFS rather than being flattened to a single number.",
"segments": "/home/pln/Work/Sound/Prod/Opal26_master/segments_v3.json"
} }
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