from __future__ import annotations import logging from functools import lru_cache from pathlib import Path from typing import Any import numpy as np from PIL import Image from src.config import DEFAULT_MODEL_NAME, ConfigurationError logger = logging.getLogger(__name__) def _detect_device() -> str: try: import torch return "cuda" if torch.cuda.is_available() else "cpu" except Exception: return "cpu" @lru_cache(maxsize=2) def _load_model(model_name: str, device: str) -> Any: try: from sentence_transformers import SentenceTransformer except ImportError as exc: raise ConfigurationError( "The sentence-transformers package is not installed. Run `pip install -r requirements.txt`." ) from exc logger.info("Loading CLIP model %s on %s", model_name, device) model = SentenceTransformer(model_name, device=device) logger.info("CLIP model loaded") return model class ClipEmbedder: def __init__(self, model_name: str = DEFAULT_MODEL_NAME) -> None: self.model_name = model_name self.device = _detect_device() self._model = _load_model(self.model_name, self.device) def encode_text(self, text: str) -> list[float]: text = (text or "").strip() if not text: raise ValueError("Text query is empty.") logger.info("Encoding text query") embedding = self._model.encode( [text], batch_size=1, convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=False, )[0] return self._normalize_to_list(embedding) def encode_image(self, image: Image.Image | str | Path) -> list[float]: logger.info("Encoding image query") if isinstance(image, Image.Image): pil_image = image.convert("RGB") else: with Image.open(image) as opened: pil_image = opened.convert("RGB") embedding = self._model.encode( [pil_image], batch_size=1, convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=False, )[0] return self._normalize_to_list(embedding) @staticmethod def _normalize_to_list(vector: np.ndarray) -> list[float]: array = np.asarray(vector, dtype=np.float32).reshape(-1) norm = float(np.linalg.norm(array)) if norm > 0: array = array / norm return array.astype(float).tolist()