Named taste profiles with anonymous guest support #3
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
All listen events and taste profiles are global — there's a single "default" taste profile built from every listen event in the system. This means:
Goal
Support named taste profiles so household members get personalized recommendations, while keeping the system simple and usable for guests who don't have (or want) a profile.
Design Principles
profileparameter (uses "default")Data Model Changes
New:
profilestableidnamedisplay_namecreated_atSeed with a "default" profile on migration.
Modify:
listen_eventsAdd nullable
profile_idcolumn (FK → profiles.id). Existing rows keepprofile_id = NULL, treated as belonging to "default" profile.Modify:
taste_profilesAdd
profile_idcolumn (FK → profiles.id). Replace thenameunique constraint — now keyed by profile_id instead. The existing "default" taste profile gets linked to the "default" profile.New:
speaker_profile_mappingstableidspeaker_nameprofile_idOptional. When a listen event comes in with a
speaker_namethat matches a mapping, auto-setprofile_id. No mapping = stays on "default".API Changes
All recommendation/playlist endpoints: add
profilequery/body paramGET /api/recommendations?profile=antialias&vibe=chillPOST /api/playlists/generatebody:{"profile": "antialias", "vibe": "chill ambient", ...}profileis omitted → use "default" (backward compatible)profiledoesn't exist → 404 or create on-the-fly (TBD)Webhook: attribute listen events to profiles
POST /api/history/webhookpayload gains optionalprofilefield:Resolution order:
profilein payload → use itspeaker_namematches a speaker mapping → use mapped profileNew admin endpoints
GET /api/profiles— list all profilesPOST /api/profiles— create a profile ({"name": "antialias", "display_name": "Me"})DELETE /api/profiles/{name}— delete a profile (reassign events to "default"?)GET /api/profiles/{name}— get profile details + stats (event count, track count, last listen)POST /api/profiles/{name}/build-taste— rebuild taste profile for specific personPUT /api/profiles/{name}/speakers— set speaker mappings for a profileTaste profile building
build_taste_profile()changes:profile_idparameterListenEventbyprofile_id(orprofile_id IS NULLfor "default")On webhook: rebuild only the affected profile's taste (not all profiles).
Guest / Anonymous Flow
Guests don't need a profile at all:
{"vibe": "chill dinner party music", "total_tracks": 15, "auto_play": true}— noprofileparam, works via CLAP text embedding against the full track catalog (cold start path already implemented)Migration Strategy
Alembic migration (003)
profilestable, seed "default" rowspeaker_profile_mappingstableprofile_id(nullable) tolisten_eventsprofile_idtotaste_profiles, link existing "default" rowprofile_id = NULL(treated as "default")Data backfill (optional, manual)
If desired, could retroactively assign historical events by speaker_name:
But this is a manual admin decision, not automated.
OpenClaw Integration
Update the haunt-fm skill doc so OpenClaw can:
"profile": "antialias"in API calls{"profile": "antialias", "vibe": "chill ambient", "speaker_entity": "media_player.study_speaker_2", "auto_play": true}Implementation Order
What does NOT change