soulsync/core/matching/version_mismatch.py
Broque Thomas b42cafa150 AcoustID + quarantine modal: three bug fixes (closes #607, closes #608)
Issue #607 (AfonsoG6) -- two AcoustID problems:

1. Live recordings false-quarantining as "Version mismatch: expected
   '... (Live at Venue)' (live) but file is '...' (original)" because
   MusicBrainz often stores the recording entity with a bare title --
   the venue / live annotation lives on the release entity, not the
   recording. The audio fingerprint correctly identifies the live
   recording, but the title-text comparison flagged it as wrong.

   New pure helper `core/matching/version_mismatch.py:is_acceptable_version_mismatch`
   accepts the mismatch only when:
     - One-sided AND involves 'live': exactly one side is 'live' and
       the other is bare 'original'. Two-sided mismatches stay strict.
     - Fingerprint score >= 0.85 (stricter than the existing 0.80
       minimum -- escape valve only fires when AcoustID is more
       confident than its own threshold).
     - Bare title similarity >= 0.70.
     - Artist similarity >= 0.60.

   Other version markers (instrumental, remix, acoustic, demo, etc)
   stay strict -- those have distinct fingerprints AND MB always
   annotates them in the recording title. The existing
   test_acoustid_version_mismatch.py suite passes unchanged.

2. Audio-mismatch failure message reported "identified as '' by ''
   (artist=100%)" when AcoustID returned multiple recordings -- prior
   code mixed `recordings[0]`'s strings (which can be empty) with
   `best_rec`'s scores. Now uses `matched_title` / `matched_artist`
   consistently in both the high-confidence-skip path and the final
   fail message.

Issue #608 (AfonsoG6) -- quarantine modal:

3. Approve / Delete buttons silently no-op'd when the filename
   contained an apostrophe -- the unescaped quote broke the inline JS
   in the onclick handler. Now wraps the id via
   `escapeHtml(JSON.stringify(id))`, which round-trips quotes /
   backslashes / unicode / newlines safely through the HTML attribute
   to JS string boundary.

4. Bonus UX: quarantine entry expanded view now shows source uploader
   (username) and original soulseek filename when the sidecar carries
   that context -- helps trace which uploader the bad file came from.
   Backend exposes `source_username` + `source_filename` fields from
   `sidecar.context.original_search_result`. Degrades to '' on legacy
   thin sidecars.

Tests:
- 23 new boundary tests in tests/matching/test_version_mismatch.py
  pin every shape: equal versions trivial, one-sided live both
  directions, threshold floors (each just below default -> reject),
  two-sided strict, non-live one-sided strict (covers exact
  test_instrumental_returned_for_vocal_request_fails scenario),
  custom-threshold overrides.
- 4 existing test_acoustid_version_mismatch.py tests pass unchanged.
- 507 AcoustID / matching / imports tests pass.
2026-05-15 08:55:06 -07:00

115 lines
4.7 KiB
Python

"""Decide when an AcoustID version-annotation mismatch should still
pass verification.
Issue #607 (AfonsoG6): live recordings were quarantining as
"Version mismatch: expected '... (Live at Venue)' (live) but file is
'Song' (original)" because MusicBrainz often stores the recording
entity with a bare title — the venue / live annotation lives on the
release entity, not the recording. The audio fingerprint correctly
identifies the live recording (live audio has its own distinct
fingerprint), but the title-text comparison flagged it as the wrong
version.
Strict version mismatching stays for genuinely-different recordings
(instrumental vs vocal, remix vs original, acoustic vs studio) —
those have distinct fingerprints AND MB always annotates them in the
recording title. We only loosen for the **live** direction
specifically, since that's the asymmetry users actually hit:
- LIVE: MB often stores live recordings with bare titles. The
fingerprint correctly identifies the live recording even when the
recording's text title lacks a "(Live at ...)" annotation.
- INSTRUMENTAL / REMIX / ACOUSTIC / DEMO / etc: MB always carries
the version marker in the recording title. If AcoustID returns an
instrumental for a vocal file query (or vice versa), it's a real
wrong-recording match — the fingerprint matched the instrumental
audio, which IS the wrong file.
Two-sided version mismatches stay strict — "live" vs "remix" really
are different recordings even if MB titled them similarly."""
from __future__ import annotations
_BARE_VERSION = 'original'
# Versions where a one-sided annotation difference is plausibly
# explained by MB metadata gaps rather than a different recording.
# Live recordings are the primary case; venue annotations live on the
# release-track entity, not the recording entity, so the recording can
# be bare-titled even though the audio is genuinely live.
_LIVE_AWARE_VERSIONS = frozenset({'live'})
def is_acceptable_version_mismatch(
expected_version: str,
matched_version: str,
*,
fingerprint_score: float,
title_similarity: float,
artist_similarity: float,
score_threshold: float = 0.85,
title_threshold: float = 0.70,
artist_threshold: float = 0.60,
) -> bool:
"""Return True when an expected-vs-matched version annotation
difference is likely a MusicBrainz metadata gap rather than a
genuinely different recording.
Conditions (all must hold):
1. The mismatch is **one-sided AND involves a live-aware version**
— exactly one side is ``'live'`` and the other is bare
``'original'``. Other version markers (instrumental, remix,
acoustic, etc) carry distinct fingerprints AND are always
annotated in MB's recording title — we don't loosen for them.
2. Fingerprint score ``>= score_threshold`` (default 0.85). The
AcoustID fingerprint is high-confidence — we trust it.
3. Bare title similarity ``>= title_threshold`` (default 0.70).
After the version annotation is stripped, the underlying titles
agree.
4. Artist similarity ``>= artist_threshold`` (default 0.60). Same
artist credit.
When ``expected_version == matched_version`` returns ``True``
trivially — no mismatch to decide.
Examples that ACCEPT (return True):
- expected=``'live'``, matched=``'original'``, fp=0.95, title=0.95,
artist=1.0 — typical live-recording MB-bare-title case (issue
#607 example 2).
- expected=``'original'``, matched=``'live'`` — same case in the
other direction.
Examples that REJECT (return False):
- expected=``'instrumental'``, matched=``'original'`` — fingerprint
matched the instrumental recording; if user asked for vocal, the
file is genuinely wrong. Stays strict regardless of confidence.
- expected=``'remix'``, matched=``'original'`` — same logic.
- expected=``'live'``, matched=``'remix'`` — both versioned,
both different. Real mismatch.
- expected=``'live'``, matched=``'original'``, fp=0.50 — one-sided
live but low confidence. Fall through to FAIL.
- expected=``'live'``, matched=``'original'``, fp=0.95 but title
sim 0.30 — bare titles don't agree. Different song.
"""
if expected_version == matched_version:
return True
one_sided_live = (
(expected_version in _LIVE_AWARE_VERSIONS and matched_version == _BARE_VERSION)
or (expected_version == _BARE_VERSION and matched_version in _LIVE_AWARE_VERSIONS)
)
if not one_sided_live:
return False
return (
fingerprint_score >= score_threshold
and title_similarity >= title_threshold
and artist_similarity >= artist_threshold
)
__all__ = ['is_acceptable_version_mismatch']