Files
pipu-music/services/recommender/pipu_rec/cli.py
T
mrleo1nidandClaude Opus 5.5 ef29b85320
ci / api / api (push) Successful in 27s
ci / recommender / recommender (push) Successful in 1m0s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m26s
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>
2026-10-20 12:20:55 +03:00

62 lines
2.0 KiB
Python

"""Точка входа: `pipu-rec serve` (онлайн-API) или `pipu-rec analyze` (воркер аудиоанализа)."""
import argparse
import logging
import psycopg
import redis.asyncio as aioredis
import uvicorn
from pipu_rec.analysis.worker import Worker
from pipu_rec.api.app import create_app
from pipu_rec.settings import Settings
from pipu_rec.wave.contexts import load_contexts
from pipu_rec.wave.service import WaveService
log = logging.getLogger("pipu_rec")
def _serve(settings: Settings) -> None:
rdb = aioredis.Redis.from_url(settings.redis_url, socket_connect_timeout=2, socket_timeout=2)
async def check_postgres() -> None:
async with await psycopg.AsyncConnection.connect(settings.database_url, connect_timeout=2) as conn:
await conn.execute("SELECT 1")
async def check_redis() -> None:
await rdb.ping()
wave = WaveService(settings.database_url, settings.redis_url, load_contexts(settings.contexts_path))
app = create_app(settings, {"postgres": check_postgres, "redis": check_redis}, wave)
uvicorn.run(app, host=settings.http_host, port=settings.http_port, log_level=settings.log_level)
def _analyze(settings: Settings, workers: int) -> None:
Worker(
settings.database_url,
settings.library_root,
settings.library_host_root,
workers,
settings.ffmpeg_path,
).run_forever()
def main() -> None:
parser = argparse.ArgumentParser(prog="pipu-rec")
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("serve", help="внутренний API рекомендаций")
analyze = sub.add_parser("analyze", help="воркер аудиоанализа")
analyze.add_argument("--workers", type=int, default=4)
args = parser.parse_args()
settings = Settings()
logging.basicConfig(level=settings.log_level.upper())
if args.cmd == "serve":
_serve(settings)
else:
_analyze(settings, args.workers)
if __name__ == "__main__":
main()