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