""" NLP encoder using sentence-transformers (all-MiniLM-L6-v2). Encodes user profile_text and content synopsis into 384-dim vectors. Falls back to deterministic hash-based encoding if models can't be loaded. """ import numpy as np import sys HAS_ST = False # Will be set to True when successfully loaded _st_model = None # Will hold SentenceTransformer once loaded _load_attempted = False def _ensure_st_loaded(): """Lazy import sentence_transformers on first use with timeout.""" global HAS_ST, _st_model, _load_attempted if _load_attempted: return # Already tried, don't retry _load_attempted = True print("[NLPEncoder] Loading sentence-transformers model...", file=sys.stderr) sys.stderr.flush() try: print("[NLPEncoder] Attempting to import sentence_transformers...", file=sys.stderr) sys.stderr.flush() from sentence_transformers import SentenceTransformer print("[NLPEncoder] Importing SentenceTransformer...", file=sys.stderr) sys.stderr.flush() _st_model = SentenceTransformer('all-MiniLM-L6-v2') print("[NLPEncoder] Model loaded successfully!", file=sys.stderr) sys.stderr.flush() HAS_ST = True except Exception as e: print(f"[NLPEncoder] Failed to load SentenceTransformer: {e}", file=sys.stderr) print(f"[NLPEncoder] Will use fallback deterministic embeddings", file=sys.stderr) sys.stderr.flush() HAS_ST = False _st_model = None class NLPEncoder: def __init__(self): self.model = None self._loaded = False def _ensure_loaded(self): """Lazy load the model on first use.""" if not self._loaded: _ensure_st_loaded() self.model = _st_model self._loaded = True def encode(self, text: str) -> np.ndarray: self._ensure_loaded() if self.model is not None: try: emb = self.model.encode(text, convert_to_numpy=True) emb = emb.astype('float32') except Exception as e: print(f"[NLPEncoder] Encoding failed: {e}, using fallback", file=sys.stderr) sys.stderr.flush() # fallback vec = np.frombuffer(text.encode('utf-8'), dtype=np.uint8) rng = np.random.default_rng(np.sum(vec)) emb = rng.standard_normal(384).astype('float32') else: # fallback deterministic hash-based vector vec = np.frombuffer(text.encode('utf-8'), dtype=np.uint8) rng = np.random.default_rng(np.sum(vec)) emb = rng.standard_normal(384).astype('float32') # L2-normalise norm = np.linalg.norm(emb) if norm == 0: return emb return emb / norm def encode_user(self, user: dict) -> np.ndarray: text = user.get('profile_text', '') + ' | ' + ','.join(user.get('watch_history', [])) return self.encode(text) def encode_content(self, content: dict) -> np.ndarray: return self.encode(content.get('synopsis', ''))