Skip to main content

max / audiofiles

Drop a dead field and correct the classifier docs ReviewGroup::expanded was left over from the collapsing subsection the review queue used to be: written on construction, never read since the screen replaced it. The name-resolution comments still said "on expand" when selection is what drives it now. docs/description.md still advertised "Classification (16 Categories)" and listed them. That taxonomy was deleted on 2026-07-30 along with the threshold tree and the classification column, so a public doc was promising a feature the app does not have. Replaced with what actually took over each of its jobs: measured ranges for browsing, filename rules for instrument names, k-NN for suggestions. Also documents the two ways Layer B's answers reach a library, since "auto-apply vs review" is now a property of the layer rather than a setting, and the provenance difference is what makes one undoable in a single action and the other the user's own tag.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-07 15:28 UTC
Signed with PGP, not checked
Commit: 0eae44b0c8aa4767a519f9607866265d03d53c54
Parent: f7a7c09
6 files changed, +42 insertions, -19 deletions
M Cargo.lock +4 -4
@@ -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