feat(taste): phase 2a — apply the taste profile via a TasteMatch scoring term (#796)
test-go / test (push) Successful in 39s
test-go / integration (push) Successful in 4m34s

The profile built in phase 1 now changes what gets surfaced. Adds a TasteMatch
term to the weighted-shuffle score so candidates are re-ranked by their fit to
the user's learned taste (positive draws toward it; negative reflects passive
avoidance; 0 at cold start).

- recommendation/score.go: ScoringInputs.TasteMatchScore ([-1,+1]) +
  ScoringWeights.TasteWeight + the term in Score.
- recommendation/taste.go: LoadTasteProfile reads the taste_profile_* tables;
  TasteProfile.Match blends the candidate's artist weight (0.7) and avg genre-tag
  weight (0.3), each tanh-squashed by a fixed scale so one outlier artist can't
  compress the rest. Unknown artist/tags and empty profiles → 0 (neutral).
- candidates.go: both candidate loaders set TasteMatchScore per candidate, so
  every Score caller (system playlists incl. You-might-like, radio) becomes
  taste-aware automatically.
- weights: systemMixWeights.TasteWeight = 1.5 (daily mixes are the primary
  taste surface); config.RecommendationConfig gains taste_weight (default 1.0,
  lighter — radio is seed-directed) wired into the radio handler.
- tests: pure (Match curve incl. saturation/clamp/empty-neutral, Score term
  add+subtract) + DB round-trip (seed taste rows → Match positive). All green
  vs real Postgres; existing playlist/radio tests unaffected (empty profile →
  zero taste effect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 21:29:42 -04:00
parent 13b3fca949
commit aff346c731
7 changed files with 237 additions and 7 deletions
+11 -7
View File
@@ -98,6 +98,7 @@ type RecommendationConfig struct {
JitterMagnitude float64 `yaml:"jitter_magnitude"`
ContextWeight float64 `yaml:"context_weight"`
SimilarityWeight float64 `yaml:"similarity_weight"`
TasteWeight float64 `yaml:"taste_weight"`
RecentlyPlayedHours int `yaml:"recently_played_hours"`
RadioSize int `yaml:"radio_size"`
RadioSizeMax int `yaml:"radio_size_max"`
@@ -119,13 +120,16 @@ func Default() Config {
SkipMaxDurationPlayedMs: 30000,
},
Recommendation: RecommendationConfig{
BaseWeight: 1.0,
LikeBoost: 2.0,
RecencyWeight: 1.0,
SkipPenalty: 1.0,
JitterMagnitude: 0.1,
ContextWeight: 2.0,
SimilarityWeight: 2.0,
BaseWeight: 1.0,
LikeBoost: 2.0,
RecencyWeight: 1.0,
SkipPenalty: 1.0,
JitterMagnitude: 0.1,
ContextWeight: 2.0,
SimilarityWeight: 2.0,
// Radio is seed-directed (the user picked a direction), so taste
// is a lighter nudge here than in the daily mixes (1.5).
TasteWeight: 1.0,
RecentlyPlayedHours: 1,
RadioSize: 50,
RadioSizeMax: 200,