FR-API: cotejo de rostros, búsqueda de parecidos, deduplicación y conciliación de identidades entre bases de datos. Asistivo — siempre requiere verificación humana.
El FR-API convierte cada rostro en un vector de 512 dimensiones y los compara por similitud de coseno sobre una base vectorial. Detecta si dos fotos corresponden a la misma persona, aunque provengan de plataformas distintas. Su objetivo es ayudar a cruzar y unificar reportes de personas desaparecidas tras el terremoto de Venezuela 2026.
Es asistivo: cada resultado trae un score de similitud y siempre debe confirmarse por una persona antes de tomar decisiones (fusionar, eliminar, contactar).
Subes una foto y el servicio responde si ya existe una persona registrada con ese rostro. Útil al dar de alta un reporte para no crear duplicados.
Subes una foto y obtienes la lista de personas más parecidas ordenadas por score. Asistivo: siempre requiere verificación humana.
Cruzas tu base contra sí misma y obtienes los pares de registros que probablemente sean la misma persona.
Encuentra a la misma persona reportada en bases DISTINTAS y trae sus imágenes de cada base, para unificar identidades entre plataformas.
Subes tus registros (foto + metadatos) al índice vectorial para que sean buscables y comparables por rostro.
https://fr-api.reportavnzla.com:8443/openapi.json.https://reportavnzla.com/fr-api (límite 4.5 MB/archivo).Las rutas /v1/* requieren API key en el header X-API-Key. Nunca la incluyas en código que corra en el navegador: llama al FR-API desde tu servidor(patrón servidor-a-servidor). Las rutas /health y /openapi.json son públicas.
X-API-Key: TU_API_KEYAntes de integrar, confirma que tu .env está bien. Descarga fr-doctor (script sin dependencias) y córrelo en la carpeta de tu .env — te dice en segundos si la key autentica, si tu FR_SOURCE coincide y si ya indexaste datos:
python fr-doctor.py # lee tu .env y diagnosticaO a mano, con /v1/whoami:
# Paso 0: ¿tu key sirve? ¿qué source y cuántos indexados tienes?
curl "https://fr-api.reportavnzla.com:8443/v1/whoami" -H "X-API-Key: TU_API_KEY"
# 401 = key inválida/truncada (deben ser 48 caracteres)
# -> { "key_label":"…", "source_default":"…", "indexed_my_source":0,
# "min_score_default":0.51, "reference_threshold":0.35 }El FR-API es un índice vectorial centralizado. No se conecta a tu Neon/Supabase/Postgres. Solo encuentra caras que tú hayas subido con POST /v1/index.
Flujo: tu BD → /v1/index → índice del FR-API → /v1/check-duplicate busca ahí. Si nunca indexaste, check-duplicate siempre dirá “sin duplicado” (no hay con qué comparar). Haz el backfill primero.
source: entrada vs. salidasource es la etiqueta de la plataforma de origen de un registro. Significa cosas distintas según el endpoint:
| Endpoint | source | Qué significa |
|---|---|---|
| POST /v1/search | salida | En cada resultado: de qué base viene ese parecido. No se envía. |
| POST /v1/check-duplicate | salida | En cada candidato: de qué base es. No se envía. |
| POST /v1/index | entrada (opc.) | Etiqueta de TU plataforma al subir un registro. Default = la de tu key. |
| GET /v1/duplicates?source= | entrada | Qué base depurar (pares dentro de UNA base). Sin él = tu propia base. |
| GET /v1/reconcile?sources= | entrada (opc.) | Limitar la conciliación a ciertas bases. |
Regla simple: comparar una foto (search / check-duplicate) → no mandas source; depurar o conciliar bases → sí indicas la(s) fuente(s).
Dos pasos: (1) subes tus registros al índice una vez, y (2) pides los duplicados. Re-ejecuta el paso 1 al agregar registros nuevos (es idempotente, solo añade lo nuevo).
Un POST /v1/index por persona. Solo entran fotos con rostro (las demás devuelven indexed:false, sin fallar).
# Sube/indexa UN registro de tu base. Idempotente por external_id (tu id).
curl -X POST "https://fr-api.reportavnzla.com:8443/v1/index" \
-H "X-API-Key: TU_API_KEY" \
-F "external_id=ID_EN_TU_BD" \
-F "person_name=Katherine Mendoza" \
-F "last_seen_location=Catia la Mar" \
-F "image_url=https://tu-cdn/fotos/123.jpg" # o -F "file=@foto.jpg"
# -> {"ok":true,"indexed":true,"record_id":"tu-fuente:ID_EN_TU_BD"}Recorriendo tu BD (Node.js):
const FR = "https://fr-api.reportavnzla.com:8443";
const KEY = "TU_API_KEY";
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// 1) trae TUS registros (ajusta a tu BD)
const personas = await db.query(
"SELECT id, nombre, ubicacion, foto_url FROM personas"
);
// 2) súbelos al índice (idempotente; reanudable)
for (const p of personas) {
if (!p.foto_url) continue; // sin foto no se compara
const fd = new FormData();
fd.append("external_id", String(p.id)); // tu id → no duplica
fd.append("person_name", p.nombre ?? "");
fd.append("last_seen_location", p.ubicacion ?? "");
fd.append("image_url", p.foto_url);
const r = await fetch(FR + "/v1/index",
{ method: "POST", headers: { "X-API-Key": KEY }, body: fd });
console.log(p.id, (await r.json()).indexed ? "ok" : "sin-rostro");
await sleep(550); // ~110/min < límite 120/min
}Opción rápida: /v1/duplicates te devuelve los pares (con los dos ids de tu BD, el score y las dos fotos para revisar). Pasa el mismo source con que indexaste en el paso 1.
# Cruza TU base por rostro y lista pares duplicados.
# "source" debe ser EL MISMO con que indexaste (paso 1). Por defecto es la
# etiqueta de tu API key; si indexaste con otro source, pásalo igual aquí.
curl "https://fr-api.reportavnzla.com:8443/v1/duplicates?source=TU_FUENTE&min_score=0.6&limit=500" \
-H "X-API-Key: TU_API_KEY"{
"checked": 500,
"duplicates": [
{ "a": "tu-fuente:12", "b": "tu-fuente:88", "score": 0.94,
"a_name": "José Pérez", "b_name": "Jose Perez",
"a_image": "https://…", "b_image": "https://…" }
]
}⚠️ /v1/duplicates revisa hasta 500 registros por llamada. Si tu base es mayor y quieres cobertura total con un solo endpoint, escríbenos para subir el límite/añadir paginación — o usa la opción de abajo.
Opción de cobertura total (bases > 500): una búsqueda por registro; de paso detecta si la persona está también en otras plataformas.
// Cobertura TOTAL (bases > 500): una búsqueda por registro.
const UMBRAL = 0.6, vistos = new Set(), duplicados = [];
for (const p of personas) {
if (!p.foto_url) continue;
const fd = new FormData();
fd.append("file", await (await fetch(p.foto_url)).blob());
const { results = [] } = await (await fetch(FR + "/v1/search",
{ method: "POST", headers: { "X-API-Key": KEY }, body: fd })).json();
for (const m of results) {
const yoMismo = m.record_id === "tu-fuente:" + p.id;
if (yoMismo || m.score < UMBRAL) continue;
if (m.source === "tu-fuente") { // duplicado DENTRO de tu base
const k = [String(p.id), m.record_id].sort().join("|");
if (!vistos.has(k)) { vistos.add(k); duplicados.push({ a: p.id, b: m.record_id, score: m.score }); }
}
// m.source !== "tu-fuente" → la misma persona en OTRA plataforma
}
await sleep(550);
}
console.table(duplicados); // pares para revisión humanaEmpieza por los pares de banda alta. Muestra las dos fotos a un revisor; si confirma, fusionas/eliminas en tu BD. El FR no borra nada por ti.
| Método | Ruta | Auth | Parámetros | Descripción |
|---|---|---|---|---|
| GET | /health | pública | — | Estado del servicio y de la colección vectorial. |
| GET | /openapi.json | pública | — | Esquema OpenAPI público (también en /api/fr/openapi). |
| GET | /v1/whoami | API key | — | Autodiagnóstico: valida tu key y devuelve tu source, cuántos registros tienes indexados y el min_score. Empieza por aquí. |
| POST | /v1/check-duplicate | API key | file | image_url, min_score? | Foto → ¿ya hay una persona registrada con ese rostro? Acepta file, URL http o data-URI base64; cualquier formato. |
| POST | /v1/search | API key | file | image_url, min_score? | Foto → personas más parecidas (top-10) con score y source. Acepta file, URL http o data-URI. Busca en TODAS las bases. |
| POST | /v1/index | API key | external_id, file|image_url, person_name?, last_seen_location?, age?, contact_phone?, source? | Sube/indexa UN registro de tu base. Idempotente por external_id. |
| POST | /v1/index/commit | API key | — | Cierre de lote (no-op con el backend actual; devuelve el total). |
| GET | /v1/duplicates | API key | source?, min_score=0.6, limit=500 | Cruza UNA base contra sí misma → pares duplicados. source = el mismo con que indexaste (default = etiqueta de tu key). |
| GET | /v1/reconcile | API key | min_score=0.55, limit=800, sources? | Misma persona entre bases DISTINTAS → grupos con las imágenes de cada base. |
| GET | /v1/groups | API key | limit=20, offset=0, q? | Lista de grupos/clusters de identidad. |
| GET | /v1/groups/{id}/cluster | API key | — | Miembros de un grupo/cluster concreto. |
Foto como multipart/form-data en el campo file. 422 = sin rostro (deja registrar igual).
curl -X POST "https://fr-api.reportavnzla.com:8443/v1/check-duplicate" \
-H "X-API-Key: TU_API_KEY" \
-F "file=@foto.jpg"Respuesta:
{
"ok": true, "faces_detected": 1,
"possible_duplicate": true, "best_score": 0.97,
"candidates": [
{ "record_id": "tu-fuente:123", "person_name": "Katherine Mendoza",
"last_seen_location": "Catia la Mar", "image_url": "https://…",
"score": 0.97, "band": "alta", "source": "reportavnzla" }
]
}Encuentra a la misma persona reportada en bases distintas y devuelve sus imágenes de cada base. Ajusta min_score y limit; opcional sources=A,B.
# Misma persona en bases distintas (azure ↔ reportavnzla ↔ …)
curl "https://fr-api.reportavnzla.com:8443/v1/reconcile?min_score=0.55&limit=800" \
-H "X-API-Key: TU_API_KEY"
# opcional: limita a ciertas fuentes con &sources=azure,tu-fuenteCada coincidencia trae un score (0–1, similitud de coseno) y una band:
Piso de 0.51: /v1/check-duplicate y /v1/search solo devuelven coincidencias con score ≥ 0.51 (trae solo lo más cercano). Puedes ajustarlo por petición con ?min_score=.
Si la imagen tiene varias personas, se coteja cada rostro. La respuesta de /v1/check-duplicate y /v1/search incluye un arreglo faces con un elemento por rostro detectado, listo para dibujar recuadros: verde si coincide (≥ 0.51), amarillo si no.
{
"ok": true, "faces_detected": 2, "min_score": 0.51,
"possible_duplicate": true,
"faces": [
{ "bbox": [67,117,132,215], "matched": true, "color": "green",
"best_score": 0.99, "band": "alta", "candidates": [ /* … */ ] },
{ "bbox": [240,110,300,205], "matched": false, "color": "yellow",
"best_score": 0.0, "band": null, "candidates": [] }
],
"candidates": [ /* coincidencias agregadas de todos los rostros, score >= 0.51 */ ]
}bbox = [x1,y1,x2,y2] en píxeles de la imagen enviada. matched/color indican el recuadro; best_score el mejor parecido de ese rostro; candidates sus coincidencias.
Casi siempre la API key está mal copiada (truncada). Corre fr-doctor o GET /v1/whoami: un 401 confirma la key. Las claves son de 48 caracteres. Los proxies no rompen el registro, así que un 401 se ve como “sin coincidencias”.
Probablemente no has indexado tu base. GET /v1/whoami → indexed_my_source. Si es 0, haz el backfill con POST /v1/index. El FR-API no lee tu BD.
Es el campo threshold (referencia histórica) — NO controla nada. El piso real es min_score (0.51). Para cambiarlo, pasa ?min_score= en la petición; FR_MIN_SCORE en tu .env no aplica por sí solo.
Tu source no coincide con el de indexación. Usa el mismo source con que indexaste (= source_default de /v1/whoami = la etiqueta de tu key), o pásalo explícito con ?source=.
Límite de 120 peticiones/min por clave. En backfills, separa ~550 ms entre llamadas.
Importa el esquema OpenAPI en tu cliente (Postman, Insomnia, openapi-generator), lee la guía completa, o entrega la guía para agentes de IA a tu equipo: es un archivo único con código de referencia (proxies, registro, backfill) para que un agente de IA replique la integración. Guárdalo en tu repo como AGENTS.md o docs/FR-API.md.