Add named taste profiles for per-person recommendations

Named profiles allow each household member to get personalized
recommendations without polluting each other's taste. Includes
profile CRUD API, speaker→profile auto-attribution, recent listen
history endpoint, and profile param on all existing endpoints.
All endpoints backward compatible (no profile param = "default").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 19:14:34 -06:00
parent 1b739fbd20
commit 094621a9a8
14 changed files with 556 additions and 33 deletions

View File

@@ -33,11 +33,29 @@ class Track(Base):
embedding: Mapped["TrackEmbedding | None"] = relationship(back_populates="track")
class Profile(Base):
__tablename__ = "profiles"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
name: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
display_name: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class SpeakerProfileMapping(Base):
__tablename__ = "speaker_profile_mappings"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
speaker_name: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
profile_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("profiles.id"), nullable=False)
class ListenEvent(Base):
__tablename__ = "listen_events"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
track_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("tracks.id"), nullable=False)
profile_id: Mapped[int | None] = mapped_column(BigInteger, ForeignKey("profiles.id"))
source: Mapped[str] = mapped_column(Text, nullable=False, default="music_assistant")
speaker_name: Mapped[str | None] = mapped_column(Text)
listened_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
@@ -82,6 +100,7 @@ class TasteProfile(Base):
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
name: Mapped[str] = mapped_column(Text, unique=True, nullable=False, default="default")
profile_id: Mapped[int | None] = mapped_column(BigInteger, ForeignKey("profiles.id"), unique=True)
embedding = mapped_column(Vector(512), nullable=False)
track_count: Mapped[int] = mapped_column(Integer, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())