The taste profile is just a weighted average of 512-dim vectors — trivially
cheap even with thousands of tracks. Rebuilding on every listen event keeps
recommendations always up to date.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full documentation covering architecture, deployment, API endpoints,
speaker entity mapping, pipeline stages, and how recommendations
improve over time. Fixed stale speaker entity reference.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a track plays, multiple HA entities (Cast, WiFi, MA) all fire
the automation simultaneously, creating 3x duplicate listen events.
Now skips logging if the same track was recorded within the last 60s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The play_playlist_on_speaker function was sending text search queries
to raw Cast entities which can't resolve them. Now uses enqueue: "replace"
for the first track and "add" for subsequent tracks. Added 1s delay between
requests so MA can process each Apple Music search. Increased HTTP timeout
to 30s for search latency.
The caller must pass a Music Assistant entity (_2 suffix) for text-based
search to work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
profile.embedding was being passed as str(numpy_array) which produces
scientific notation format. pgvector needs [n1,n2,...] format. Now
explicitly formats as comma-separated float list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In transformers 5.x, ClapModel.get_audio_features() returns a
BaseModelOutputWithPooling instead of a raw tensor. The 512-dim
embedding is in .pooler_output[0], not directly indexed. Added
backward-compatible extraction with hasattr check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues:
1. CLAP model output needed .flatten() to produce a 1-D vector for
pgvector. Without it, the nested array caused "expected ndim to be 1".
2. Worker now uses a fresh session per track instead of sharing one
across a batch, preventing PendingRollbackError cascading from one
failure to the next.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The except clause wasn't binding the exception to a variable, so
str(Exception) stored the class name "<class 'Exception'>" instead of
the actual error message. Now properly captures `as e` and stores str(e).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SQLAlchemy relationships require ForeignKey on the column definitions,
not just in the migration. Without them, mapper initialization fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pyproject.toml references the package source, so src/ must be
present when pip resolves metadata.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full music recommendation pipeline: listening history capture via webhook,
Last.fm candidate discovery, iTunes preview download, CLAP audio embeddings
(512-dim), pgvector cosine similarity recommendations, playlist generation
with known/new track interleaving, and Music Assistant playback via HA.
Includes: FastAPI app, SQLAlchemy models, Alembic migrations, Docker Compose
with pgvector/pg17, status dashboard, and all API endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>