Spaces:
Sleeping
Sleeping
| """Backend shared play service.""" | |
| from __future__ import annotations | |
| from collections import defaultdict | |
| from typing import Any | |
| from sqlmodel import Session, select | |
| from models import PromptDismissal, SimilarityEdge | |
| from backend.constants import ( | |
| BLOCKED_RATINGS, | |
| EDGE_TYPE_WEIGHTS, | |
| POSITIVE_RATINGS, | |
| STRONG_POSITIVE_RATINGS, | |
| SUPPRESSED_RATINGS, | |
| ) | |
| from backend.scenarios import play_excluded_from_surfacing | |
| _RELATED_EDGE_LIMIT = 36 | |
| _RELATED_TWO_HOP_LIMIT = 24 | |
| _RELATED_DIRECT_MIN = 0.30 | |
| _RELATED_TWO_HOP_MIN = 0.40 | |
| _RELATED_TWO_HOP_DECAY = 0.72 | |
| def _directions_compatible(self, left: list[str], right: list[str], kink: dict[str, Any]) -> bool: | |
| left_set = set(left) | |
| right_set = set(right) | |
| mode = self._direction_mode_for_kink(kink) | |
| if "together" in left_set and "together" in right_set: | |
| return True | |
| if "to_me" in left_set and "by_me" in right_set: | |
| return True | |
| if "by_me" in left_set and "to_me" in right_set: | |
| return True | |
| if mode == "shared" and ("together" in left_set or "together" in right_set): | |
| return True | |
| return False | |
| def _directions_compatible_group(self, directions_list: list[list[str]], kink: dict[str, Any]) -> bool: | |
| if len(directions_list) < 2: | |
| return False | |
| direction_sets = [set(item) for item in directions_list] | |
| if all("together" in direction_set for direction_set in direction_sets): | |
| return True | |
| directed = [direction_set & {"to_me", "by_me"} for direction_set in direction_sets] | |
| if all(direction_set for direction_set in directed): | |
| has_receiver = any("to_me" in direction_set for direction_set in directed) | |
| has_giver = any("by_me" in direction_set for direction_set in directed) | |
| if has_receiver and has_giver: | |
| return True | |
| mode = self._direction_mode_for_kink(kink) | |
| if mode == "shared" and any("together" in direction_set for direction_set in direction_sets): | |
| return all(direction_set & {"together", "to_me", "by_me"} for direction_set in direction_sets) | |
| return False | |
| def _safe_prompt_candidate(self, kink: dict[str, Any], support_count: int) -> bool: | |
| if play_excluded_from_surfacing(kink): | |
| return False | |
| if not kink.get("prompt_eligible"): | |
| return False | |
| penalty = self._discoverability_penalty(kink["cluster"]) | |
| if penalty >= 0.9: | |
| return False | |
| if support_count >= 2 and kink["popularity"] >= 60: | |
| return True | |
| return kink["popularity"] >= 320 | |
| def _direct_shared_ids(self, left: dict[str, Any], right: dict[str, Any]) -> list[str]: | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| direct_ids: list[str] = [] | |
| shared_ids = set(left["plays"]) & set(right["plays"]) | |
| for kink_id in sorted(shared_ids): | |
| left_state = left["plays"][kink_id] | |
| right_state = right["plays"][kink_id] | |
| kink = detail_by_id.get(kink_id) | |
| if not kink: | |
| continue | |
| if play_excluded_from_surfacing(kink): | |
| continue | |
| if ( | |
| left_state["interest_state"] in POSITIVE_RATINGS | |
| and right_state["interest_state"] in POSITIVE_RATINGS | |
| and self._directions_compatible(left_state["directions"], right_state["directions"], kink) | |
| ): | |
| direct_ids.append(kink_id) | |
| return direct_ids | |
| def _related_edge_strength(edge: dict[str, Any]) -> float: | |
| return min(float(edge["score"]) * float(EDGE_TYPE_WEIGHTS.get(edge.get("type", ""), 0.8)), 1.0) | |
| def _similarity_edges_by_source( | |
| self, | |
| source_ids: set[str], | |
| *, | |
| per_source_limit: int = _RELATED_EDGE_LIMIT, | |
| ) -> dict[str, list[dict[str, Any]]]: | |
| if not source_ids: | |
| return {} | |
| with Session(self.engine) as session: | |
| rows = session.exec( | |
| select(SimilarityEdge) | |
| .where(SimilarityEdge.left_kink_id.in_(sorted(source_ids))) | |
| .order_by(SimilarityEdge.left_kink_id, SimilarityEdge.score.desc()) | |
| ).all() | |
| grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) | |
| for row in rows: | |
| items = grouped[row.left_kink_id] | |
| if len(items) >= per_source_limit: | |
| continue | |
| items.append({"id": row.right_kink_id, "score": row.score, "type": row.similarity_type}) | |
| return dict(grouped) | |
| def _positive_play_states(user: dict[str, Any]) -> dict[str, dict[str, Any]]: | |
| return { | |
| kink_id: state | |
| for kink_id, state in user.get("plays", {}).items() | |
| if state.get("interest_state") in POSITIVE_RATINGS | |
| } | |
| def _positive_scenario_states(user: dict[str, Any]) -> dict[str, dict[str, Any]]: | |
| return { | |
| scenario_id: state | |
| for scenario_id, state in user.get("scenario_preferences", {}).items() | |
| if state.get("interest_state") in POSITIVE_RATINGS | |
| } | |
| def _overlap_eligible_scenario_states(user: dict[str, Any]) -> dict[str, dict[str, Any]]: | |
| plays = user.get("plays", {}) | |
| out: dict[str, dict[str, Any]] = {} | |
| for scenario_id, state in user.get("scenario_preferences", {}).items(): | |
| if state.get("interest_state") not in STRONG_POSITIVE_RATINGS: | |
| continue | |
| parent_id = str(state.get("parent_kink_id", "") or "") | |
| parent_state = plays.get(parent_id, {}) if parent_id else {} | |
| if parent_state.get("interest_state") not in STRONG_POSITIVE_RATINGS: | |
| continue | |
| out[scenario_id] = { | |
| **state, | |
| "parent_kink_id": parent_id, | |
| "directions": state.get("directions", []) or parent_state.get("directions", []), | |
| } | |
| return out | |
| def _related_direction_compatible( | |
| self, | |
| left_state: dict[str, Any], | |
| right_state: dict[str, Any], | |
| left_kink: dict[str, Any], | |
| right_kink: dict[str, Any], | |
| ) -> bool: | |
| left_dirs = left_state.get("directions", []) | |
| right_dirs = right_state.get("directions", []) | |
| return self._directions_compatible(left_dirs, right_dirs, left_kink) or self._directions_compatible( | |
| left_dirs, | |
| right_dirs, | |
| right_kink, | |
| ) | |
| def _related_match_entry( | |
| self, | |
| left_user: dict[str, Any], | |
| right_user: dict[str, Any], | |
| left_kink_id: str, | |
| right_kink_id: str, | |
| *, | |
| score: float, | |
| hops: int, | |
| path: list[str], | |
| edge_types: list[str], | |
| ) -> dict[str, Any] | None: | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| left_kink = detail_by_id.get(left_kink_id) | |
| right_kink = detail_by_id.get(right_kink_id) | |
| if not left_kink or not right_kink: | |
| return None | |
| left_state = left_user["plays"][left_kink_id] | |
| right_state = right_user["plays"][right_kink_id] | |
| if not self._related_direction_compatible(left_state, right_state, left_kink, right_kink): | |
| return None | |
| bridge = None | |
| if len(path) == 3 and path[1] not in {left_kink_id, right_kink_id}: | |
| bridge = detail_by_id.get(path[1]) | |
| reason = ( | |
| f"{left_kink['name']} is graph-near {right_kink['name']}" | |
| if hops == 1 | |
| else f"{left_kink['name']} connects to {right_kink['name']} via {bridge['name'] if bridge else 'a nearby kink'}" | |
| ) | |
| return { | |
| "left_user_id": left_user["id"], | |
| "right_user_id": right_user["id"], | |
| "left_kink": left_kink, | |
| "right_kink": right_kink, | |
| "bridge_kink": bridge, | |
| "score": round(float(score), 4), | |
| "hops": hops, | |
| "path": path, | |
| "edge_types": edge_types, | |
| "reasons": [reason], | |
| "partner_reactions": { | |
| left_user["id"]: { | |
| "kink_id": left_kink_id, | |
| "interest_state": left_state["interest_state"], | |
| "directions": left_state.get("directions", []), | |
| }, | |
| right_user["id"]: { | |
| "kink_id": right_kink_id, | |
| "interest_state": right_state["interest_state"], | |
| "directions": right_state.get("directions", []), | |
| }, | |
| }, | |
| } | |
| def _related_positive_matches_for_pair( | |
| self, | |
| left: dict[str, Any], | |
| right: dict[str, Any], | |
| *, | |
| limit: int = 12, | |
| ) -> list[dict[str, Any]]: | |
| left_positive = _positive_play_states(left) | |
| right_positive = _positive_play_states(right) | |
| if not left_positive or not right_positive: | |
| return [] | |
| exact_shared = set(left_positive) & set(right_positive) | |
| left_only = set(left_positive) - exact_shared | |
| right_only = set(right_positive) - exact_shared | |
| if not left_only or not right_only: | |
| return [] | |
| first_edges = _similarity_edges_by_source(self, left_only | right_only, per_source_limit=_RELATED_EDGE_LIMIT) | |
| mid_ids = { | |
| edge["id"] | |
| for source in left_only | right_only | |
| for edge in first_edges.get(source, []) | |
| if edge["id"] not in exact_shared | |
| } | |
| second_edges = _similarity_edges_by_source(self, mid_ids, per_source_limit=_RELATED_TWO_HOP_LIMIT) | |
| best_by_pair: dict[tuple[str, str], dict[str, Any]] = {} | |
| def add_candidate( | |
| left_kink_id: str, | |
| right_kink_id: str, | |
| *, | |
| score: float, | |
| hops: int, | |
| path: list[str], | |
| edge_types: list[str], | |
| ) -> None: | |
| threshold = _RELATED_DIRECT_MIN if hops == 1 else _RELATED_TWO_HOP_MIN | |
| if score < threshold: | |
| return | |
| entry = _related_match_entry( | |
| self, | |
| left, | |
| right, | |
| left_kink_id, | |
| right_kink_id, | |
| score=score, | |
| hops=hops, | |
| path=path, | |
| edge_types=edge_types, | |
| ) | |
| if not entry: | |
| return | |
| key = (left_kink_id, right_kink_id) | |
| current = best_by_pair.get(key) | |
| if current is None or entry["score"] > current["score"] or (entry["score"] == current["score"] and entry["hops"] < current["hops"]): | |
| best_by_pair[key] = entry | |
| for left_kink_id in left_only: | |
| for edge in first_edges.get(left_kink_id, []): | |
| strength = _related_edge_strength(edge) | |
| if edge["id"] in right_only: | |
| add_candidate( | |
| left_kink_id, | |
| edge["id"], | |
| score=strength, | |
| hops=1, | |
| path=[left_kink_id, edge["id"]], | |
| edge_types=[edge["type"]], | |
| ) | |
| for edge2 in second_edges.get(edge["id"], []): | |
| if edge2["id"] not in right_only: | |
| continue | |
| add_candidate( | |
| left_kink_id, | |
| edge2["id"], | |
| score=strength * _related_edge_strength(edge2) * _RELATED_TWO_HOP_DECAY, | |
| hops=2, | |
| path=[left_kink_id, edge["id"], edge2["id"]], | |
| edge_types=[edge["type"], edge2["type"]], | |
| ) | |
| for right_kink_id in right_only: | |
| for edge in first_edges.get(right_kink_id, []): | |
| strength = _related_edge_strength(edge) | |
| if edge["id"] in left_only: | |
| add_candidate( | |
| edge["id"], | |
| right_kink_id, | |
| score=strength, | |
| hops=1, | |
| path=[edge["id"], right_kink_id], | |
| edge_types=[edge["type"]], | |
| ) | |
| for edge2 in second_edges.get(edge["id"], []): | |
| if edge2["id"] not in left_only: | |
| continue | |
| add_candidate( | |
| edge2["id"], | |
| right_kink_id, | |
| score=strength * _related_edge_strength(edge2) * _RELATED_TWO_HOP_DECAY, | |
| hops=2, | |
| path=[edge2["id"], edge["id"], right_kink_id], | |
| edge_types=[edge["type"], edge2["type"]], | |
| ) | |
| related = sorted( | |
| best_by_pair.values(), | |
| key=lambda item: ( | |
| int(item["hops"]), | |
| -float(item["score"]), | |
| item["left_kink"]["name"].lower(), | |
| item["right_kink"]["name"].lower(), | |
| ), | |
| ) | |
| return related[:limit] | |
| def _related_positive_matches_for_users( | |
| self, | |
| users: list[dict[str, Any]], | |
| *, | |
| limit: int = 12, | |
| ) -> list[dict[str, Any]]: | |
| if len(users) != 2: | |
| return [] | |
| return self._related_positive_matches_for_pair(users[0], users[1], limit=limit) | |
| def _pair_safe_similarity_floor(self, direct_count: int, support_count: int, popularity: float) -> bool: | |
| if support_count >= 2: | |
| return True | |
| if direct_count >= 3 and popularity >= 75: | |
| return True | |
| if direct_count >= 2 and popularity >= 180: | |
| return True | |
| return direct_count == 1 and popularity >= 450 | |
| def _direct_shared_ids_for_users(self, users: list[dict[str, Any]]) -> list[str]: | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| if len(users) < 2: | |
| return [] | |
| shared_ids = set(users[0]["plays"]) | |
| for user in users[1:]: | |
| shared_ids &= set(user["plays"]) | |
| direct_ids: list[str] = [] | |
| for kink_id in sorted(shared_ids): | |
| states = [user["plays"][kink_id] for user in users] | |
| kink = detail_by_id.get(kink_id) | |
| if not kink: | |
| continue | |
| if play_excluded_from_surfacing(kink): | |
| continue | |
| if all(state["interest_state"] in POSITIVE_RATINGS for state in states) and self._directions_compatible_group( | |
| [state["directions"] for state in states], | |
| kink, | |
| ): | |
| direct_ids.append(kink_id) | |
| return direct_ids | |
| def _scenario_direct_matches_for_users( | |
| self, | |
| users: list[dict[str, Any]], | |
| *, | |
| limit: int = 24, | |
| ) -> list[dict[str, Any]]: | |
| if len(users) < 2: | |
| return [] | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| positive_by_user = [_overlap_eligible_scenario_states(user) for user in users] | |
| if any(not prefs for prefs in positive_by_user): | |
| return [] | |
| shared_ids = set(positive_by_user[0]) | |
| for prefs in positive_by_user[1:]: | |
| shared_ids &= set(prefs) | |
| out: list[dict[str, Any]] = [] | |
| for scenario_id in sorted(shared_ids): | |
| states = [prefs[scenario_id] for prefs in positive_by_user] | |
| scenario = detail_by_id.get(scenario_id) | |
| if not scenario: | |
| continue | |
| parent_ids = [state.get("parent_kink_id", "") for state in states] | |
| parent_id = next((pid for pid in parent_ids if pid), "") | |
| parent = detail_by_id.get(parent_id) if parent_id else None | |
| compatibility_kink = parent or scenario | |
| if not self._directions_compatible_group([state.get("directions", []) for state in states], compatibility_kink): | |
| continue | |
| out.append( | |
| { | |
| "scenario_kink": scenario, | |
| "parent_kink": parent, | |
| "score": round(1.0 + min(float(scenario.get("popularity", 0.0) or 0.0) / 100000.0, 0.2), 4), | |
| "partner_reactions": { | |
| user["id"]: { | |
| "parent_kink_id": positive_by_user[i][scenario_id].get("parent_kink_id", ""), | |
| "interest_state": positive_by_user[i][scenario_id].get("interest_state", ""), | |
| "directions": positive_by_user[i][scenario_id].get("directions", []), | |
| } | |
| for i, user in enumerate(users) | |
| }, | |
| } | |
| ) | |
| out.sort( | |
| key=lambda item: ( | |
| -float(item.get("score", 0.0) or 0.0), | |
| (item.get("parent_kink") or {}).get("name", ""), | |
| item["scenario_kink"]["name"].lower(), | |
| ) | |
| ) | |
| return out[:limit] | |
| def _scenario_related_matches_for_pair( | |
| self, | |
| left: dict[str, Any], | |
| right: dict[str, Any], | |
| *, | |
| limit: int = 24, | |
| ) -> list[dict[str, Any]]: | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| left_positive = _overlap_eligible_scenario_states(left) | |
| right_positive = _overlap_eligible_scenario_states(right) | |
| if not left_positive or not right_positive: | |
| return [] | |
| left_by_parent: dict[str, list[tuple[str, dict[str, Any]]]] = defaultdict(list) | |
| right_by_parent: dict[str, list[tuple[str, dict[str, Any]]]] = defaultdict(list) | |
| for sid, state in left_positive.items(): | |
| parent_id = state.get("parent_kink_id", "") | |
| if parent_id: | |
| left_by_parent[parent_id].append((sid, state)) | |
| for sid, state in right_positive.items(): | |
| parent_id = state.get("parent_kink_id", "") | |
| if parent_id: | |
| right_by_parent[parent_id].append((sid, state)) | |
| out: list[dict[str, Any]] = [] | |
| for parent_id in sorted(set(left_by_parent) & set(right_by_parent)): | |
| parent = detail_by_id.get(parent_id) | |
| if not parent: | |
| continue | |
| for left_sid, left_state in left_by_parent[parent_id]: | |
| for right_sid, right_state in right_by_parent[parent_id]: | |
| if left_sid == right_sid: | |
| continue | |
| left_scenario = detail_by_id.get(left_sid) | |
| right_scenario = detail_by_id.get(right_sid) | |
| if not left_scenario or not right_scenario: | |
| continue | |
| if not self._directions_compatible(left_state.get("directions", []), right_state.get("directions", []), parent): | |
| continue | |
| score = 0.7 + min(float(parent.get("popularity", 0.0) or 0.0) / 100000.0, 0.18) | |
| out.append( | |
| { | |
| "left_user_id": left["id"], | |
| "right_user_id": right["id"], | |
| "parent_kink": parent, | |
| "left_scenario": left_scenario, | |
| "right_scenario": right_scenario, | |
| "score": round(score, 4), | |
| "reasons": [f"Both are scenarios under {parent['name']}"], | |
| "partner_reactions": { | |
| left["id"]: { | |
| "scenario_kink_id": left_sid, | |
| "interest_state": left_state.get("interest_state", ""), | |
| "directions": left_state.get("directions", []), | |
| }, | |
| right["id"]: { | |
| "scenario_kink_id": right_sid, | |
| "interest_state": right_state.get("interest_state", ""), | |
| "directions": right_state.get("directions", []), | |
| }, | |
| }, | |
| } | |
| ) | |
| out.sort( | |
| key=lambda item: ( | |
| -float(item.get("score", 0.0) or 0.0), | |
| item["parent_kink"]["name"].lower(), | |
| item["left_scenario"]["name"].lower(), | |
| item["right_scenario"]["name"].lower(), | |
| ) | |
| ) | |
| return out[:limit] | |
| def _scenario_related_matches_for_users( | |
| self, | |
| users: list[dict[str, Any]], | |
| *, | |
| limit: int = 24, | |
| ) -> list[dict[str, Any]]: | |
| if len(users) != 2: | |
| return [] | |
| return self._scenario_related_matches_for_pair(users[0], users[1], limit=limit) | |
| def _shared_play_list_for_users(self, users: list[dict[str, Any]], limit: int = 24) -> dict[str, list[dict[str, Any]]]: | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| direct_ids = self._direct_shared_ids_for_users(users) | |
| candidate_scores: dict[str, float] = defaultdict(float) | |
| candidate_reasons: dict[str, set[str]] = defaultdict(set) | |
| candidate_support: dict[str, set[str]] = defaultdict(set) | |
| suppressed_ids = { | |
| kink_id | |
| for user in users | |
| for kink_id, state in user["plays"].items() | |
| if state["interest_state"] in SUPPRESSED_RATINGS | BLOCKED_RATINGS | |
| } | |
| for kink_id in direct_ids: | |
| source = detail_by_id.get(kink_id) | |
| if not source: | |
| continue | |
| for edge in self._edges_for_kink(kink_id, limit=12): | |
| if edge["id"] in direct_ids or any(edge["id"] in user["plays"] for user in users): | |
| continue | |
| target = detail_by_id.get(edge["id"]) | |
| if not target or edge["id"] in suppressed_ids: | |
| continue | |
| if not target.get("shared_eligible"): | |
| continue | |
| score = edge["score"] - self._discoverability_penalty(target["cluster"]) | |
| score += min(target["popularity"] / 60000.0, 0.3) | |
| if score <= 0: | |
| continue | |
| candidate_scores[target["id"]] += score | |
| candidate_support[target["id"]].add(kink_id) | |
| candidate_reasons[target["id"]].add(f"close to {source['name']}") | |
| direct_count = len(direct_ids) | |
| similar = [] | |
| for kink_id, score in sorted(candidate_scores.items(), key=lambda item: item[1], reverse=True): | |
| if kink_id not in detail_by_id: | |
| continue | |
| kink = detail_by_id[kink_id] | |
| support_count = len(candidate_support.get(kink_id, set())) | |
| if not self._pair_safe_similarity_floor(direct_count, support_count, kink["popularity"]): | |
| continue | |
| similar.append( | |
| { | |
| "kink": kink, | |
| "score": score + min(support_count * 0.08, 0.24), | |
| "reasons": self._finalize_reasons( | |
| candidate_reasons[kink_id], | |
| fallback="close to what you both already like", | |
| ), | |
| "support_count": support_count, | |
| } | |
| ) | |
| if len(similar) >= limit: | |
| break | |
| direct = [] | |
| for kink_id in direct_ids: | |
| kink = detail_by_id.get(kink_id) | |
| if not kink: | |
| continue | |
| entry = dict(kink) | |
| entry["partner_reactions"] = { | |
| user["id"]: user["plays"][kink_id]["interest_state"] | |
| for user in users | |
| if kink_id in user.get("plays", {}) | |
| } | |
| direct.append(entry) | |
| direct.sort(key=lambda item: (-item["popularity"], item["name"].lower())) | |
| related = self._related_positive_matches_for_users(users, limit=limit) | |
| scenario_matches = self._scenario_direct_matches_for_users(users, limit=limit) | |
| scenario_related = self._scenario_related_matches_for_users(users, limit=limit) | |
| return { | |
| "direct_matches": direct, | |
| "related_matches": related, | |
| "scenario_matches": scenario_matches, | |
| "scenario_related_matches": scenario_related, | |
| "similar_matches": similar, | |
| } | |
| def _empty_shared_play_response() -> dict[str, Any]: | |
| return { | |
| "direct_matches": [], | |
| "related_matches": [], | |
| "scenario_matches": [], | |
| "scenario_related_matches": [], | |
| "similar_matches": [], | |
| "viewer_boundary_alerts": {"count": 0}, | |
| } | |
| def _viewer_boundary_alert_count(viewer: dict[str, Any], others: list[dict[str, Any]]) -> int: | |
| """Count the viewer's hard_no kinks that any other participant marks love/like. | |
| Returns a count only — kink identities are intentionally not exposed. The viewer | |
| is the only side that sees their own number; partners querying the same group | |
| get a different number computed against their own hard_nos. | |
| """ | |
| viewer_id = viewer.get("id") | |
| viewer_hard_no_ids = { | |
| kink_id | |
| for kink_id, state in viewer.get("plays", {}).items() | |
| if state.get("interest_state") in BLOCKED_RATINGS | |
| } | |
| if not viewer_hard_no_ids: | |
| return 0 | |
| conflicting: set[str] = set() | |
| for other in others: | |
| if other.get("id") == viewer_id: | |
| continue | |
| for kink_id, state in other.get("plays", {}).items(): | |
| if ( | |
| kink_id in viewer_hard_no_ids | |
| and state.get("interest_state") in STRONG_POSITIVE_RATINGS | |
| ): | |
| conflicting.add(kink_id) | |
| return len(conflicting) | |
| def shared_play_list(self, user_id: str, partner_id: str, limit: int = 24) -> dict[str, Any]: | |
| left = self.get_user(user_id) | |
| right = self.get_user(partner_id) | |
| if not left or not right: | |
| return _empty_shared_play_response() | |
| result = self._shared_play_list_for_users([left, right], limit=limit) | |
| result["viewer_boundary_alerts"] = { | |
| "count": _viewer_boundary_alert_count(left, [right]), | |
| } | |
| return result | |
| def shared_play_list_for_group(self, owner_user_id: str, group_id: str, limit: int = 24) -> dict[str, Any]: | |
| users = self._group_participants(owner_user_id, group_id) | |
| if len(users) < 2: | |
| return _empty_shared_play_response() | |
| result = self._shared_play_list_for_users(users, limit=limit) | |
| viewer = next((u for u in users if u.get("id") == owner_user_id), None) | |
| others = [u for u in users if u.get("id") != owner_user_id] | |
| count = _viewer_boundary_alert_count(viewer, others) if viewer else 0 | |
| result["viewer_boundary_alerts"] = {"count": count} | |
| return result | |
| def prompt_candidates(self, user_id: str, limit: int = 8, group_id: str | None = None) -> list[dict[str, Any]]: | |
| if group_id: | |
| return self.group_prompt_candidates(user_id, group_id, limit=limit) | |
| user = self.get_user(user_id) | |
| if not user or not user["partners"]: | |
| return [] | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| dismissed: set[str] = set() | |
| with Session(self.engine) as session: | |
| dismissed = { | |
| row.kink_id | |
| for row in session.exec(select(PromptDismissal).where(PromptDismissal.user_id == user_id)).all() | |
| } | |
| user_play_ids = set(user["plays"]) | |
| blocked_ids = { | |
| kink_id | |
| for kink_id, state in user["plays"].items() | |
| if state["interest_state"] in SUPPRESSED_RATINGS | BLOCKED_RATINGS | |
| } | |
| candidate_scores: dict[str, float] = defaultdict(float) | |
| candidate_support: dict[str, set[str]] = defaultdict(set) | |
| candidate_reasons: dict[str, set[str]] = defaultdict(set) | |
| for partner_id in user["partners"]: | |
| partner = self.get_user(partner_id) | |
| if not partner: | |
| continue | |
| partner_positive = { | |
| kink_id: state | |
| for kink_id, state in partner["plays"].items() | |
| if state["interest_state"] in POSITIVE_RATINGS | |
| } | |
| direct_shared_ids = self._direct_shared_ids(user, partner) | |
| direct_neighbor_ids = { | |
| edge["id"] | |
| for direct_id in direct_shared_ids | |
| for edge in self._edges_for_kink(direct_id, limit=12) | |
| } | |
| for kink_id in direct_shared_ids: | |
| source = detail_by_id.get(kink_id) | |
| if not source: | |
| continue | |
| for edge in self._edges_for_kink(kink_id, limit=10): | |
| candidate_id = edge["id"] | |
| if candidate_id in user_play_ids or candidate_id in blocked_ids or candidate_id in dismissed: | |
| continue | |
| target = detail_by_id.get(candidate_id) | |
| if not target: | |
| continue | |
| candidate_scores[candidate_id] += edge["score"] + min(target["popularity"] / 80000.0, 0.18) | |
| candidate_support[candidate_id].add(kink_id) | |
| candidate_reasons[candidate_id].add(f"aligned with {source['name']}") | |
| for kink_id in partner_positive: | |
| if kink_id in user_play_ids or kink_id in blocked_ids or kink_id in dismissed: | |
| continue | |
| target = detail_by_id.get(kink_id) | |
| supported_by_shared_lane = kink_id in direct_neighbor_ids and target and target["popularity"] >= 80 | |
| if not target or (not self._safe_prompt_candidate(target, 1) and not supported_by_shared_lane): | |
| continue | |
| candidate_scores[kink_id] += 0.55 + min(target["popularity"] / 70000.0, 0.22) | |
| candidate_support[kink_id].add(f"partner:{partner_id}") | |
| if supported_by_shared_lane: | |
| candidate_support[kink_id].add("shared-lane") | |
| candidate_reasons[kink_id].add("worth getting your take on") | |
| prompts = [] | |
| for kink_id, score in sorted(candidate_scores.items(), key=lambda item: item[1], reverse=True): | |
| kink = detail_by_id.get(kink_id) | |
| if not kink: | |
| continue | |
| support_count = len(candidate_support.get(kink_id, set())) | |
| if not self._safe_prompt_candidate(kink, support_count): | |
| continue | |
| prompts.append( | |
| { | |
| "kink": kink, | |
| "score": score, | |
| "support_count": support_count, | |
| "reasons": sorted(candidate_reasons[kink_id]), | |
| } | |
| ) | |
| if len(prompts) >= limit: | |
| break | |
| return prompts | |
| def group_prompt_candidates(self, requester_user_id: str, group_id: str, limit: int = 8) -> list[dict[str, Any]]: | |
| requester = self.get_user(requester_user_id) | |
| users = self._group_participants(requester_user_id, group_id) | |
| if not requester or len(users) < 2: | |
| return [] | |
| detail_by_id = self._catalog()["detail_by_id"] | |
| with Session(self.engine) as session: | |
| dismissed = { | |
| row.kink_id | |
| for row in session.exec(select(PromptDismissal).where(PromptDismissal.user_id == requester_user_id)).all() | |
| } | |
| requester_play_ids = set(requester["plays"]) | |
| blocked_ids = { | |
| kink_id | |
| for kink_id, state in requester["plays"].items() | |
| if state["interest_state"] in SUPPRESSED_RATINGS | BLOCKED_RATINGS | |
| } | |
| direct_shared_ids = self._direct_shared_ids_for_users(users) | |
| shared_neighbor_ids = { | |
| edge["id"] | |
| for direct_id in direct_shared_ids | |
| for edge in self._edges_for_kink(direct_id, limit=12) | |
| } | |
| candidate_scores: dict[str, float] = defaultdict(float) | |
| candidate_support: dict[str, set[str]] = defaultdict(set) | |
| candidate_reasons: dict[str, set[str]] = defaultdict(set) | |
| for kink_id in direct_shared_ids: | |
| source = detail_by_id.get(kink_id) | |
| if not source: | |
| continue | |
| for edge in self._edges_for_kink(kink_id, limit=10): | |
| candidate_id = edge["id"] | |
| if candidate_id in requester_play_ids or candidate_id in blocked_ids or candidate_id in dismissed: | |
| continue | |
| target = detail_by_id.get(candidate_id) | |
| if not target: | |
| continue | |
| candidate_scores[candidate_id] += edge["score"] + min(target["popularity"] / 80000.0, 0.18) | |
| candidate_support[candidate_id].add(kink_id) | |
| candidate_reasons[candidate_id].add(f"aligned with {source['name']}") | |
| for partner in users: | |
| if partner["id"] == requester_user_id: | |
| continue | |
| partner_positive = { | |
| kink_id: state | |
| for kink_id, state in partner["plays"].items() | |
| if state["interest_state"] in POSITIVE_RATINGS | |
| } | |
| for kink_id in partner_positive: | |
| if kink_id in requester_play_ids or kink_id in blocked_ids or kink_id in dismissed: | |
| continue | |
| target = detail_by_id.get(kink_id) | |
| if not target: | |
| continue | |
| if kink_id in shared_neighbor_ids: | |
| continue | |
| candidate_scores[kink_id] += 0.55 + min(target["popularity"] / 70000.0, 0.22) | |
| candidate_support[kink_id].add(partner["id"]) | |
| candidate_reasons[kink_id].add("worth getting your take on") | |
| prompts = [] | |
| for kink_id, score in sorted(candidate_scores.items(), key=lambda item: item[1], reverse=True): | |
| kink = detail_by_id.get(kink_id) | |
| if not kink: | |
| continue | |
| support_count = len(candidate_support.get(kink_id, set())) | |
| if not self._safe_prompt_candidate(kink, support_count): | |
| continue | |
| prompts.append( | |
| { | |
| "kink": kink, | |
| "score": score, | |
| "support_count": support_count, | |
| "reasons": self._finalize_reasons( | |
| candidate_reasons[kink_id], | |
| fallback="worth getting your take on", | |
| ), | |
| } | |
| ) | |
| if len(prompts) >= limit: | |
| break | |
| return prompts | |