Add librosa beat/mood analysis and an Ambience background driven by it
The background worker now runs a real librosa analyzer (tempo, beat grid, per-second energy/valence curves) instead of only the null baseline, kept behind build_analyzer() so a plain checkout without the analysis extra still runs fine. The web player reads that per-track analysis and drives a new animated "Ambience" background (bubbles, colour, current) that reacts to the beat and the mood curve as the track plays, plus a debug overlay for tuning it. Also adds a one-off script to backfill podcast cover art from iTunes.
This commit is contained in:
@@ -17,6 +17,8 @@ import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
from musicmouse.config import WebConfig, load_config
|
||||
from musicmouse.library.analysis import BeatGrid, TrackCurves
|
||||
from musicmouse.library.models import track_key
|
||||
from musicmouse.services.web.service import build_app
|
||||
from musicmouse.simulator.harness import Simulation, build_simulation
|
||||
from tests.conftest import VALID_CONFIG, write_config
|
||||
@@ -96,6 +98,50 @@ async def test_unanalyzed_tracks_report_no_analysis(client: Client) -> None:
|
||||
assert (await client.get(f"/api/tracks/{album['id']}/0/analysis")).status_code == 404
|
||||
|
||||
|
||||
async def test_an_analyzed_track_reports_its_beats_and_curves(
|
||||
client: Client, sim: Simulation
|
||||
) -> None:
|
||||
album = await album_by_title(client, "Eule")
|
||||
library_album = sim.app.library.get(album["id"])
|
||||
assert library_album is not None
|
||||
key = track_key(library_album.tracks[0].path)
|
||||
sim.app.library.cache.store_beats(key, BeatGrid((0.5, 1.0), (1.0, 0.5)))
|
||||
sim.app.library.cache.store_curve(
|
||||
key, TrackCurves(hop_seconds=1.0, energy=(0.4, 0.6), valence=(0.5, 0.5), drive=(0.3, 0.7))
|
||||
)
|
||||
|
||||
response = await client.get(f"/api/tracks/{album['id']}/0/analysis")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["times"] == [0.5, 1.0]
|
||||
assert body["curve"]["energy"] == [0.4, 0.6]
|
||||
assert body["curve"]["valence"] == [0.5, 0.5]
|
||||
assert body["curve"]["drive"] == [0.3, 0.7]
|
||||
|
||||
|
||||
async def test_a_track_with_a_curve_but_no_reliable_beat_still_reports_200(
|
||||
client: Client, sim: Simulation
|
||||
) -> None:
|
||||
"""A free-tempo/spoken-word track has no usable beat grid, but energy/valence/
|
||||
drive are computed regardless - the endpoint must not 404 just because `beats`
|
||||
came back empty."""
|
||||
album = await album_by_title(client, "Eule")
|
||||
library_album = sim.app.library.get(album["id"])
|
||||
assert library_album is not None
|
||||
key = track_key(library_album.tracks[0].path)
|
||||
sim.app.library.cache.store_curve(
|
||||
key, TrackCurves(hop_seconds=1.0, energy=(0.5,), valence=(0.5,), drive=(0.5,))
|
||||
)
|
||||
|
||||
response = await client.get(f"/api/tracks/{album['id']}/0/analysis")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["times"] == []
|
||||
assert body["curve"]["energy"] == [0.5]
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------- state
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user