"""Профили занятий и настроений: соответствие трека контексту (context_fit).""" from __future__ import annotations from importlib import resources from pathlib import Path import numpy as np import yaml from numpy.typing import NDArray from pydantic import BaseModel, model_validator FEATURES = ("bpm", "energy", "danceability", "valence", "acousticness") class Rule(BaseModel): feature: str min: float | None = None max: float | None = None sigma: float half_time: bool = False @model_validator(mode="after") def check(self) -> Rule: if self.feature not in FEATURES: raise ValueError(f"unknown feature {self.feature!r}; known: {', '.join(FEATURES)}") if self.min is None and self.max is None: raise ValueError("rule needs min or max") if self.sigma <= 0: raise ValueError("sigma must be positive") return self class Context(BaseModel): icon: str rules: list[Rule] class Contexts(BaseModel): activities: dict[str, Context] moods: dict[str, Context] def load_contexts(path: str = "") -> Contexts: """contexts.yaml из пути или встроенный; ошибка в файле — ошибка запуска (Pydantic).""" raw = ( Path(path).read_text("utf-8") if path else resources.files(__package__).joinpath("contexts.yaml").read_text("utf-8") ) return Contexts.model_validate(yaml.safe_load(raw)) def _bell(x: NDArray[np.float64], rule: Rule) -> NDArray[np.float64]: lo = -np.inf if rule.min is None else rule.min hi = np.inf if rule.max is None else rule.max dist = np.where(x < lo, lo - x, np.where(x > hi, x - hi, 0.0)) return np.asarray(np.exp(-0.5 * (dist / rule.sigma) ** 2)) def fit(features: dict[str, NDArray[np.float64]], rules: list[Rule]) -> NDArray[np.float64]: """Произведение «колоколов» по правилам; NaN (признака нет) — нейтрально (1). half_time: для темпа засчитывается и половинный (бег под 85 BPM — тот же шаг, что 170).""" n = len(next(iter(features.values()))) out = np.ones(n) for rule in rules: x = features[rule.feature] known = ~np.isnan(x) b = _bell(np.nan_to_num(x), rule) if rule.half_time: b = np.maximum(b, _bell(np.nan_to_num(x) * 2, rule)) out *= np.where(known, b, 1.0) return out