feat(taste): mood taste facet — #1534
Milestone #160 Opt 2b (mood half of the era+mood option). A fourth taste facet alongside artists + genre tags + eras: signed weights over canonical mood buckets (melancholic / energetic / chill / …) derived from a track's enriched folksonomy tags (#1490). - internal/mood: shared vocabulary — Of(tags) maps folksonomy tags to canonical mood buckets (synonyms collapse). Imported by both the taste builder and the scorer so a track's mood is derived identically. - Migration 0047: taste_profile_moods table + taste_tuning.mood_scale (DEFAULT 0.5). - Build side (internal/taste): Config.MoodScale ([0,1] damper, mirrors EraScale); accumulate folds each play/like's mood buckets at base*MoodScale; persist atomic-replaces the mood rows. - Scorer (internal/recommendation): TasteProfile gains a mood term (own tanh scale + additive 0.12 share, so it never weakens the existing signal when a track has no mood tags). Match now takes the candidate's mood buckets; loaded per candidate (ListTrackTagsForTracks → mood.Of) in the primary similarity loader only — the near-whole-library fallback pool passes nil (mood → 0) to avoid a full-library tag scan. - Tuning lab: mood_scale threaded through recsettings + admin API + web card ("Mood weight" row) + Go/web tests. Coverage is partial (grows with tag enrichment; richer once Last.fm is keyed), so mood is a supplement — neutral for tracks with no mood tags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// Package mood maps folksonomy tags (from the track_tags cache, #1490) to a
|
||||
// small set of canonical mood buckets (#1534, milestone #160 Opt 2b). Both the
|
||||
// taste-profile builder and the recommendation scorer classify a track's tags
|
||||
// through Of() so a track's mood is derived the same way on both sides.
|
||||
//
|
||||
// The vocabulary is intentionally narrow: only words that clearly denote mood
|
||||
// (not genre). A track with no mood-word tags contributes nothing — the mood
|
||||
// facet is a supplement whose coverage grows with folksonomy enrichment
|
||||
// (thin on MusicBrainz-only, richer once Last.fm is keyed).
|
||||
package mood
|
||||
|
||||
import "strings"
|
||||
|
||||
// vocab maps a lowercased folksonomy tag to its canonical mood bucket. Several
|
||||
// synonyms collapse to one bucket so "sad" / "melancholy" / "melancholic" all
|
||||
// score the same dimension.
|
||||
var vocab = map[string]string{
|
||||
// melancholic
|
||||
"melancholic": "melancholic", "melancholy": "melancholic", "sad": "melancholic",
|
||||
"sombre": "melancholic", "somber": "melancholic", "wistful": "melancholic",
|
||||
"bittersweet": "melancholic", "mournful": "melancholic",
|
||||
// energetic
|
||||
"energetic": "energetic", "energy": "energetic", "upbeat": "energetic",
|
||||
"uplifting": "energetic", "party": "energetic", "anthemic": "energetic",
|
||||
// chill
|
||||
"chill": "chill", "chillout": "chill", "chilled": "chill", "mellow": "chill",
|
||||
"relaxing": "chill", "relaxed": "chill", "calm": "chill", "laid-back": "chill",
|
||||
"laidback": "chill", "soothing": "chill",
|
||||
// aggressive
|
||||
"aggressive": "aggressive", "angry": "aggressive", "intense": "aggressive",
|
||||
"heavy": "aggressive", "brutal": "aggressive",
|
||||
// dark
|
||||
"dark": "dark", "moody": "dark", "brooding": "dark", "haunting": "dark", "eerie": "dark",
|
||||
// dreamy
|
||||
"dreamy": "dreamy", "ethereal": "dreamy", "atmospheric": "dreamy",
|
||||
"ambient": "dreamy", "hypnotic": "dreamy",
|
||||
// happy
|
||||
"happy": "happy", "cheerful": "happy", "feel good": "happy", "feel-good": "happy",
|
||||
"feelgood": "happy", "joyful": "happy", "fun": "happy",
|
||||
// romantic
|
||||
"romantic": "romantic", "sensual": "romantic", "sexy": "romantic", "sentimental": "romantic",
|
||||
}
|
||||
|
||||
// Of returns the distinct canonical mood buckets present in tags, lowercasing +
|
||||
// trimming each tag before lookup. Returns nil when no tag maps to a mood, so
|
||||
// callers get no mood signal for a track with only genre/descriptive tags.
|
||||
func Of(tags []string) []string {
|
||||
var out []string
|
||||
seen := make(map[string]bool)
|
||||
for _, t := range tags {
|
||||
key := strings.ToLower(strings.TrimSpace(t))
|
||||
if m, ok := vocab[key]; ok && !seen[m] {
|
||||
seen[m] = true
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user