# haunt-fm Personal music recommendation service. Captures listening history from Music Assistant, discovers similar tracks via Last.fm, embeds audio with CLAP, and generates playlists. ## Quick Start ```bash # On NAS cd /volume1/homes/antialias/projects/haunt-fm docker compose up -d docker compose exec haunt-fm alembic upgrade head ``` ## Architecture - **FastAPI** app with async SQLAlchemy + asyncpg - **PostgreSQL + pgvector** for tracks, embeddings, and vector similarity search - **CLAP model** (laion/larger_clap_music) for 512-dim audio embeddings - **Last.fm API** for track similarity discovery - **iTunes Search API** for 30-second audio previews - **Music Assistant** (via Home Assistant REST API) for playback ## Key Commands ```bash # Health check curl http://192.168.86.51:8321/health # Log a listen event curl -X POST http://192.168.86.51:8321/api/history/webhook \ -H "Content-Type: application/json" \ -d '{"title":"Song","artist":"Artist"}' # Run discovery curl -X POST http://192.168.86.51:8321/api/admin/discover -H "Content-Type: application/json" -d '{}' # Get recommendations curl http://192.168.86.51:8321/api/recommendations?limit=20 # Get recommendations for a specific profile curl "http://192.168.86.51:8321/api/recommendations?limit=20&profile=antialias" # Generate and play a playlist curl -X POST http://192.168.86.51:8321/api/playlists/generate \ -H "Content-Type: application/json" \ -d '{"total_tracks":20,"known_pct":30,"speaker_entity":"media_player.living_room_speaker_2","auto_play":true}' # Generate a vibe-based playlist (mood/activity matching) curl -X POST http://192.168.86.51:8321/api/playlists/generate \ -H "Content-Type: application/json" \ -d '{"total_tracks":15,"vibe":"chill ambient lo-fi","speaker_entity":"media_player.living_room_speaker_2","auto_play":true}' # Vibe with custom blend (alpha: 0=pure vibe, 0.5=blend, 1=pure taste) curl -X POST http://192.168.86.51:8321/api/playlists/generate \ -H "Content-Type: application/json" \ -d '{"total_tracks":15,"vibe":"upbeat party music","alpha":0.3,"auto_play":true,"speaker_entity":"media_player.living_room_speaker_2"}' # Generate playlist for a specific profile curl -X POST http://192.168.86.51:8321/api/playlists/generate \ -H "Content-Type: application/json" \ -d '{"total_tracks":20,"profile":"antialias","speaker_entity":"media_player.study_speaker_2","auto_play":true}' # Create a profile curl -X POST http://192.168.86.51:8321/api/profiles \ -H "Content-Type: application/json" \ -d '{"name":"antialias","display_name":"Me"}' # Map speakers to a profile curl -X PUT http://192.168.86.51:8321/api/profiles/antialias/speakers \ -H "Content-Type: application/json" \ -d '{"speakers":["Study speaker","Master bathroom speaker"]}' # Build taste profile for a specific profile curl -X POST "http://192.168.86.51:8321/api/admin/build-taste-profile?profile=antialias" ``` ## Environment Variables All prefixed with `HAUNTFM_`. See `.env.example` for full list. ## Database - Alembic migrations in `alembic/versions/` - Run migrations: `alembic upgrade head` - Schema: tracks, listen_events, track_embeddings, similarity_links, taste_profiles, playlists, playlist_tracks, profiles, speaker_profile_mappings ## Named Profiles Named profiles allow per-person taste tracking. No auth — just named buckets. - **Default behavior**: All endpoints without `profile` param use the "default" profile (backward compatible) - **Profile CRUD**: `GET/POST /api/profiles`, `GET/DELETE /api/profiles/{name}` - **Speaker mappings**: `PUT/GET /api/profiles/{name}/speakers` — auto-attributes listen events from mapped speakers - **Attribution**: Webhook accepts `"profile": "name"` or auto-resolves from speaker→profile mapping - **Recommendations/playlists**: Pass `profile=name` to use that profile's taste