Commit e5c604dc by PLN (Algolia)

feat(tide-table): weight sample evidence by 1/df — PLN's ears found two tracks my reporting hid

PLN, on hearing a rose_rouge sample inside a seam I had labelled quand_on_decolle:
"where is our samples mathcing? gosh, i should not do so excruciating details
myself. you have all the data bro." He was right. I had 86 offset-consistent
detections and printed 4, because `locate` reported each bank's single BEST window
and threw away temporal extent — which is the informative part.

What was hiding in data I already had:
  rose        -> rose_rouge            22:35-24:00 (max cons 0.81), and on to 31:30
  movie_paris -> something_about_drums   6:15 (0.62), 10:30 (0.49)
  take5       -> take_5_drops           32:10-35:30
  love_parade -> livecode_parade        55:20-56:10

So rose_rouge is continuously present 22:35-31:45 — a stretch I had split into
quand_on_decolle, blue_gold and UNRESOLVED. And something_about_drums sits in the
5:04-11:33 hole I reported as unclaimed, exactly where PLN said he played it
("im sure i played something about drums between bombe and do it right").

Then his second point, which is the principled fix: "its almost tfidf we should do
here. when we know rose sample is around a time slot, its a tell. when we see e.g.
_jungle_BreakS_, it's way less idf relevant."

Implemented as `fuse`: evidence for track T at time t is the sum, over detected
banks that T uses, of offset-consistency divided by that bank's df. A bank in one
track hands over all its evidence; a bank in eighty splits it eighty ways.

One refinement, from measuring rather than assuming: plain log-IDF is too flat for
this. jungle_breaks (df=80) carries 0.50x the weight of rose (df=2) — a 2x penalty
for being 40x more common, which would still let it dominate since it fires
constantly. 1/df gives 0.0125 vs 0.50.

Two normalisations that are not decoration. Per track, divide by the total 1/df its
banks could contribute, or a track referencing many rare banks beats a track with
one perfect tell — the reference-size bias in a third costume. And close gaps
before deciding: a rare sample fires intermittently INSIDE the track that owns it,
so `rose` at 22:35, 25:15, 28:15, 30:00, 31:10 is one track, not five appearances.

A NEGATIVE result worth recording. Weighting let MAX_DF rise from 6 to 40, taking
fingerprint coverage from 15/23 to 23/23 candidate tracks — and it added no
evidence at all: every new bank scored consistency 0.02-0.21 against a 0.30
threshold. I guessed reference duration was the binding constraint and that was
WRONG: correlation with best consistency is -0.144, and the three longest
references are among the worst detectors. Then the confound surfaced — suns_keys
belongs to blue_gold, which PLN did not play, so its low score is a TRUE NEGATIVE.
Failures and true negatives are indistinguishable without the setlist, so the
reason remains unestablished rather than explained away.

Also fixes two things that wasted PLN's time. The Boundary Lab query key is `set`,
not `gig` — bounds.tsx does get('set') ?? 'opal-festival-2026', so a wrong key
silently loads the DEFAULT document, and he auditioned OPAL's Sunset Forest seams
believing they were CosmicFest. The variable there is named `gig`, which is exactly
how I made the mistake: grepping found the word, not the key. And the generator
offered "bombe_dj -> bombe_dj" as cut #1, a seam between a track and itself,
because it read the un-merged segment file.
parent b8bfec85
[
{
"start": 0.0,
"end": 84.89206349206349,
"end": 300.0,
"verdict": "bombe_dj",
"lenses": 3,
"lens": "crossmatch only",
"evidence": [
"crossmatch=bombe_dj (margin 0.67)",
"locate CONFIRMS",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
"crossmatch only"
]
},
{
"start": 99.84580498866214,
"end": 304.5528344671202,
"verdict": "bombe_dj",
"lenses": 3,
"start": 425.0,
"end": 475.0,
"verdict": "quand_on_decolle",
"lens": "crossmatch only",
"evidence": [
"crossmatch only"
]
},
{
"start": 530.0,
"end": 580.0,
"verdict": "something_about_drums",
"lens": "crossmatch only",
"evidence": [
"crossmatch=bombe_dj (margin 0.56)",
"locate CONFIRMS",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
"crossmatch only"
]
},
{
"start": 693.9283446712018,
"end": 1018.4503401360545,
"start": 630.0,
"end": 685.0,
"verdict": "something_about_drums",
"lens": "locate idf 0.494",
"evidence": [
"locate idf 0.494"
]
},
{
"start": 690.0,
"end": 1015.0,
"verdict": "do_it_right",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch=do_it_right (margin 0.87)",
"tempo OK 178.3~89/2"
],
"kick_bpm": 178.30188679245282
"crossmatch only"
]
},
{
"start": 1093.3115646258505,
"end": 1352.9337868480725,
"verdict": null,
"lenses": 1,
"start": 1020.0,
"end": 1065.0,
"verdict": "blue_gold",
"lens": "crossmatch only",
"evidence": [
"crossmatch=you_my_sunshine (margin 0.82)",
"tempo CLASH 82.6 vs 144",
"-> UNRESOLVED; tempo allows 4 candidates"
],
"kick_bpm": 82.58426966292134
"crossmatch only"
]
},
{
"start": 1352.910657596372,
"end": 1437.8027210884354,
"verdict": "quand_on_decolle",
"lenses": 1,
"start": 1090.0,
"end": 1350.0,
"verdict": "you_my_sunshine",
"lens": "crossmatch only",
"evidence": [
"crossmatch=quand_on_decolle (margin 0.87)"
],
"kick_bpm": 200.45454545454547
"crossmatch only"
]
},
{
"start": 1512.6639455782313,
"end": 1602.548299319728,
"verdict": null,
"lenses": 1,
"start": 1355.0,
"end": 1600.0,
"verdict": "rose_rouge",
"lens": "locate idf 0.793",
"evidence": [
"crossmatch=blue_gold (margin 0.11)",
"tempo CLASH 118.1 vs 124",
"-> UNRESOLVED; tempo allows 7 candidates"
],
"kick_bpm": 118.125
"locate idf 0.793"
]
},
{
"start": 1797.2244897959183,
"end": 1877.1242630385489,
"start": 1695.0,
"end": 1905.0,
"verdict": "rose_rouge",
"lenses": 2,
"lens": "locate idf 0.475",
"evidence": [
"crossmatch=blue_gold (margin 0.44)",
"tempo CLASH 118.1 vs 124",
"-> OVERRIDE to rose_rouge: locate + tempo agree"
],
"kick_bpm": 118.125
"locate idf 0.475"
]
},
{
"start": 1887.0857142857142,
"end": 2146.7079365079367,
"start": 1905.0,
"end": 2145.0,
"verdict": "take_5_drops",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch=take_5_drops (margin 0.58)",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
"crossmatch only"
]
},
{
"start": 2136.70022675737,
"end": 2241.5614512471657,
"verdict": null,
"lenses": 1,
"start": 2145.0,
"end": 2230.0,
"verdict": "piment_bresilien",
"lens": "crossmatch only",
"evidence": [
"crossmatch=piment_bresilien (margin 0.83)",
"tempo CLASH 192.3 vs 124",
"-> UNRESOLVED; tempo allows 0 candidates"
],
"kick_bpm": 192.29651162790697
"crossmatch only"
]
},
{
"start": 2311.4303854875284,
"end": 2391.330158730159,
"start": 2230.0,
"end": 2275.0,
"verdict": "ouais_je_funk",
"lens": "crossmatch only",
"evidence": [
"crossmatch only"
]
},
{
"start": 2310.0,
"end": 2390.0,
"verdict": "piment_bresilien",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch=piment_bresilien (margin 1.19)",
"tempo OK 62.0~124x2"
],
"kick_bpm": 61.9962511715089
"crossmatch only"
]
},
{
"start": 2436.237641723356,
"end": 2501.160544217687,
"start": 2435.0,
"end": 2500.0,
"verdict": "piment_bresilien",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch=piment_bresilien (margin 0.40)",
"tempo OK 124.1~124="
],
"kick_bpm": 124.10881801125704
"crossmatch only"
]
},
{
"start": 2526.0988662131517,
"end": 2675.890702947846,
"start": 2525.0,
"end": 2675.0,
"verdict": "ouais_je_funk",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch=ouais_je_funk (margin 0.41)",
"tempo OK 120.1~120="
],
"kick_bpm": 120.05444646098003
"crossmatch only"
]
},
{
"start": 2745.7596371882087,
"end": 2905.5360544217688,
"verdict": null,
"lenses": 1,
"start": 2745.0,
"end": 2905.0,
"verdict": "gimme_acid",
"lens": "crossmatch only",
"evidence": [
"crossmatch=gimme_acid (margin 0.48)",
"tempo CLASH 128.2 vs 80",
"-> UNRESOLVED; tempo allows 9 candidates"
],
"kick_bpm": 128.19767441860466
"crossmatch only"
]
},
{
"start": 2905.5129251700682,
"end": 3080.2662131519273,
"start": 2905.0,
"end": 3080.0,
"verdict": "punkachien",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch=punkachien (margin 1.06)",
"tempo OK 170.1~170="
],
"kick_bpm": 170.05141388174806
"crossmatch only"
]
},
{
"start": 3090.227664399093,
"end": 3250.004081632653,
"start": 3090.0,
"end": 3250.0,
"verdict": "mafia",
"lenses": 1,
"lens": "crossmatch only",
"evidence": [
"crossmatch=mafia (margin 0.69)"
],
"kick_bpm": 162.13235294117646
"crossmatch only"
]
},
{
"start": 3299.9038548752833,
"end": 3384.795918367347,
"start": 3320.0,
"end": 3385.0,
"verdict": "livecode_parade",
"lenses": 2,
"lens": "locate idf 0.638",
"evidence": [
"crossmatch=gimme_acid (margin 0.46)",
"tempo CLASH 130.0 vs 80",
"-> OVERRIDE to livecode_parade: locate + tempo agree"
],
"kick_bpm": 129.9607072691552
"locate idf 0.638"
]
},
{
"start": 3449.6725623582765,
"end": 3629.4181405895692,
"start": 3445.0,
"end": 3615.0,
"verdict": "vague_de_crime",
"lenses": 2,
"lens": "crossmatch only",
"evidence": [
"crossmatch only"
]
},
{
"start": 3615.0,
"end": 3660.0,
"verdict": "desire",
"lens": "crossmatch only",
"evidence": [
"crossmatch=vague_de_crime (margin 0.82)",
"tempo OK 119.8~120="
],
"kick_bpm": 119.83695652173913
"crossmatch only"
]
},
{
"start": 3699.287074829932,
"end": 3779.1868480725625,
"start": 3695.0,
"end": 3775.0,
"verdict": "the_revolution_will_be_sampled",
"lenses": 1,
"lens": "crossmatch only",
"evidence": [
"crossmatch=the_revolution_will_be_sampled (margin 0.48)"
],
"kick_bpm": 93.69688385269122
"crossmatch only"
]
}
]
\ No newline at end of file
......@@ -98,7 +98,32 @@ def main(argv=None) -> int:
a = ap.parse_args(argv)
rec = pathlib.Path(a.recording)
segs = json.load(open(a.fused))
raw = json.load(open(a.fused))
# MERGE consecutive segments carrying the same verdict, before emitting seams.
# Without this the document offered "bombe_dj -> bombe_dj" as cut #1, which is
# not a seam at all: the crossmatch median filter simply produced two adjacent
# windows with the same label. The consolidated tracklist merged them; this
# generator read the un-merged fused file and faithfully published nonsense.
#
# Adjacent UNRESOLVED spans merge too. They might be different tracks, but
# nothing here can tell, and offering two boundaries where we cannot justify
# even one is asking PLN to adjudicate our uncertainty rather than the audio.
segs = []
for r in raw:
if segs and segs[-1].get("verdict") == r.get("verdict"):
segs[-1]["end"] = r["end"]
ev = segs[-1].setdefault("evidence", [])
for e in r.get("evidence", []):
if e not in ev:
ev.append(e)
if r.get("kick_bpm") and not segs[-1].get("kick_bpm"):
segs[-1]["kick_bpm"] = r["kick_bpm"]
else:
segs.append(dict(r))
if len(segs) != len(raw):
print(f"merged {len(raw)} raw segments -> {len(segs)} "
f"({len(raw)-len(segs)} same-label neighbours collapsed)")
outdir = pathlib.Path(a.outdir)
dur = probe_dur(rec)
print(f"{rec.name}: {dur/60:.1f} min · {len(segs)} segments · pad {a.pad}s")
......@@ -116,6 +141,9 @@ def main(argv=None) -> int:
continue
prev = segs[i - 1] if i else None
verdict = s.get("verdict")
if prev is not None and prev.get("verdict") == verdict:
print(f" {i:02d} skipped: {verdict} -> {verdict} is not a seam")
continue
marks = [{"key": "mid", "label": "seg mid",
"t": round((s["start"] + s["end"]) / 2, 2)}]
# The lens that disagreed is worth SEEING on the waveform, because that is
......@@ -162,8 +190,15 @@ def main(argv=None) -> int:
else:
print(f"mount '{a.mount}' already registered")
# The query key is `set`, NOT `gig`. bounds.tsx reads
# new URLSearchParams(location.search).get('set') ?? 'opal-festival-2026'
# so a wrong key silently loads the DEFAULT document instead of erroring — PLN
# spent time auditioning OPAL's Sunset Forest seams believing they were his
# CosmicFest ones. The variable in that file is named `gig`, which is exactly
# how the mistake was made: grepping found the word and not the key.
print(f"\nAudition: python3 armada/serve.py --dir armada/ui/dist --port 8742")
print(f" then open /bounds.html?gig={a.gig}")
print(f" then open /bounds.html?set={a.gig}")
print(" (the key is ?set= — ?gig= silently loads the default doc)")
print("Your calls export as boundaries-%s.json from the page." % a.gig)
return 0
......
......@@ -75,9 +75,21 @@ SCAN_OUT = REPO / "armada" / "tide-table" / "set_finder_scan.json"
SR = 22050
N_FFT = 2048
HOP = 512
# A sound must be rare to be a fingerprint. df<=6 keeps `take5`(1) and
# `bogdan_grime`(5) while rejecting `jungle_breaks`(80) and `bassWarsaw`(266).
MAX_DF = 6
# PLN: "its almost tfidf we should do here. when we know rose sample is around a
# time slot, its a tell. when we see e.g. _jungle_BreakS_, it's way less idf
# relevant."
#
# Exactly, and it lets the threshold RISE rather than stay defensive. At df<=6 only
# 19 banks qualify and 15 of 23 candidate tracks get any fingerprint at all; at
# df<=40 it is 35 banks and 23 of 23. Common banks can be admitted precisely
# because they are down-weighted.
#
# But plain log-IDF is too flat to do the job — measured, not assumed:
# jungle_breaks (df=80) carries 0.50x the weight of rose (df=2), a 2x penalty for
# being 40x more common. It would still dominate, because it fires constantly.
# So the weight is 1/df: if a bank occurs in df tracks, a detection of it gives
# EACH of those tracks 1/df of the evidence. rose -> 0.50, jungle_breaks -> 0.0125.
MAX_DF = 40
MAX_FILES_PER_BANK = 12 # bounds DB size; longest files first (most hashes)
PEAK_NEIGHBOURHOOD = (24, 12) # (freq bins, time frames) for local-max filter
PEAKS_PER_FRAME_CAP = 5
......@@ -520,6 +532,15 @@ def main(argv=None) -> int:
q.add_argument("--margin-warn", type=float, default=0.03)
q.add_argument("--no-cache", action="store_true")
q.set_defaults(func=cmd_crossmatch)
q = sub.add_parser("fuse", help="IDF-weighted fusion of locate + crossmatch")
q.add_argument("--step", type=float, default=5.0)
q.add_argument("--min-consistency", type=float, default=0.30)
q.add_argument("--min-peak", type=int, default=40)
q.add_argument("--close-gap-s", type=float, default=90.0)
q.add_argument("--floor", type=float, default=0.01)
q.add_argument("--min-seg-s", type=float, default=45.0)
q.set_defaults(func=cmd_fuse)
a = p.parse_args(argv)
return a.func(a)
......@@ -768,5 +789,113 @@ def cmd_crossmatch(args) -> int:
print(f"\nwrote {out.name}")
return 0
# ----------------------------------------------------------------------- fuse ----
#
# PLN: "its almost tfidf we should do here. when we know rose sample is around a
# time slot, its a tell. when we see e.g. _jungle_BreakS_, it's way less idf
# relevant."
#
# So: evidence for track T at time t is the sum, over banks detected at t that T
# uses, of that detection's offset-consistency divided by the bank's df. A bank in
# one track hands over all its evidence; a bank in eighty splits it eighty ways.
#
# Two normalisations that are not decoration:
#
# Per track, divide by the total 1/df its banks could contribute. Otherwise a
# track that happens to reference many rare banks outscores a track with one
# perfect tell, which is the reference-size bias yet again in a third costume
# (feedback_count_what_you_bound).
#
# Close gaps before deciding. A rare sample fires intermittently INSIDE the track
# that owns it — `rose` fired at 22:35, went quiet, returned at 25:15, 28:15,
# 30:00, 31:10. Read literally that is five appearances; read musically it is one
# track. PLN heard exactly this and said so: a rose_rouge sample in a seam I had
# labelled quand_on_decolle. So a 90 s silence inside one bank's run is closed.
#
# What locate CANNOT do is name a track whose banks never sound, so crossmatch
# still fills the silence — at lower priority, because a timbral resemblance is
# weaker evidence than a specific sample landing on a specific offset.
def cmd_fuse(args) -> int:
from scipy.ndimage import binary_closing # noqa: PLC0415
loc = json.load(open(REPO / "armada/tide-table/set_finder_locate.json"))
xm = json.load(open(REPO / "armada/tide-table/set_crossmatch.json"))
rep = json.load(open(TFIDF))
df = rep["df"]
track_banks = loc["track_banks"] # track -> [rare banks]
dur, STEP = loc["duration_s"], args.step
N = int(dur // STEP) + 1
def w(bank): # 1/df, PLN's tf-idf point
return 1.0 / max(1, df.get(bank, 1))
denom = {t: sum(w(b) for b in bs) or 1.0 for t, bs in track_banks.items()}
score = {t: np.zeros(N) for t in track_banks}
fired = {t: np.zeros(N, dtype=bool) for t in track_banks}
for win in loc["windows"]:
i = int(win["t"] // STEP)
for bank, v in win["banks"].items():
if v["consistency"] < args.min_consistency or v["peak"] < args.min_peak:
continue
for t, bs in track_banks.items():
if bank in bs:
for k in range(i, min(N, i + int(loc["window_s"] // STEP))):
score[t][k] += v["consistency"] * w(bank) / denom[t]
fired[t][k] = True
close = int(args.close_gap_s // STEP)
for t in score:
m = binary_closing(fired[t], structure=np.ones(close))
score[t] = np.where(m, np.maximum(score[t], args.floor), 0.0)
xm_lab = [None] * N
XM_ALIAS = {"am_i_doing_it_right": "do_it_right", "bombe": "bombe_dj",
"take5_drops": "take_5_drops",
"revolution": "the_revolution_will_be_sampled"}
for s in xm["segments"]:
for k in range(int(s["start"] // STEP), min(N, int(s["end"] // STEP))):
xm_lab[k] = XM_ALIAS.get(s["track"], s["track"])
lab, why = [None] * N, [""] * N
for k in range(N):
c = {t: score[t][k] for t in score if score[t][k] > 0}
if c:
t = max(c, key=c.get)
runner = sorted(c.values())[-2] if len(c) > 1 else 0.0
lab[k] = t
why[k] = f"locate idf {c[t]:.3f}" + (f" (2nd {runner:.3f})" if runner else "")
elif xm_lab[k]:
lab[k], why[k] = xm_lab[k], "crossmatch only"
segs = []
for k in range(N):
if segs and segs[-1]["verdict"] == lab[k]:
segs[-1]["end"] = (k + 1) * STEP
else:
segs.append({"start": k * STEP, "end": (k + 1) * STEP,
"verdict": lab[k], "lens": why[k]})
segs = [s for s in segs if s["verdict"] and s["end"] - s["start"] >= args.min_seg_s]
def mm(t): return f"{int(t)//60}:{int(t)%60:02d}"
print(f"{'start':>7} {'end':>7} {'len':>6} {'track':<26} evidence")
print("-" * 82)
for s in segs:
print(f"{mm(s['start']):>7} {mm(s['end']):>7} {int(s['end']-s['start']):>5}s "
f"{s['verdict'][:24]:<26} {s['lens']}")
cov = sum(s["end"] - s["start"] for s in segs)
print(f"\n{len(segs)} segments · coverage {cov/dur*100:.0f}% of {dur/60:.0f} min")
by = collections.Counter(s["lens"].split()[0] for s in segs)
print("by lens: " + ", ".join(f"{k}={v}" for k, v in by.most_common()))
for s in segs:
s["evidence"] = [s["lens"]]
json.dump(segs, open(REPO / "armada/tide-table/cosmicfest26_fused.json", "w"), indent=1)
print("wrote cosmicfest26_fused.json")
return 0
if __name__ == "__main__":
sys.exit(main())
This source diff could not be displayed because it is too large. You can view the blob instead.
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