feat(rec): audio analyzer and My Wave v1 engine (M4)
- migration 00006: track features and embeddings (pgvector, HNSW), analysis queue filled by a trigger on new tracks, wave sessions and impressions, recommender profiles, model versions - analyzer basic-1 (ADR-014): ffmpeg decode, three 30 s excerpts, 256-d timbre/rhythm embedding, tempo by onset autocorrelation, key, loudness, energy, danceability, acousticness, intro length; worker with SKIP LOCKED claims, retries and re-analysis on version change - wave engine v1: candidates (session/taste similarity, fresh, liked artists, rediscover, popular, exploration, seed), filters (dislikes, played in 3 h, already served), heuristic score, activity and mood fit from contexts.yaml with relaxing thresholds, MMR with one track per artist, exploration share, session vector updated by reactions and a change of course after three skips - POST /internal/wave/batch with the catalog index cached in memory and the session kept in Redis Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
976ffb574b
commit
ef29b85320
20 files changed
+1530
-13
No files matched your search
@@ -10,6 +10,8 @@ from fastapi import Depends, FastAPI, Header, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from pipu_rec.settings import Settings
|
||||
from pipu_rec.wave.engine import MODEL_VER
|
||||
from pipu_rec.wave.service import BatchRequest, BatchResponse, WaveService
|
||||
|
||||
Check = Callable[[], Awaitable[None]]
|
||||
|
||||
@@ -18,7 +20,9 @@ log = logging.getLogger(__name__)
|
||||
CHECK_TIMEOUT_S = 2.0
|
||||
|
||||
|
||||
def create_app(settings: Settings, checks: dict[str, Check] | None = None) -> FastAPI:
|
||||
def create_app(
|
||||
settings: Settings, checks: dict[str, Check] | None = None, wave: WaveService | None = None
|
||||
) -> FastAPI:
|
||||
app = FastAPI(title="pipu-recommender", version=settings.version, docs_url=None, redoc_url=None)
|
||||
checks = checks or {}
|
||||
|
||||
@@ -54,6 +58,13 @@ def create_app(settings: Settings, checks: dict[str, Check] | None = None) -> Fa
|
||||
|
||||
@app.get("/internal/status", dependencies=[Depends(require_token)])
|
||||
async def internal_status() -> dict[str, object]:
|
||||
return {"version": settings.version, "models": [], "ranker": "heuristic-v1"}
|
||||
return {"version": settings.version, "models": [], "ranker": MODEL_VER}
|
||||
|
||||
@app.post("/internal/wave/batch", dependencies=[Depends(require_token)])
|
||||
async def wave_batch(req: BatchRequest) -> BatchResponse:
|
||||
if wave is None:
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail="wave.unavailable")
|
||||
# Сборка — numpy и синхронные запросы к БД: в пуле потоков, чтобы не блокировать цикл событий.
|
||||
return await asyncio.to_thread(wave.batch, req)
|
||||
|
||||
return app
|
||||
Reference in new issue
Block a user