Commit d97dda3f by PLN (Algolia)

feat(cosmicfest): the first stemless gig reaches segments — 14 tracks, 63.0 min

R-0 and C-0, the two moves the release board put ahead of everything else.

R-0. The CosmicFest master existed in exactly one place: ~/Downloads. It is now
in Prod/Cosmic26_master/ and mirrored to the freebox, sha256 7a197589…d671b
identical on both, and the Downloads original was removed only after both copies
were verified — a move, not a hopeful mv. feedback_freebox_ssot names the freebox
as the SSOT for audio, and an irreplaceable 63-minute gig in a scratch directory
was the cheapest large risk on the board. The spec records the naming trap that
nearly cost an hour: Prod/cosmicfest/ and Prod/cosmicfestv0.live1.* are the 2025
edition, files dated 2025-06-27/28, and nothing in them belongs to this release.

C-0. POSTPROD.md's rule is that a new gig is a copy of judge_specs/<gig>.json and
never a copy of a script — so the question was whether a gig with NO stems can
use the pipeline at all. Answered by reading the code, not by trying it and
hoping: apply_boundaries and render_release touch no stem key whatsoever, only
segments/master/variants/releaseRoot/releaseTag. The single tool that genuinely
needs stems is build_judge_set, which derives nominal segments from per-orbit
activity. That is the one hole, and setlist_to_segments.py fills it.

The converter turns judge_specs/<gig>_setlist_ear.json into the two inputs
apply_boundaries wants, and its real job is keeping two grades of truth apart.
The setlist silently mixes them: some starts are playhead calls PLN made while
listening, others were reconstructed from his notes on boundaries he SKIPPED.
Only the former go into `verified`, so apply_boundaries prints ear or nominal per
row and asserts every playhead call reached the output. An inferred edge can
therefore never be mistaken later for one he actually heard.

That distinction immediately corrected the board. It had named #10 and #12 as the
soft edges, taken from the recovered tracklist's unresolved list. The setlist ear
file supersedes that: ten starts (#2-#11) are playhead calls, #1 is the origin,
and the inferred ones are #12 LiveCode Parade, #13 Vague de CRIME and #14
Outro: La Dub Sirène. #10 PunkAChien was ear-called all along — "perfect cut at
48:27.4 into PunkAChien!". Three tracks to audition, not two, and a different
three. The board also claimed 11 decided boundaries; there are 10.

Titles resolve authored -> OPAL-release -> catalog -> NEEDS_PLN, and never by
title-casing a slug. The authoritative source for a shared track is what actually
SHIPPED: OPAL-26's ear-signed segments_v4.json joined to that gig's tracks.json
on performance order, since neither file alone holds both the score path and the
released title. Four tracks were in no shipped release and no catalog entry, so
the first run emitted them as NEEDS_PLN rather than inventing anything; PLN then
gave all four, and they live in judge_specs/cosmicfest-2026_titles.json with
value+source+locator+date per feedback_metadata_provenance. The same file carries
a `score` alias, because the gig slug "mafia" is mafia_sans_serif.tidal — and
that alias then cross-checked itself, yielding a declared 160 BPM that matches
OPAL's Mafia exactly. Outro: La Dub Sirène has no .tidal at all, being a played
outro rather than a composed track, which is also why the catalog can never hold
its title: the catalog is keyed by score path.

`album` is deliberately absent from the spec. CosmicFest 2026 has no page under
content/lives/2026/, so there is no canonical set title to copy, and inventing
one would be exactly what feedback_metadata_vs_mastering forbids.

The source measures better than "raw stage mic with FOH effects" suggests:
I = -25.2 LUFS, true peak -5.1 dBFS, LRA 11.1 LU over the full 62:59. Not
clipped, not crushed, just quiet — reaching -14 LUFS needs about +11 dB and
therefore limiting, but that is a gain problem rather than a repair job. Recorded
in the spec's sourceProvenance so the next pass starts from the number.

apply_boundaries output: 14 tracks, 63.0 min kept of a 63.0 min master, zero
gaps, all assertions green. variants still point at mastered files that do not
exist; the mix is the next step and render_release will say MISSING until it runs.
parent 39ca4def
......@@ -1147,7 +1147,7 @@ club mix, per-track album split}.
|---|---|---|
| source | 12 orbit stems + master, 48 k/24-bit | **ONE stage mic MP3**, 320 k/44.1, 62:59 |
| source location | `Prod/Opal26_master/` | **`~/Downloads/ZOOM0067.MP3` — single copy** |
| boundaries | ✅ ear-signed 2026-08-16 | ✅ ear pass 2026-08-29 (11 decided + 8 skipped) |
| boundaries | ✅ ear-signed 2026-08-16 | ✅ ear pass 2026-08-29 (**10** decided + 8 skipped) |
| setlist | 14 tracks (Desire cut) | 14 tracks, `cosmicfest-2026_setlist_ear.json` |
| gig spec | ✅ `judge_specs/opal26.json` | 🔴 **DOES NOT EXIST** |
| segments | ✅ `segments_v4.json` | 🔴 none |
......@@ -1161,12 +1161,14 @@ club mix, per-track album split}.
mix is done and ear-reviewed for OPAL *only*. CosmicFest has had zero mastering work —
its ear pass covered *where the cuts are*, never *how it sounds*.
### 🔴 R-0 · The CosmicFest master has exactly one copy, in Downloads
### ✅ R-0 DONE 2026-09-01 · the CosmicFest master is now backed up
`~/Downloads/ZOOM0067.MP3`, 151 MB, 2026-08-28. Not on the freebox, not in `Prod/`.
Per `feedback_freebox_ssot` the freebox is the SSOT for audio; an irreplaceable gig
recording sitting in a scratch directory is the single highest-risk item on this
board and the cheapest to fix. **Do this before any processing.**
Was `~/Downloads/ZOOM0067.MP3`, 144 MB, a single copy of an irreplaceable gig.
Now two verified copies, `sha256 7a197589…d671b` identical on both, Downloads
original removed only after both were confirmed:
/home/pln/Work/Sound/Prod/Cosmic26_master/ZOOM0067.MP3
/mnt/freebox/PLN/Work/Sound/Prod/Cosmic26_master/ZOOM0067.MP3
(Note the naming trap: `Prod/cosmicfest/` and `Prod/cosmicfestv0.live1.*` are the
**2025** edition — files dated 2025-06-27/28. Nothing there belongs to this release.)
......@@ -1186,7 +1188,22 @@ The 80/20 is therefore genuinely reachable: gain-stage, broadband/room EQ,
harshness control, limit. **No premix is possible** — one stereo file, no stems, so
none of OPAL's per-orbit surgery (the d4 trim, the d5 cut) has an analogue here.
### 🔑 KEYSTONE · C-0 — write `judge_specs/cosmicfest-2026.json`
### ✅ KEYSTONE C-0 DONE 2026-09-01 — `judge_specs/cosmicfest-2026.json` exists
**and the stemless path is VERIFIED, not assumed**
`apply_boundaries` ran clean on it: 14 tracks, 63.0 min of a 63.0 min master, no
gaps, every assertion passed, `segments_v1.json` written. Confirmed by reading the
code rather than by trying: `apply_boundaries` and `render_release` touch **no** stem
key at all — only `segments`, `master`, `variants`, `releaseRoot`, `releaseTag`. The
one tool that genuinely needs stems is `build_judge_set`, which is precisely why the
nominal segments came from the new `setlist_to_segments.py` instead.
That converter is the reusable part: it turns a `_setlist_ear.json` into nominal
segments plus an `apply_boundaries`-schema ear file, and it keeps the two grades of
truth apart so a playhead call and an inferred edge can never be confused later.
Titles resolve authored → OPAL-release → catalog → `NEEDS_PLN`, never slug-cased.
Original framing, kept because it is still the rule for the NEXT stemless gig:
`POSTPROD.md` is explicit: *"A new gig is a copy of `judge_specs/<gig>.json` — never a
copy of a script."* That file is the **only** thing standing between CosmicFest and
......@@ -1210,9 +1227,14 @@ not assume**, per `feedback_verify_the_plumbing`.
- **D-3 · Is "Outro Dub Siren" (#14, 103 s) an album track?** PLN's note was a laugh —
*"pure dubsiren noise and fun ahahah it could be called 'Outro Dub Siren'"*. It
clearly belongs in the club mix. Whether it ships as a numbered track is a call.
- **D-4 · Soft edges.** CosmicFest #10 `punkachien` and #12 `livecode_parade` were
inferred from adjacent skip notes, not called on the playhead. They are the two
boundaries most likely to need an ear before rendering.
- **D-4 · Soft edges — CORRECTED 2026-09-01.** The first draft of this line named
#10 and #12 from the *recovered tracklist's* unresolved list. Checked against
`setlist_ear.json`, which supersedes it, the real split is: **10 starts are
playhead calls** (#2-#11), #1 is the origin, and **#12 `LiveCode Parade`,
#13 `Vague de CRIME`, #14 `Outro: La Dub Sirène` were INFERRED** from notes on
skipped boundaries and have never been heard as cuts. #10 `PunkAChien` *was*
ear-called (*"perfect cut at 48:27.4 into PunkAChien!"*). Those **three** are the
audition list before any render is final.
- **D-5 · Rights gate the album split, not the club mix.** `rights_ledger.json`
re-measured: **103 banks — 43 third_party, 31 unknown, 19 dirt_samples, 10 cleared**.
PLN's rule stands: *"cant push the single tracks when samples hit, but full mix might
......
{
"_comment": [
"Gig spec for COSMICFEST 2026-08-23. 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-23",
"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",
"club": "/home/pln/Work/Sound/Prod/Cosmic26_master/Cosmic26_v1_club.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."
}
{
"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": {}
}
{
"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