"""Singleton менеджер sentence-transformers модели. Модель загружается один раз при первом обращении и кэшируется в памяти GPU. """ import logging import numpy as np from app.config import settings logger = logging.getLogger(__name__) class ModelManager: """Singleton для управления эмбеддинг-моделью.""" _instance = None _model = None _device: str = settings.EMBED_DEVICE @classmethod def get_model(cls): """Получить или загрузить модель sentence-transformers.""" if cls._model is None: # Импорт здесь, чтобы не блокировать импорт модуля если torch не установлен from sentence_transformers import SentenceTransformer logger.info( f"Загрузка модели {settings.EMBED_MODEL!r} на устройство {settings.EMBED_DEVICE!r}..." ) try: cls._model = SentenceTransformer( settings.EMBED_MODEL, device=settings.EMBED_DEVICE, ) logger.info( f"Модель загружена. Размерность вектора: {cls._model.get_sentence_embedding_dimension()}" ) except Exception as e: # Graceful degradation на CPU если CUDA недоступна logger.warning(f"Не удалось загрузить модель на CUDA: {e}. Переключение на CPU.") cls._device = "cpu" cls._model = SentenceTransformer(settings.EMBED_MODEL, device="cpu") return cls._model @classmethod def encode(cls, texts: list[str]) -> np.ndarray: """ Закодировать тексты в векторы. Args: texts: Список текстов для кодирования Returns: numpy массив формы (len(texts), 768), нормализованный для cosine similarity """ if not texts: return np.array([]).reshape(0, settings.EMBED_DIM) model = cls.get_model() vectors = model.encode( texts, batch_size=settings.EMBED_BATCH_SIZE, normalize_embeddings=True, # Нормализация для cosine через inner product show_progress_bar=len(texts) > 100, convert_to_numpy=True, ) return vectors.astype(np.float32) @classmethod def encode_single(cls, text: str) -> np.ndarray: """Закодировать один текст. Удобный метод.""" return cls.encode([text])[0] @classmethod def unload(cls) -> None: """Выгрузить модель из памяти (для тестов/диагностики).""" cls._model = None logger.info("Модель выгружена из памяти")