petalert-backend / utils /multi_embed.py
Tomasv00805's picture
Fix: embeddings en thread (to_thread) + create no bloqueante (evita congelar el event loop)
26dbb8d
Raw
History Blame Contribute Delete
1.88 kB
"""
Helper de embeddings multi-foto (pipeline de despliegue).
- embed_photo_urls: descarga y embebe CADA foto de un reporte (YOLO + TTA).
- build_report_update: arma el dict para actualizar el reporte con:
* photo_embeddings: lista con el embedding de cada foto (jsonb)
* embedding: el promedio normalizado (compatibilidad / fallback / filtros)
El matching all-pairs usa photo_embeddings; embedding queda como respaldo.
"""
import asyncio
import numpy as np
import httpx
from services.embeddings import image_bytes_to_vec
async def embed_photo_urls(photo_urls):
"""Devuelve lista de np.ndarray (un embedding por foto descargable).
El embedding (CPU pesada) se ejecuta en un THREAD aparte (asyncio.to_thread)
para NO bloquear el event loop: así el servidor sigue respondiendo a /health
y otras peticiones mientras genera los embeddings (clave en CPU lenta)."""
vecs = []
timeout = httpx.Timeout(60.0, connect=60.0)
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
for url in photo_urls:
if not url:
continue
try:
r = await client.get(url)
r.raise_for_status()
content = r.content
vec = await asyncio.to_thread(image_bytes_to_vec, content)
vecs.append(vec)
except Exception as e:
print(f"⚠️ [multi_embed] no se pudo embeber {url}: {e}")
return vecs
def build_report_update(vecs):
"""dict listo para .update() en reports, o None si no hubo embeddings."""
if not vecs:
return None
arr = np.stack(vecs).astype("float32")
mean = arr.mean(axis=0)
mean = mean / (np.linalg.norm(mean) + 1e-9)
return {
"photo_embeddings": [v.astype("float32").tolist() for v in vecs],
"embedding": mean.tolist(),
}