Cover art: a sequel digit glued to a CJK title ('…サウンドトラック2') now blocks the wrong-album match

Sokhi (again): downloading the base 'Mushoku Tensei S2 Original Soundtrack' embedded
the cour-2 '…サウンドトラック2' cover. numeric_tokens_differ stripped titles to
[a-z0-9], turning CJK into spaces — so the trailing '2' collapsed to a bare '2'
that '第2期' (season 2) already supplied on BOTH sides, leaving the digit sets equal
and the guard blind. Tokenise on \W (Unicode word-aware) instead, so a digit stays
attached to its word ('サウンドトラック2' is its own digit-bearing token). Latin
behaviour is byte-identical (Vol.4 vs Vol.4.5 etc.). Shared guard, so the art picker
AND the MusicBrainz->CAA path are both fixed. Regression tests added.
This commit is contained in:
BoulderBadgeDad 2026-06-18 08:24:54 -07:00
parent 6c0d79a84a
commit 7e175fec02
3 changed files with 39 additions and 2 deletions

View file

@ -193,9 +193,18 @@ def numeric_tokens_differ(title_a: str, title_b: str) -> bool:
string similarity ('Vol.4' vs 'Vol.4.5' = 0.97) and token-subset checks
both wave these through, which hung volume 4.5's cover art on volume 4
(Sokhi). Shared digits on both sides ('1989' vs '1989 (Deluxe)') are
fine."""
fine.
Tokenises on non-word runs but KEEPS word characters of every script, so a
digit glued to a non-latin word stays its own digit-bearing token. Stripping
to [a-z0-9] turned CJK into spaces, collapsing 'サウンドトラック2' to a bare
'2' that a shared number elsewhere ('第2期' = season 2) already covered so
'Soundtrack' and 'Soundtrack2' both reduced to {'2'} and matched, hanging the
wrong cover (Sokhi again)."""
def _digit_tokens(text: str) -> frozenset:
tokens = re.sub(r"[^a-z0-9]+", " ", (text or "").casefold()).split()
# \W is Unicode-aware for str: CJK/kana count as word chars, so a digit
# stays attached to its word instead of collapsing to a bare '2'.
tokens = re.sub(r"\W+", " ", (text or "").casefold()).split()
return frozenset(t for t in tokens if any(c.isdigit() for c in t))
return _digit_tokens(title_a) != _digit_tokens(title_b)

View file

@ -19,6 +19,12 @@ from core.musicbrainz_service import MusicBrainzService
VOL4 = "B小町 - TVアニメ「【推しの子】」キャラクターソングCD Vol.4"
VOL45 = "B小町 - TVアニメ「【推しの子】」キャラクターソングCD Vol.4.5"
# Sokhi #2: the sequel number is glued straight onto a CJK word ('…トラック2'),
# with the SAME digit already present elsewhere ('第2期' = season 2). Stripping
# to [a-z0-9] collapsed both titles to {'2'} and the wrong (cour-2) cover won.
OST = "『無職転生 〜異世界行ったら本気だす〜』 第2期 オリジナル・サウンドトラック"
OST2 = "『無職転生 〜異世界行ったら本気だす〜』 第2期 オリジナル・サウンドトラック2"
def test_helper_volume_and_sequel_differ():
assert numeric_tokens_differ(VOL4, VOL45)
@ -26,11 +32,20 @@ def test_helper_volume_and_sequel_differ():
assert numeric_tokens_differ("Now 99", "Now 100")
def test_helper_cjk_trailing_sequel_digit_differs():
# The trailing '2' must register as a difference even though '第2期' already
# puts a '2' on both sides.
assert numeric_tokens_differ(OST, OST2)
assert numeric_tokens_differ(OST2, OST)
def test_helper_shared_or_no_digits_match():
assert not numeric_tokens_differ("1989", "1989 (Deluxe)")
assert not numeric_tokens_differ(VOL4, VOL4)
assert not numeric_tokens_differ("IGOR", "IGOR (Deluxe)")
assert not numeric_tokens_differ("", "")
# Same CJK album on both sides (incl. the shared 第2期) still matches.
assert not numeric_tokens_differ(OST, OST)
def _service_with_results(results):

View file

@ -202,6 +202,19 @@ def test_album_matches_rejects_numeric_difference():
assert art_lookup._album_matches("Taylor Swift", "1989", "Taylor Swift", "1989 (Deluxe)")
def test_album_matches_rejects_cjk_trailing_sequel_digit():
"""Sokhi #2: the sequel '2' is glued straight onto a CJK word
('…サウンドトラック2'), and '第2期' (season 2) already puts a '2' on both
sides so the digit-strip collapsed both to {'2'} and the cour-2
soundtrack's cover hung on the base soundtrack."""
ART = "藤澤慶昌"
OST = "『無職転生 〜異世界行ったら本気だす〜』 第2期 オリジナル・サウンドトラック"
assert not art_lookup._album_matches(ART, OST, ART, OST + "2")
assert not art_lookup._album_matches(ART, OST + "2", ART, OST)
# The genuine base-album hit still matches (incl. its shared 第2期).
assert art_lookup._album_matches(ART, OST, ART, OST)
# ---------------------------------------------------------------------------
# build_art_lookup — caching + guarding
# ---------------------------------------------------------------------------