feat: autotag scoring overhaul, dump-based explore index, and lyrics search
Consolidates in-progress work across autotag, explore, and library: - autotag: beets/Picard-informed scoring engine — ID-first matching, VA handling, recommendation tiers, and a merged distance/rank cascade, with an eval harness for regression tracking. - explore: offline MusicBrainz dump import/incremental refresh replaces the legacy tier crawl; index-first local search with fuzzy matching and a dedicated ranker; disk-free guards for dump downloads. - library: artist-credit extraction and matching. - lyrics: owned-library lyric search (FTS) with LRCLIB backfill. Also: rewrite README to be user-focused, and migrate upstream to git.ljones.me/yonlu/yellowjacket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package explore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDiceCoefficientTypoTolerance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b string
|
||||
wantMin float64 // score must be at least this
|
||||
wantMax float64 // ...and at most this
|
||||
}{
|
||||
{name: "identical", a: "beatles", b: "beatles", wantMin: 1.0, wantMax: 1.0},
|
||||
{name: "single typo", a: "beetles", b: "beatles", wantMin: 0.5, wantMax: 0.9},
|
||||
{name: "missing char", a: "nirvna", b: "nirvana", wantMin: 0.5, wantMax: 0.95},
|
||||
// Longer-name typo: a single substitution stays comfortably above
|
||||
// the rescue threshold, which is the realistic case (artist and
|
||||
// album names are rarely as short as five characters).
|
||||
{name: "longer name typo", a: "metalica", b: "metallica", wantMin: 0.5, wantMax: 0.95},
|
||||
// Adjacent transposition in a short word is bigrams' known weak
|
||||
// spot — it breaks most windows, so it scores below threshold.
|
||||
// Documented, not a bug: bigram overlap targets substitution,
|
||||
// insertion, and deletion typos.
|
||||
{name: "short transposition", a: "raido", b: "radio", wantMin: 0.0, wantMax: 0.34},
|
||||
{name: "case and space", a: "The Beatles", b: "the beatles", wantMin: 1.0, wantMax: 1.0},
|
||||
{name: "unrelated", a: "beatles", b: "metallica", wantMin: 0.0, wantMax: 0.34},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := diceCoefficient(fuzzyBigrams(tc.a), fuzzyBigrams(tc.b))
|
||||
if got < tc.wantMin || got > tc.wantMax {
|
||||
t.Errorf("diceCoefficient(%q, %q) = %.3f, want in [%.2f, %.2f]",
|
||||
tc.a, tc.b, got, tc.wantMin, tc.wantMax)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyBigramsShortInput(t *testing.T) {
|
||||
if got := fuzzyBigrams("a"); got != nil {
|
||||
t.Errorf("fuzzyBigrams(%q) = %v, want nil", "a", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user