mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 03:41:40 +00:00
feat(enrichment): alias-aware scoring — auto-confirm non-Latin-primary artists (#772)
ship-ci / ci (push) Waiting to run
ship-ci / ci (push) Waiting to run
* feat(enrichment): loose MusicBrainz search fallback (find aliased artists)
The MB text search used a strict field-phrase query
(`recording:"<title>" AND artist:"<artist>"`). A field phrase only matches
MusicBrainz's *primary* artist/title — it never searches ALIASES — so a
recording stored under a non-Latin primary name (大橋純子) whose romanized
form ("Junko Ohashi") is only an alias returns ZERO results, even though MB
has it. Whole swaths of a community library (e.g. romanized J-pop / city-pop
charts) were unsearchable.
- `build_recording_query(..., loose=True)` drops the field scoping + phrases
for plain AND-ed term groups (`(telephone number) AND (junko ohashi)`),
which searches the whole document incl. aliases.
- `_mb_search_recordings` runs the strict query first (unchanged, high
precision) and only on an EMPTY result retries once with the loose query —
so mainstream matches are untouched and the extra throttled request is spent
only on a miss. Results are re-scored by rank_candidates, so recall goes up
without lowering match quality (auto-accept still needs the per-field floors).
Verified live: "Junko Ohashi / Telephone Number" and "Anri / Windy Summer"
(both 0 under the strict query) now surface the real records; "AC/DC /
Highway to Hell" still hits strict at score 1.0 with no loose retry.
Follow-up (separate): alias-aware SCORING so these can auto-confirm, not just
appear as manual candidates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* fix(enrichment): keep live exclusion in the loose search fallback
The loose fallback dropped the strict path's -secondarytype:Live filter, so a
studio chart whose strict query missed could fall back to — and, since
score_candidate doesn't penalize live takes, auto-confirm — a live-only
recording. Apply the same live gate to the loose query (skipped only when the
source title is itself a live take).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
18c4e229e1
commit
74cff4e0d6
@@ -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):
|
||||
|
||||
@@ -216,6 +216,27 @@ def test_build_recording_query_loose_keeps_live_for_live_charts():
|
||||
assert loose == "(highway to hell) AND (ac dc)"
|
||||
|
||||
|
||||
# ── 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 = {
|
||||
|
||||
Reference in New Issue
Block a user