max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
6 files changed,
+42 insertions,
-19 deletions
| @@ -7297,6 +7297,10 @@ | |||
| 7297 | 7297 | "winnow 1.0.4", | |
| 7298 | 7298 | ] | |
| 7299 | 7299 | ||
| 7300 | + | [[patch.unused]] | |
| 7301 | + | name = "docengine" | |
| 7302 | + | version = "0.4.0" | |
| 7303 | + | ||
| 7300 | 7304 | [[patch.unused]] | |
| 7301 | 7305 | name = "kberg" | |
| 7302 | 7306 | version = "0.1.0" | |
| @@ -7304,7 +7308,3 @@ | |||
| 7304 | 7308 | [[patch.unused]] | |
| 7305 | 7309 | name = "painhours" | |
| 7306 | 7310 | version = "0.1.0" | |
| 7307 | - | ||
| 7308 | - | [[patch.unused]] | |
| 7309 | - | name = "docengine" | |
| 7310 | - | version = "0.4.0" |
| @@ -41,22 +41,28 @@ | |||
| 41 | 41 | - Musical key detection | |
| 42 | 42 | - Spectral: centroid, flatness, rolloff, zero-crossing rate, onset strength | |
| 43 | 43 | - Loop detection: cross-correlation + beat alignment | |
| 44 | - | - Classification: rule-based into 16 categories | |
| 45 | 44 | - Waveform generation: downsampled peak pairs for display | |
| 46 | 45 | - Background worker thread (non-blocking UI) | |
| 47 | 46 | - Progress bar with cancel support | |
| 48 | 47 | - Re-analyze previously analyzed samples | |
| 49 | 48 | ||
| 50 | - | ### Classification (16 Categories) | |
| 51 | - | Kick, Snare, HiHat, Cymbal, Percussion, Bass, Vocal, Synth, Pad, FX, Noise, Music, Ambience, Impact, Foley, Texture | |
| 49 | + | ### Classification | |
| 50 | + | There is no fixed category list. A single category per sample was tried and dropped: the same 35 features that separate coarse families reliably do not carry specific instrument identity, and a filename says what a sample is far more often than the audio does. | |
| 52 | 51 | ||
| 53 | - | Classification runs on deterministic signal measurement: 35 extracted features per sample (spectral shape, crest factor, attack time, MFCCs), then rules you write and k-NN over the samples you have already tagged. No trained model ships in the app, so nothing about your library leaves it and the classifier carries no training-data licence baggage. | |
| 52 | + | What replaced it: | |
| 53 | + | ||
| 54 | + | - **Browsing** by the measured values themselves, as ranges rather than bins: brightness, tonal vs noisy, attack, alongside BPM, key, duration and loudness. Nothing to misclassify. | |
| 55 | + | - **Instrument names** from filename rules you can read and edit. | |
| 56 | + | - **Suggestions** from nearest-neighbour matching against the samples you have already tagged. | |
| 57 | + | ||
| 58 | + | No trained model ships in the app, so nothing about your library leaves it and the classifier carries no training-data licence baggage. | |
| 54 | 59 | ||
| 55 | 60 | ### Tag System | |
| 56 | 61 | - Hierarchical dot-notation tags (e.g., `genre.electronic.house`, `instrument.drum.kick`) | |
| 57 | 62 | - Auto-suggested tags from analysis with confidence scores and reasoning: | |
| 58 | - | - Classification, BPM, genre hint, key, loop/oneshot, loudness character | |
| 59 | - | - Review suggestions screen: accept/reject per-suggestion with Accept All / Reject All | |
| 63 | + | - BPM, genre hint, key, loop/oneshot, loudness character | |
| 64 | + | - Import review screen: accept or reject per suggestion, with Accept All / Reject All | |
| 65 | + | - Library review screen: suggestions grouped by tag, so a whole tag can be accepted at once or picked over sample by sample. Nothing is applied until you accept it | |
| 60 | 66 | - Tag entire folders during import | |
| 61 | 67 | - Add/remove tags manually in detail panel | |
| 62 | 68 | - Bulk tagging across multi-selection (Cmd+T) |
| @@ -54,6 +54,26 @@ | |||
| 54 | 54 | ||
| 55 | 55 | All five are wired into the app today. | |
| 56 | 56 | ||
| 57 | + | ### Two ways Layer B's answers reach a library | |
| 58 | + | ||
| 59 | + | Auto-apply and review, and which one a layer gets is a property of the layer rather | |
| 60 | + | than a global setting. | |
| 61 | + | ||
| 62 | + | **Auto-apply** (`exemplar::auto_apply_library`) writes every above-threshold tag across | |
| 63 | + | the library with provenance `ml`. "Undo auto-tagging" is `remove_tags_by_source("ml")`, | |
| 64 | + | which is what makes that reversible in one action. This is for a user's own labels, | |
| 65 | + | where the model is their library and they already agree with it. | |
| 66 | + | ||
| 67 | + | **Review** (`exemplar::preview_library`) writes nothing. It collects the same | |
| 68 | + | suggestions, groups them by tag, and hands them to a screen where they are accepted a | |
| 69 | + | tag at a time. An accepted suggestion is written as the user's own tag, with no | |
| 70 | + | provenance row, so undoing auto-tagging cannot reach into it. | |
| 71 | + | ||
| 72 | + | The bundled `.afcl` layer ships review-only. It answers for a library it has never | |
| 73 | + | seen, so it proposes rather than writes. Grouping by tag is what makes that reviewable | |
| 74 | + | at library scale: "are these 340 all kicks?" is one question, where the same 340 rows | |
| 75 | + | one sample at a time is 340 questions nobody finishes. | |
| 76 | + | ||
| 57 | 77 | ## The threshold tree is gone, and so is the class column | |
| 58 | 78 | ||
| 59 | 79 | `classify_full()` assigned a `SampleClass` from a priority-ordered tree of about 40 |
| @@ -369,7 +369,7 @@ | |||
| 369 | 369 | } | |
| 370 | 370 | ||
| 371 | 371 | /// Resolve display names for the first `limit` candidates of one group, on | |
| 372 | - | /// first expand. | |
| 372 | + | /// selection. | |
| 373 | 373 | /// | |
| 374 | 374 | /// One backend call per candidate, so it is scoped twice over: not for the | |
| 375 | 375 | /// whole queue (the collapsed view needs a count, not a name), and not for a | |
| @@ -572,7 +572,6 @@ | |||
| 572 | 572 | accepted: false, | |
| 573 | 573 | }) | |
| 574 | 574 | .collect(), | |
| 575 | - | expanded: false, | |
| 576 | 575 | names_loaded: false, | |
| 577 | 576 | }) | |
| 578 | 577 | .collect(), |
| @@ -3413,7 +3413,6 @@ | |||
| 3413 | 3413 | accepted: false, | |
| 3414 | 3414 | }, | |
| 3415 | 3415 | ], | |
| 3416 | - | expanded: false, | |
| 3417 | 3416 | names_loaded: false, | |
| 3418 | 3417 | }], | |
| 3419 | 3418 | samples_considered: 10, | |
| @@ -3634,7 +3633,6 @@ | |||
| 3634 | 3633 | confident: true, | |
| 3635 | 3634 | accepted: false, | |
| 3636 | 3635 | }], | |
| 3637 | - | expanded: false, | |
| 3638 | 3636 | names_loaded: false, | |
| 3639 | 3637 | }); | |
| 3640 | 3638 |
| @@ -1145,10 +1145,10 @@ | |||
| 1145 | 1145 | /// Every candidate, not a page of them. Accepting the group has to mean the | |
| 1146 | 1146 | /// whole group; only rendering is windowed. | |
| 1147 | 1147 | pub candidates: Vec<ReviewCandidate>, | |
| 1148 | - | pub expanded: bool, | |
| 1149 | - | /// Display names are resolved on expand, for the rendered window only. One | |
| 1150 | - | /// backend call per candidate, so resolving a 44,000-row group to draw 200 of | |
| 1151 | - | /// them would freeze the frame. | |
| 1148 | + | /// Display names are resolved when the group is selected, and only for the | |
| 1149 | + | /// rendered window. One backend call per candidate, so resolving a 44,000-row | |
| 1150 | + | /// group to draw 200 of them would freeze the frame. Cleared whenever | |
| 1151 | + | /// candidates are removed, since that slides the window onto unresolved rows. | |
| 1152 | 1152 | pub names_loaded: bool, | |
| 1153 | 1153 | } | |
| 1154 | 1154 | ||
| @@ -1166,7 +1166,7 @@ | |||
| 1166 | 1166 | /// One sample's candidacy for a tag. | |
| 1167 | 1167 | pub struct ReviewCandidate { | |
| 1168 | 1168 | pub hash: String, | |
| 1169 | - | /// Resolved on expand; the hash stands in until then. | |
| 1169 | + | /// Resolved when the group is selected; the hash stands in until then. | |
| 1170 | 1170 | pub name: Option<String>, | |
| 1171 | 1171 | pub score: f64, | |
| 1172 | 1172 | /// Cleared the tag's auto threshold. Bands the list so "accept the confident |