feat(social): add listening activity, shared rooms and modern chat
ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
This commit is contained in:
1 parent
0a05bfe2cd
commit
11a16cd023
59 files changed
+5022
-542
No files matched your search
@@ -313,6 +313,7 @@ def build_batch(
|
||||
rng: np.random.Generator | None = None,
|
||||
ranker: Ranker | None = None,
|
||||
ctx: Context | None = None,
|
||||
friend_ids: list[str] | None = None,
|
||||
) -> list[Pick]:
|
||||
"""Кандидаты → фильтры → оценка → соответствие контексту → MMR с правилами разнообразия."""
|
||||
rng = rng or np.random.default_rng()
|
||||
@@ -363,6 +364,8 @@ def build_batch(
|
||||
if seeds:
|
||||
q = index.emb[seeds].mean(axis=0)
|
||||
add(_top((index.emb @ q).astype(np.float64), allowed & index.has_emb, 100), "seed")
|
||||
friends = [index.pos[t] for t in (friend_ids or []) if t in index.pos and allowed[index.pos[t]]][:10]
|
||||
add(friends, "friends")
|
||||
query = np.maximum(sim_session, sim_taste) if svec is not None or len(centroids) else None
|
||||
if query is not None:
|
||||
# «Новое» помечаем раньше audio_sim: неслышанный трек рядом со вкусом считается исследованием.
|
||||
@@ -446,6 +449,8 @@ def build_batch(
|
||||
pred = ranker.predict(x)
|
||||
score = np.argsort(np.argsort(pred)) / max(len(pred) - 1, 1)
|
||||
|
||||
# Social influence is small, context-sensitive, and survives the learned ranker.
|
||||
score = score + 0.06 * np.isin(cand, friends) * f
|
||||
# 5. MMR + правила: 1 трек исполнителя на пачку, не больше 2 на последние 10; доля «нового».
|
||||
picks: list[Pick] = []
|
||||
chosen: list[int] = []
|
||||
@@ -457,6 +462,8 @@ def build_batch(
|
||||
best_j, best_v = -1, -np.inf
|
||||
for j in order:
|
||||
i = int(cand[j])
|
||||
if sources[i] == "friends" and any(p.source == "friends" for p in picks):
|
||||
continue
|
||||
a = index.artist[i]
|
||||
if any(index.artist[c] == a for c in chosen) or recent_artists.count(a) >= 2:
|
||||
continue
|
||||
@@ -471,7 +478,9 @@ def build_batch(
|
||||
continue
|
||||
if chosen:
|
||||
chosen_artists = {index.artist[c] for c in chosen}
|
||||
rest = [j for j in order if index.artist[int(cand[j])] in chosen_artists]
|
||||
social_chosen = any(p.source == "friends" for p in picks)
|
||||
rest = [j for j in order if index.artist[int(cand[j])] in chosen_artists
|
||||
and not (sources[int(cand[j])] == "friends" and social_chosen)]
|
||||
if not rest:
|
||||
break
|
||||
best_j = max(rest, key=lambda j: score[j])
|
||||
|
||||
@@ -38,6 +38,7 @@ class BatchRequest(BaseModel):
|
||||
mood: str | None = None
|
||||
seed: Seed | None = None
|
||||
exclude_ids: list[str] = Field(default_factory=list, max_length=1000)
|
||||
friend_track_ids: list[str] = Field(default_factory=list, max_length=30)
|
||||
feedback: list[Feedback] = Field(default_factory=list, max_length=200)
|
||||
n: int = Field(default=5, ge=1, le=20)
|
||||
|
||||
@@ -93,6 +94,7 @@ class WaveService:
|
||||
n=req.n,
|
||||
exclude=set(req.exclude_ids),
|
||||
seed_ids=seeds,
|
||||
friend_ids=req.friend_track_ids,
|
||||
rng=np.random.default_rng(),
|
||||
ranker=ranker,
|
||||
ctx=Context(has_activity=bool(req.activity), has_mood=bool(req.mood)),
|
||||
|
||||
@@ -45,6 +45,23 @@ def profile(**kw: object) -> Profile:
|
||||
return Profile(**base) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_friend_signal_is_bounded_and_respects_negative_feedback() -> None:
|
||||
index = library()
|
||||
blocked = {"t0-0", "t1-0"}
|
||||
picks = build_batch(index, profile(disliked=blocked), Session(), [], n=5,
|
||||
friend_ids=index.ids, rng=np.random.default_rng(8))
|
||||
assert not blocked.intersection(p.id for p in picks)
|
||||
assert sum(p.source == "friends" for p in picks) <= 1
|
||||
assert len(picks) == 5
|
||||
|
||||
|
||||
def test_empty_friend_signal_does_not_change_ranking() -> None:
|
||||
index = library()
|
||||
first = build_batch(index, profile(), Session(), [], rng=np.random.default_rng(17))
|
||||
second = build_batch(index, profile(), Session(), [], friend_ids=[], rng=np.random.default_rng(17))
|
||||
assert [p.id for p in first] == [p.id for p in second]
|
||||
|
||||
|
||||
def cluster(track_id: str) -> int:
|
||||
return int(track_id[1])
|
||||
|
||||
|
||||
Reference in new issue
Block a user