max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
11 files changed,
+94 insertions,
-30 deletions
| @@ -7297,10 +7297,6 @@ | |||
| 7297 | 7297 | "winnow 1.0.4", | |
| 7298 | 7298 | ] | |
| 7299 | 7299 | ||
| 7300 | - | [[patch.unused]] | |
| 7301 | - | name = "supernote-push" | |
| 7302 | - | version = "0.1.0" | |
| 7303 | - | ||
| 7304 | 7300 | [[patch.unused]] | |
| 7305 | 7301 | name = "kberg" | |
| 7306 | 7302 | version = "0.1.0" | |
| @@ -7309,6 +7305,10 @@ | |||
| 7309 | 7305 | name = "painhours" | |
| 7310 | 7306 | version = "0.1.0" | |
| 7311 | 7307 | ||
| 7308 | + | [[patch.unused]] | |
| 7309 | + | name = "supernote-push" | |
| 7310 | + | version = "0.1.0" | |
| 7311 | + | ||
| 7312 | 7312 | [[patch.unused]] | |
| 7313 | 7313 | name = "docengine" | |
| 7314 | 7314 | version = "0.4.0" |
| @@ -127,27 +127,44 @@ | |||
| 127 | 127 | Today: no model and no third-party data. Every number the classifier uses is either | |
| 128 | 128 | computed from the user's audio at analysis time or written by hand as a threshold. | |
| 129 | 129 | ||
| 130 | - | That is changing, and the change is deliberate. Layer A ships empty and Layer B needs the | |
| 131 | - | user's own labels, so a new library starts with nothing to match against. The intended fix | |
| 132 | - | is a bundled official classifier layer: a `kind = "official"` `.afcl` built from the | |
| 133 | - | Reverb Drum Machines corpus, imported on first run and weighted below anything the user | |
| 130 | + | Layer A ships empty and Layer B needs the user's own labels, so a new library starts with | |
| 131 | + | nothing to match against. The intended fix is a bundled official classifier layer: a | |
| 132 | + | `kind = "official"` `.afcl`, imported on first run and weighted below anything the user | |
| 134 | 133 | labels themselves. | |
| 135 | 134 | ||
| 136 | - | That layer will put data derived from a third-party dataset in the binary. The terms it | |
| 137 | - | rests on: | |
| 135 | + | That layer puts data derived from a third-party dataset in the binary. The terms it rests | |
| 136 | + | on: | |
| 138 | 137 | ||
| 139 | 138 | - The corpus is CC-BY 4.0, which permits derivatives. | |
| 140 | 139 | - Attribution travels with the artifact in `AfclManifest.license_note`, which the UI | |
| 141 | - | surfaces. | |
| 140 | + | surfaces. It credits the dataset the labels came from (`training_source` in the corpus | |
| 141 | + | manifest), not everything the corpus holds. | |
| 142 | 142 | - An `.afcl` carries no audio. Each exemplar is the 35-number feature vector, 26 of which | |
| 143 | 143 | are MFCC means and variances, plus its labels. The vector is not reversible to audio. | |
| 144 | 144 | ||
| 145 | - | The corpus is drum-machine one-shots, so an official layer built from it has no exemplars | |
| 146 | - | for bass, vocal, pad, synth, texture, ambience, or foley. Those categories fall back to | |
| 147 | - | the user's own labels. | |
| 145 | + | ### Status: built, not shipped | |
| 148 | 146 | ||
| 149 | - | No `.afcl` is in the repo yet. The import side is complete and gated on `feat_version`; | |
| 150 | - | nothing produces an official layer, and `ExportOptions` has no `kind` field to mark one. | |
| 147 | + | The whole path exists and is tested. `scripts/corpus.py` lays out the labelled corpus, | |
| 148 | + | `cargo run --release -p audiofiles-bench -- afcl` turns it into a layer, | |
| 149 | + | `crates/audiofiles-core/assets/official.afcl` is the checked-in artifact, and | |
| 150 | + | `analysis::official` embeds it and imports it once per vault. | |
| 151 | + | ||
| 152 | + | It is behind the **`bundled-layer` cargo feature, which is off**, so no shipped build | |
| 153 | + | carries it. Two things have to land before that flips: | |
| 154 | + | ||
| 155 | + | 1. **The layer is drums only.** Built from Reverb Drum Machines one-shots, so it has | |
| 156 | + | exemplars for clap, cymbal, hihat, kick, snare, tom and percussion and none for bass, | |
| 157 | + | vocal, pad, synth, texture, ambience or foley. It would answer for a bass with the | |
| 158 | + | nearest drum it knows. NSynth (CC-BY 4.0, ground-truth `instrument_family_str`) is the | |
| 159 | + | candidate source for the pitched families; it is 16 kHz mono against the drums' 44.1 | |
| 160 | + | kHz, and five of the 35 features are Nyquist-bounded, so the two cannot simply be mixed | |
| 161 | + | without the classifier learning sample rate as a proxy for instrument. | |
| 162 | + | 2. **Nothing measures whether the layer is any good.** The accuracy bench scores BPM and | |
| 163 | + | key only; class accuracy went away with the retired sample-class label. A default | |
| 164 | + | classifier that is confidently wrong is worse than no default, so shipping needs a | |
| 165 | + | held-out measurement and a threshold agreed before the number is read. | |
| 166 | + | ||
| 167 | + | Both are tracked in the audiofiles GoingsOn project. | |
| 151 | 168 | ||
| 152 | 169 | ## Feature vector | |
| 153 | 170 |
| @@ -7,6 +7,9 @@ | |||
| 7 | 7 | [features] | |
| 8 | 8 | default = ["device-profiles"] | |
| 9 | 9 | device-profiles = ["dep:audiofiles-rhai", "dep:rayon"] | |
| 10 | + | # Seed a fresh vault with the bundled official classifier layer. Off; see | |
| 11 | + | # audiofiles-core's feature of the same name for why. | |
| 12 | + | bundled-layer = ["audiofiles-core/bundled-layer"] | |
| 10 | 13 | ||
| 11 | 14 | [dependencies] | |
| 12 | 15 | audiofiles-core = { workspace = true } |
| @@ -7,6 +7,13 @@ | |||
| 7 | 7 | [features] | |
| 8 | 8 | default = ["analysis"] | |
| 9 | 9 | analysis = ["dep:bs1770", "dep:realfft"] | |
| 10 | + | # The bundled official classifier layer. OFF: the layer is drums-only and | |
| 11 | + | # unmeasured, and a default classifier that is wrong on a user's library is worse | |
| 12 | + | # than none, because it puts wrong tags on their samples with the app's authority | |
| 13 | + | # behind it. Off also means the 1.1 MiB embedded blob is not compiled in. | |
| 14 | + | # Turn back on when the corpus is broadened and the layer measures well enough to | |
| 15 | + | # ship; see the audiofiles GoingsOn project. | |
| 16 | + | bundled-layer = [] | |
| 10 | 17 | ||
| 11 | 18 | [dependencies] | |
| 12 | 19 | rusqlite = { workspace = true } |
| @@ -110,6 +110,12 @@ | |||
| 110 | 110 | /// removed it on. The value is the version rather than a bare flag so a | |
| 111 | 111 | /// later `FEATURE_VERSION` bump can tell a stale bundled layer from a | |
| 112 | 112 | /// current one without a second key. | |
| 113 | + | /// | |
| 114 | + | /// Only written by builds with the `bundled-layer` feature, which is off, so | |
| 115 | + | /// nothing writes it today. Declared unconditionally anyway: this registry is | |
| 116 | + | /// the sync contract, and unknown keys fail closed on import, so gating the | |
| 117 | + | /// row behind the feature would have a default build silently drop this key | |
| 118 | + | /// from a peer that has the feature on. | |
| 113 | 119 | OfficialLayerImported => "official_layer_imported": Synced, | |
| 114 | 120 | ||
| 115 | 121 | // --- Local: local paths and safety gates. Never sync these. --- |
| @@ -7,13 +7,14 @@ | |||
| 7 | 7 | /// on-disk SQLite database. Each test gets full isolation. | |
| 8 | 8 | fn make_state() -> (BrowserState, tempfile::TempDir) { | |
| 9 | 9 | let dir = tempfile::TempDir::new().unwrap(); | |
| 10 | - | // Skip the bundled official layer. A real fresh vault imports it (that is | |
| 11 | - | // what `bundled_official_layer_is_imported_on_first_run` covers), but here it | |
| 12 | - | // would put 1,049 exemplars and a second layer into every test library, so | |
| 13 | - | // every layer-count and exemplar-count assertion would be measuring the | |
| 14 | - | // bundle rather than what the test set up. Pre-setting the marker on the | |
| 15 | - | // vault's database is what `official::ensure_imported` reads, so opening it | |
| 16 | - | // once before the backend does is enough. | |
| 10 | + | // Skip the bundled official layer. A `bundled-layer` build imports it into | |
| 11 | + | // every fresh vault (that is what `bundled_official_layer_is_imported_on_first_run` | |
| 12 | + | // covers), which here would put 1,049 exemplars and a second layer into every | |
| 13 | + | // test library, so every layer-count and exemplar-count assertion would be | |
| 14 | + | // measuring the bundle rather than what the test set up. Pre-setting the | |
| 15 | + | // marker on the vault's database is what `official::ensure_imported` reads, | |
| 16 | + | // so opening it once before the backend does is enough. Unconditional so the | |
| 17 | + | // suite behaves identically with the feature on or off. | |
| 17 | 18 | { | |
| 18 | 19 | let db = audiofiles_core::db::Database::open(dir.path().join("audiofiles.db")).unwrap(); | |
| 19 | 20 | db.set_config( | |
| @@ -28,6 +29,7 @@ | |||
| 28 | 29 | } | |
| 29 | 30 | ||
| 30 | 31 | /// A state whose vault gets the real first-run treatment, bundled layer included. | |
| 32 | + | #[cfg(feature = "bundled-layer")] | |
| 31 | 33 | fn make_state_first_run() -> (BrowserState, tempfile::TempDir) { | |
| 32 | 34 | let dir = tempfile::TempDir::new().unwrap(); | |
| 33 | 35 | let shared = Arc::new(SharedState::new()); | |
| @@ -2260,6 +2262,7 @@ | |||
| 2260 | 2262 | } | |
| 2261 | 2263 | ||
| 2262 | 2264 | #[test] | |
| 2265 | + | #[cfg(feature = "bundled-layer")] | |
| 2263 | 2266 | fn bundled_official_layer_is_imported_on_first_run() { | |
| 2264 | 2267 | // Covers the wiring rather than the mechanism (`analysis::official` has | |
| 2265 | 2268 | // the mechanism's tests): opening a fresh vault is what triggers it, and |
| @@ -1067,7 +1067,13 @@ | |||
| 1067 | 1067 | // user to re-import one would send them looking for | |
| 1068 | 1068 | // something that does not exist. It is imported once and | |
| 1069 | 1069 | // stays removed on every device. | |
| 1070 | - | let warning = if layer.kind == "official" { | |
| 1070 | + | // | |
| 1071 | + | // Keyed on `source`, not on `kind == "official"`: kind is | |
| 1072 | + | // free text read straight out of an imported file's | |
| 1073 | + | // manifest, so a third-party .afcl can declare itself | |
| 1074 | + | // official, and that one DOES come back from its file. | |
| 1075 | + | // `source` is set by us at import and identifies the bundle. | |
| 1076 | + | let warning = if layer.source.as_deref() == Some("bundled") { | |
| 1071 | 1077 | "Removing deletes this layer's exemplars. The bundled layer is added \ | |
| 1072 | 1078 | once, so this will not come back." | |
| 1073 | 1079 | } else { |
| @@ -41,6 +41,16 @@ | |||
| 41 | 41 | /// imported exemplars fill gaps and break ties without overriding the user's labels. | |
| 42 | 42 | pub const DEFAULT_IMPORT_WEIGHT: f64 = 0.5; | |
| 43 | 43 | ||
| 44 | + | /// `source` recorded on the bundled layer's row, and the only reliable way to tell it | |
| 45 | + | /// from a file the user imported. | |
| 46 | + | /// | |
| 47 | + | /// Not a path: nothing on disk corresponds to it. Lives here rather than beside the | |
| 48 | + | /// bundled layer itself ([`super::official`]) because that module is behind the | |
| 49 | + | /// `bundled-layer` feature while readers of this value are not, and because `kind` is | |
| 50 | + | /// the wrong test for the same question: it is free text out of an imported file's | |
| 51 | + | /// manifest, so any `.afcl` can call itself `official`. | |
| 52 | + | pub const BUNDLED_SOURCE: &str = "bundled"; | |
| 53 | + | ||
| 44 | 54 | // File format | |
| 45 | 55 | ||
| 46 | 56 | /// `.afcl` manifest. `feat_version` MUST match the importing app's [`FEATURE_VERSION`]. |
| @@ -17,6 +17,7 @@ | |||
| 17 | 17 | #[cfg(feature = "analysis")] | |
| 18 | 18 | pub mod loudness; | |
| 19 | 19 | pub mod mfcc; | |
| 20 | + | #[cfg(feature = "bundled-layer")] | |
| 20 | 21 | pub mod official; | |
| 21 | 22 | pub mod spectral; | |
| 22 | 23 | pub mod suggest; |