AcoustID returns a recording's title/artist in their ORIGINAL script (e.g. "久石譲" for Joe Hisaishi) while SoulSync's expected metadata is romanized/English. A correct download then fails verification on two walls: the title can never clear the 0.70 similarity bar cross-script, and the only skip path that ignores the title required a near-perfect 0.95 fingerprint plus a resolved alias. Result: every non-English artist trips it. Two complementary fixes, per the reporter's two ideas. Graceful fix (automatic): - New pure core/matching/script_compat.py detects when two strings are in genuinely different writing systems (CJK/Hangul/Cyrillic/Greek/ Arabic/Hebrew/Thai vs Latin). Accented Latin (Beyoncé, Sigur Rós) stays Latin — no false trigger. - acoustid_verification.py: when the EXPECTED artist and the matched artist span scripts AND the artist is confirmed via the existing MusicBrainz alias bridge, SKIP instead of quarantine, without the 0.95 floor (the 0.80 trust floor already gates the fingerprint). - Deliberately narrow: keyed on the ARTIST spanning scripts + being confirmed. A same-script artist with only a cross-script title keeps the stricter 0.95 floor, so the #607 wrong-file protection (Kendrick R.O.T.C, low-fingerprint Japanese-title) is untouched. Per-request toggle (manual escape hatch): - New "Skip AcoustID verification" checkbox in the download-missing modal beside "Force Download All". - skip_acoustid threads request -> batch -> per-track track_info -> download context (same path as _playlist_folder_mode), landing on the existing _skip_quarantine_check='acoustid' bypass. No new mechanism; only the AcoustID gate is bypassed (integrity/bit-depth still run). Tests: - tests/matching/test_script_compat.py — script-boundary cases. - test_acoustid_skip_logic.py — Joe Hisaishi SKIPs at 0.85; unconfirmed cross-script artist still FAILs; same-script low-fingerprint still FAILs. - test_downloads_candidates.py — toggle injects the bypass; absent toggle keeps verification. Full suite: 5169 passed; only pre-existing soundcloud /app env failures remain. Zero regressions.
278 lines
11 KiB
Python
278 lines
11 KiB
Python
"""Tighten the AcoustID "language/script" skip exemption.
|
|
|
|
User report (Mr. Morale download): three different track requests
|
|
(Rich Interlude, Savior Interlude, Savior) each received the same
|
|
WRONG audio file (Kendrick's R.O.T.C Interlude from his 2010 mixtape).
|
|
AcoustID flagged the title mismatch but the verification logic
|
|
SKIPPED rather than FAILED with the reason "likely same song in
|
|
different language/script."
|
|
|
|
The old condition was:
|
|
best_score >= 0.95 AND (title_sim >= 0.55 OR artist_sim >= match)
|
|
|
|
That OR-clause fired for English-vs-English titles by the same artist
|
|
that share NO actual content — same artist + word "interlude" in both
|
|
titles cleared the bar. The skip then trusted the wrong file as
|
|
correct.
|
|
|
|
New condition: only skip when there's positive evidence the mismatch
|
|
is a transliteration / language-script case:
|
|
- (a) Either side of the comparison contains non-ASCII characters AND
|
|
artist matches strongly. Real cases: Japanese kanji ↔ romaji,
|
|
Korean hangul ↔ romaji, etc.
|
|
- (b) BOTH title AND artist similarity are very high (>=0.80, ARTIST
|
|
threshold). Real cases: title differs only by punctuation /
|
|
casing that fell below strict-match thresholds.
|
|
|
|
For English-vs-English with very different titles by the same artist,
|
|
the skip no longer fires — verification correctly returns FAIL,
|
|
quarantining the wrong file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from core.acoustid_verification import (
|
|
AcoustIDVerification,
|
|
VerificationResult,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def verifier(monkeypatch):
|
|
"""A verifier with the network/fingerprint side stubbed so we can
|
|
drive the title/artist comparison logic directly."""
|
|
v = AcoustIDVerification()
|
|
|
|
# Stub availability check to avoid touching real chromaprint
|
|
class _StubClient:
|
|
def is_available(self):
|
|
return True, 'available'
|
|
|
|
def lookup_with_status(self, path):
|
|
# Each test injects its own desired return value via
|
|
# monkeypatch on this method; default is empty.
|
|
return None
|
|
|
|
v.acoustid_client = _StubClient()
|
|
return v
|
|
|
|
|
|
def _stub_lookup(verifier, *, recordings, best_score):
|
|
"""Make `lookup_with_status` return a fabricated AcoustID result."""
|
|
verifier.acoustid_client.lookup_with_status = lambda path: {
|
|
'recordings': recordings,
|
|
'best_score': best_score,
|
|
'recording_mbids': [r.get('id') for r in recordings if r.get('id')],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The headline regression — Rich Interlude vs R.O.T.C Interlude
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_english_titles_same_artist_no_longer_skipped(verifier):
|
|
"""User's actual case: requested 'Rich (Interlude)' by Kendrick
|
|
Lamar, AcoustID identified the file as 'R.O.T.C. (interlude)' by
|
|
Kendrick Lamar. Same artist, same word 'interlude', but completely
|
|
different songs. Old skip-logic let it pass; new logic must FAIL
|
|
so the file gets quarantined."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': 'R.O.T.C. (interlude)', 'artist': 'Kendrick Lamar feat. BJ the Chicago Kid'},
|
|
], best_score=0.96)
|
|
|
|
result, msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Rich (Interlude)',
|
|
'Kendrick Lamar',
|
|
)
|
|
assert result == VerificationResult.FAIL
|
|
# Message should be the wrong-file message, NOT the language/script skip
|
|
assert 'mismatch' in msg.lower()
|
|
assert 'language/script' not in msg.lower()
|
|
|
|
|
|
def test_savior_request_returning_rotc_no_longer_skipped(verifier):
|
|
"""Same bug surface, different track. Confirms the fix isn't
|
|
Rich-Interlude-specific."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': 'R.O.T.C. (interlude)', 'artist': 'Kendrick Lamar feat. BJ the Chicago Kid'},
|
|
], best_score=0.96)
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Savior',
|
|
'Kendrick Lamar',
|
|
)
|
|
assert result == VerificationResult.FAIL
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The legitimate skip cases — must STILL fire
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_japanese_kanji_to_romaji_still_skipped(verifier):
|
|
"""Real language/script case: AcoustID's database has the kanji
|
|
title, the user requested the romaji version. Same artist (in
|
|
Latin script), high fingerprint confidence. Skip should still
|
|
fire so a correct file isn't false-quarantined."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': '残酷な天使のテーゼ', 'artist': 'Yoko Takahashi'},
|
|
], best_score=0.97)
|
|
|
|
result, msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Zankoku na Tenshi no Theze',
|
|
'Yoko Takahashi',
|
|
)
|
|
assert result == VerificationResult.SKIP
|
|
assert 'language/script' in msg.lower()
|
|
|
|
|
|
def test_minor_punctuation_difference_passes_outright(verifier):
|
|
"""Punctuation-only difference: both 'MAAD' and 'M.A.A.D' normalize
|
|
similarly enough that the strict TITLE_MATCH_THRESHOLD is met and
|
|
verification PASSES (better outcome than SKIP). Pin this so a
|
|
future tightening of the strict thresholds doesn't accidentally
|
|
push these into the FAIL bucket."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': 'M.A.A.D City', 'artist': 'Kendrick Lamar'},
|
|
], best_score=0.97)
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'MAAD City',
|
|
'Kendrick Lamar',
|
|
)
|
|
# PASS or SKIP both fine — the critical assertion is "not FAIL".
|
|
assert result != VerificationResult.FAIL
|
|
|
|
|
|
def test_low_fingerprint_score_never_skipped(verifier):
|
|
"""Below the 0.95 confidence floor, the skip exemption should
|
|
never fire — even for plausibly-real language/script cases. We
|
|
don't have enough signal to be sure the audio matches."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': '残酷な天使のテーゼ', 'artist': 'Yoko Takahashi'},
|
|
], best_score=0.80) # below 0.95 floor
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Zankoku na Tenshi no Theze',
|
|
'Yoko Takahashi',
|
|
)
|
|
assert result == VerificationResult.FAIL
|
|
|
|
|
|
def test_high_score_but_artist_mismatch_no_longer_skipped(verifier):
|
|
"""Even with high fingerprint AND non-ASCII chars present, if the
|
|
artist DOESN'T match well, we don't have enough signal to skip.
|
|
Could be a cover by a different artist."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': '残酷な天使のテーゼ', 'artist': 'Some Other Singer'},
|
|
], best_score=0.97)
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Zankoku na Tenshi no Theze',
|
|
'Yoko Takahashi',
|
|
)
|
|
assert result == VerificationResult.FAIL
|
|
|
|
|
|
def test_low_fingerprint_score_never_skipped_same_script_artist(verifier):
|
|
"""#797 guard — the #607 protection for a SAME-SCRIPT artist (Latin
|
|
'Yoko Takahashi' on both sides) with only a cross-script TITLE must
|
|
stay FAIL below the 0.95 floor. The #797 relaxation is keyed on the
|
|
ARTIST spanning scripts, which this case is NOT, so nothing changes
|
|
here. (Duplicates test_low_fingerprint_score_never_skipped's intent
|
|
explicitly against the new code path.)"""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': '残酷な天使のテーゼ', 'artist': 'Yoko Takahashi'},
|
|
], best_score=0.85)
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Zankoku na Tenshi no Theze',
|
|
'Yoko Takahashi',
|
|
)
|
|
assert result == VerificationResult.FAIL
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Issue #797 — non-English ARTIST whose name spans scripts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_cross_script_artist_confirmed_via_alias_skips_below_095(verifier):
|
|
"""#797 headline: requested 'Joe Hisaishi' (romanized), AcoustID
|
|
returns the recording with the artist/title in their native kanji
|
|
('久石譲'). The alias bridge confirms 久石譲 IS Joe Hisaishi, the
|
|
fingerprint is solid (0.85, above the 0.80 trust floor) but below
|
|
the old 0.95 language/script bar. Pre-#797 this FAILed and the
|
|
correct file was quarantined. Now it SKIPs."""
|
|
with patch(
|
|
'core.acoustid_verification._resolve_expected_artist_aliases',
|
|
return_value=['久石譲'],
|
|
):
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': '風のとおり道', 'artist': '久石譲'},
|
|
], best_score=0.85)
|
|
|
|
result, msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'The Path of the Wind',
|
|
'Joe Hisaishi',
|
|
)
|
|
assert result == VerificationResult.SKIP
|
|
assert 'language/script' in msg.lower()
|
|
|
|
|
|
def test_cross_script_artist_NOT_confirmed_still_fails(verifier):
|
|
"""#797 tight scope: if the alias bridge can't confirm the artist
|
|
(lookup returns nothing), we have no positive evidence the kanji
|
|
artist IS the expected one — so the #797 relaxation must NOT fire.
|
|
The relaxation only rescues a CONFIRMED cross-script artist.
|
|
|
|
Constructed so best_rec is set (partial title overlap → non-zero
|
|
combined score) and the title stays under the strict threshold, so
|
|
the flow reaches the same skip-decision point the rescue lives at —
|
|
proving it doesn't fire without a confirmed artist."""
|
|
with patch(
|
|
'core.acoustid_verification._resolve_expected_artist_aliases',
|
|
return_value=[],
|
|
):
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': 'Summer Night', 'artist': '久石譲'},
|
|
], best_score=0.85)
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Summer',
|
|
'Joe Hisaishi',
|
|
)
|
|
assert result == VerificationResult.FAIL
|
|
|
|
|
|
def test_old_loose_threshold_no_longer_fires_for_unrelated_titles(verifier):
|
|
"""Pin the negative case for the old loose threshold (title_sim
|
|
>= 0.55). 'Crown' vs 'Crown of Thorns' had similarity around 0.6
|
|
in some normalizations — under old logic with high confidence
|
|
and matching artist that would skip. New logic requires title_sim
|
|
>= 0.80 OR non-ASCII presence."""
|
|
_stub_lookup(verifier, recordings=[
|
|
{'title': 'Crown of Thorns', 'artist': 'Kendrick Lamar'},
|
|
], best_score=0.96)
|
|
|
|
result, _msg = verifier.verify_audio_file(
|
|
'/fake/path.flac',
|
|
'Crown',
|
|
'Kendrick Lamar',
|
|
)
|
|
# User asked for 'Crown', got 'Crown of Thorns' — should FAIL now
|
|
assert result == VerificationResult.FAIL
|