""" 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(), }