Recommendation Resolvers¶
This document explains how seed input becomes a ranked recommendation list.
Entry Points¶
- CLI command:
musicseed recommendincli/src/musicseed_cli/commands/recommend.py. - Orchestration:
core/src/musicseed/recommender/playlist.py. - Candidate generation:
core/src/musicseed/recommender/candidates.py. - Scoring:
core/src/musicseed/recommender/scoring.py. - Sonic vectors (read from Plex at query time):
core/src/musicseed/sonic.py.
Seed Resolution¶
Seeds can be provided as database IDs or text.
--seed-idresolves directly byTrack.id.--seed "Artist - Title"first attempts exact lowercase artist and title matching.- Text without
Artist - Titlematches by title. - Ambiguous text matches raise an error and include candidate IDs.
This is intentional. For a personal music library, choosing the wrong seed silently is worse than asking the user for a better seed.
Seed Profile¶
Resolved seed tracks are combined into a SeedProfile:
- Track IDs to exclude from recommendations.
- Average of the seeds' Plex sonic vectors (looked up by
plex_id); absent when no seed has one. - Union of seed styles and genres.
- Average seed year.
- Average seed popularity on a 0-100 scale.
Multiple seeds should represent a shared target vibe. If a change makes multi-seed behavior less
predictable, update this doc and the --explain output.
Candidate Pool¶
build_candidate_pool() gathers overlapping candidate IDs from available signals:
- Sonic neighbors: top-N cosine-nearest Plex sonic vectors, computed in memory with one numpy
matmul over the vector matrix and mapped from
plex_idback to tracks. - Tracks sharing seed genres.
- Tracks sharing seed styles.
- Tracks near the seed era.
- Tracks near seed popularity.
- Low-play-count tracks for novelty.
The candidate pool is intentionally larger than the requested playlist length, allowing scoring and artist diversity constraints to shape the final result.
Scoring¶
calculate_score() computes component scores and a weighted total:
sonic: cosine similarity normalized to 0-1.popularity: proximity to seed popularity.style: Jaccard overlap.genre: Jaccard overlap.era: proximity within a 50-year window.novelty: inverse function of play count.
Default weights live in Weights and mirror RecommendationWeights in config:
sonic=0.30
popularity=0.15
style=0.10
genre=0.15
era=0.05
novelty=0.10
Weights are normalized by their sum at scoring time, so removing a signal shifts relative weight to the remaining signals proportionally — no manual rebalancing needed.
Mood was removed from scoring because it introduced noise for this library. Plex mood tags are
still stored and visible in status, but excluded from Weights, ScoreBreakdown, SeedProfile,
and build_candidate_pool().
Selection¶
After scoring:
- Seed tracks are excluded.
- Candidates are sorted by total score descending.
min_scoreacts as an early-exit gate: the first candidate whosescore.totalfalls below the threshold stops selection — because the list is sorted descending, everything after it would also fail the threshold.max_tracks_per_artistis applied as a diversity constraint within the passing candidates.- The top
limitselected recommendations are returned.
min_score defaults to None (no cutoff). When supplied via --min-score, it must be in
[0.0, 1.0]. The result may contain fewer than limit tracks when the threshold is active.
Artist diversity should remain a final constraint unless there is a clear reason to make it part of scoring.
Explainability¶
--explain should expose enough detail to answer:
- Which sources produced this candidate?
- Which score components were strong or weak?
- Did a selection constraint affect the final playlist?
When adding a signal, update:
- Candidate source labels.
ScoreBreakdown.- CLI explain output.
- This document.
Safe Change Checklist¶
- Run
python3 -m compileall -q src/musicseed. - Run
uv run ruff check srcif dependencies are available. - Use a dry run:
musicseed-cli recommend --seed-id 123 --limit 20 --dry-run --explain. - Confirm tracks without sonic vectors, missing popularity, and missing tags do not crash scoring.
- Confirm a missing Plex blobs database fails
recommendwith a clearNotFoundError. - Confirm ambiguous seed text still fails clearly.