Spaces:
Sleeping
Sleeping
File size: 4,719 Bytes
b02461f 8f028d3 3fc2b22 05d11c3 b02461f e0eb667 26777af 8f028d3 b02461f e0eb667 26777af b02461f 05d11c3 233841c b02461f 26777af 3fc2b22 e624423 420aeff 8f028d3 b02461f 8f028d3 b02461f 8f028d3 b02461f 233841c b02461f 26777af b02461f 8f028d3 233841c 26777af 3fc2b22 26777af 8f028d3 b02461f e0eb667 b02461f 8f028d3 e2113c0 3fc2b22 233841c 8f028d3 e2113c0 233841c b02461f 233841c 3fc2b22 233841c b02461f 3fc2b22 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# app.py
# -*- coding: utf-8 -*-
import os, pickle, numpy as np
from typing import Dict, Tuple
from fastapi import FastAPI
from pydantic import BaseModel, Field
from sentence_transformers import SentenceTransformer
from transformers import pipeline
# ---- Performance flags ----
os.environ["TOKENIZERS_PARALLELISM"] = "false"
try:
import torch
torch.set_num_threads(1) # evita thrashing en CPU básica
except Exception:
pass
# ---- Carga artefactos una vez ----
lw: Dict = pickle.load(open("predictor.pkl", "rb"))
sbert = SentenceTransformer(lw["model_name"])
# centroides normalizados
centroides = {int(k): np.array(v, dtype=np.float32) for k, v in lw["centroides"].items()}
for k, v in centroides.items():
n = np.linalg.norm(v) + 1e-12
centroides[k] = (v / n).astype(np.float32)
cids = sorted(centroides.keys())
meta = lw.get("meta", {}) or {}
# (OPCIONAL) umbrales por cluster guardados en el pickle, p.ej. similitud mínima histórica
# formato esperado: { "margenes": { "0": 0.50, "1": 0.58, ... } }
_margenes_raw = lw.get("margenes") or lw.get("margins") or {}
MARGENES = {int(k): float(v) for k, v in _margenes_raw.items()} if isinstance(_margenes_raw, dict) else {}
# Umbral global por defecto (puede sobreescribirse por env o por lw["tau_otros"])
TAU_OTROS = float(os.getenv("TAU_OTROS", str(lw.get("tau_otros", 0.7))))
# Sentimiento (modelo liviano; recorta a 256 tokens)
sentiment = pipeline(
"text-classification",
model="UMUTeam/roberta-spanish-sentiment-analysis",
device=-1
)
EMOTIONS = ["alegría", "tristeza", "ira", "asco", "miedo", "sorpresa", "neutral"]
HYP = "El texto expresa {}."
# Precompute embeddings de emociones con el mismo encoder (rápido)
_emotion_texts = [HYP.format(e) for e in EMOTIONS]
_emotion_embs = sbert.encode(
_emotion_texts, convert_to_numpy=True, normalize_embeddings=True
).astype(np.float32)
app = FastAPI(title="Predicción de clusters/sentimiento/emoción")
# -------- Helpers --------
def _encode(text: str) -> np.ndarray:
emb = sbert.encode(text, convert_to_numpy=True, normalize_embeddings=True).astype(np.float32)
return emb[None, :] if emb.ndim == 1 else emb
def _mejor_cluster_y_similitud(vec: np.ndarray) -> Tuple[int, float]:
"""
Devuelve (cid_mejor, similitud_cos). Como todo está normalizado, cos = dot.
"""
# apilamos centroides en una matriz para multiplicar de una
C = np.vstack([centroides[c] for c in cids]).astype(np.float32) # shape: (K, D)
sims = (C @ vec.reshape(-1, 1)).squeeze(-1) # shape: (K,)
i = int(np.argmax(sims))
return cids[i], float(sims[i])
def _truncate_for_classifier(text: str, max_chars: int = 1000) -> str:
return text if len(text) <= max_chars else text[:max_chars]
def _fast_emotion(emb: np.ndarray) -> str:
sims = (_emotion_embs @ emb.reshape(-1, 1)).squeeze(-1)
return EMOTIONS[int(np.argmax(sims))]
# -------- Schema de entrada --------
class Entrada(BaseModel):
# acepta "asunto" o "subject"
asunto: str = Field(default="", alias="subject")
# acepta "cuerpo" o "body"
cuerpo: str = Field(default="", alias="body")
class Config:
populate_by_name = True # permite usar los nombres sin alias también
# -------- Endpoint --------
@app.post("/predict")
def predict(item: Entrada):
subject = (item.asunto or "").strip()
body = (item.cuerpo or "").strip()
text = f"{subject} — {body}".strip(" —")
emb = _encode(text)[0]
cid_mejor, sim = _mejor_cluster_y_similitud(emb)
# umbral: usa específico del cluster si existe; si no, global
tau = float(MARGENES.get(cid_mejor, TAU_OTROS))
# si la similitud no supera el umbral -> "otros"
es_otros = sim < tau
if es_otros:
cid = -1
nombre = "otros"
desc = "No se parece lo suficiente a ningún cluster conocido."
else:
cid = cid_mejor
m = meta.get(str(cid), meta.get(cid, {})) or {}
nombre = m.get("nombre")
desc = m.get("descripcion")
# RÁPIDO: sentimiento con truncado
s = sentiment(_truncate_for_classifier(text), truncation=True, max_length=256)[0]["label"]
# RÁPIDO: emoción por similitud con SBERT (sin segundo Transformer)
e = _fast_emotion(emb)
return {
"asunto": subject,
"cuerpo": body,
"cluster": cid,
"cluster_nombre": nombre,
"cluster_desc": desc,
"similitud_cluster": round(sim, 4),
"umbral_usado": round(tau, 4),
"sentimiento": s,
"emocion": e
}
# -------- Entrypoint opcional --------
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", "8000")))
|