docs: add product idea, design prototypes and development documentation
- IDEA.md: product vision from the user's perspective (input for design)
- Architecture, ADRs, data model, REST API overview
- Auth and roles, library storage (NFS, path regexes, scanner, search),
playback, recommendation system ("My Wave"), track upload queue
- Web, Android (offline, self-update), i18n, design system
- Testing strategy, Gitea Actions CI/CD, deployment, conventions, roadmap
- Clickable web/Android design prototypes and wave button variants
- .claude/launch.json to serve the prototypes locally
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
36e99fe960
commit
9eb1c25147
30 files changed
+7538
No files matched your search
@@ -0,0 +1,239 @@
|
||||
# Рекомендательная система и «Моя волна»
|
||||
|
||||
## 1. Цели и как измеряем успех
|
||||
|
||||
Волна должна: (а) почти не включать то, что пользователь пропустит; (б) регулярно находить новое,
|
||||
что ему понравится; (в) мгновенно реагировать на реакции и выбранный контекст.
|
||||
|
||||
| Метрика (онлайн, в админке) | Что показывает | Цель |
|
||||
|---|---|---|
|
||||
| Listen-through rate | Доля треков волны, прослушанных ≥ 50% | > 70% |
|
||||
| Early skip rate | Доля пропусков в первые 30 с | < 20% |
|
||||
| Like rate | Лайков на 100 треков волны | растёт |
|
||||
| Discovery rate | Доля впервые услышанных треков, прослушанных ≥ 50% | 20–40% |
|
||||
| Session length | Медианная длительность сессии волны | растёт |
|
||||
|
||||
Офлайн-метрики при обучении: NDCG@10, Recall@50 на отложенной по времени выборке, AUC предсказания раннего скипа.
|
||||
|
||||
## 2. Масштаб и следствия
|
||||
|
||||
~100 пользователей × ≥ 11k треков. Матрица взаимодействий разрежена: коллаборативная фильтрация
|
||||
полезна, но **не может быть основой** — особенно для новых пользователей и треков, которые никто не слушал.
|
||||
Поэтому система гибридная:
|
||||
|
||||
1. **Звучание трека** (аудиоанализ) — работает с первого дня для любого трека.
|
||||
2. **Личный вкус** — по собственной истории пользователя.
|
||||
3. **Коллективный опыт** — ALS и обученный на всех пользователях ранжировщик; вес растёт вместе с объёмом данных.
|
||||
|
||||
Весь объём (эмбеддинги 11k–100k × 256, векторы ~100 пользователей) помещается в память одного процесса —
|
||||
кандидаты считаются за миллисекунды, без распределённой инфраструктуры.
|
||||
|
||||
## 3. Сигналы и неявная оценка
|
||||
|
||||
Из `play_events` для каждой пары (пользователь, трек) считается `implicit_score` — сумма вкладов
|
||||
с экспоненциальным затуханием по времени (полупериод 90 дней):
|
||||
|
||||
| Сигнал | Вклад |
|
||||
|---|---|
|
||||
| Доиграл до конца | +1.0 |
|
||||
| Засчитанное прослушивание (≥ 50%) без доигрывания | +0.7 |
|
||||
| Скип в первые 10 с | −1.0 |
|
||||
| Скип 10–30 с | −0.7 |
|
||||
| Скип 30 с – 50% | −0.2 |
|
||||
| Лайк | +3.0 |
|
||||
| Дизлайк | −5.0 и исключение из рекомендаций |
|
||||
| Повтор по своей инициативе (не волна) за 24 ч | +0.5 |
|
||||
| Нашёл через поиск и включил | +0.5 |
|
||||
|
||||
Поправка на контекст: скип внутри волны — сильный сигнал (×1.0); скип при прослушивании альбома / исполнителя —
|
||||
слабее (×0.5), потому что человек мог просто листать. Перемотка к концу трека засчитывается как скип в точке начала перемотки.
|
||||
|
||||
Производные агрегаты:
|
||||
- **artist affinity** — сглаженное среднее `implicit_score` треков исполнителя (с байесовским приором к 0);
|
||||
- **genre prefs** — то же по жанрам (тегам и предсказанным моделью);
|
||||
- **familiarity** — сколько раз трек слышан, давность последнего прослушивания.
|
||||
|
||||
## 4. Представление трека
|
||||
|
||||
Заполняет сервис `analyzer` (Essentia + предобученные модели Essentia Models):
|
||||
|
||||
| Признак | Модель / алгоритм |
|
||||
|---|---|
|
||||
| Аудио-эмбеддинг | `discogs-effnet-bs64` (1280-d, среднее по патчам) → PCA до 256, L2-нормализация |
|
||||
| Жанры (400 стилей Discogs) | `genre_discogs400` поверх эмбеддинга, топ-5 с вероятностями → `track_genres(source='analysis')` |
|
||||
| Настроение | `mood_happy`, `mood_sad`, `mood_relaxed`, `mood_aggressive`, `mood_party` |
|
||||
| Valence / arousal | `deam` / `emomusic` регрессия |
|
||||
| Танцевальность, вокал / инструментал | `danceability`, `voice_instrumental` |
|
||||
| BPM | `TempoCNN` (+ уверенность) |
|
||||
| Тональность | `KeyExtractor` |
|
||||
| Громкость | EBU R128 (`LoudnessEBUR128`) → `loudness_lufs` |
|
||||
| Energy | Комбинация RMS / spectral flux / arousal, нормирована по библиотеке в 0..1 |
|
||||
| Длина вступления | Онсеты + энергия → `intro_ms` (для мягкого старта) |
|
||||
|
||||
Производительность: анализируются 3 фрагмента по 30 с (из начала, середины и конца) — ~2–4 с на трек на CPU.
|
||||
11k треков ≈ 3–6 часов при 4 воркерах при первом запуске; новые треки — в течение минут после скана.
|
||||
Пайплайн версионируется (`analyzer_ver`); смена версии ставит фоновый пересчёт.
|
||||
|
||||
Плюс метаданные: исполнители, год, теги-жанры, глобальная популярность.
|
||||
|
||||
## 5. Профиль пользователя
|
||||
|
||||
**Долгосрочный** (`user_profiles_rec`, пересчитывается онлайн после каждого события и полностью ночью):
|
||||
- `taste_audio` — взвешенное среднее аудио-эмбеддингов треков с положительным `implicit_score`
|
||||
(веса = score × затухание). Хранится также **несколько центроидов** (k-means, k = 3–8 в зависимости от объёма истории):
|
||||
у людей обычно несколько «вкусов», одно среднее их размазывает;
|
||||
- `taste_cf` — пользовательский фактор ALS;
|
||||
- `genre_prefs`, artist affinity;
|
||||
- параметры бандита по источникам кандидатов.
|
||||
|
||||
**Сессионный** (Redis, на время сессии волны):
|
||||
- `session_vec` — экспоненциальное скользящее среднее эмбеддингов треков сессии с учётом реакций:
|
||||
`s ← normalize(s + η · r · e_track)`, где `r` = +1 доиграл, +2 лайк, −1 ранний скип, −2 дизлайк, η = 0.3;
|
||||
- счётчик подряд идущих скипов, использованные исполнители, история выдачи.
|
||||
|
||||
## 6. Конвейер «Моей волны»
|
||||
|
||||
```
|
||||
контекст (занятие, настроение, seed) + профиль + сессия
|
||||
│
|
||||
1. Кандидаты (≈ 500) ← несколько источников
|
||||
│
|
||||
2. Фильтры (дизлайки, недоступные, недавно игравшие)
|
||||
│
|
||||
3. Ранжирование (v1: формула → v2: LightGBM)
|
||||
│
|
||||
4. Соответствие контексту (занятие / настроение)
|
||||
│
|
||||
5. Переранжирование (разнообразие, правила, исследование)
|
||||
│
|
||||
пачка из 5 треков → клиент
|
||||
```
|
||||
|
||||
### 6.1 Источники кандидатов
|
||||
|
||||
| Источник | Как | Сколько |
|
||||
|---|---|---|
|
||||
| `audio_sim` | kNN по `audio` к сессионному вектору и центроидам вкуса | 150 |
|
||||
| `cf` | kNN по `cf`-факторам к `taste_cf` (ALS) | 100 |
|
||||
| `item_cf` | «С этим треком слушают»: соседи последних лайков по ALS item-item | 60 |
|
||||
| `artist` | Треки любимых исполнителей + похожих исполнителей (по эмбеддингу исполнителя = среднее треков) | 60 |
|
||||
| `genre` | Популярные внутри предпочитаемых жанров | 40 |
|
||||
| `fresh` | Ещё не слышанные пользователем треки, близкие к вкусу (с ослабленным порогом) | 50 |
|
||||
| `rediscover` | Любимые треки, не игравшие > 60 дней | 30 |
|
||||
| `popular` | Глобально популярные за 30 дней (для холодного старта) | 30 |
|
||||
| `seed` | Если волна запущена от исполнителя / трека — его соседи | 100 |
|
||||
|
||||
Пулы кандидатов кэшируются в Redis на 10 минут; пересчитываются, если сильно сдвинулся сессионный вектор.
|
||||
|
||||
### 6.2 Фильтры
|
||||
- дизлайкнутые треки и исполнители — исключены навсегда (пока не снята реакция);
|
||||
- трек недоступен (источник офлайн, файл пропал) или не проанализирован — исключён из `audio_sim`;
|
||||
- сыгран в последние 3 часа — исключён; лайкнутый сыгран в последние 24 ч — штраф;
|
||||
- уже выдан в этой сессии — исключён.
|
||||
|
||||
### 6.3 Ранжирование
|
||||
|
||||
**v1 (эвристика, с первого дня):**
|
||||
```
|
||||
score = 0.35·cos(e, session_vec) + 0.25·max_c cos(e, centroid_c) + 0.15·artist_affinity
|
||||
+ 0.10·genre_pref + 0.10·cf_score + 0.05·popularity − penalties
|
||||
```
|
||||
|
||||
**v2 (LightGBM, `lambdarank`)** — обучается на `wave_impressions` всех пользователей.
|
||||
Метка: 3 — лайк, 2 — доиграл, 1 — ≥ 50%, 0 — скип после 30 с, −1 → 0 с весом ×2 — ранний скип / дизлайк.
|
||||
Признаки:
|
||||
- сходства: с `session_vec`, с каждым центроидом (max / mean), с последними 5 треками сессии, `cf` dot-product;
|
||||
- трек: аудиопризнаки, популярность, возраст в библиотеке, длительность;
|
||||
- пользователь × трек: `implicit_score`, play / skip counts, давность, artist affinity, genre pref;
|
||||
- контекст: занятие, настроение, час суток, день недели, позиция в сессии, число скипов подряд,
|
||||
`context_fit` (см. 6.4), источник кандидата;
|
||||
- взаимодействия «контекст × аудиопризнаки» модель находит сама.
|
||||
|
||||
Обучение — ночью; новая модель активируется, только если офлайн-NDCG@10 не хуже активной более чем на 2%.
|
||||
Пока размеченных показов меньше ~20k, используется v1 (переключение автоматическое по порогу).
|
||||
|
||||
### 6.4 Занятия и настроения
|
||||
|
||||
Каждый контекст — профиль целевых значений аудиопризнаков (`services/recommender/config/contexts.yaml`,
|
||||
меняется без перекомпиляции). `context_fit` = произведение гауссовых «колоколов» по признакам профиля;
|
||||
трек с `context_fit` ниже порога выпадает, остальные домножаются. Если после фильтра осталось < 50 кандидатов,
|
||||
порог ослабляется (лучше чуть мимо, чем повторы).
|
||||
|
||||
| Контекст | Профиль |
|
||||
|---|---|
|
||||
| 🏃 Бегаю | BPM 150–185 (или половина в 75–92 — двойной темп), energy ≥ 0.7, danceability ≥ 0.5, стабильный темп |
|
||||
| 💪 Тренировка | energy ≥ 0.75, arousal ≥ 0.7, aggressive допускается |
|
||||
| ☀️ Просыпаюсь | **траектория**: energy 0.3 → 0.7 за первые 20 минут сессии, valence ≥ 0.5, без aggressive |
|
||||
| 🎧 Работаю / фокус | energy 0.3–0.6, instrumentalness ≥ 0.6 (предпочтительно), низкая вариативность, без резких переходов |
|
||||
| 🚗 В дороге | energy 0.4–0.8, ближе к ядру вкуса (меньше исследования) |
|
||||
| 🛋 Отдыхаю | energy ≤ 0.5, relaxed ≥ 0.5 |
|
||||
| 🌙 Засыпаю | **траектория**: energy 0.35 → 0.15, BPM ≤ 95, instrumental предпочтительно, громкость ровная |
|
||||
| ⚡ Энергичное | energy ≥ 0.7, arousal ≥ 0.65 |
|
||||
| 😊 Весёлое | valence ≥ 0.6, mood_happy ≥ 0.5 |
|
||||
| 🌿 Спокойное | arousal ≤ 0.4, mood_relaxed ≥ 0.5 |
|
||||
| 🌧 Грустное | valence ≤ 0.4, mood_sad ≥ 0.5 |
|
||||
|
||||
Занятие и настроение комбинируются (пересечение профилей). Если не выбрано ничего — `context_fit = 1`.
|
||||
Ранжировщик v2 дополнительно выучивает, как конкретный пользователь реагирует в каждом контексте.
|
||||
|
||||
### 6.5 Переранжирование и правила
|
||||
- **Разнообразие** — MMR: `λ·score − (1−λ)·max_sim(с уже выбранными)`, λ = 0.75;
|
||||
- не более 1 трека исполнителя в пачке и не более 2 на 10 последовательных треков;
|
||||
- **переходы**: штраф за резкий скачок energy / BPM между соседними треками (кроме «Тренировки»);
|
||||
- **исследование**: доля «новых» треков = `exploration` пользователя (по умолчанию 15%, адаптируется бандитом);
|
||||
- **3 скипа подряд** ⇒ «смена курса»: сессионный вектор сбрасывается к ближайшему другому центроиду вкуса,
|
||||
следующая пачка собирается из другого кластера.
|
||||
|
||||
### 6.6 Бандит по источникам
|
||||
|
||||
Для каждого пользователя и источника — Beta(α, β); награда = трек прослушан ≥ 50% или лайкнут.
|
||||
При сборке пачки слоты распределяются по источникам семплированием Томпсона. Так система сама узнаёт,
|
||||
что одному пользователю лучше заходит `fresh`, а другому — `rediscover`. Параметры затухают
|
||||
(×0.99 в день), чтобы подстраиваться под смену вкуса.
|
||||
|
||||
### 6.7 Реакция в реальном времени
|
||||
|
||||
- Клиент держит в очереди 2 трека вперёд и запрашивает новую пачку, когда осталось ≤ 2.
|
||||
- Запрос `next` несёт реакции на уже выданные треки → сессионный вектор обновляется **до** расчёта пачки.
|
||||
- Лайк / дизлайк / ранний скип дополнительно вызывают немедленный пересчёт: клиент отбрасывает ещё не начатые
|
||||
треки очереди волны и берёт свежую пачку.
|
||||
- Смена занятия / настроения (`PATCH /wave/sessions/{id}`) — очередь волны пересобирается сразу.
|
||||
|
||||
## 7. Холодный старт
|
||||
|
||||
- **Новый пользователь**: опциональный онбординг (выбрать 3+ жанра или исполнителя) → начальные центроиды.
|
||||
Без онбординга — первые пачки из `popular` и максимально разнообразных кластеров библиотеки
|
||||
(по одному треку из разных кластеров), exploration 40% на первые 30 треков.
|
||||
- **Новый трек**: сразу доступен через `audio_sim` / `fresh` (как только проанализирован), без единого прослушивания.
|
||||
|
||||
## 8. Обучение на всех пользователях
|
||||
|
||||
Ночной пайплайн (03:00, APScheduler в `recommender`):
|
||||
1. Пересчёт `user_track_stats.implicit_score` (затухание), artist / genre affinity.
|
||||
2. **ALS** (`implicit`, 64 фактора, confidence `c = 1 + 20·max(score, 0)`, отрицательные — через исключение
|
||||
и штраф в ранжировании) → `track_embeddings.cf`, `user_profiles_rec.taste_cf`.
|
||||
3. Кластеризация вкусов (центроиды) для каждого пользователя.
|
||||
4. Обучение LightGBM-ранжировщика на показах за 90 дней (time-split: последние 7 дней — валидация).
|
||||
5. Проверка метрик, запись `model_versions`, горячая подмена моделей в памяти сервиса.
|
||||
6. Пересчёт глобальной популярности, «похожих исполнителей», top-tracks.
|
||||
|
||||
Время на ~100 пользователей и ~100k треков — минуты.
|
||||
|
||||
## 9. Отказоустойчивость
|
||||
|
||||
- Таймаут вызова `api → recommender` — 300 мс. При ошибке `api` отдаёт **fallback-волну** (Go): лайки и треки
|
||||
любимых исполнителей + случайные треки из предпочитаемых жанров, с теми же фильтрами дизлайков.
|
||||
- Если анализ трека не завершён — трек участвует только в источниках без эмбеддинга.
|
||||
|
||||
## 10. Требования к производительности
|
||||
|
||||
- `POST /wave/sessions/{id}/next`: p95 < 150 мс (включая вызов recommender).
|
||||
- Онлайн-обновление профиля по событию: < 50 мс, асинхронно из Redis Stream.
|
||||
|
||||
## 11. Эволюция
|
||||
|
||||
| Версия | Содержимое |
|
||||
|---|---|
|
||||
| v1 | Аудиоанализ, эвристическое ранжирование, контексты, сессионный вектор, правила, fallback |
|
||||
| v2 | ALS, LightGBM-ранжировщик, бандит, центроиды вкуса, онбординг |
|
||||
| v3 | Последовательная модель по сессиям (SASRec / небольшой трансформер) как дополнительный источник кандидатов и признак — когда накопится ≥ 1 млн событий; «волна по исполнителю / треку», ежедневные персональные подборки |
|
||||
Reference in new issue
Block a user