| 7 |
7 |
|
That tree is gitignored and has to be rebuilt per machine. This script fetches
|
| 8 |
8 |
|
the source datasets and maps them into that layout.
|
| 9 |
9 |
|
|
| 10 |
|
- |
Only CC-BY datasets are enabled by default. The large ones are opt-in because
|
| 11 |
|
- |
they are tens of gigabytes and, for NSynth, 16 kHz mono, which skews per-file
|
| 12 |
|
- |
decode timings away from what a real 44.1/48 kHz library costs.
|
|
10 |
+ |
The large datasets are opt-in because they are tens of gigabytes and, for
|
|
11 |
+ |
NSynth, 16 kHz mono, which skews per-file decode timings away from what a real
|
|
12 |
+ |
44.1/48 kHz library costs.
|
|
13 |
+ |
|
|
14 |
+ |
Nothing here is licence-clean as a whole. Two of the five mix per-sound
|
|
15 |
+ |
licences, including CC-BY-NC, so the registry's `license` field describes the
|
|
16 |
+ |
mix rather than claiming one label. See summarize_fsl10k_licenses.
|
| 13 |
17 |
|
|
| 14 |
18 |
|
./scripts/corpus.py --list
|
| 15 |
19 |
|
./scripts/corpus.py --dest /media/max/T9/af-corpus
|
| 34 |
38 |
|
|
| 35 |
39 |
|
# Dataset registry.
|
| 36 |
40 |
|
#
|
| 37 |
|
- |
# `default` marks the ones that run without --datasets. The two defaults are
|
| 38 |
|
- |
# both CC-BY 4.0, which matters here: docs/ml_classifier.md keeps a trained
|
| 39 |
|
- |
# model out of the binary specifically to avoid training-data copyright
|
| 40 |
|
- |
# surface, so anything that could feed a shipped model needs a clean license.
|
|
41 |
+ |
# `default` marks the ones that run without --datasets. It is a size-and-time
|
|
42 |
+ |
# judgement and says nothing about licence: fsl10k is a default and a sixth of
|
|
43 |
+ |
# it is CC-BY-NC. The two questions were conflated here until 2026-08-08, when
|
|
44 |
+ |
# the entry still claimed a flat "CC-BY 4.0" for a corpus with four licences in
|
|
45 |
+ |
# it.
|
|
46 |
+ |
#
|
|
47 |
+ |
# Nothing derived from any of this ships today. The bundled `.afcl` layer that
|
|
48 |
+ |
# would have was retired on 2026-08-08 (docs/ml_classifier.md, "What ships in
|
|
49 |
+ |
# the binary, and under what licence"), so every dataset here is measurement
|
|
50 |
+ |
# material only, which the NC clips are fine for. The licence is recorded
|
|
51 |
+ |
# accurately anyway: that position has already flipped once, and reconstructing
|
|
52 |
+ |
# provenance after the fact is what this file exists to avoid.
|
| 41 |
53 |
|
DATASETS = {
|
| 42 |
54 |
|
"reverb-drums": {
|
| 43 |
55 |
|
"default": True,
|
| 54 |
66 |
|
"default": True,
|
| 55 |
67 |
|
"size": "8.8 GB",
|
| 56 |
68 |
|
"count": "9,455 loops",
|
| 57 |
|
- |
"license": "CC-BY 4.0",
|
|
69 |
+ |
"license": "per sound: CC0, CC-BY, CC-BY-NC, Sampling+",
|
| 58 |
70 |
|
"url": "https://zenodo.org/api/records/3967852/files/FSL10K.zip/content",
|
| 59 |
71 |
|
"archive": "FSL10K.zip",
|
| 60 |
72 |
|
"extra": {
|
| 61 |
73 |
|
"annotations.zip": "https://zenodo.org/api/records/3967852/files/annotations.zip/content"
|
| 62 |
74 |
|
},
|
| 63 |
75 |
|
"desc": "Freesound Loop Dataset. The only source here with ground-truth "
|
| 64 |
|
- |
"tempo and key, so it is what BPM/key accuracy can be scored against.",
|
|
76 |
+ |
"tempo and key, so it is what BPM/key accuracy can be scored against. "
|
|
77 |
+ |
"Zenodo publishes it as CC-BY; the sounds are not. 1,436 of 9,493 are "
|
|
78 |
+ |
"NC or Sampling+: fine for measurement, not for anything that ships. "
|
|
79 |
+ |
"Select on metadata.json, never on this entry.",
|
| 65 |
80 |
|
},
|
| 66 |
81 |
|
"nsynth": {
|
| 67 |
82 |
|
"default": False,
|
| 95 |
110 |
|
},
|
| 96 |
111 |
|
}
|
| 97 |
112 |
|
|
|
113 |
+ |
# Freesound licence URLs as they appear in FSL10K's metadata.json, mapped to a
|
|
114 |
+ |
# short name and whether a derived work under that licence could ship.
|
|
115 |
+ |
#
|
|
116 |
+ |
# Sampling+ is counted as cannot-ship rather than argued about: it permits
|
|
117 |
+ |
# sampling into a new work but bars advertising use and verbatim
|
|
118 |
+ |
# redistribution, which is ambiguous for a corpus-derived artifact and worth
|
|
119 |
+ |
# 221 sounds out of 9,493.
|
|
120 |
+ |
FSL10K_LICENSES = {
|
|
121 |
+ |
"http://creativecommons.org/publicdomain/zero/1.0/": ("CC0 1.0", True),
|
|
122 |
+ |
"http://creativecommons.org/licenses/by/3.0/": ("CC-BY 3.0", True),
|
|
123 |
+ |
"http://creativecommons.org/licenses/by-nc/3.0/": ("CC-BY-NC 3.0", False),
|
|
124 |
+ |
"http://creativecommons.org/licenses/sampling+/1.0/": ("Sampling+ 1.0", False),
|
|
125 |
+ |
}
|
|
126 |
+ |
|
|
127 |
+ |
|
|
128 |
+ |
def summarize_fsl10k_licenses(raw: Path) -> dict | None:
|
|
129 |
+ |
"""Count FSL10K's per-sound licences off its metadata.json.
|
|
130 |
+ |
|
|
131 |
+ |
The dataset-level label cannot answer the licence question for this corpus
|
|
132 |
+ |
and the registry no longer pretends it can, so the real mix is measured on
|
|
133 |
+ |
every run and recorded in the manifest. Anything that later selects sounds
|
|
134 |
+ |
out of FSL10K reads this, or reads metadata.json itself; the point is that
|
|
135 |
+ |
it does not read a single label off the registry entry.
|
|
136 |
+ |
|
|
137 |
+ |
An unrecognised licence URL counts as cannot-ship and is named in the
|
|
138 |
+ |
result. Defaulting the other way would let a new Freesound licence into a
|
|
139 |
+ |
shippable set silently, which is the exact failure this function exists to
|
|
140 |
+ |
close.
|
|
141 |
+ |
|
|
142 |
+ |
Returns None when the metadata is not there -- --no-build runs and partial
|
|
143 |
+ |
extractions are normal, and a missing count is better than a wrong one.
|
|
144 |
+ |
"""
|
|
145 |
+ |
meta = raw / "metadata.json"
|
|
146 |
+ |
if not meta.is_file():
|
|
147 |
+ |
return None
|
|
148 |
+ |
try:
|
|
149 |
+ |
sounds = json.loads(meta.read_text())
|
|
150 |
+ |
except (OSError, json.JSONDecodeError) as e:
|
|
151 |
+ |
print(f" warning: cannot read {meta} ({e}); licence mix not recorded")
|
|
152 |
+ |
return None
|
|
153 |
+ |
if not isinstance(sounds, dict):
|
|
154 |
+ |
return None
|
|
155 |
+ |
|
|
156 |
+ |
counts: dict[str, int] = {}
|
|
157 |
+ |
unrecognised: dict[str, int] = {}
|
|
158 |
+ |
shippable = 0
|
|
159 |
+ |
for sound in sounds.values():
|
|
160 |
+ |
url = sound.get("license") if isinstance(sound, dict) else None
|
|
161 |
+ |
name, ok = FSL10K_LICENSES.get(url, (None, False))
|
|
162 |
+ |
if name is None:
|
|
163 |
+ |
name = str(url)
|
|
164 |
+ |
unrecognised[name] = unrecognised.get(name, 0) + 1
|
|
165 |
+ |
counts[name] = counts.get(name, 0) + 1
|
|
166 |
+ |
shippable += ok
|
|
167 |
+ |
|
|
168 |
+ |
summary = {
|
|
169 |
+ |
"total": len(sounds),
|
|
170 |
+ |
"by_license": dict(sorted(counts.items())),
|
|
171 |
+ |
"shippable": shippable,
|
|
172 |
+ |
"excluded": len(sounds) - shippable,
|
|
173 |
+ |
"source": "metadata.json, per sound",
|
|
174 |
+ |
}
|
|
175 |
+ |
if unrecognised:
|
|
176 |
+ |
summary["unrecognised"] = dict(sorted(unrecognised.items()))
|
|
177 |
+ |
return summary
|
|
178 |
+ |
|
|
179 |
+ |
|
| 98 |
180 |
|
# Filename/dirname keyword -> bench training class.
|
| 99 |
181 |
|
#
|
| 100 |
182 |
|
# Ordered, first match wins. Order is load-bearing: "loop" has to be tested
|
| 371 |
453 |
|
def main() -> None:
|
| 372 |
454 |
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 373 |
455 |
|
ap.add_argument("--dest", type=Path, default=Path("/media/max/T9/af-corpus"))
|
| 374 |
|
- |
ap.add_argument("--datasets", help="comma-separated; default = the CC-BY defaults")
|
|
456 |
+ |
ap.add_argument("--datasets", help="comma-separated; default = the small ones")
|
| 375 |
457 |
|
ap.add_argument("--list", action="store_true", help="show the registry and exit")
|
| 376 |
458 |
|
ap.add_argument("--no-build", action="store_true", help="fetch and extract only")
|
| 377 |
459 |
|
ap.add_argument("--formats-count", type=int, default=120,
|
| 379 |
461 |
|
args = ap.parse_args()
|
| 380 |
462 |
|
|
| 381 |
463 |
|
if args.list:
|
| 382 |
|
- |
print(f"{'dataset':<14} {'size':>8} {'default':<8} {'license':<38} count")
|
|
464 |
+ |
print(f"{'dataset':<14} {'size':>8} {'default':<8} {'license':<46} count")
|
| 383 |
465 |
|
for name, d in DATASETS.items():
|
| 384 |
466 |
|
print(
|
| 385 |
467 |
|
f"{name:<14} {d['size']:>8} {str(d['default']):<8} "
|
| 386 |
|
- |
f"{d['license']:<38} {d['count']}"
|
|
468 |
+ |
f"{d['license']:<46} {d['count']}"
|
| 387 |
469 |
|
)
|
| 388 |
470 |
|
print(f"{'':>14} {d['desc']}")
|
| 389 |
471 |
|
return
|
| 419 |
501 |
|
download(extra_url, extra_path)
|
| 420 |
502 |
|
if extra_path.suffix.lower() in (".zip", ".7z", ".gz"):
|
| 421 |
503 |
|
extract(extra_path, raw / name, marker=raw / name / Path(extra_name).stem)
|
| 422 |
|
- |
fetched.append({"name": name, "license": d["license"], "source": d["url"]})
|
|
504 |
+ |
|
|
505 |
+ |
entry = {"name": name, "license": d["license"], "source": d["url"]}
|
|
506 |
+ |
# FSL10K is the one dataset here shipping a per-sound licence field, so
|
|
507 |
+ |
# it is the one where the manifest can carry something better than the
|
|
508 |
+ |
# registry's prose. Measured rather than copied: the registry entry is
|
|
509 |
+ |
# what was wrong in the first place.
|
|
510 |
+ |
if name == "fsl10k":
|
|
511 |
+ |
summary = summarize_fsl10k_licenses(raw / name)
|
|
512 |
+ |
if summary is not None:
|
|
513 |
+ |
entry["per_sound_licenses"] = summary
|
|
514 |
+ |
print(
|
|
515 |
+ |
f" licences: {summary['shippable']} shippable, "
|
|
516 |
+ |
f"{summary['excluded']} excluded, of {summary['total']}"
|
|
517 |
+ |
)
|
|
518 |
+ |
for lic, n in summary["by_license"].items():
|
|
519 |
+ |
print(f" {lic:<16} {n}")
|
|
520 |
+ |
fetched.append(entry)
|
| 423 |
521 |
|
|
| 424 |
522 |
|
manifest["datasets"] = merge_datasets(manifest.get("datasets", []), fetched)
|
| 425 |
523 |
|
|
| 430 |
528 |
|
print(f" {k:<14} {v}")
|
| 431 |
529 |
|
manifest["training_counts"] = counts
|
| 432 |
530 |
|
# Names the dataset the labels came from, which is what anything
|
| 433 |
|
- |
# shipping those labels has to credit. The bundled .afcl generator
|
| 434 |
|
- |
# reads this key and refuses to build without it.
|
|
531 |
+ |
# shipping those labels would have to credit. Nothing reads it as of
|
|
532 |
+ |
# 2026-08-08: the bundled .afcl generator that required it was retired
|
|
533 |
+ |
# with the layer itself. Kept because it costs a line and the credit is
|
|
534 |
+ |
# unreconstructable once the corpus is rebuilt with other datasets in
|
|
535 |
+ |
# it, which is how it got lost the first time (see load_manifest).
|
| 435 |
536 |
|
manifest["training_source"] = TRAINING_DATASET
|
| 436 |
537 |
|
|
| 437 |
538 |
|
# Runs off the built layout, not off a dataset, so it is gated on the loops
|