Commit 9089a988 by PLN (Algolia)

feat(boundary_bleed): auto-detect cross-boundary sound leaks → EDL

Embodies the division-of-labor principle: the machine flags OBJECTIVE boundary
leaks so PLN's ears judge only feel. Detector = exclusive-orbit ignition: at each
cut, an orbit core to one neighbour but not the other, lit on the wrong side (down
to a sensitive -55dB floor for quiet foreign onsets).

Validated on Take89 (Montreuil): independently caught BOTH of PLN's hand-flagged
bad_cuts — cut1 (Take5 bass pre-echo, "changement de basse avant le cut") and cut5
(Plosive→Jeudi Drill, the Bogdan stabs on d9/orbit-09, ~-48dB) — plus 5 new
candidates. Auto-populates master_edl_take89.json: 16 edits = 9 ear + 7 derived.

Mapping nailed down (was wrong first pass): stemmap orbit-NN == dN; Tidal's
`# orbit X` is SuperDirt-0-indexed → d(X+1) (so `# orbit 8` == d9). d8 = always
breaks. Honest coverage cap logged: a foreign SOUND on a shared+active orbit is
invisible at orbit level → v2 = spectral/per-sound novelty.
parent ce494fda
#!/usr/bin/env python3
"""boundary_bleed — auto-detect cross-boundary sound LEAKS in a split set.
The principle (feedback_verify_own_renders): the machine catches OBJECTIVE errors so
PLN's ears are spent on subjective feel. A "bad cut" — next track's element audible in
this track's tail, or this track's tail ringing into the next — is mechanically
detectable and should never need a human to notice it.
Mechanism (purely from stem activity, no audio re-listen):
- Each track segment's CORE orbits = orbits active through its middle.
- At a cut between track i and i+1:
incoming-exclusive = core(i+1) − core(i) → if ACTIVE in i's TAIL ⇒ next leaked early
outgoing-exclusive = core(i) − core(i+1) → if ACTIVE in i+1's HEAD ⇒ tail rings in
Each leak → a typed MasterEdit(bad_cut) appended to the EDL, pre-filled for the renderer
AND usable as ground-truth for tuning. PLN then only judges what FEELS off.
Inputs: stemmap_take89.json (orbit×2s-bin RMS) + boundaries_take89_validated.json.
Run: python3 boundary_bleed.py [--write-edl]
"""
import argparse, json, re
from datetime import date
from pathlib import Path
from models import EditKind, EditOp, MasterEdit, Provenance
HERE = Path(__file__).parent
STEMMAP = HERE / "punkachien" / "stemmap_take89.json"
BOUNDS = HERE / "boundaries_take89_validated.json"
TRACKS = Path("/home/pln/Work/Web/www/next/content/lives/2026/montreuil-algorave/tracks.json")
OUT = HERE / "boundary_bleed_take89.json"
EDL = HERE / "master_edl_take89.json"
ACTIVE_DB = -38.0 # orbit "on" (core) above this (matches build_player_data)
LEAK_DB = -55.0 # sensitive floor for a FOREIGN onset in the leak window — a
# quiet next-track element (e.g. Bogdan stabs ~-48dB) still counts
W = 10.0 # leak window each side of a cut (s)
CORE_FRAC = 0.45 # orbit is "core" if active ≥ this fraction of a track's middle
LEAK_BINS = 2 # need ≥ this many bins (×2s) in the window to call a leak
EDGE = 0.20 # trim this fraction off each segment edge for the "middle"
# stemmap "orbit-NN" == dN (Ardour "Tidal N" channel). NB: Tidal's `# orbit X` param
# is SuperDirt-0-indexed → dN where N=X+1 (so `# orbit 8` == d9 == stemmap orbit-09).
# Convention: d8 is ALWAYS breaks. (per PLN)
FAMILY = {1:"kick",2:"snare",3:"hats",4:"bass",5:"bass/riff",6:"bass",7:"synth",
8:"breaks",9:"sub/voice",10:"lead",11:"atmos",12:"atmos"}
def mmss(t): return f"{int(t)//60}:{int(t)%60:02d}"
def load():
sm = json.loads(STEMMAP.read_text())
orbits = [int(re.search(r"(\d+)", o).group(1)) for o in sm["orbits"]]
times = sm["times"]; rms = sm["rms_db"]; bins = sm.get("bin_s", 2.0)
bj = json.loads(BOUNDS.read_text())["boundaries"]
# ordered segments (name, start, end) — leading track precedes cut 0
try:
tj = json.loads(TRACKS.read_text())
first = (tj.get("tracks", tj))[0].get("name", "track 1")
except Exception:
first = "track 1"
segs, cuts = [], sorted(bj, key=lambda x: x["t"])
end = times[-1] + bins
segs.append((first, 0.0, float(cuts[0]["t"])))
for k, c in enumerate(cuts):
e = float(cuts[k+1]["t"]) if k+1 < len(cuts) else end
segs.append((c["into"], float(c["t"]), e))
return orbits, times, rms, bins, cuts, segs
def active_set(orbits, times, rms, bins, t0, t1, frac):
"""orbits active ≥ frac of bins in [t0,t1)."""
idx = [i for i, t in enumerate(times) if t0 <= t < t1]
if not idx:
return set()
out = set()
for r, o in enumerate(orbits):
on = sum(1 for i in idx if rms[r][i] > ACTIVE_DB)
if on / len(idx) >= frac:
out.add(o)
return out
def active_bins(orbits, times, rms, o, t0, t1, floor=ACTIVE_DB):
r = orbits.index(o)
return [t for i, t in enumerate(times) if t0 <= t < t1 and rms[r][i] > floor]
def core(orbits, times, rms, bins, seg):
_, s, e = seg
mid0, mid1 = s + (e - s) * EDGE, e - (e - s) * EDGE
return active_set(orbits, times, rms, bins, mid0, mid1, CORE_FRAC)
def detect():
orbits, times, rms, bins, cuts, segs = load()
cores = [core(orbits, times, rms, bins, sg) for sg in segs]
findings = []
for k, c in enumerate(cuts):
b = float(c["t"])
i, j = k, k + 1 # segs[i] ends at b, segs[j] starts at b
ci, cj = cores[i], cores[j]
name_i, name_j = segs[i][0], segs[j][0]
# incoming: next-track-only orbit lit in THIS track's tail (sensitive floor)
for o in sorted(cj - ci):
lit = active_bins(orbits, times, rms, o, b - W, b, floor=LEAK_DB)
if len(lit) >= LEAK_BINS:
findings.append({
"cut": k, "boundary_t": b, "mmss": mmss(b),
"from": name_i, "into": name_j, "side": "incoming",
"orbit": o, "family": FAMILY.get(o, "?"),
"leak_s": round(b - min(lit), 1), "from_t": mmss(min(lit)),
"detail": f"{name_j}'s orbit-{o:02d} ({FAMILY.get(o,'?')}) lit "
f"{round(b-min(lit),1)}s BEFORE the cut → next track leaked "
f"early / cut too late"})
# outgoing: this-track-only orbit still lit in NEXT track's head (sensitive floor)
for o in sorted(ci - cj):
lit = active_bins(orbits, times, rms, o, b, b + W, floor=LEAK_DB)
if len(lit) >= LEAK_BINS:
findings.append({
"cut": k, "boundary_t": b, "mmss": mmss(b),
"from": name_i, "into": name_j, "side": "outgoing",
"orbit": o, "family": FAMILY.get(o, "?"),
"leak_s": round(max(lit) - b + bins, 1), "to_t": mmss(max(lit)),
"detail": f"{name_i}'s orbit-{o:02d} ({FAMILY.get(o,'?')}) still lit "
f"{round(max(lit)-b+bins,1)}s AFTER the cut → tail rings into "
f"{name_j}"})
return findings, segs, cores
def to_edits(findings):
edits = []
for f in findings:
op = EditOp.trim_out if f["side"] == "incoming" else EditOp.trim_in
edits.append(MasterEdit(
take="Take89", name=f"{f['from']} → {f['into']}",
op=op, at=f["mmss"], value=round(f["leak_s"], 1),
kind=EditKind.bad_cut, reason=f["detail"], device="",
provenance=Provenance(source="derived", as_of=date(2026, 6, 5),
locator=f"boundary_bleed cut{f['cut']} orbit-{f['orbit']:02d} "
f"{f['side']} {f['leak_s']}s")))
return edits
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--write-edl", action="store_true",
help="append detected bad_cut rows to master_edl_take89.json")
a = ap.parse_args()
findings, segs, cores = detect()
print(f"\nboundary bleed scan — {len(segs)} tracks, {len(findings)} leaks\n")
print("core orbits per track:")
for (nm, s, e), c in zip(segs, cores):
print(f" {mmss(s):>6}-{mmss(e):<6} {nm:<26} {sorted(c)}")
print("\nLEAKS (objective — for PLN to convert to feel-judgements):")
if not findings:
print(" (none above threshold)")
for f in findings:
arrow = "◀" if f["side"] == "incoming" else "▶"
print(f" {f['mmss']:>6} cut{f['cut']:<2} {arrow} {f['detail']}")
OUT.write_text(json.dumps({"take": "Take89", "params":
{"active_db": ACTIVE_DB, "window_s": W, "core_frac": CORE_FRAC},
"coverage": "v1 = EXCLUSIVE-orbit ignition (foreign orbit lit on the wrong side "
"of a cut, down to a sensitive -55dB floor). LIMITATION: a foreign "
"SOUND on a genuinely SHARED+active orbit is invisible at orbit level "
"→ v2 = spectral/per-sound novelty at the boundary.",
"findings": findings}, ensure_ascii=False, indent=1))
print(f"\n✓ {OUT} ({len(findings)} findings)")
print("⚠ coverage: exclusive-orbit ignition only. A foreign SOUND sharing an active "
"orbit with the neighbour is invisible at orbit level → v2 = spectral novelty.")
if a.write_edl:
edits = to_edits(findings)
edl = json.loads(EDL.read_text())
# drop prior derived bleed rows, then append fresh
edl["edits"] = [e for e in edl["edits"]
if (e.get("provenance") or {}).get("source") != "derived"]
edl["edits"] += [e.model_dump(mode="json") for e in edits]
EDL.write_text(json.dumps(edl, ensure_ascii=False, indent=1))
print(f"✓ appended {len(edits)} derived bad_cut rows → {EDL}")
if __name__ == "__main__":
main()
{
"take": "Take89",
"params": {
"active_db": -38.0,
"window_s": 10.0,
"core_frac": 0.45
},
"coverage": "v1 = EXCLUSIVE-orbit ignition (foreign orbit lit on the wrong side of a cut, down to a sensitive -55dB floor). LIMITATION: a foreign SOUND on a genuinely SHARED+active orbit is invisible at orbit level → v2 = spectral/per-sound novelty at the boundary.",
"findings": [
{
"cut": 0,
"boundary_t": 359.0,
"mmss": "5:59",
"from": "Quand on décolle",
"into": "Piment brésilien",
"side": "outgoing",
"orbit": 11,
"family": "atmos",
"leak_s": 5.0,
"to_t": "6:02",
"detail": "Quand on décolle's orbit-11 (atmos) still lit 5.0s AFTER the cut → tail rings into Piment brésilien"
},
{
"cut": 0,
"boundary_t": 359.0,
"mmss": "5:59",
"from": "Quand on décolle",
"into": "Piment brésilien",
"side": "outgoing",
"orbit": 12,
"family": "atmos",
"leak_s": 5.0,
"to_t": "6:02",
"detail": "Quand on décolle's orbit-12 (atmos) still lit 5.0s AFTER the cut → tail rings into Piment brésilien"
},
{
"cut": 1,
"boundary_t": 725.0,
"mmss": "12:05",
"from": "Piment brésilien",
"into": "Take 5 Drops",
"side": "incoming",
"orbit": 4,
"family": "bass",
"leak_s": 9.0,
"from_t": "11:56",
"detail": "Take 5 Drops's orbit-04 (bass) lit 9.0s BEFORE the cut → next track leaked early / cut too late"
},
{
"cut": 3,
"boundary_t": 1402.0,
"mmss": "23:22",
"from": "Super Sunny Side Up",
"into": "W.I.P. W.A.P.",
"side": "incoming",
"orbit": 8,
"family": "breaks",
"leak_s": 10.0,
"from_t": "23:12",
"detail": "W.I.P. W.A.P.'s orbit-08 (breaks) lit 10.0s BEFORE the cut → next track leaked early / cut too late"
},
{
"cut": 5,
"boundary_t": 1822.0,
"mmss": "30:22",
"from": "'Plosive",
"into": "Jeudi Drill",
"side": "incoming",
"orbit": 9,
"family": "sub/voice",
"leak_s": 8.0,
"from_t": "30:14",
"detail": "Jeudi Drill's orbit-09 (sub/voice) lit 8.0s BEFORE the cut → next track leaked early / cut too late"
},
{
"cut": 8,
"boundary_t": 2607.0,
"mmss": "43:27",
"from": "PunkAChien",
"into": "You My Sunshine",
"side": "incoming",
"orbit": 11,
"family": "atmos",
"leak_s": 5.0,
"from_t": "43:22",
"detail": "You My Sunshine's orbit-11 (atmos) lit 5.0s BEFORE the cut → next track leaked early / cut too late"
},
{
"cut": 13,
"boundary_t": 4173.0,
"mmss": "69:33",
"from": "Techno Orage",
"into": "La Révolution Sera Samplée",
"side": "incoming",
"orbit": 3,
"family": "hats",
"leak_s": 9.0,
"from_t": "69:24",
"detail": "La Révolution Sera Samplée's orbit-03 (hats) lit 9.0s BEFORE the cut → next track leaked early / cut too late"
}
]
}
\ No newline at end of file
......@@ -181,6 +181,146 @@
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": null,
"name": "Quand on décolle → Piment brésilien",
"tidal": null,
"op": "trim_in",
"at": "5:59",
"to": null,
"value": 5.0,
"kind": "bad_cut",
"reason": "Quand on décolle's orbit-11 (atmos) still lit 5.0s AFTER the cut → tail rings into Piment brésilien",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut0 orbit-11 outgoing 5.0s",
"as_of": "2026-06-05",
"note": ""
}
},
{
"take": "Take89",
"track_no": null,
"name": "Quand on décolle → Piment brésilien",
"tidal": null,
"op": "trim_in",
"at": "5:59",
"to": null,
"value": 5.0,
"kind": "bad_cut",
"reason": "Quand on décolle's orbit-12 (atmos) still lit 5.0s AFTER the cut → tail rings into Piment brésilien",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut0 orbit-12 outgoing 5.0s",
"as_of": "2026-06-05",
"note": ""
}
},
{
"take": "Take89",
"track_no": null,
"name": "Piment brésilien → Take 5 Drops",
"tidal": null,
"op": "trim_out",
"at": "12:05",
"to": null,
"value": 9.0,
"kind": "bad_cut",
"reason": "Take 5 Drops's orbit-04 (bass) lit 9.0s BEFORE the cut → next track leaked early / cut too late",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut1 orbit-04 incoming 9.0s",
"as_of": "2026-06-05",
"note": ""
}
},
{
"take": "Take89",
"track_no": null,
"name": "Super Sunny Side Up → W.I.P. W.A.P.",
"tidal": null,
"op": "trim_out",
"at": "23:22",
"to": null,
"value": 10.0,
"kind": "bad_cut",
"reason": "W.I.P. W.A.P.'s orbit-08 (breaks) lit 10.0s BEFORE the cut → next track leaked early / cut too late",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut3 orbit-08 incoming 10.0s",
"as_of": "2026-06-05",
"note": ""
}
},
{
"take": "Take89",
"track_no": null,
"name": "'Plosive → Jeudi Drill",
"tidal": null,
"op": "trim_out",
"at": "30:22",
"to": null,
"value": 8.0,
"kind": "bad_cut",
"reason": "Jeudi Drill's orbit-09 (sub/voice) lit 8.0s BEFORE the cut → next track leaked early / cut too late",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut5 orbit-09 incoming 8.0s",
"as_of": "2026-06-05",
"note": ""
}
},
{
"take": "Take89",
"track_no": null,
"name": "PunkAChien → You My Sunshine",
"tidal": null,
"op": "trim_out",
"at": "43:27",
"to": null,
"value": 5.0,
"kind": "bad_cut",
"reason": "You My Sunshine's orbit-11 (atmos) lit 5.0s BEFORE the cut → next track leaked early / cut too late",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut8 orbit-11 incoming 5.0s",
"as_of": "2026-06-05",
"note": ""
}
},
{
"take": "Take89",
"track_no": null,
"name": "Techno Orage → La Révolution Sera Samplée",
"tidal": null,
"op": "trim_out",
"at": "69:33",
"to": null,
"value": 9.0,
"kind": "bad_cut",
"reason": "La Révolution Sera Samplée's orbit-03 (hats) lit 9.0s BEFORE the cut → next track leaked early / cut too late",
"device": "",
"status": "open",
"provenance": {
"source": "derived",
"locator": "boundary_bleed cut13 orbit-03 incoming 9.0s",
"as_of": "2026-06-05",
"note": ""
}
}
]
}
\ No newline at end of file
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