Make the artist a gate, not a quarter of the score
Requesting "Michael Jackson - Chicago" filed The Fall's "Chicago, Now!" from Extricate. The chain: the artist name is filtered by the Soulseek server, so the ladder correctly fell back to the bare title -- at which point the result set is every song anyone has ever called Chicago, and the artist is the only thing telling them apart. match_ratio weights the artist at 0.25, so a perfect title with no artist anywhere scores 0.75. That clears MIN_MATCH, and the ~22 point gap it leaves against a genuine match is nothing next to the 340 points score() awards for a free upload slot and a short queue. An idle peer holding the wrong song therefore beat a busy peer holding the right one, every time. That is the failure the confidence score was explicitly designed to prevent -- "a fast peer must not be able to raise the confidence of a wrong match" -- and rewriting score() to rank availability first reintroduced it on the other side of the wall. Availability is a tiebreak among plausible files; it is never a reason to take a different song. Candidates whose path names the artist are now kept outright and the rest discarded, falling back to title-only matches only when there are none. The downstream confidence gate still ran correctly here: it scored 0.75, said "artist not found in path", and held the file for review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0c5b5af00
commit
5b17d5fc8d
3 files changed
+85
-3
No files matched your search
@@ -218,9 +218,24 @@ and finally the artist itself. Since most requests are answered by the first
|
|||||||
query, the extra searches cost nothing in the common case — `MUSE_MAX_QUERIES`
|
query, the extra searches cost nothing in the common case — `MUSE_MAX_QUERIES`
|
||||||
caps how far it will go.
|
caps how far it will go.
|
||||||
|
|
||||||
Dropping the artist from the *query* does not mean ignoring it. The artist is
|
Dropping the artist from the *query* does not mean ignoring it — it makes the
|
||||||
then matched against the whole **folder path**, which is where a well-organised
|
artist matter more. A bare-title search stops returning "this song from various
|
||||||
share puts it anyway:
|
peers" and starts returning *every song anyone ever called Chicago*: The Fall,
|
||||||
|
Groove Armada, Sufjan Stevens. The artist is then the only thing separating the
|
||||||
|
right recording from a different song entirely, so it is a **gate**: candidates
|
||||||
|
whose path names the artist are preferred outright, and ones that do not are
|
||||||
|
used only when nothing else is left.
|
||||||
|
|
||||||
|
That has to be a gate rather than a weighting, and the reason is worth stating.
|
||||||
|
A perfect title with no artist scores 0.75, comfortably above `MUSE_MIN_MATCH`,
|
||||||
|
so as a mere contribution it loses to availability — and an idle peer holding
|
||||||
|
the wrong song beats a busy one holding the right song. That is not
|
||||||
|
hypothetical either: it filed The Fall's "Chicago, Now!" as Michael Jackson.
|
||||||
|
The same principle the confidence score is built on applies here: a fast peer
|
||||||
|
must not be able to promote a wrong match.
|
||||||
|
|
||||||
|
The artist is matched against the whole **folder path**, which is where a
|
||||||
|
well-organised share puts it anyway:
|
||||||
|
|
||||||
```
|
```
|
||||||
anjya music/Michael Jackson/1982 - Thriller [flac]/06 - Billie Jean.flac
|
anjya music/Michael Jackson/1982 - Thriller [flac]/06 - Billie Jean.flac
|
||||||
|
|||||||
@@ -481,6 +481,30 @@ def similarity(want: str, got: str) -> float:
|
|||||||
return max(seq, len(a & b) / len(a) if a else 0.0)
|
return max(seq, len(a & b) / len(a) if a else 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def artist_confirmed(remote_path: str, artist: str) -> bool:
|
||||||
|
"""Is the requested artist actually named somewhere in this path?
|
||||||
|
|
||||||
|
This is a gate rather than a score because of what the query ladder does.
|
||||||
|
Once a filtered artist name forces the search down to the bare title, the
|
||||||
|
result set stops being "this song from various peers" and becomes "every
|
||||||
|
song anyone ever called Chicago" -- The Fall, Groove Armada, Sufjan
|
||||||
|
Stevens. The artist is then the only thing distinguishing the right
|
||||||
|
recording from a completely different song, and a weighted contribution is
|
||||||
|
not enough: 0.75 for a perfect title with no artist still clears MIN_MATCH,
|
||||||
|
and an available stranger will out-score a queued match every time.
|
||||||
|
"""
|
||||||
|
na = norm(artist)
|
||||||
|
if not na:
|
||||||
|
return False
|
||||||
|
path = norm(remote_path.replace("\\", "/"))
|
||||||
|
if na in path:
|
||||||
|
return True
|
||||||
|
# Every distinctive word, in any order: catches "Jackson, Michael" and
|
||||||
|
# "MJ - Chicago (Michael Jackson)" without matching on "the" or "and".
|
||||||
|
words = [w for w in na.split() if len(w) > 3]
|
||||||
|
return bool(words) and all(w in path for w in words)
|
||||||
|
|
||||||
|
|
||||||
def match_ratio(remote_path: str, artist: str, title: str) -> float:
|
def match_ratio(remote_path: str, artist: str, title: str) -> float:
|
||||||
"""How well a remote path matches the track asked for, 0..1.
|
"""How well a remote path matches the track asked for, 0..1.
|
||||||
|
|
||||||
@@ -1078,6 +1102,20 @@ async def pick_and_enqueue(job_id: str, artist: str, title: str,
|
|||||||
set_state(job_id, "failed", no_candidate_detail(seen, stats, queries))
|
set_state(job_id, "failed", no_candidate_detail(seen, stats, queries))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Prefer candidates whose path actually names the artist, and only settle
|
||||||
|
# for ones that do not when there is nothing else. Availability is a
|
||||||
|
# tiebreak among plausible files, never a reason to take the wrong song.
|
||||||
|
named = {k: v for k, v in pool.items()
|
||||||
|
if artist_confirmed(v[2]["filename"], artist)}
|
||||||
|
if named:
|
||||||
|
if len(named) < len(pool):
|
||||||
|
log.info("job %s keeping %d/%d candidates that name %r",
|
||||||
|
job_id[:8], len(named), len(pool), artist)
|
||||||
|
pool = named
|
||||||
|
else:
|
||||||
|
log.info("job %s no candidate names %r; falling back to title-only "
|
||||||
|
"matches, which review will have to judge", job_id[:8], artist)
|
||||||
|
|
||||||
ranked = sorted(pool.values(), key=lambda r: -r[0])
|
ranked = sorted(pool.values(), key=lambda r: -r[0])
|
||||||
cands = spread_by_peer(ranked)[:CANDIDATES]
|
cands = spread_by_peer(ranked)[:CANDIDATES]
|
||||||
with db() as c:
|
with db() as c:
|
||||||
|
|||||||
@@ -161,6 +161,35 @@ def main() -> int:
|
|||||||
check("reordered filename still matches",
|
check("reordered filename still matches",
|
||||||
app.match_ratio(r"x\Creep - Radiohead.mp3", "Radiohead", "Creep") == 1.0)
|
app.match_ratio(r"x\Creep - Radiohead.mp3", "Radiohead", "Creep") == 1.0)
|
||||||
|
|
||||||
|
print("\nthe artist gate")
|
||||||
|
check("artist named in the folder is confirmed",
|
||||||
|
app.artist_confirmed(r"m\Michael Jackson\Xscape\02 Chicago.flac",
|
||||||
|
"Michael Jackson"))
|
||||||
|
check("a different band's song with the same title is not",
|
||||||
|
not app.artist_confirmed(
|
||||||
|
r"@@jwbvv\Music\The Fall\1990-Extricate\08-Chicago, Now!.flac",
|
||||||
|
"Michael Jackson"))
|
||||||
|
check("out-of-order naming still counts",
|
||||||
|
app.artist_confirmed(r"x\Jackson, Michael - Chicago.flac",
|
||||||
|
"Michael Jackson"))
|
||||||
|
|
||||||
|
# The real regression: bare-title search returns every song called
|
||||||
|
# "Chicago", and an available stranger used to outrank a queued match.
|
||||||
|
gate = FakeSlskd(per_query={"Chicago": [
|
||||||
|
peer("stranger", r"@@x\Music\The Fall\1990-Extricate\08-Chicago, Now!.flac",
|
||||||
|
slot=True, queue=0),
|
||||||
|
peer("correct", r"m\Michael Jackson\XSCAPE\02 - Chicago.flac",
|
||||||
|
slot=False, queue=300),
|
||||||
|
]})
|
||||||
|
app.slskd_client = gate
|
||||||
|
make_job("j6", "Michael Jackson", "Chicago")
|
||||||
|
asyncio.run(app.pick_and_enqueue("j6", "Michael Jackson", "Chicago"))
|
||||||
|
r = job_row("j6")
|
||||||
|
check("the right artist wins even from a peer with 300 queued",
|
||||||
|
r["username"] == "correct", f"picked {r['username']}")
|
||||||
|
check("and the wrong-artist file is dropped, not merely ranked lower",
|
||||||
|
len(json.loads(r["candidates"] or "[]")) == 0, r["candidates"])
|
||||||
|
|
||||||
print("\ncandidate ladder")
|
print("\ncandidate ladder")
|
||||||
fake = FakeSlskd(per_query={
|
fake = FakeSlskd(per_query={
|
||||||
"Billie Jean": [peer("alice", r"m\Michael Jackson\Billie Jean.flac"),
|
"Billie Jean": [peer("alice", r"m\Michael Jackson\Billie Jean.flac"),
|
||||||
|
|||||||
Reference in new issue
Block a user