feat(tuning): scoring weights → DB-backed admin tuning lab
The recommendation scoring knobs move out of YAML (radio profile) and out of the systemMixWeights hard-code (daily_mix profile) into DB-backed settings with live effect (#1250) — the defaults-discovery lab per decision #1247: the operator turns knobs to find good values, which then get baked back into shipped defaults; end users and other operators should never need the card. - Migration 0040: recommendation_weight_profiles (radio / daily_mix, 8 weight columns), taste_tuning singleton (engagement half-life + completion-curve points), recommendation_tuning_audit (one row per change with a {field, old, new} diff — the trend view's markers, #1251). - internal/recsettings: boot reconcile seeds shipped defaults without clobbering tuned rows (coverart SettingsService pattern), validates patches (bounds, curve ordering), writes audit rows, and pushes daily_mix weights + taste config into package playlists. No-op patches write no audit row. - playlists gains SetSystemMixWeights / SetTasteConfig swap points under a RWMutex — no signature threading through the producers; the scheduler's taste rebuild reads the pushed config. - Radio reads its weight profile from the service per request; the 8 weight fields leave config.RecommendationConfig (YAML keeps only RecentlyPlayedHours / RadioSize / RadioSizeMax). - Admin API: GET/PATCH/reset under /api/admin/recommendation-tuning, echoing current + shipped values. - Web: new admin Tuning tab — two weight profiles side by side, taste card, per-scope save (changed fields only) + reset, deviation dots against shipped defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
@@ -88,20 +88,14 @@ type EventsConfig struct {
|
||||
SkipMaxDurationPlayedMs int `yaml:"skip_max_duration_played_ms"`
|
||||
}
|
||||
|
||||
// RecommendationConfig governs the M3 weighted-shuffle scoring (spec §6).
|
||||
// All weights are operator-tunable; defaults match the spec recommendations.
|
||||
// RecommendationConfig holds the radio path's operational knobs. The
|
||||
// scoring WEIGHTS moved to DB-backed admin settings (#1250,
|
||||
// internal/recsettings) — YAML is bootstrap-only; anything an operator
|
||||
// tunes lives in the UI with live effect.
|
||||
type RecommendationConfig struct {
|
||||
BaseWeight float64 `yaml:"base_weight"`
|
||||
LikeBoost float64 `yaml:"like_boost"`
|
||||
RecencyWeight float64 `yaml:"recency_weight"`
|
||||
SkipPenalty float64 `yaml:"skip_penalty"`
|
||||
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"`
|
||||
RecentlyPlayedHours int `yaml:"recently_played_hours"`
|
||||
RadioSize int `yaml:"radio_size"`
|
||||
RadioSizeMax int `yaml:"radio_size_max"`
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
@@ -120,16 +114,6 @@ 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,
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user