Skip to content
codeC02003Public

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

UReco

U R recognised

A music recommender that works out why you skipped: bad song, or wrong moment?

Live mood detection built on its own audio analysis. No API keys, no ratings, no thumbs.

Python 3.12 Tests No API key

UReco interface showing the live queue, mood axes and the engine explaining its reasoning


Every music recommendation system knows that you skipped a track. Almost none know why. So they treat two opposite facts as the same thumbs down:

"I hate this song." and "I love this song, it is wrong for right now."

Conflating those is why skipping a favourite once can bury it for months. UReco splits them apart using signals it can actually observe: how far into the track you got, whether you have ever finished it before, whether it matched the mood you were already in, and whether you happen to be skipping everything right now.

There are no rating buttons. The only input is how long you stayed before moving on.


Quick start

git clone https://github.com/codeC02003/UReco.git
cd UReco
pip install -r requirements.txt

python seed.py                                    # build a pool from Deezer charts
python -m uvicorn ureco.server:app --port 8420    # then open http://127.0.0.1:8420

No API keys and no OAuth. Deezer's public API needs neither, and ships a 30 second MP3 preview per track which is downloaded and analysed locally.

python -m pytest                       # 74 tests
python sim.py                          # simulated listener: does the queue learn?
python sim_open.py                     # does remembering a listener actually help?
python seed.py --collection bollywood  # curated collections

Full length tracks

Point it at your own music. Deezer's public API caps at exactly 30 seconds per track and there is no full length endpoint, which is a licensing limit rather than a missing feature. Spotify's Web Playback SDK does play whole songs, but it plays them inside a black box with no access to the samples, which would remove the one thing this project is built on.

Local files are the only source that gives you both the whole song and the audio to analyse, and they sit in the same pool as streamed tracks:

# use "Add your own music" in the sidebar, or:
curl -X POST localhost:8420/api/library -H 'Content-Type: application/json' \
     -d '{"folder": "~/Music"}'

A full file is also better to analyse than a preview. The analysis window is taken from the middle of the track rather than a fixed slice near the start, so an intro heavy song is not judged on its intro.


What makes it different

1. It measures the audio itself

Spotify removed /audio-features, /audio-analysis and /recommendations for all new applications in November 2024. There is no API left to ask. So every number here is computed from the waveform with librosa, and reduced to eight axes a person can read:

axis low to high measured from
energy quiet, loud RMS, onset rate, energy above 2 kHz
tempo slow, fast beat tracked BPM
pulse loose, driving tempogram autocorrelation peak
density sparse, busy onsets per second
brightness dark, bright spectral centroid and rolloff
roughness smooth, gritty zero crossing rate, flatness, percussive share
dynamics flat, dynamic dB gap between quiet and loud moments
lift heavy, upbeat major and minor lean, Krumhansl-Kessler profiles

pulse reads 0.00 for a Chopin nocturne and 0.92 for a Calvin Harris single. dynamics reads -3 dB for brickwalled metal and -15 dB for acoustic jazz.

2. It reads a skip instead of counting it

In rough order of how much each signal tells you:

  1. Have you ever finished this track before? The strongest signal available. If you have, you demonstrably like it, so skipping it now is about your state, not your taste.
  2. How far in did you get? Inside the first 15% is rejection on contact. A third of the way in means you gave it a real chance and it stopped working.
  3. Did it match the mood you were already in? A track that sat where you were listening, that you still bailed on, is about the song.
  4. Are you skipping everything? Four skips in a row is not four bad songs.
  5. Heard it three times this week? Loved it, wore it out. Temporary.

Verdicts: completed engaged replayed, mood_mismatch context_shift not_now satiated, dislike hate

The rule the whole design rests on: a taste verdict never moves the mood, and a mood verdict never damages an artist or counts towards a ban. Learning that you dislike a song tells you nothing about whether you want something calmer right now.

3. No "never again" button, and it still learns to drop things

what you do what it concludes
move on within seconds, twice that is the song. Dropped.
move on midway, every time it comes round forgiven as "wrong moment" three times. On the fourth, the moments were all different and the answer never changed, so it goes.
move on from something you once played through always the moment, never the song. Never dropped, however often it happens.

4. It learns you, not your history

Learning is split so that songs stay temporary and the listener does not:

Remembered. Your bail point, how fast your mood turns, which axes decide your skips, where you sit per genre and per hour, and the recurring moods you return to.

Forgotten each session. The current mood, dropped songs, per track counts, artist affinity.

The profile holds no track id and no artist name. A song dropped last Tuesday starts today with a clean slate.

Alongside it, a model trained on your own plays predicts whether you will stay past your own halfway bar. It is blended into the ranking in proportion to how well it has actually done, cross validated against its own shuffled baseline, and capped at 0.65 so a bad patch of data can never take the queue over. Cold, the rules run everything.

5. Pick a song and the queue follows its mood

Searching for a track is the most deliberate thing you can do here, so it outranks whatever the session had settled into. The song's audio profile becomes the mood anchor, strongly at first and fading as real listening replaces it, and the queue continues from that sound.

A curated collection will lock the queue, because it is a real musical grouping. A raw Deezer genre will not, because it is too coarse to cage a mood in: a Bollywood ballad resolves to "Films/Games", which it shares with Western film scores.

6. One session, one genre

The first track you play locks the session's genre. Everything after is chosen inside it while all eight axes still move freely, so you can start on a jazz record and stay in jazz while the queue follows you from sparse and dark towards busy and bright.

Collections exist because Deezer has no Bollywood genre. Checked against real tracks, the repertoire scatters across six genres at once:

Films/Games  17    Indian Music  4    (none)  2
Pop           7    Asian Music   4    Dance   2

A collection is a curated artist list tagged with an id of its own, which the lock treats like any genre. Bollywood, Classic Bollywood, Punjabi and Tamil ship with it.


Does it work?

sim.py runs a synthetic listener with a hidden preference against a pool where 25% of tracks would satisfy them by chance, then flips their mood halfway through.

40.5% of tracks played past the halfway bar, against a 25% random baseline. That is 1.6 times random, measured over 12 seeds on a 297 track pool. A good run climbs from around 10% at cold start to around 67% once it has a read.

sim_open.py tests whether remembering a listener helps, using a listener with two genuine moods who picks one each evening. 24 listeners, 6 sessions each:

arm whole session first 8 tracks
habit memory only 55% ±1.7 42% ±2.8
reads the mood 55% ±1.6 47% ±3.0
oracle (told the answer) 63% ±1.1 75% ±2.1

Read honestly, mood reading is worth about +5 points at session open at roughly 1.2 standard errors, which is suggestive rather than established. The oracle row is the interesting one. Simply knowing which mood is live is worth +33 points, so the information is enormously valuable and the detector currently delivers a fraction of it. The engineering notes work through exactly why.


Things that were measured and cut

Kept in the repo because a negative result is worth more than a feature nobody checked.

  • Mood extrapolation. Aiming ahead of a listener whose energy is climbing reads well and does not work, costing 3 to 4 points even against a listener who genuinely drifts. The slope is fitted to six 8 dimensional vectors off 30 second previews, so extrapolating mostly amplifies noise. VELOCITY_HORIZON = 0.0.
  • Predicting listened ratio with a linear model. Cross validated R² of exactly 0.000 on data generated from those very axes. Listening is bimodal, since people either bail in seconds or stay for most of a song, and liking is peaked, so a signed delta cannot express it. Fixed by predicting "will they stay past halfway" from the absolute per axis gap.
  • Clustering moods over tracks rather than sessions. No better, at 49% to 44% of true separation.
  • Probing from the first track. Chose informative openers over enjoyable ones even when the hour already said 82% which mood it was.

Honest limits

  • Previews are 30 seconds. "Played 50%" means 15 seconds, and an intro heavy track is judged on its intro. Local files do not have this problem.
  • The simulated listener is not a person. It reacts to feature distance with Gaussian noise. No lyrics, no memories, no friends in the room.
  • lift is a major and minor lean, not valence, and it is Western tonal theory, so it means less for repertoire built on ragas.
  • Tempo comes back an octave out sometimes. Portishead's "Roads" reads 152 BPM against a true 76.
  • Cold start is genuinely cold, and the interface says so rather than pretending.

Architecture

ureco/
  audio.py         preview or file, to waveform, to the eight axes
  features.py      the mood space, distance, per listener axis weighting
  skip.py          why a skip happened            <- the core idea
  mood.py          live mood, and what may move it
  modes.py         the moods you return to, and reading which one is live
  profile.py       what persists between sessions (no track ids)
  learner.py       a model of you, gated so that it has to earn its influence
  taste.py         long run affinity, Beta posteriors, fatigue
  recommender.py   scoring and ranking
  engine.py        play, classify, update, re-rank, explain
  explain.py       turns the numbers into sentences
  catalog.py       Deezer client, rate limited, with collections
  local.py         your own files, analysed at full length
  store.py         SQLite, one connection per thread
  server.py        FastAPI
web/               player interface: transport, queue, live mood axes, reasoning feed

Stack: Python 3.12, librosa, numpy, scikit-learn, FastAPI, SQLite, vanilla JavaScript.

Full engineering write up, including the bugs a simulator caught that unit tests did not: docs/ENGINEERING.md

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages