This model was mostly vibecoded by Claude (Anthropic's AI assistant). The data pipeline, training scripts, evaluation and this card were largely written by Claude under light human direction. The numbers come from real runs, but nobody has carefully audited the code, the data or the methodology. Treat it as an experiment, not a vetted model.
musiclib learned pair matcher (v19 + rel-v8)
LightGBM models that decide whether two music-library entries (tracks,
releases, release groups, artists) from different sources are the same
entity, derived one from the other (remix, cover, live, …), siblings,
or unrelated. They are the decision stage of a soft-dedup pipeline. The
candidate pairs come from
gbnam8/jp-music-title-encoder
nearest-neighbour retrieval.
The input is 217 hand-built pair features, computed from musiclib-rs "pair facts" (musiclib-pair-facts/1): title-encoder
cosines, lexical/trigram similarity, artist and credit overlap, durations,
years, track lists and MV/version markers. The models
are not useful without that feature code. The runtime is the inference
cdylib (matcher feature) in
nrs-org/learned-matcher,
driven by rhai/match.learned.rhai from musiclib-rs's softmatch.
Files
| File | Model |
|---|---|
model.txt |
main 4-class model: same / related / sibling / unrelated (v19) |
kind.txt, kinds.json |
relation kind: arrangement, cover, edit, instrumental, live, other, remix (rel-v8) |
direction.txt |
which side is the original (rel-v8) |
structure.txt |
sibling-vs-structure guard (rel-v8) |
manifest.json |
output layout, classes, kinds, dev-chosen per-type p_same thresholds |
The title encoder is not duplicated here. The runtime bundle expects it in
encoder/. That folder holds config.json, tokenizer.json,
model.safetensors and, for the Vulkan GPU path, model-f16.gguf, all from
the encoder repo.
The previous main model (v17) is in this repo's commit history. The relation heads (rel-v8) did not change.
Assembling the runtime bundle
B=data/learned-matcher/bundle/v19
hf download gbnam8/musiclib-learned-matcher --local-dir $B --exclude README.md
hf download gbnam8/jp-music-title-encoder config.json tokenizer.json \
model.safetensors model-f16.gguf --local-dir $B/encoder
MUSICLIB_MATCHER_BUNDLE=$PWD/$B ... # the default path in match.learned.rhai
Training data
All labels are generated; none are hand-labelled:
- Library split probes (
same). Entries that hard-ID linking unified from ≥ 2 sources are cut into two disjoint source halves, such as a YouTube MV versus Spotify + MusicBrainz. Hard-ID overlap is removed, so the model has to rely on soft evidence. - MusicBrainz relations (
related, with kind and direction): recording relations (remix, edit, karaoke/instrumental, live, cover) and recordings of the same work. MusicBrainz also supplies hard negatives (same title, different entity). - Jev (TypeSafe) teacher labels on library candidate pairs. A separate Jev silver set is held out from training and used for testing.
The library is a ~26k-entry Japanese-domain collection (J-Pop, anime, VTuber, Vocaloid, game, idol).
Changes in v19
v17 merged artists such as Mikito-P - Topic × 環みちる at p_same 0.988,
with no shared credits and a name cosine of 0.55. The cause was the
video_sides feature (how many sides are video-only, e.g. a YouTube "Topic"
channel). Every MusicBrainz-labelled artist negative has an MB id on both
sides, so none has a video-only side. video_sides = 1 therefore mostly
identified the positive tiers, and pushed p_same by about +2.8 log-odds.
Dropping the feature did not help: the model found proxies, and correct track merges (Topic audio × official MV) were lost. v19 keeps the v17 features and settings and adds 4,238 Jev labels from a full library run. Among them are 1,134 extra artist negatives in exactly the missing region: a video-only side next to a similar-looking name.
The policy script keeps the artist merge threshold at v17's 0.976 rather than
v19's dev-chosen 0.941 (the value in manifest.json). On the library, about
1 in 6 artist merges between those two values was wrong.
Evaluation
Dev split (generated labels), per entry type:
| type | pairs | same |
AP same |
recall @ P 0.99 | AP related |
accuracy |
|---|---|---|---|---|---|---|
| track | 50,797 | 1,324 | 0.990 | 0.829 | 0.882 | 0.972 |
| artist | 31,272 | 535 | 0.979 | 0.807 | 0.686 | 0.997 |
| release | 8,295 | 277 | 0.960 | 0.181 | 0.978 | 0.994 |
| release group | 3,860 | 14 | 0.960 | 0.786 | — | 0.999 |
v17's dev artist numbers (AP 0.986, recall 0.853) were inflated by the
video_sides shortcut, because the dev split is built the same way.
Held-out silver (Jev confidence ≥ 0.8), merge precision / recall:
| v17 | v19 | |
|---|---|---|
| tracks | 0.986 / 0.919 | 0.987 / 0.926 |
| artists | 1.000 / 0.926 | 0.996 / 0.934 |
The human-labelled set is still tiny (18 pairs). v19 makes the same one
false merge as v17: an original and a remix with identical titles and no
durations, at p_same 0.837 against the 0.812 track threshold. Its weighted
relate recall on those 18 pairs is lower than v17's (0.24 vs 0.47), but the
confidence intervals overlap widely.
On the full library (26k entries, ~460k candidate pairs, Jev off), v19 makes 984 artist and 8,487 track merges (v17: 1,006 and 8,714). About 30 of the 49 artist merges it drops are v17's false ones.
The Rust runtime matches the Python pipeline. Parity was measured on v17 (same features): on 3,473 pairs it gives the same verdict every time, with a maximum |Δp| of 6.6e-3 when using the Rust encoder.
Limitations
- Thresholds were chosen on the dev split and live in the policy script,
not the model. Re-tune them before trusting
samenear the threshold. - Release recall at 99% precision is low: the releases that are hard to merge have identical titles with different track lists or editions.
- Soft merges are pairwise. Applied transitively, they can chain into large wrong groups, e.g. a music video matched to each of its stems. The model does not check whether a whole group is consistent.
- The features are specific to musiclib-rs. Python
train/features.pyand Rustinference/src/matcher/features.rs(both in nrs-org/learned-matcher) define them, and they must match exactly. Cosines are computed in float64 and rounded to float32, because the trees split at ulp level near 1.0.