You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(enrichment): external audio-features provider as primary energy source (+ Lexicon override, LLM fallback) #544
Part of the pool→builder data-contract epic (#539). Provides the energy (+ mood/danceability/valence) columns for the global store + request-time enrichment.
Why
Energy is the one required field no current integration supplies: Beatport/Tidal give bpm/key, MusicBrainz gives genre, but none give energy/danceability, and Spotify's audio-features API (energy/danceability/valence/…) was deprecated on 2024-11-27 (new apps get 403; no official replacement). The chosen strategy is an external audio-features API as the primary, whole-pool-coverage energy source, with Lexicon measured energy as an override where the DJ has it, and LLM-inferred energy as the last-resort fallback.
Scope
New provider adapter behind a clean interface, config-gated, writing energy (0–10 normalized) + source/freshness into the global tracks row (and thus available at request time and in the setbuilder pool).
Capture the whole audio-feature payload, not just energy. The chosen providers return danceability + valence (and acousticness/instrumentalness/speechiness/liveness) in the same call as energy, so storing them is zero marginal API cost. Add energy, danceability, valence as the near-term trio and the rest as nullable columns in the same migration — re-fetching the whole catalog later to backfill them would pay the call twice. This feeds future WrzDJSet pass-1 sequencing (energy/mood/danceability-aware), where energy × valence forms the canonical mood quadrant for emotional-arc sequencing. (YAGNI deliberately does not apply — see epic(setbuilder): pool→builder data contract + global enriched-track store #539.)
Also capture explicit (boolean). Soundcharts returns it in the same payload. Unlike the audio features (which weight ordering), explicit is a hard pool pre-filter enabling a future "clean set / all-ages event" toggle (e.g. family events) — a one-line pool filter later instead of a re-enrichment pass. Free to store now.
Match by ISRC (preferred) / normalized signature; cache forever in the global store (one call per unique track).
Vendor decision — REVISED after deep research (2026-06-23); still requires contract/ToS confirmation before pinning
Multi-source, adversarially-verified research (see comment below) reorders the candidates. The recommended FIRST provider is Soundcharts — which WrzDJ already licenses and already uses for genre/bpm/key.
★ PRIMARY — Soundcharts (https://soundcharts.com/en/audio-features-api). As of v2.46.1 (Nov 2025) its Audio Features API returns the full Spotify-style descriptor set — energy, danceability, valence, acousticness, instrumentalness, speechiness, liveness, bpm, key/mode, time signature, loudness — on the 0–1 scale, and supports ISRC lookup (GET /song/by-isrc/{isrc} → UUID, then GET /song/{uuid} → features). ISRC-native (our exact key, no audio upload), no new vendor/secret/supply-chain trust decision.
Empirically confirmed (2026-06-23) against GET /api/v2.25/song/{uuid} with our existing key (test track: "bad guy", ISRC USUM71900764). Features are nested under object.audio.* and the same call also returns tempo (bpm), key+mode, genres (root/sub), duration, and the echoed isrc — i.e. one ISRC-resolvable call can backfill bpm+key+genre+duration+energy+danceability+valence together (a single-source-consolidation opportunity for feat(enrichment): write the global track store at request time (enrich once, reuse everywhere) #541/feat(setbuilder): pool reads global store + pool→builder contract & build coverage gate #542, not just an energy source). Response headers observed: x-ratelimit-limit: 10000 (reset 31s — generous), x-quota-remaining: 1000 (plan credit allotment — the real ceiling).
Gates before pinning: (a) caching + product-use rights — the public website ToS (https://soundcharts.com/en/terms) is NOT the API data license; it is restrictive by default (Art. 5.1 prohibits copying/distributing Soundcharts data without consent) and requires a visible "Source: www.soundcharts.com" attribution (Art. 6.2), and is silent on API caching/TTL. The right to cache per-track features in our DB indefinitely lives in the separate API/commercial agreement tied to the subscription — must check that agreement (or ask Soundcharts directly). Mitigation/lever: WrzDJ mostly consumes features as an internal algorithmic input (the DJ sees a sequenced set, not raw energy=0.43); storing them as a private input and not displaying/redistributing the raw values sidesteps most of the redistribution restriction — only the storage right still needs the agreement's blessing. (b) plan quota capacity — x-quota-remaining was 1000, so a bulk catalog backfill could exhaust the allotment even though steady-state (one cached call per new track) is cheap. Adapter must treat quota-exhaustion → fall back to ReccoBeats, not retry. (Plan-tier inclusion of the Audio Features API is already proven by the live test.) If caching/product-use is disallowed by the API agreement, ReccoBeats becomes primary (explicitly free for commercial use, no attribution; its catch is undocumented Spotify-data provenance, not a restrictive grant).
FREE FALLBACK — ReccoBeats (https://reccobeats.com). Returns the same nine Spotify features on the identical 0–1 scale; free incl. commercial use, no attribution. Caveats: no ISRC lookup (only ReccoBeats UUID / Spotify ID / audio upload — requires our existing ISRC→Spotify-ID resolve), feature-value provenance undocumented (the claim that values are re-derived vs. stale Spotify values was refuted 1-2), operator "LatteBits" opaque, and its ToS makes the user "responsible for compliance with Spotify's terms" (Spotify-aggregated data → redistribution risk on cached values).
NOT a numeric drop-in — Cyanite.ai (commercial). Rich mood (13 labels) + valence/arousal + bpm/key, but energy is categorical (variable/medium/high/low) and there is no danceability field; submission is by audio upload, not ISRC.
DEAD — AcousticBrainz. Frozen to submissions since Jun 2022, site shut down early 2023; only a static dump (29.4M rows, 2022-07-06) survives, keyed by MBID not ISRC, and MetaBrainz itself disowned the data quality (no per-value confidence). Unusable as a live source.
Core MusicBrainz exposes no audio features (genre + folksonomy tags + relationships only) — confirms our MusicBrainz=genre-only usage is correct. ListenBrainz is listen data, not features.
Essentia (https://essentia.upf.edu/) — self-hosted, open-source, needs the audio file; "someday / audio-on-hand" only. License (AGPL?) + classifier accuracy unverified — vet separately before relying on it.
Soundcharts plan tier confirmed to include the Audio Features API, and caching/redistribution ToS confirmed to permit indefinite local caching (else fall back to ReccoBeats as primary). Documented license + provenance + CVE check per security rules; secret via env (never hardcoded); config-gated.
Provider adapter writes the full feature payload (energy + danceability + valence + acoustic/instr/speech/live) with per-field source + freshness into the global tracks row; unit-tested.
Energy resolves through the cascade external → Lexicon override → LLM fallback, with source recorded; unit-tested.
One unique track = one provider call (cached in the global store); coverage stays at/above the enforced gate.
Refs
Feeds the global tracks table (Task 0) + request-time enrichment. Cascade ties to #526 (Lexicon override) and #391 (LLM fallback). See also #527 (enrichment audit).
Part of the pool→builder data-contract epic (#539). Provides the energy (+ mood/danceability/valence) columns for the global store + request-time enrichment.
Why
Energy is the one required field no current integration supplies: Beatport/Tidal give bpm/key, MusicBrainz gives genre, but none give energy/danceability, and Spotify's audio-features API (energy/danceability/valence/…) was deprecated on 2024-11-27 (new apps get 403; no official replacement). The chosen strategy is an external audio-features API as the primary, whole-pool-coverage energy source, with Lexicon measured energy as an override where the DJ has it, and LLM-inferred energy as the last-resort fallback.
Scope
tracksrow (and thus available at request time and in the setbuilder pool).energy,danceability,valenceas the near-term trio and the rest as nullable columns in the same migration — re-fetching the whole catalog later to backfill them would pay the call twice. This feeds future WrzDJSet pass-1 sequencing (energy/mood/danceability-aware), where energy × valence forms the canonical mood quadrant for emotional-arc sequencing. (YAGNI deliberately does not apply — see epic(setbuilder): pool→builder data contract + global enriched-track store #539.)explicit(boolean). Soundcharts returns it in the same payload. Unlike the audio features (which weight ordering),explicitis a hard pool pre-filter enabling a future "clean set / all-ages event" toggle (e.g. family events) — a one-line pool filter later instead of a re-enrichment pass. Free to store now.Vendor decision — REVISED after deep research (2026-06-23); still requires contract/ToS confirmation before pinning
Multi-source, adversarially-verified research (see comment below) reorders the candidates. The recommended FIRST provider is Soundcharts — which WrzDJ already licenses and already uses for genre/bpm/key.
GET /song/by-isrc/{isrc}→ UUID, thenGET /song/{uuid}→ features). ISRC-native (our exact key, no audio upload), no new vendor/secret/supply-chain trust decision.GET /api/v2.25/song/{uuid}with our existing key (test track: "bad guy", ISRCUSUM71900764). Features are nested underobject.audio.*and the same call also returnstempo(bpm),key+mode,genres(root/sub),duration, and the echoedisrc— i.e. one ISRC-resolvable call can backfill bpm+key+genre+duration+energy+danceability+valence together (a single-source-consolidation opportunity for feat(enrichment): write the global track store at request time (enrich once, reuse everywhere) #541/feat(setbuilder): pool reads global store + pool→builder contract & build coverage gate #542, not just an energy source). Response headers observed:x-ratelimit-limit: 10000(reset 31s — generous),x-quota-remaining: 1000(plan credit allotment — the real ceiling).energy=0.43); storing them as a private input and not displaying/redistributing the raw values sidesteps most of the redistribution restriction — only the storage right still needs the agreement's blessing. (b) plan quota capacity —x-quota-remainingwas 1000, so a bulk catalog backfill could exhaust the allotment even though steady-state (one cached call per new track) is cheap. Adapter must treat quota-exhaustion → fall back to ReccoBeats, not retry. (Plan-tier inclusion of the Audio Features API is already proven by the live test.) If caching/product-use is disallowed by the API agreement, ReccoBeats becomes primary (explicitly free for commercial use, no attribution; its catch is undocumented Spotify-data provenance, not a restrictive grant).Spotify deprecation context — https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api
Acceptance criteria
tracksrow; unit-tested.Refs
Feeds the global
trackstable (Task 0) + request-time enrichment. Cascade ties to #526 (Lexicon override) and #391 (LLM fallback). See also #527 (enrichment audit).