Commit b747cc70 by PLN (Algolia)

merge: the CosmicFest release pipeline, and a shelf that cannot collide

PLN: "lets merge on main all these tooling improvements and go on".

17 commits, 16 files, all reviewed as a diff against master before merging and
not just at the tip — the lesson from the 200-file merge in ea52a32c..2f947c2c.
Twelve are new files, four are edits: build_release_plan.py, catalog_view.json
(a regeneration, 73 -> 81 tracks, dropping none), the CosmicFest spec, and
TASKS_DUMP.md.

What it adds, in one line each:

  master_stemless.py     a bus-only master for a gig with no stems
  demucs_sections.py     separation in bounded chunks, sections and global
  eda_stems.py           the required stem EDA, from separations instead
  setlist_to_segments.py an ear-called setlist becomes segments
  compare_masters.py     an A/B in numbers, so the ear spends itself on taste
  build_gig_tracksjson.py  a gig page generated from sourced facts
  check_permalinks.py    the shelf is one namespace; refuse a collision
  finish_cosmic.sh       everything downstream of the masters, unattended

Delivered: two masters in spec (-14.0 LUFS, -0.9 dBTP, 44.1/24, duration exact
to a microsecond against the source), a 14-track split that verifies clean, 14
section stem sets plus a global pass, and three EDA questions answered — the
missing air is cymbals, the kick is present 44.8% of the set against OPAL's
59%, and the global separation pass is a measured waste of time (corr
0.943-0.977, level deltas under a quarter of a dB).

Four things I got wrong are recorded in the branch rather than tidied out of it,
because three shared one shape worth naming: a check whose failure looks exactly
like success. A dropped rtk grep plus a block-buffered log made a live render
look dead, and two processes wrote the same paths for forty minutes. A speed
claim counted section directories, which appear when a writer OPENS. An edit
no-op'd while printing that it had worked. The fourth was not new — the first
full render came out at 192 kHz, a loudnorm trap this project had already
documented and I did not read before writing a new chain.

Not merged and still PLN's: the +5 vs +8 air-shelf call, and a release_signoff.
The CosmicSet upload on the shelf right now is PRIVATE and its plan says
_UNSIGNED.
parents 2f947c2c 34b02925
......@@ -33,6 +33,7 @@ from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
......@@ -94,7 +95,14 @@ def check_signoff(ear: dict, tracks: list[Path], force: bool) -> list[str]:
problems.append(
"no `release_signoff` in the ear file — nobody has confirmed this "
"record by ear. Run render_release + build_release_joins and listen.")
return problems
# NO early return. This branch used to `return problems` right here,
# which meant --force — documented as "emit even without a valid ear
# signoff" — could only ever override the STALE-signoff problem, never
# the missing-signoff one it was named for. The first gig to need it
# (CosmicFest 2026: private audition upload, because SoundCloud is where
# PLN listens, so the signoff cannot precede the upload) is how that came
# out. Fall through to the shared override below.
else:
signed = datetime.fromisoformat(so["date"]).date()
newest = max((datetime.fromtimestamp(p.stat().st_mtime).date() for p in tracks),
default=None)
......@@ -116,6 +124,13 @@ def main() -> int:
ap.add_argument("spec")
ap.add_argument("--variant", default="streaming")
ap.add_argument("--artwork", help="cover image applied to every track")
ap.add_argument("--permalink-suffix", default="",
help="append to every permalink, e.g. `cosmicfest-2026`. A "
"live set's tracks are NOT unique on the shelf: the same "
"score gets played at several gigs, and SoundCloud is "
"idempotent by permalink, so the second gig's upload "
"either silently skips or overwrites the first's. Scope "
"the permalinks per gig and neither can happen.")
ap.add_argument("--include-mix", action="store_true",
help="also publish the continuous *_nodrop mix, listed first")
ap.add_argument("--out", required=True)
......@@ -123,7 +138,11 @@ def main() -> int:
help="track names from the canonical tracklist (default) or "
"from the rendered filenames")
ap.add_argument("--force", action="store_true",
help="emit even without a valid ear signoff (say why in the log)")
help="emit even without a valid ear signoff (say why in --why)")
ap.add_argument("--why", default="",
help="required with --force: why this plan may exist unsigned. "
"Recorded IN the plan, because a terminal warning scrolls "
"away and the JSON is what the uploader reads.")
a = ap.parse_args()
spec = json.loads(Path(a.spec).read_text())
......@@ -146,6 +165,10 @@ def main() -> int:
if len(files) != len(segs):
sys.exit(f"{len(files)} rendered files but {len(segs)} segments in {tdir}")
if a.force and not a.why.strip():
sys.exit("--force needs --why. An override with no stated reason is "
"indistinguishable from an approved release once the terminal "
"is closed, and this plan is what the uploader trusts.")
problems = check_signoff(ear, files, a.force)
if problems:
print("REFUSING TO WRITE A PLAN — the record is not ear-approved:")
......@@ -170,6 +193,10 @@ def main() -> int:
print(f" {t['name']!r}")
return 1
def permalink(title: str) -> str:
base = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", title.lower())).strip("-")
return f"{base}-{a.permalink_suffix}" if a.permalink_suffix else base
tracks, renamed = [], []
for seg, f in zip(segs, files):
src = by_name[match_key(seg["title"])]
......@@ -187,6 +214,7 @@ def main() -> int:
tags = base_tags + [t for t in (src.get("style"), src.get("section")) if t]
tracks.append({
"title": title,
"permalink": permalink(title),
"audio": str(f),
"artwork": a.artwork,
"genre": src.get("style", ""),
......@@ -235,6 +263,9 @@ def main() -> int:
# Composed from canonical FIELDS (title · venue · date), never from
# a remembered string. The format is here; the facts are theirs.
"title": f"{album} — {fm.get('address','')} {fm.get('date','')[:4]} (full set)",
# No year in the base: the suffix already carries the gig, and
# "cosmicset-2026-2026-full-set-cosmicfest-2026" is nobody's URL.
"permalink": permalink(f"{album} full set"),
"audio": str(mix),
"artwork": a.artwork,
"genre": "livecoding",
......@@ -259,6 +290,16 @@ def main() -> int:
],
"album": album,
"artist": spec.get("artist", "ParVagues"),
# An unsigned plan must announce itself. The uploader's --sharing flag
# decides private vs public, so nothing here can enforce it; what this
# CAN do is make sure a plan built for a private audition is never
# mistaken for an approved record when someone re-runs it later.
"_UNSIGNED": ({"reason": a.why,
"forced": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"means": "NOT ear-approved. Private audition only — do not "
"flip these permalinks public without a "
"release_signoff in the ear file."}
if a.force else None),
"date": fm.get("date"),
"_provenance": {
"album/date/description/tags": str(md_path.relative_to(WWW)),
......
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python3
"""check_permalinks — refuse a release plan whose permalinks are already taken.
## Why this is a gate and not a warning
SoundCloud is idempotent by permalink, and the uploader treats an existing
permalink as "already done". That is the right behaviour for resuming a
half-finished upload and the wrong behaviour for a *different* recording of the
same track — and a live catalogue is full of those. ParVagues played Piment
Brésilien at Opal in August and at CosmicFest a fortnight later; both render to
`/piment-bresilien`.
So the failure mode is not an error. It is a release that reports success with
four tracks quietly missing, or — with `--overwrite` — the previous gig's
release silently replaced by this one's. Both are invisible from the terminal
that ran the upload. Caught on CosmicFest-2026 before `--go`: four of fourteen
permalinks were already Opal's private uploads.
## What it does
Resolves the plan's permalinks (explicit `permalink`, else the same slugify the
uploader applies to `title`) against the live account, and separates:
* FREE — nothing there, a normal upload
* OURS-RESUME — taken, and the duration matches this plan's audio, so it is
almost certainly a resumed upload of THIS release
* COLLISION — taken by audio of a different length: a different recording
A COLLISION is fatal unless `--allow-collisions`. Duration is the discriminator
because it is the one property that survives SoundCloud's transcode and is
already in the plan (`_duration_s`).
python3 check_permalinks.py <plan.json> [--tolerance 3]
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, "/home/pln/Work/Sound/tidal-ears/src")
def slugify(title: str) -> str:
return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", title.lower())).strip("-")
def shelf() -> dict[str, dict]:
from tidal_ears.sc import SC, resolve_creds
api = SC(resolve_creds())
me = api.get("/me")
out, path = {}, f"/users/{me['id']}/tracks"
# `access=playable,preview,blocked` is load-bearing: scform.py's own notes
# record that without it the owner's PRIVATE tracks are omitted, which would
# make every private permalink look free — the exact opposite of this gate's
# job, since an audition release is private by definition.
r = api.get(path, limit=200, access="playable,preview,blocked")
for t in (r.get("collection", []) if isinstance(r, dict) else r):
out[t["permalink"]] = {"title": t.get("title"),
"dur_s": round((t.get("duration") or 0) / 1000, 1),
"sharing": t.get("sharing")}
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("plan")
ap.add_argument("--tolerance", type=float, default=3.0,
help="seconds of duration difference still counted as the "
"same audio (transcode + the uploader's own trim)")
ap.add_argument("--allow-collisions", action="store_true",
help="proceed anyway — say why in the release log")
a = ap.parse_args()
plan = json.loads(Path(a.plan).read_text())
live = shelf()
free, resume, collide = [], [], []
for t in plan["tracks"]:
pl = t.get("permalink") or slugify(t["title"])
got = live.get(pl)
if not got:
free.append((pl, t["title"]))
elif abs(got["dur_s"] - (t.get("_duration_s") or -1)) <= a.tolerance:
resume.append((pl, t["title"], got))
else:
collide.append((pl, t["title"], got, t.get("_duration_s")))
def hms(s):
s = int(s or 0)
return f"{s // 60}:{s % 60:02d}"
print(f"{plan.get('album')} · {len(plan['tracks'])} planned upload(s)")
print(f" {len(free):>3} free · {len(resume):>3} already up as this same audio "
f"· {len(collide):>3} COLLISION(S)")
if resume:
print("\nalready up, same length — an upload run would resume, not duplicate:")
for pl, title, got in resume:
print(f" /{pl:<44} {hms(got['dur_s'])} {got['sharing']}")
if collide:
print("\nCOLLISIONS — the permalink is taken by DIFFERENT audio:")
for pl, title, got, want in collide:
print(f" /{pl:<44} shelf {hms(got['dur_s'])} ({got['title']!r}) "
f"vs plan {hms(want)} ({title!r})")
print("\n An upload would SKIP these tracks (release silently short) or,")
print(" with --overwrite, replace the other release's audio. Re-generate")
print(" the plan with `build_release_plan.py --permalink-suffix <gig>`.")
if not a.allow_collisions:
return 1
print("\n ! OVERRIDDEN by --allow-collisions")
print("\n✓ no collisions" if not collide else "")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""compare_masters — put two or more finished masters side by side, in numbers.
Built for an A/B that PLN has to judge by ear. The ear is the authority and this
does not try to replace it — but "which one do you prefer" is a much easier
question when you also know WHAT differs and by how much. A tilt of a few dB in a
shelf changes more than the band it names: it moves true peak, it changes how hard
the limiter works, and it shifts where the loudness normaliser lands.
So this reports, per file: integrated loudness, true peak, loudness range, and the
band-energy distribution — then a delta table against the first file, because the
differences are the point and eyeballing two absolute tables is how you miss a
0.3% change that matters.
Deliberately measured on the RENDERED files rather than predicted from the filter
settings. `feedback_verify_own_renders`: the machine's job is to catch objective
facts so PLN's ears are spent on taste. A shelf set to +3 dB does not necessarily
put +3 dB in the master — the limiter downstream has opinions.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
import numpy as np
import soundfile as sf
BANDS = [(20, 60), (60, 120), (120, 300), (300, 1000),
(1000, 4000), (4000, 8000), (8000, 16000), (16000, 22050)]
def ebur128(path: Path) -> dict:
p = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-i", str(path),
"-af", "ebur128=peak=true", "-f", "null", "-"],
capture_output=True, text=True)
tail = p.stderr[-2000:]
def grab(label):
m = re.search(rf"{label}:\s*(-?[\d.]+)", tail)
return float(m.group(1)) if m else None
return {"I": grab("I"), "LRA": grab("LRA"), "peak": grab("Peak")}
def bands(path: Path) -> tuple[dict, float]:
"""Band shares over the whole file, streamed — these are hour-long masters."""
n = 1 << 15
win = np.hanning(n)
acc = np.zeros(len(BANDS))
corr_num = corr_l = corr_r = 0.0
with sf.SoundFile(str(path)) as f:
sr = f.samplerate
fr = np.fft.rfftfreq(n, 1 / sr)
idx = [(fr >= lo) & (fr < hi) for lo, hi in BANDS]
while True:
blk = f.read(n, dtype="float32", always_2d=True)
if blk.shape[0] < n:
break
L, R = blk[:, 0], blk[:, 1]
corr_num += float((L * R).sum())
corr_l += float((L * L).sum())
corr_r += float((R * R).sum())
S = np.abs(np.fft.rfft(blk.mean(axis=1) * win)) ** 2
for j, m in enumerate(idx):
acc[j] += float(S[m].sum())
tot = acc.sum() or 1.0
denom = (corr_l * corr_r) ** 0.5
return ({f"{lo}-{hi}": 100 * v / tot for (lo, hi), v in zip(BANDS, acc)},
corr_num / denom if denom > 0 else 0.0)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("files", nargs="+")
ap.add_argument("--label", action="append", default=[],
help="label per file, in order (default: the filename)")
a = ap.parse_args()
paths = [Path(f) for f in a.files]
for p in paths:
if not p.exists():
print(f"MISSING {p}", file=sys.stderr)
return 1
labels = [a.label[i] if i < len(a.label) else paths[i].stem
for i in range(len(paths))]
rows = []
for p in paths:
lo = ebur128(p)
bs, corr = bands(p)
rows.append({"loud": lo, "bands": bs, "corr": corr,
"size_mb": p.stat().st_size / 1e6})
w = 13
print(f"{'':<14}" + "".join(f"{l[:w]:>{w}}" for l in labels))
print("-" * (14 + w * len(labels)))
for key, fmt in (("I", "{:.1f} LUFS"), ("peak", "{:.1f} dBTP"), ("LRA", "{:.1f} LU")):
print(f"{key:<14}" + "".join(
f"{(fmt.format(r['loud'][key]) if r['loud'][key] is not None else '—'):>{w}}"
for r in rows))
print(f"{'L/R corr':<14}" + "".join(f"{r['corr']:>{w}.3f}" for r in rows))
print(f"{'size':<14}" + "".join(f"{r['size_mb']:>{w-3}.0f} MB" for r in rows))
print(f"\nband energy share (%)")
print(f"{'':<14}" + "".join(f"{l[:w]:>{w}}" for l in labels))
for band in rows[0]["bands"]:
print(f"{band:<14}" + "".join(f"{r['bands'][band]:>{w}.3f}" for r in rows))
if len(rows) > 1:
print(f"\nDELTA vs {labels[0]} — the differences are the point")
print(f"{'':<14}" + "".join(f"{l[:w]:>{w}}" for l in labels[1:]))
base = rows[0]
for key in ("I", "peak", "LRA"):
if base["loud"][key] is None:
continue
print(f"{key:<14}" + "".join(
f"{r['loud'][key] - base['loud'][key]:>+{w}.2f}" for r in rows[1:]))
for band in base["bands"]:
b0 = base["bands"][band]
cells = []
for r in rows[1:]:
d = r["bands"][band]
# ratio, not difference: a band at 0.12% going to 0.24% is a
# doubling, and "+0.12 points" hides that completely.
cells.append(f"{d / b0:>{w}.2f}x" if b0 > 1e-9 else f"{'—':>{w}}")
print(f"{band:<14}" + "".join(cells))
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env bash
# finish_cosmic — everything downstream of the masters, unattended.
#
# PLN went to bed with "your autonomous now going to bed tell me results tomo".
# The master and demucs runs are systemd units so they outlive the session, but
# the steps AFTER them were sitting in my session — which means a teardown would
# have left him with rendered masters and nothing else. So the rest is a unit
# too: it waits for the masters, splits the album, runs the stem EDA, and leaves
# a summary on disk that stands on its own.
#
# Ordered after cosmic-master.service in the unit file, so systemd does the
# waiting rather than a polling loop.
set -uo pipefail
TT=/home/pln/Work/Sound/Tidal/armada/tide-table
SPEC=$TT/judge_specs/cosmicfest-2026.json
ROOT=/home/pln/Work/Sound/Prod/Cosmic26_master
PY=/home/pln/Work/Sound/tidal-ears/.venv/bin/python
SUM=$ROOT/FINISH_SUMMARY.txt
exec > >(tee -a "$SUM") 2>&1
echo "=============================================================="
echo "finish_cosmic $(date -Is)"
echo "=============================================================="
if [ ! -f "$ROOT/Cosmic26_v1_streaming.flac" ]; then
echo "FATAL: no streaming master — the master unit did not produce one."
exit 1
fi
echo
echo "--- masters on disk ---"
for f in "$ROOT"/Cosmic26_v1_*.flac; do
[ -e "$f" ] || continue
printf '%s %s ' "$(basename "$f")" "$(du -h "$f" | cut -f1)"
ffprobe -v error -show_entries format=duration -of csv=p=0 "$f"
done
# The spec now lists only the streaming variant, so this splits exactly what was
# rendered. The -9 club target moved to `variants_deferred` — not a failure, a
# decision recorded there with its reasoning, and moving the key back renders it.
echo
echo "--- split + verify ---"
"$PY" "$TT/render_release.py" "$SPEC" --only split
"$PY" "$TT/render_release.py" "$SPEC" --only verify
echo
echo "--- stem EDA ---"
# Gate on LENGTH, not existence. A soundfile writer creates the file the instant
# it opens, so `-f` is true from the first second of an hour-long pass — the same
# trap that made me misread "4 of 14 sections done" earlier tonight. The global
# comparison seeks into these by absolute time, so a truncated global stem would
# silently mis-align every later track rather than fail.
# Compare against the MASTER's own duration, not a hardcoded number. A threshold
# of 3700 was a rubber stamp: the pass writes in 120 s chunks, so it sits at
# 3720 s — 31 chunks, one short — for minutes while looking "basically done" to
# any loose comparison. My own monitor fired a premature COMPLETE on that number.
WANT=$(ffprobe -v error -show_entries format=duration -of csv=p=0 \
"$ROOT/ZOOM0067.MP3" 2>/dev/null | cut -d. -f1); WANT=${WANT:-3779}
GDUR=0
if [ -f "$ROOT/stems_demucs/global/vocals.wav" ]; then
GDUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$ROOT/stems_demucs/global/vocals.wav" 2>/dev/null | cut -d. -f1)
GDUR=${GDUR:-0}
fi
if [ "$GDUR" -ge "$((WANT - 2))" ]; then
echo "global stems complete (${GDUR}s of ${WANT}s) — full EDA with local-vs-global"
"$PY" "$TT/eda_stems.py" "$SPEC"
else
echo "global stems SHORT (${GDUR}s of ${WANT}s) — section-only EDA."
echo " A short global file does not FAIL the comparison — compare_global takes"
echo " min(len(a),len(b)) — it compares fewer seconds per track and reports"
echo " thinner evidence under the same headings. Worth saying out loud."
"$PY" "$TT/eda_stems.py" "$SPEC" --no-global
fi
echo
echo "--- the air-shelf A/B, for PLN's ears ---"
# The EDA proved the 8-16 kHz deficit is cymbals (drums 0.367% vs 0.084%
# runner-up, 4.4x), which licenses opening the shelf but cannot say how far.
for pair in "+5:$ROOT/Cosmic26_v1_streaming.flac" "+8:$ROOT/ab_air8/streaming.flac"; do
g=${pair%%:*}; f=${pair#*:}
if [ -f "$f" ]; then
printf 'air %s dB %s ' "$g" "$(du -h "$f" | cut -f1)"
ffprobe -v error -show_entries stream=sample_rate,channels \
-show_entries format=duration -of csv=p=0 "$f" | tr '\n' ' '
echo
else
echo "air $g dB MISSING ($f)"
fi
done
# Numbers beside the A/B, because "which do you prefer" is an easier question
# when you also know what differs. Measured on the RENDERED files: a shelf set to
# +3 dB does not necessarily put +3 dB in the master, since the limiter
# downstream has opinions.
if [ -f "$ROOT/Cosmic26_v1_streaming.flac" ] && [ -f "$ROOT/ab_air8/streaming.flac" ]; then
echo
echo "--- A/B measured side by side ---"
"$PY" "$TT/compare_masters.py" \
"$ROOT/Cosmic26_v1_streaming.flac" "$ROOT/ab_air8/streaming.flac" \
--label "air +5" --label "air +8"
fi
echo
echo "--- what is on disk now ---"
for v in streaming; do
d="$ROOT/tracks_v1_$v"
[ -d "$d" ] && echo "$v: $(ls "$d"/*.flac 2>/dev/null | wc -l) tracks"
done
echo
echo "finish_cosmic done $(date -Is)"
{
"_comment": [
"Gig spec for COSMICFEST 2026-08-22 — jour J of cosmicfest_v2.67Hz. The date is",
"sourced in cosmicfest-2026_gigmeta.json and CORRECTS an unsourced 2026-08-23",
"this file carried until 2026-09-02. Per POSTPROD.md a new gig is a copy of",
"this file, never a copy of a script — this one is the first STEMLESS gig, so",
"read the differences from opal26.json as the point of the file:",
"",
" * NO stems. One stage-mic recording, so `stemsDir`/`stemmap`/`clipsDir`/",
" `keeps` are absent, not empty. Verified: apply_boundaries and",
" render_release read none of them; they need `segments`, `master`,",
" `variants`, `releaseRoot`, `releaseTag` only. build_judge_set DOES need",
" stems, which is exactly why the nominal segments came from",
" setlist_to_segments.py instead.",
" * NO premix is possible. OPAL's v4 was a per-orbit pass (it trimmed d4,",
" which owned 76-91% of the LF bed). Nothing here has an analogue: any",
" fix is broadband, or comes from demucs-separated stems used as a lens.",
" * `master` is the RAW recording because that is the timeline PLN's ear",
" calls were made against. `variants` point at mastered files that DO NOT",
" EXIST YET — the mix pass is the next step, and render_release will say",
" MISSING until it runs.",
" * `album` is deliberately ABSENT. CosmicFest 2026 has no page under",
" content/lives/2026/, so there is no canonical set title to copy;",
" inventing one would violate feedback_metadata_vs_mastering. Ask PLN."
],
"gig": "cosmicfest-2026",
"date": "2026-08-22",
"artist": "ParVagues",
"liveRoot": "/home/pln/Work/Sound/Tidal",
"source": "/home/pln/Work/Sound/Prod/Cosmic26_master/ZOOM0067.MP3",
"sourceProvenance": {
"device": "stage microphone, Zoom recorder",
"format": "MP3 320 kbps CBR, 44100 Hz, stereo",
"duration_s": 3779.318,
"sha256": "7a19758994dbfd121b1a30b010ac0be272a4f95a327ba666147ab548fe7d671b",
"measured_2026-09-01": {
"integrated_lufs": -25.2,
"true_peak_dbfs": -5.1,
"lra_lu": 11.1,
"tool": "ffmpeg ebur128=peak=true, full duration",
"reading": "Not clipped and not crushed — conservatively recorded, simply quiet. ~+11 dB to reach -14 LUFS, which needs limiting but is a gain problem, not a repair job.",
"caveat": "Lossy source AND a live FOH chain that people adjusted mid-set. Every derived master inherits both. PLN: 'we can only do our best, 80/20 likely'."
},
"copies": [
"/home/pln/Work/Sound/Prod/Cosmic26_master/ZOOM0067.MP3",
"/mnt/freebox/PLN/Work/Sound/Prod/Cosmic26_master/ZOOM0067.MP3"
],
"note": "Was a single copy in ~/Downloads until 2026-09-01. Do not confuse with Prod/cosmicfest/ or Prod/cosmicfestv0.live1.* — those are the 2025 edition (files dated 2025-06-27/28)."
},
"master": "/home/pln/Work/Sound/Prod/Cosmic26_master/ZOOM0067.MP3",
"segments": "/home/pln/Work/Sound/Prod/Cosmic26_master/segments_nominal.json",
"segmentsRelease": "/home/pln/Work/Sound/Prod/Cosmic26_master/segments_v1.json",
"releaseRoot": "/home/pln/Work/Sound/Prod/Cosmic26_master",
"releaseTag": "v1",
"variants": {
"streaming": "/home/pln/Work/Sound/Prod/Cosmic26_master/Cosmic26_v1_streaming.flac"
},
"earBoundaries": "/home/pln/Work/Sound/Tidal/armada/tide-table/judge_specs/cosmicfest-2026_ear_verified.json",
"earSetlist": "/home/pln/Work/Sound/Tidal/armada/tide-table/judge_specs/cosmicfest-2026_setlist_ear.json",
"titles": "/home/pln/Work/Sound/Tidal/armada/tide-table/judge_specs/cosmicfest-2026_titles.json",
"binS": 2.0,
"note": "14 tracks. 10 starts are PLN playhead calls; #1 is the origin; #12 #13 #14 were INFERRED from his notes on skipped boundaries and have never been heard as cuts. Those three are the ones to audition before any render is called final.",
"variants_deferred": {
"club": "/home/pln/Work/Sound/Prod/Cosmic26_master/Cosmic26_v1_club.flac",
"_why": [
"NOT rendered, deliberately, and not a failure. Two axes were being",
"conflated: PLN's 'one live club mix' means the CONTINUOUS set as opposed",
"to the album split — a FORM. This `club` entry is a -9 LUFS LOUDNESS",
"target, our own convention. Since no CosmicFest track is cut, the",
"continuous mix simply IS the master file, so both of PLN's forms are",
"already covered by the streaming master plus tracks_v1_streaming/.",
"It would also have missed: reaching -9 from a -25.6 LUFS source with",
"4.7 dB of linear headroom tripped the LRA floor at -10.5 on the trial,",
"i.e. the target costs more dynamics than it is worth on this source.",
"The CPU went to an air-shelf A/B instead, which answers a question PLN",
"actually has. Move this key back into `variants` to render it."
]
},
"abVariants": {
"_why": [
"The stem EDA settled that the 8-16 kHz deficit is CYMBALS, not codec",
"noise: drums hold 0.367% of their own energy above 8 kHz against 0.084%",
"for the runner-up, a 4.4x concentration. That licenses opening the air",
"shelf but cannot say how far, which is taste — so two full masters are",
"rendered, identical but for the shelf, and judged in place rather than",
"on a clip (OPAL's reverb lesson)."
],
"air+5": "/home/pln/Work/Sound/Prod/Cosmic26_master/Cosmic26_v1_streaming.flac",
"air+8": "/home/pln/Work/Sound/Prod/Cosmic26_master/ab_air8/streaming.flac"
}
}
{
"gig": "cosmicfest-2026",
"_provenance": [
"Generated by setlist_to_segments.py from cosmicfest-2026_setlist_ear.json",
"and cosmicfest-2026_boundaries_ear.json. DO NOT HAND-EDIT: edit those two and re-run.",
"`verified` holds ONLY starts PLN called on the playhead. Starts absent",
"here were inferred from his notes on SKIPPED boundaries and stay",
"`nominal` in apply_boundaries' output, so the two grades never merge."
],
"verified": {
"2": {
"start": 406.63,
"source": "playhead",
"boundary": 1,
"note": "from here its clearly something about drums, might even start earlier idk"
},
"3": {
"start": 698.63,
"source": "playhead",
"boundary": 3,
"note": "crossfade starts a bit earlier, this 11:38.6 is a good start point"
},
"4": {
"start": 1095.32,
"source": "playhead",
"boundary": 5,
"note": "that's the exact first sound of the next track you_my_sunshine"
},
"5": {
"start": 1364.91,
"source": "playhead",
"boundary": 6,
"note": "at 22:42 its end of reose_rouge, theres a loud blip noise, at 22:44.9 its the start of rose rouge, could we cut noise and blend better?"
},
"6": {
"start": 1897.76,
"source": "playhead",
"boundary": 7,
"note": "at 31:37.8 we're enough in the crosfade it makes a good 5 drops start."
},
"7": {
"start": 2145.61,
"source": "playhead",
"boundary": 8,
"note": "starting at 35:45.6 i hear proper only piment start sound"
},
"8": {
"start": 2525.44,
"source": "playhead",
"boundary": 11,
"note": "start of ouais_je_funk for real"
},
"9": {
"start": 2735.79,
"source": "playhead",
"boundary": 12,
"note": "start of perfect! there was no gimme_acid in this performance."
},
"10": {
"start": 2907.36,
"source": "playhead",
"boundary": 13,
"note": "this is end of perfect -> perfect cut at 48:27.4 into PunkAChien!"
},
"11": {
"start": 3072.47,
"source": "playhead",
"boundary": 14,
"note": "proper start of mafia"
}
},
"edits": {},
"cut_from_release": {}
}
{
"_what": [
"The fields of CosmicFest-2026's canonical www page that are NOT derivable from",
"the audio, the scores or the segments. Consumed by build_gig_tracksjson.py --meta.",
"Every entry carries {value, source, locator, date} per feedback_metadata_provenance,",
"so a reader in six months can tell PLN's words from a page-derived fact from a",
"drafted line. Nothing here is a guess: the two fields nobody could source are",
"absent, not filled in."
],
"title": {
"value": "CosmicSet 2026",
"source": "PLN",
"locator": "chat 2026-09-02: \"title id go with 'CosmicSet 2026'?\"",
"date": "2026-09-02"
},
"venue": {
"value": "CosmicFest",
"source": "PLN",
"locator": "chat 2026-09-02: \"'CosmicFest' is the venue\"",
"date": "2026-09-02"
},
"date": {
"value": "2026-08-22",
"source": "PLN, corroborating Web/www — and it CORRECTS the unsourced 2026-08-23 this gig's spec carried until 2026-09-02",
"locator": "PLN chat 2026-09-02: \"yea it was samedi 22 indeed :)\" · www/PRODUCT.md:21 \"cosmicfest_v2.67Hz (20-23 August 2026, jour J Saturday 22)\" + www/app/cosmicfest/CosmicFestClient.js:234 \"le 22 aout, dans le jardin\" (the lineup section) + :396 \"jour J - samedi 22 aout 2026\"",
"date": "2026-09-02"
},
"stage": {
"value": "Le Jardin",
"source": "derived from the invite page's lineup heading — PLN to confirm the name and casing",
"locator": "www/app/cosmicfest/CosmicFestClient.js:234 \"le 22 aout, dans le jardin\"",
"date": "2026-09-02"
},
"location": {
"value": "Labenne-Océan, France",
"source": "Web/www — canonical",
"locator": "www/PRODUCT.md:14 \"Labenne-Ocean (French Atlantic coast)\" + www/app/cosmicfest/page.js:4",
"date": "2026-09-02"
},
"description": {
"value": "CosmicSet 2026 — live coding set @ cosmicfest_v2.67Hz, Labenne-Océan, dans le jardin",
"source": "drafted from the canonical fields above — no fact of its own; PLN to approve the wording",
"locator": "mirrors the shape of content/lives/2026/opal-festival-2026.md's description",
"date": "2026-09-02"
},
"ctaURL": {
"value": "https://me.nech.pl/cosmicfest",
"source": "the edition's own invite micro-site, same CTA the 2025 page uses",
"locator": "www/content/lives/2025/cosmicfest.md ctaURL + the live /cosmicfest App Router route",
"date": "2026-09-02"
},
"ctaText": {
"value": "cosmicfest_v2.67Hz",
"source": "the festival's own wordmark, verbatim casing",
"locator": "www/DESIGN.md:53 \"Wordmark: cosmicfest (display) + v2.67Hz (mono badge)\"",
"date": "2026-09-02"
},
"tags": {
"value": [
"livecoding",
"cosmicfest",
"labenne",
"tidalcycles",
"summer",
"live"
],
"source": "same tag vocabulary as the 2025 CosmicFest page",
"locator": "www/content/lives/2025/cosmicfest.md tags",
"date": "2026-09-02"
},
"sections": {
"1": "Ouverture",
"2": "SUNSET",
"3": "SUNSET",
"4": "We call it NuJazz",
"5": "We call it NuJazz",
"6": "We call it NuJazz",
"7": "We call it NuJazz",
"8": "We call it NuJazz",
"9": "We call it NuJazz",
"10": "NUIT",
"11": "NUIT",
"12": "NUIT",
"13": "FINALE",
"14": "Outro"
},
"_sections_provenance": {
"source": "PLN's own set plan in backlog.md — the canonical informal tracklist source (reference_gig_tracklist_sources). NOT invented.",
"locator": "Tidal/backlog.md ## COSMICFEST (~line 1986): Ouverture / SUNSET / _We call it NuJazz_ / NUIT / FINALE",
"date": "2026-09-02",
"caveat": "The planned order matches the measured set exactly, with ONE difference: the plan lists 'WAP' [133] second in Ouverture and it does not appear in the recording, so the mapping shifts up by one from track 2 onward. Track 14 ('Outro: La Dub Sirene') is not in the plan at all - its section is taken from PLN's own title for it.",
"open_question": "Was WAP played? Track 1 is 6.8 min, in line with other single tracks (3 is 6.6, 7 is 6.3), so it does not look like two tracks merged - but that is an inference, not a measurement."
}
}
{
"gig": "cosmicfest-2026",
"_provenance": [
"Release titles PLN could not be derived from: tracks absent from the OPAL-26",
"release (the ear-signed title source) AND from catalog.generated.json.",
"Given verbatim by PLN in conversation, 2026-09-01. Per feedback_metadata_provenance",
"each entry carries its source; per feedback_metadata_vs_mastering these are",
"RELEASE metadata and belong to the gig, not to the mastering scripts.",
"`score` resolves a gig slug whose name does not match its .tidal filename."
],
"titles": {
"rose_rouge": {
"title": "Rose Rouge",
"source": "user",
"locator": "PLN, conversation 2026-09-01",
"as_of": "2026-09-01"
},
"mafia": {
"title": "Mafia sans Serif",
"score": "live/collab/raph/mafia_sans_serif.tidal",
"source": "user",
"locator": "PLN, conversation 2026-09-01",
"as_of": "2026-09-01",
"note": "The OPAL-26 release shipped this same score as plain \"Mafia\"; PLN's CosmicFest call is the fuller name. The gig slug `mafia` does not match the filename, hence `score`."
},
"livecode_parade": {
"title": "LiveCode Parade",
"source": "user",
"locator": "PLN, conversation 2026-09-01",
"as_of": "2026-09-01"
},
"Outro Dub Siren": {
"title": "Outro: La Dub Sirène",
"source": "user",
"locator": "PLN, conversation 2026-09-01",
"as_of": "2026-09-01",
"note": "PLN gave this lowercase (\"outro: la dub sirene\") and asked for proper French accents; the accent on sirène is his instruction, the capitalisation is a house-style guess. No .tidal exists — this is a played outro, not a composed track, so the catalog (keyed by score path) can never hold its title."
}
}
}
#!/usr/bin/env python3
"""setlist_to_segments — build the nominal segments for a gig recorded WITHOUT stems.
`build_judge_set` derives nominal segments from per-orbit stem activity. A set
captured on a single stage mic has no stems, so that door is shut — but the ear
pass still happened, and `judge_specs/<gig>_setlist_ear.json` already holds one
row per track. This turns that file into the two inputs `apply_boundaries` wants:
1. a NOMINAL segments list (what the spec's `segments` key points at), and
2. an ear file in `apply_boundaries`' own schema — `verified` keyed by TRACK.
Why both, when the setlist already carries the ear's numbers: the setlist mixes
two grades of truth. Some starts are playhead calls PLN made while listening;
others were inferred from the notes on boundaries he SKIPPED. Splitting them
means `apply_boundaries` prints `ear` or `nominal` per row and asserts that
every playhead call reached the output — so an inferred edge can never be
mistaken later for one he actually heard.
Titles come from what SHIPPED, never from a slug: the OPAL-26 release is
ear-signed, so its `segments_v4.json` joined to that gig's `tracks.json` (score
path in performance order) is the authoritative slug -> release-title map. The
catalog is a second source. A track in neither is emitted as NEEDS_PLN rather
than title-cased into something plausible — `feedback_metadata_provenance`.
Declared BPM is parsed from the track's own `.tidal` (`setcps (N/60/4)`), which
is the score's claim, not a measurement. A tempo-knob range is recorded as a
range so nobody reads a floor as a fact.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent.parent
HERE = Path(__file__).resolve().parent
# `setcps (124/60/4)` — the BPM is the numerator. Also catches `cps (N/60/4)`.
CPS_STATIC = re.compile(r'setcps\s*\(\s*([0-9.]+)\s*/\s*60\s*/\s*4\s*\)')
# the knob idiom: `# cps ((range LO HI "^NN")/60/4)`
CPS_RANGE = re.compile(r'range\s+(\(?[0-9.+\- ]+\)?)\s+(\(?[0-9.+\- ]+\)?)\s+"\^\d+"')
def opal_title_map() -> dict[str, str]:
"""slug -> release title, from the one release that is signed off by ear.
The join key is performance order: `tracks.json` lists score paths 1..N in
the order played, and `segments_v4.json` carries `perf_track` alongside the
title that actually shipped on the file. Neither file alone has both halves.
"""
tj = Path("/home/pln/Work/Web/www/content/lives/2026/opal-festival-2026/tracks.json")
sv = Path("/home/pln/Work/Sound/Prod/Opal26_master/segments_v4.json")
if not (tj.exists() and sv.exists()):
return {}
scores = [t.get("file") or t.get("score") or "" for t in json.loads(tj.read_text())["tracks"]]
by_perf = {r["perf_track"]: r["title"] for r in json.loads(sv.read_text())}
out = {}
for i, path in enumerate(scores, start=1): # perf_track is 1-based
if path and i in by_perf:
out[Path(path).stem] = by_perf[i]
return out
def catalog_title_map() -> dict[str, str]:
p = HERE / "catalog.generated.json"
if not p.exists():
return {}
tracks = json.loads(p.read_text())["tracks"]
tracks = list(tracks.values()) if isinstance(tracks, dict) else tracks
return {Path(t["id"]).stem: t["title"] for t in tracks
if isinstance(t, dict) and t.get("id") and t.get("title")}
def find_score(slug: str) -> Path | None:
"""The score for a slug. One track = one standalone .tidal, per CLAUDE.md."""
hits = [p for p in REPO.rglob(f"{slug}.tidal") if ".git" not in p.parts and ".claude" not in p.parts]
return sorted(hits, key=lambda p: len(p.parts))[0] if hits else None
def declared_bpm(score: Path | None):
if not score or not score.exists():
return None
text = score.read_text(errors="replace")
live = [ln for ln in text.splitlines() if not ln.lstrip().startswith("--")]
body = "\n".join(live)
if (m := CPS_STATIC.search(body)):
return float(m.group(1))
if (m := CPS_RANGE.search(body)):
return {"range": [m.group(1).strip(), m.group(2).strip()], "note": "tempo is a knob"}
return None
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--setlist", required=True, help="judge_specs/<gig>_setlist_ear.json")
ap.add_argument("--boundaries", required=True, help="judge_specs/<gig>_boundaries_ear.json")
ap.add_argument("--out-segments", required=True)
ap.add_argument("--out-ear", required=True)
ap.add_argument("--titles", help="judge_specs/<gig>_titles.json — PLN's authored "
"release titles for tracks no shipped release or catalog can name")
ap.add_argument("--tol", type=float, default=0.15,
help="seconds within which a setlist start counts as matching a playhead call")
a = ap.parse_args()
setlist = json.loads(Path(a.setlist).read_text())
bounds = json.loads(Path(a.boundaries).read_text())
decided = bounds.get("decided", {})
tracks = setlist["tracks"]
opal, cat = opal_title_map(), catalog_title_map()
authored = {}
if a.titles:
authored = json.loads(Path(a.titles).read_text()).get("titles", {})
segs, verified, needs_pln, unmatched = [], {}, [], []
for t in tracks:
n, slug, start = int(t["n"]), t["track"], float(t["start"])
# Which playhead call, if any, produced this start?
hit = next((k for k, v in decided.items()
if abs(float(v["start"]) - start) <= a.tol), None)
auth = authored.get(slug, {})
title = auth.get("title") or opal.get(slug) or cat.get(slug)
if not title:
title = f"NEEDS_PLN:{slug}"
needs_pln.append(slug)
# An authored `score` resolves a gig slug that is not its filename
# (CosmicFest's "mafia" is mafia_sans_serif.tidal).
score = (REPO / auth["score"]) if auth.get("score") else find_score(slug)
if score is None:
unmatched.append(slug)
segs.append({
"track": n,
"title": title,
"start": round(start, 3),
"bpm": declared_bpm(score),
"score": str(score.relative_to(REPO)) if score else None,
"slug": slug,
"title_source": ("authored" if auth.get("title") else
"opal-release" if slug in opal else
"catalog" if slug in cat else "NEEDS_PLN"),
"title_prov": ({k: auth[k] for k in ("source", "locator", "as_of") if k in auth}
or None),
"start_source": f"ear:boundary#{hit}" if hit else ("origin" if start == 0.0 else "inferred"),
})
if hit:
verified[str(n)] = {
"start": round(float(decided[hit]["start"]), 3),
"source": "playhead",
"boundary": int(hit),
"note": decided[hit].get("note", ""),
}
Path(a.out_segments).write_text(json.dumps(segs, indent=1) + "\n")
Path(a.out_ear).write_text(json.dumps({
"gig": setlist["gig"],
"_provenance": [
f"Generated by setlist_to_segments.py from {Path(a.setlist).name}",
f"and {Path(a.boundaries).name}. DO NOT HAND-EDIT: edit those two and re-run.",
"`verified` holds ONLY starts PLN called on the playhead. Starts absent",
"here were inferred from his notes on SKIPPED boundaries and stay",
"`nominal` in apply_boundaries' output, so the two grades never merge.",
],
"verified": verified,
"edits": {},
"cut_from_release": {},
}, indent=2) + "\n")
print(f"{'#':>3} {'start':>9} {'bpm':>7} {'start_source':<20} {'title_source':<13} title")
for s in segs:
bpm = s["bpm"]
bpm = f"{bpm:g}" if isinstance(bpm, (int, float)) else ("knob" if bpm else "—")
print(f"{s['track']:>3} {s['start']:>9.2f} {bpm:>7} "
f"{s['start_source']:<20} {s['title_source']:<13} {s['title']}")
print(f"\n{len(segs)} tracks · {len(verified)} playhead-called starts · "
f"{len(segs) - len(verified)} not")
if needs_pln:
print(f"⚠ {len(needs_pln)} title(s) NEED PLN (no shipped release, no catalog entry): "
+ ", ".join(needs_pln))
if unmatched:
print(f"⚠ {len(unmatched)} slug(s) with no .tidal found: " + ", ".join(unmatched))
print(f"✓ {a.out_segments}\n✓ {a.out_ear}")
return 0
if __name__ == "__main__":
sys.exit(main())
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