feat(enrichment): alias-aware scoring (auto-confirm non-Latin-primary artists)

Builds on the loose-search fallback: that surfaces a recording stored under a
Japanese primary name (大橋純子) via its romanized alias, but the SCORER still
compared the reference ("Junko Ohashi") against the primary only → artist
similarity 0 → below the auto floor, so it could only ever be a manual
candidate, never an auto-fill.

- mb_match: `cand_artist_sim` takes the best similarity over the candidate's
  primary name AND its `artist_aliases`; score_candidate + classify use it.
- server: `_mb_artist_aliases(id)` fetches an artist's aliases (one throttled
  lookup, process-cached — a one-artist discography costs ONE request) and
  `_alias_enrich` attaches them ONLY to promising near-misses (title agrees,
  primary artist doesn't) so a normal pass spends zero extra requests. Wired
  into both the auto-matcher (_enrich_one) and the manual search proxy.

Verified live: "Junko Ohashi / Telephone Number" → 大橋純子 candidate goes from
score 0.5 (loose-only) to 1.0 (auto-confirmable), ranked #1; "AC/DC / Highway
to Hell" unchanged at 1.0 with no alias lookup.

Stacks on #771 (feat/mb-loose-search-fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ChrisBeWithYou
2026-07-04 15:35:34 -05:00
co-authored by Claude Opus 4.8
parent 117d260723
commit c49871484f
4 changed files with 178 additions and 3 deletions
+51
View File
@@ -134,6 +134,57 @@ def test_search_does_not_retry_when_strict_hits(server, monkeypatch):
assert len(calls) == 1
# ── alias-aware scoring (non-Latin-primary artists) ──────────────────────────
_AID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
def test_artist_aliases_fetched_and_cached(server, monkeypatch):
calls = []
def fake(path, params):
calls.append(path)
return {"sort-name": "Ohashi, Junko",
"aliases": [{"name": "Junko Ohashi"}, {"name": "大橋 純子"}]}
monkeypatch.setattr(server, "_mb_http_get", fake)
names = server._mb_artist_aliases(_AID)
assert "Junko Ohashi" in names and "Ohashi, Junko" in names
server._mb_artist_aliases(_AID) # cached → no second request
assert len(calls) == 1
def test_artist_aliases_rejects_bad_id(server, monkeypatch):
def boom(path, params):
raise AssertionError("must not fetch for a non-UUID id")
monkeypatch.setattr(server, "_mb_http_get", boom)
assert server._mb_artist_aliases("not-a-uuid") == []
def test_enrich_auto_matches_japanese_primary_via_alias(server, monkeypatch):
# A pack whose (romanized) artist MB stores under a Japanese primary name.
_put(server, "x.sloppak", title="Telephone Number", artist="Junko Ohashi")
def _routed(path, params):
if path.startswith("artist/"): # alias lookup
return {"sort-name": "Ohashi, Junko",
"aliases": [{"name": "Junko Ohashi"}]}
q = params.get("query", "")
if q.startswith("recording:"): # strict phrase → nothing
return {"recordings": []}
return {"recordings": [mb_doc(rid="rec-jp", title="Telephone Number",
artist="大橋純子", artist_id=_AID)]} # loose hit
monkeypatch.setattr(server, "_mb_http_get", _routed)
monkeypatch.setattr(server, "_enrich_network_enabled", lambda: True)
server._background_enrich()
row = server.meta_db.get_enrichment("x.sloppak")
# The romanized alias lifts the artist over the auto floor → auto-confirmed.
assert row["match_state"] == "matched"
assert row["mb_recording_id"] == "rec-jp"
# ── offline safety (the pytest-never-hits-network contract) ──────────────────
def test_offline_default_skips_matching(server, monkeypatch):
+21
View File
@@ -175,6 +175,27 @@ def test_build_recording_query_loose_missing_artist():
assert m.build_recording_query("", "Fantasy", loose=True) == "(fantasy)"
# ── alias-aware artist scoring ────────────────────────────────────────────────
def test_cand_artist_sim_uses_aliases():
song = {"artist": "Junko Ohashi", "title": "Telephone Number"}
# primary is the Japanese name → romanized reference scores 0…
assert m.cand_artist_sim(song, {"artist": "大橋純子"}) == 0.0
# …but a romanized alias confirms it
assert m.cand_artist_sim(
song, {"artist": "大橋純子", "artist_aliases": ["Ohashi Junko", "Junko Ohashi"]}) == 1.0
def test_alias_lifts_candidate_to_auto():
song = {"artist": "Junko Ohashi", "title": "Telephone Number"}
jp = {"artist": "大橋純子", "title": "Telephone Number"}
# Without the alias: title matches but the artist floor fails → never auto.
assert m.classify(song, jp, m.score_candidate(song, jp)) != "auto"
# With the romanized alias attached: artist clears the floor → auto.
jp_alias = dict(jp, artist_aliases=["Junko Ohashi"])
assert m.classify(song, jp_alias, m.score_candidate(song, jp_alias)) == "auto"
# ── MusicBrainz response parsing ──────────────────────────────────────────────
MB_DOC = {