Commit b2c90154 by PLN (Algolia)

fix(foundry): the report section I added never rendered, and hits vs drums was a false positive

Two bugs in one small feature, both mine.

The 'worth a second listen' table was appended by a str.replace whose anchor did not
match, and I had not asserted on that one — so the code was present, the function
computed 39 rows, and the report silently shipped without them. Every other patch in this
file asserts its anchor; this is why.

And the comparison was at the wrong level. A `hits` kit IS percussion, so CLAP hearing
'a kick drum' in one is agreement, not contradiction — comparing family names instead of
their underlying stems reported every kick and snare in the pack as suspicious. Same for
fx/tonal/vox, which all map to `other` and genuinely cannot contradict each other.

Also adds --report-only, which rebuilds the report and cheatsheet from the manifest. The
manifest is the record and those are views of it, so anything that legitimately changes
the audio after a cut (restage staging a kit and re-grading it) leaves them describing
numbers that are no longer true — and re-cutting a whole pack to refresh a markdown table
would be absurd.
parent 34e01673
......@@ -548,7 +548,18 @@ def clap_disagreements(cuts: list[CutLoop]) -> list[tuple]:
out = []
for c in cuts:
cf = clap_family(c.tags)
if cf and cf != c.family and not (cf == "tonal" and c.family in ("fx", "vox")):
if not cf:
continue
# compare at the STEM level, not the family: a `hits` kit IS percussion, so CLAP
# hearing "a kick drum" in it agrees. And `fx`/`tonal`/`vox` all map to `other`
# and are genuinely not separable, so they never contradict each other.
want = R.FAMILIES.get(c.family, {}).get("stem")
got = R.FAMILIES.get(cf, {}).get("stem")
if want == got:
continue
soft = {"fx", "tonal", "vox"}
if c.family in soft and cf in soft:
continue
inst = c.tags["instrument"][0]
out.append((c.kit, c.name, c.stem_role, c.family, cf, inst[0], inst[1]))
return out
......@@ -589,6 +600,20 @@ def report_md(results: list[dict], cuts: list[CutLoop]) -> str:
f"{c.bars or '—'} | {c.bpm:.0f} | {c.dur_s:.2f}s | "
f"`{c.stem_role}` | {tg} |")
lines.append("")
dis = clap_disagreements(cuts)
if dis:
lines += ["## Worth a second listen", "",
"CLAP's instrument tag contradicts the kit these landed in. Nothing is "
"re-filed on this basis — CLAP is the one signal here that can separate "
"a sung line from a keyboard, so it is used to say what to audition "
"first, not to decide.", "",
"| kit | file | from stem | filed as | CLAP hears | conf |",
"|---|---|---|---|---|--:|"]
for kit, name, role, fam, cf, phrase, conf in dis:
lines.append(f"| `{kit}` | `{name}` | `{role}` | {fam} | {cf} "
f"({phrase}) | {conf:.2f} |")
lines.append("")
return "\n".join(lines)
......@@ -619,7 +644,8 @@ def main(argv=None) -> int:
import argparse
ap = argparse.ArgumentParser(prog="stempack", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("pack", type=Path, help="folder of per-track stem folders")
ap.add_argument("pack", type=Path, nargs="?",
help="folder of per-track stem folders (omit with --report-only)")
ap.add_argument("--tracks", nargs="*", default=None, help="track folders (default: all)")
ap.add_argument("--prefix", default="fred", help="kit name prefix")
ap.add_argument("--per-stem", type=int, default=6, help="candidates generated per stem")
......@@ -631,10 +657,15 @@ def main(argv=None) -> int:
ap.add_argument("--no-clap", action="store_true")
ap.add_argument("--no-link", action="store_true")
ap.add_argument("--dry-run", action="store_true", help="analyze and report, write nothing")
ap.add_argument("--report-only", action="store_true",
help="rebuild the report/cheatsheet from an existing --out, no cutting")
ap.add_argument("-j", "--jobs", type=int, default=3,
help="tracks analyzed in parallel (each pins one core)")
a = ap.parse_args(argv)
if a.report_only:
return _report_only(a.out or Path.cwd())
names = a.tracks or sorted(d.name for d in a.pack.iterdir() if d.is_dir())
todo = []
for nm in names:
......@@ -679,6 +710,33 @@ def main(argv=None) -> int:
return 0
def _report_only(out: Path) -> int:
"""Rebuild `fred_kits.{md,tidal}` from the manifest already in `out`.
The manifest is the record; the report and the cheatsheet are views of it. Anything
that legitimately changes the audio after a cut — `restage.py` staging a kit's level
and re-grading it — leaves those views describing numbers that are no longer true, and
re-cutting an entire pack to refresh a markdown table would be absurd. This also picks
up report sections added since the cut was made.
"""
cuts_p = out / "fred_kits.json"
if not cuts_p.exists():
raise SystemExit(f"no manifest at {cuts_p}")
cuts = [CutLoop(**c) for c in json.loads(cuts_p.read_text())]
results = []
tj = out / "tracks.json"
if tj.exists():
results = json.loads(tj.read_text())
else: # fan-out runs leave them per child
for f in sorted((out / "_tracks").glob("*/tracks.json")):
results += json.loads(f.read_text())
(out / "fred_kits.md").write_text(report_md(results, cuts))
(out / "fred_kits.tidal").write_text(cheatsheet(cuts))
print(f"rebuilt report + cheatsheet for {len(cuts)} samples in "
f"{len({c.kit for c in cuts})} kits from {cuts_p.name}")
return 0
def _fan_out(todo: list[Path], a, out: Path) -> tuple[list, list]:
"""Run one `--jobs 1` subprocess per track, then merge their results.
......
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