Files
haunt-fm/CLAUDE.md
Thomas Hallock 094621a9a8 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>
2026-02-22 19:14:34 -06:00

3.8 KiB

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

# 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

# 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