Raise artist threshold; add title similarity floor
In web_server.py, increase the minimum artist similarity from 0.4 to 0.5 in both cache validation and candidate scoring. Add a title similarity floor (min_title_similarity = 0.5) to _discovery_score_candidates: compute cleaned/core titles, allow a core-title exact match to bypass the floor, otherwise require title similarity >= 0.5. Only candidates that pass both artist and title floors proceed to full weighted scoring. This reduces false positives where a strong artist match could previously mask a poor title match.
This commit is contained in:
parent
e77dbf3f1e
commit
3893d75ad0
1 changed files with 27 additions and 10 deletions
|
|
@ -18078,7 +18078,7 @@ def _get_discovery_cache_key(title, artist):
|
||||||
def _validate_discovery_cache_artist(source_artist, cached_match):
|
def _validate_discovery_cache_artist(source_artist, cached_match):
|
||||||
"""Check if a cached discovery match has a valid artist. Returns False if the
|
"""Check if a cached discovery match has a valid artist. Returns False if the
|
||||||
cached result's artist doesn't match the source artist (stale/wrong cache entry)."""
|
cached result's artist doesn't match the source artist (stale/wrong cache entry)."""
|
||||||
min_artist_similarity = 0.4
|
min_artist_similarity = 0.5
|
||||||
source_artist_cleaned = matching_engine.clean_artist(source_artist)
|
source_artist_cleaned = matching_engine.clean_artist(source_artist)
|
||||||
if not source_artist_cleaned:
|
if not source_artist_cleaned:
|
||||||
return True # No source artist to validate against
|
return True # No source artist to validate against
|
||||||
|
|
@ -18108,11 +18108,10 @@ def _validate_discovery_cache_artist(source_artist, cached_match):
|
||||||
def _discovery_score_candidates(source_title, source_artist, source_duration_ms, search_results):
|
def _discovery_score_candidates(source_title, source_artist, source_duration_ms, search_results):
|
||||||
"""Score search results against a source track using the matching engine.
|
"""Score search results against a source track using the matching engine.
|
||||||
|
|
||||||
Uses matching_engine.score_track_match() which applies clean_title/clean_artist,
|
Both artist AND title must independently pass minimum similarity floors.
|
||||||
core title fast path, duration similarity, and 60/30/10 weighted scoring.
|
This prevents weighted scoring from allowing a perfect artist to carry a
|
||||||
|
garbage title (or vice versa). If either dimension doesn't match, the
|
||||||
Includes a minimum artist similarity check to prevent "right title, wrong artist"
|
candidate is rejected — no match is better than a wrong match.
|
||||||
matches (e.g., "Yoga" by bbno$ matching "Yoga" by Sleep Music Lullabies).
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_title: The source track title (already cleaned for YouTube, raw for others)
|
source_title: The source track title (already cleaned for YouTube, raw for others)
|
||||||
|
|
@ -18126,9 +18125,12 @@ def _discovery_score_candidates(source_title, source_artist, source_duration_ms,
|
||||||
best_match = None
|
best_match = None
|
||||||
best_confidence = 0.0
|
best_confidence = 0.0
|
||||||
best_index = -1
|
best_index = -1
|
||||||
min_artist_similarity = 0.4 # Reject candidates where artist doesn't match at all
|
min_artist_similarity = 0.5
|
||||||
|
min_title_similarity = 0.5
|
||||||
|
|
||||||
source_artist_cleaned = matching_engine.clean_artist(source_artist)
|
source_artist_cleaned = matching_engine.clean_artist(source_artist)
|
||||||
|
source_title_cleaned = matching_engine.clean_title(source_title)
|
||||||
|
source_core_title = matching_engine.get_core_string(source_title)
|
||||||
|
|
||||||
for idx, result in enumerate(search_results):
|
for idx, result in enumerate(search_results):
|
||||||
try:
|
try:
|
||||||
|
|
@ -18136,14 +18138,12 @@ def _discovery_score_candidates(source_title, source_artist, source_duration_ms,
|
||||||
result_name = result.name if hasattr(result, 'name') else ''
|
result_name = result.name if hasattr(result, 'name') else ''
|
||||||
result_duration = result.duration_ms if hasattr(result, 'duration_ms') else 0
|
result_duration = result.duration_ms if hasattr(result, 'duration_ms') else 0
|
||||||
|
|
||||||
# Quick artist sanity check — reject if artist doesn't match at all
|
# Artist floor — both must match, not just the weighted score
|
||||||
# This prevents "right title, wrong artist" false positives
|
|
||||||
best_artist_sim = 0.0
|
best_artist_sim = 0.0
|
||||||
for cand_artist in result_artists:
|
for cand_artist in result_artists:
|
||||||
if not cand_artist:
|
if not cand_artist:
|
||||||
continue
|
continue
|
||||||
cand_cleaned = matching_engine.clean_artist(cand_artist)
|
cand_cleaned = matching_engine.clean_artist(cand_artist)
|
||||||
# Check containment (e.g., "drake" in "drake 21 savage")
|
|
||||||
cand_normalized = matching_engine.normalize_string(cand_artist)
|
cand_normalized = matching_engine.normalize_string(cand_artist)
|
||||||
if source_artist_cleaned and source_artist_cleaned in cand_normalized:
|
if source_artist_cleaned and source_artist_cleaned in cand_normalized:
|
||||||
best_artist_sim = 1.0
|
best_artist_sim = 1.0
|
||||||
|
|
@ -18155,6 +18155,23 @@ def _discovery_score_candidates(source_title, source_artist, source_duration_ms,
|
||||||
if best_artist_sim < min_artist_similarity:
|
if best_artist_sim < min_artist_similarity:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Title floor — both must match, not just the weighted score
|
||||||
|
cand_title_cleaned = matching_engine.clean_title(result_name)
|
||||||
|
cand_core_title = matching_engine.get_core_string(result_name)
|
||||||
|
|
||||||
|
# Core title exact match bypasses the floor (e.g., "edamame" == "edamame")
|
||||||
|
title_passes = False
|
||||||
|
if source_core_title and cand_core_title and source_core_title == cand_core_title:
|
||||||
|
title_passes = True
|
||||||
|
else:
|
||||||
|
title_sim = matching_engine.similarity_score(source_title_cleaned, cand_title_cleaned)
|
||||||
|
if title_sim >= min_title_similarity:
|
||||||
|
title_passes = True
|
||||||
|
|
||||||
|
if not title_passes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Both floors passed — now do full scoring
|
||||||
confidence, match_type = matching_engine.score_track_match(
|
confidence, match_type = matching_engine.score_track_match(
|
||||||
source_title=source_title,
|
source_title=source_title,
|
||||||
source_artists=[source_artist],
|
source_artists=[source_artist],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue