feat(wave-v2): add LLM reranking, mood modes, auto-rotation scheduler and Navidrome auth

This commit is contained in:
Antigravity committed 2026-10-12 15:29:43 +03:00
1 parent 3b3d591bfd
commit fb1a5e01dd
11 files changed
+736 -37

No files matched your search

+41 -6
View File
@@ -8,6 +8,7 @@ from app.wave.recommender import WaveRecommender
from app.wave.playlist import ensure_smart_playlist
from app.core.tagger import get_track_dest_path
from app.core.lyrics import fetch_and_save_lyrics
from app.wave.llm_reranker import WaveLlmReranker
from app.providers.yandex import YandexMusicProvider
from app.providers.universal import UniversalProvider
@@ -28,6 +29,7 @@ class WaveRotator:
self.yandex_provider = YandexMusicProvider()
self.universal_provider = UniversalProvider()
self.recommender = WaveRecommender(self.yandex_provider)
self.reranker = WaveLlmReranker()
def get_current_buffer(self) -> List[Dict[str, Any]]:
"""List all audio files currently in the Wave rotation buffer."""
@@ -134,8 +136,8 @@ class WaveRotator:
return purged
async def fill_buffer(self) -> Dict[str, Any]:
"""Run rotation (promotion + purge) and download candidates up to target capacity."""
async def fill_buffer(self, mood: str = "auto", refresh_count: int = 5) -> Dict[str, Any]:
"""Run rotation (promotion + purge) and download candidates up to target capacity using LLM reranker."""
promoted = self.promote_starred_tracks()
purged = self.purge_expired_tracks()
@@ -143,22 +145,50 @@ class WaveRotator:
current_count = len(current_items)
needed = max(0, self.target_capacity - current_count)
# If buffer is full and user requests generation, rotate out oldest unstarred tracks to inject fresh recommendations
if needed == 0 and refresh_count > 0:
sorted_by_age = sorted(current_items, key=lambda x: x["mtime"])
to_remove = sorted_by_age[:refresh_count]
for item in to_remove:
try:
os.remove(item["path"])
lrc = os.path.splitext(item["path"])[0] + ".lrc"
if os.path.exists(lrc):
os.remove(lrc)
print(f"[WaveRotator] Rotating out oldest wave track: {item['filename']}")
except Exception as e:
print(f"[WaveRotator] Failed to remove {item['filename']}: {e}")
current_items = self.get_current_buffer()
current_count = len(current_items)
needed = max(0, self.target_capacity - current_count)
added: List[Dict[str, str]] = []
context = self.reranker.resolve_mood_context(mood)
if needed > 0:
print(f"[WaveRotator] Wave buffer at {current_count}/{self.target_capacity}. Sourcing {needed} new tracks...")
print(f"[WaveRotator] Wave buffer at {current_count}/{self.target_capacity}. Sourcing candidates for {needed} slots (mood: {context['label']})...")
profile = self.collector.get_anchor_profile()
existing_keys = self.collector.get_existing_track_keys()
wave_keys = self.get_wave_track_keys()
candidates = self.recommender.get_candidate_tracks(
# Source a larger candidate pool for LLM contextual reranking
candidate_pool_size = max(needed * 2, settings.WAVE_CANDIDATE_POOL)
raw_candidates = self.recommender.get_candidate_tracks(
anchor_profile=profile,
existing_keys=existing_keys,
wave_keys=wave_keys,
target_count=candidate_pool_size
)
# Contextual LLM Reranker (Qwen 2.5 / deadair fallback)
selected_candidates = self.reranker.rerank_candidates(
candidates=raw_candidates,
anchor_profile=profile,
mood=mood,
target_count=needed
)
for cand in candidates:
for cand in selected_candidates:
filename = f"{cand.artist} - {cand.title}.mp3"
# Clean unsafe filesystem chars
filename = "".join(c for c in filename if c not in '<>:"/\\|?*')
@@ -187,6 +217,8 @@ class WaveRotator:
return {
"target_capacity": self.target_capacity,
"total_in_buffer": len(self.get_current_buffer()),
"mood": mood,
"mood_label": context["label"],
"added_count": len(added),
"added_tracks": added,
"promoted_count": len(promoted),
@@ -201,6 +233,7 @@ class WaveRotator:
total_size_mb = round(sum(t["size_mb"] for t in buffer_tracks), 2)
profile = self.collector.get_anchor_profile()
anchor_artists = [a["artist"] for a in profile.get("top_artists", [])[:6]]
context = self.reranker.resolve_mood_context("auto")
return {
"capacity": self.target_capacity,
@@ -208,7 +241,9 @@ class WaveRotator:
"total_size_mb": total_size_mb,
"expire_days": self.expire_days,
"anchor_artists": anchor_artists,
"tracks": buffer_tracks[:15]
"current_context": context,
"llm_model": settings.OLLAMA_MODEL,
"tracks": buffer_tracks[:25]
}
wave_rotator = WaveRotator()