Files
pipu-music/services/recommender/pipu_rec/cli.py
T
mrleo1nidandClaude Opus 5.5 58bd14ef38
ci / db / db (push) Failing after 1m15s
ci / api / api (push) Successful in 17s
ci / recommender / recommender (push) Successful in 32s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m41s
feat(recommender): LightGBM lambdarank ranker with impression features (M6)
The wave logs each served candidate's feature vector to rec_impression_features.
Nightly training joins them with impression outcomes, validates on the last week
and activates the model only when it beats the v1 order and the active model.
The service loads the active ranker and uses its order in place of the v1 score.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:41:26 +03:00

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.reload).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()