From ffc4d1f483f6ba8aacd9da8e4183f4324290269a Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 10 Jul 2026 18:31:09 +0200 Subject: [PATCH] feat: add candidate ranker with confidence gate --- worker/lyra_worker/ranker.py | 15 +++++++++++++ worker/tests/test_ranker.py | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 worker/lyra_worker/ranker.py create mode 100644 worker/tests/test_ranker.py diff --git a/worker/lyra_worker/ranker.py b/worker/lyra_worker/ranker.py new file mode 100644 index 0000000..8cd24db --- /dev/null +++ b/worker/lyra_worker/ranker.py @@ -0,0 +1,15 @@ +from dataclasses import replace + +from lyra_worker.confidence import score_confidence +from lyra_worker.quality import rank_key +from lyra_worker.types import Candidate, MBTarget + + +def rank_candidates( + target: MBTarget, candidates: list[Candidate], min_confidence: float = 0.7 +) -> list[Candidate]: + """Score, gate by confidence, and sort best-first (quality, then confidence).""" + scored = [replace(c, confidence=score_confidence(target, c)) for c in candidates] + kept = [c for c in scored if c.confidence >= min_confidence] + kept.sort(key=lambda c: (rank_key(c), c.confidence), reverse=True) + return kept diff --git a/worker/tests/test_ranker.py b/worker/tests/test_ranker.py new file mode 100644 index 0000000..5b91e0c --- /dev/null +++ b/worker/tests/test_ranker.py @@ -0,0 +1,42 @@ +from lyra_worker.ranker import rank_candidates +from lyra_worker.types import Candidate, MBTarget, Quality + +HIRES = Quality(fmt="FLAC", lossless=True, bit_depth=24, sample_rate=96000) +CD = Quality(fmt="FLAC", lossless=True, bit_depth=16, sample_rate=44100) +MP3 = Quality(fmt="MP3", lossless=False, bitrate_kbps=320) +TARGET = MBTarget(artist="Radiohead", album="In Rainbows", track_count=10) + + +def _cand(source, quality, artist="Radiohead", album="In Rainbows", tracks=10): + return Candidate(source=source, source_ref=f"{source}:{quality.fmt}", + matched_artist=artist, matched_album=album, + quality=quality, track_count=tracks) + + +def test_returns_empty_for_no_candidates(): + assert rank_candidates(TARGET, []) == [] + + +def test_best_quality_wins_among_correct_matches(): + ranked = rank_candidates(TARGET, [ + _cand("youtube", MP3), + _cand("qobuz", HIRES), + _cand("soulseek", CD), + ]) + assert [c.source for c in ranked] == ["qobuz", "soulseek", "youtube"] + assert all(c.confidence > 0.9 for c in ranked) + + +def test_confidence_gate_drops_wrong_album_even_if_higher_quality(): + ranked = rank_candidates(TARGET, [ + _cand("qobuz", HIRES, album="OK Computer", tracks=12), # wrong release, hi-res + _cand("soulseek", CD), # correct, lossless + ]) + assert [c.source for c in ranked] == ["soulseek"] + + +def test_all_below_threshold_returns_empty(): + ranked = rank_candidates(TARGET, [ + _cand("qobuz", HIRES, artist="Nirvana", album="Nevermind"), + ]) + assert ranked == []