ci / recommender / recommender (push) Successful in 37s
- Implicit scores from play events and reactions per the signals table (90-day half-life, weaker skips outside the wave, dislikes override). - numpy ALS for implicit feedback; time-split evaluation on the last 7 days (NDCG@10, Recall@50) against a popularity baseline; a new model is activated only if NDCG@10 does not regress by more than 2%. - Vectors are written to track_embeddings.cf and user_profiles_rec.taste_cf, versions to model_versions; NightlyTrainer runs at TRAIN_HOUR and on first start, `pipu-rec train` runs it manually. - Wave: cf and item_cf candidate sources and a 0.10 cf term in the v1 score; impressions are tagged heuristic-v1+cf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
2.6 KiB
Python
69 lines
2.6 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.train.pipeline import NightlyTrainer, train_all
|
|
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))
|
|
if settings.train_hour >= 0:
|
|
# После активации новой модели каталог перечитывается сразу — горячая подмена векторов.
|
|
NightlyTrainer(settings.database_url, settings.train_hour, on_done=wave.index.refresh).start()
|
|
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)
|
|
sub.add_parser("train", help="обучить модели сейчас (то же, что ночной прогон)")
|
|
args = parser.parse_args()
|
|
|
|
settings = Settings()
|
|
logging.basicConfig(level=settings.log_level.upper())
|
|
if args.cmd == "serve":
|
|
_serve(settings)
|
|
elif args.cmd == "train":
|
|
log.info("training: %s", train_all(settings.database_url))
|
|
else:
|
|
_analyze(settings, args.workers)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|