Spaces:
Running
Running
File size: 5,894 Bytes
e3408e0 | 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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """
embeddings.py — Vectorización de textos via HuggingFace Inference API.
Convierte textos en vectores numéricos (embeddings) usando el modelo
paraphrase-multilingual-MiniLM-L12-v2 corriendo en la nube de HuggingFace.
No requiere GPU ni modelos locales — todo corre remotamente.
Manejo de errores:
- 503 (modelo cargando): espera HF_RETRY_DELAY segundos, reintenta hasta 3 veces
- 429 (rate limit): espera HF_RATELIMIT_DELAY segundos, reintenta
- Otros errores: loguea y devuelve lista vacía para ese batch
"""
import logging
import time
from typing import Any
import requests
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import (
HUGGINGFACE_API_KEY,
HF_API_URL_EMBEDDINGS,
HF_BATCH_SIZE,
HF_RETRY_DELAY,
HF_RATELIMIT_DELAY,
SCRAPING_MAX_RETRIES,
LOG_FORMAT,
LOG_DATE_FORMAT,
LOG_LEVEL,
)
# ── Logging ───────────────────────────────────────────────────────────────────
logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
logger = logging.getLogger("embeddings")
logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
def _headers() -> dict:
"""Construye los headers HTTP para la API de HuggingFace."""
headers = {"Content-Type": "application/json"}
if HUGGINGFACE_API_KEY:
headers["Authorization"] = f"Bearer {HUGGINGFACE_API_KEY}"
return headers
def _fetch_batch(textos: list[str]) -> list[list[float]]:
"""
Envía un batch de textos a HuggingFace y devuelve sus embeddings.
Maneja reintentos automáticos para errores 503 y 429.
Devuelve lista vacía si todos los intentos fallan.
"""
payload = {"inputs": textos, "options": {"wait_for_model": True}}
for intento in range(1, SCRAPING_MAX_RETRIES + 1):
try:
t0 = time.time()
resp = requests.post(
HF_API_URL_EMBEDDINGS,
headers=_headers(),
json=payload,
timeout=60,
)
elapsed = time.time() - t0
if resp.status_code == 200:
resultado = resp.json()
logger.info(
f"[embeddings] batch {len(textos)} textos — "
f"{elapsed:.1f}s — dim={len(resultado[0]) if resultado else 0}"
)
return resultado
elif resp.status_code == 503:
logger.warning(
f"[embeddings] 503 modelo cargando (intento {intento}/"
f"{SCRAPING_MAX_RETRIES}) — esperando {HF_RETRY_DELAY}s"
)
time.sleep(HF_RETRY_DELAY)
elif resp.status_code == 429:
logger.warning(
f"[embeddings] 429 rate limit — esperando {HF_RATELIMIT_DELAY}s"
)
time.sleep(HF_RATELIMIT_DELAY)
else:
logger.error(
f"[embeddings] error {resp.status_code}: {resp.text[:200]}"
)
return []
except requests.exceptions.Timeout:
logger.warning(f"[embeddings] timeout en intento {intento}/{SCRAPING_MAX_RETRIES}")
if intento < SCRAPING_MAX_RETRIES:
time.sleep(HF_RETRY_DELAY)
except Exception as e:
logger.error(f"[embeddings] error inesperado: {e}")
return []
logger.error(f"[embeddings] todos los reintentos fallaron para batch de {len(textos)} textos")
return []
def get_embeddings(texts: list[str]) -> list[list[float]]:
"""
Vectoriza una lista de textos usando HuggingFace Inference API.
Procesa en batches de HF_BATCH_SIZE para respetar los límites del free tier.
Los textos vacíos se saltan y se devuelve lista vacía en su posición.
Parámetros:
texts: lista de strings a vectorizar
Devuelve:
Lista de embeddings (lista de floats). Mismo orden que la entrada.
Las posiciones con error devuelven lista vacía [].
"""
if not texts:
return []
todos_los_embeddings: list[list[float]] = []
for i in range(0, len(texts), HF_BATCH_SIZE):
batch = texts[i: i + HF_BATCH_SIZE]
# Filtrar textos vacíos, preservar posiciones
indices_validos = [j for j, t in enumerate(batch) if t and t.strip()]
textos_validos = [batch[j] for j in indices_validos]
if not textos_validos:
todos_los_embeddings.extend([[] for _ in batch])
continue
embeddings_batch = _fetch_batch(textos_validos)
# Validar que la API devolvió el mismo número de embeddings que textos
if embeddings_batch and len(embeddings_batch) != len(textos_validos):
logger.warning(
f"[embeddings] mismatch: enviados {len(textos_validos)} textos, "
f"recibidos {len(embeddings_batch)} embeddings — se completan con []"
)
# Completar o recortar para que el zip funcione correctamente
if len(embeddings_batch) < len(textos_validos):
embeddings_batch.extend([] for _ in range(len(textos_validos) - len(embeddings_batch)))
else:
embeddings_batch = embeddings_batch[:len(textos_validos)]
# Reconstruir con posiciones originales
resultado_batch: list[list[float]] = [[] for _ in batch]
for pos, emb in zip(indices_validos, embeddings_batch):
resultado_batch[pos] = emb
todos_los_embeddings.extend(resultado_batch)
logger.info(
f"[embeddings] completado — {len(texts)} textos, "
f"{sum(1 for e in todos_los_embeddings if e)} con embedding válido"
)
return todos_los_embeddings
|