# src/expon/ml/text_embed.py from __future__ import annotations import re import numpy as np class TextEncoder: """ Encoder de texto super liviano basado en keywords. Produce una distribución de probabilidad sobre las clases. """ # Orden de clases (¡mantener igual en todo el proyecto!) labels = ["entusiasta", "neutra", "confiada", "ansiosa", "motivada", "nerviosa"] # Palabras/claves simples por clase (ajustables) _lexicon = { "entusiasta": [ r"emocionad[oa]s?", r"entusias[mt]o?", r"content[oa]s?", r"alegr[ia]", r"feliz", r"genial", r"fant[aá]stic[oa]", r"encantad[oa]" ], "neutra": [ r"present(a|are)mos", r"metodolog[ií]a", r"resultados?", r"conclusi[oó]n", r"validaci[oó]n", r"an[aá]lisis", r"datos?", r"grupo?s?" ], "confiada": [ r"segur[oa]", r"claro", r"convencid[oa]", r"garantiz[ao]?", r"confirm(a|an|amos)", r"demostr(a|amos|ado)", r"solidez", r"certeza" ], "ansiosa": [ r"preocupad[oa]s?", r"ansios[oa]s?", r"inquiet[oa]s?", r"dud[ao]?", r"nervios?", r"temor", r"incertidumbre", r"estresad[oa]" ], "motivada": [ r"motivad[oa]s?", r"impuls[oa]?", r"seguir trabajando", r"inspirad[oa]", r"proactividad", r"superaci[oó]n", r"lograr", r"objetiv[oa]s?" ], "nerviosa": [ r"nervios[oa]s?", r"tensi[oó]n", r"temblor", r"titube[oa]?", r"dificultad para", r"insegur[oa]", r"me puse", r"me siento mal" ], } def __init__(self) -> None: # Precompilar regex self._compiled = { k: [re.compile(pat, flags=re.IGNORECASE) for pat in arr] for k, arr in self._lexicon.items() } def _score_text(self, text: str) -> np.ndarray: """ Suma match de keywords por clase. Devuelve vector de scores raw (no normalizados). """ text = text or "" scores = [] for lab in self.labels: pats = self._compiled.get(lab, []) s = 0.0 for rgx in pats: for m in rgx.finditer(text): # peso por coincidencia (más largo, un poco más de score) span = m.end() - m.start() s += 1.0 + min(span / 10.0, 1.0) scores.append(s) return np.asarray(scores, dtype=float) def probs(self, text: str) -> np.ndarray: """ Devuelve una distribución [L] en el mismo orden que `labels`. Usa suavizado para evitar vectores nulos. """ raw = self._score_text(text) # Suavizado: prior uniforme pequeño eps = 1e-3 raw = raw + eps s = raw.sum() if s <= 0: return np.full(len(self.labels), 1.0 / len(self.labels), dtype=float) return raw / s