Alguien sube una foto a tu producto y afirma que fue tomada con una cámara. ¿Puede tu backend probar o refutar eso? Los generadores de imágenes ahora producen resultados que parecen reales para un revisor humano, por lo que "confiar en los ojos" dejó de funcionar hace tiempo. La buena noticia es que no necesitas entrenar tu propio modelo para ofrecer una respuesta útil. Puedes combinar dos señales independientes, un manifiesto de procedencia criptográfico y un clasificador de aprendizaje automático, en un veredicto que es más honesto que cualquiera de las señales por sí sola.
Este tutorial te guía a través de la construcción de ese backend como un servicio único con un endpoint `POST /verify`. Le proporcionas una imagen, y te devuelve un veredicto JSON con una puntuación de confianza y los detalles de procedencia que encontró. Utilizaremos Python y FastAPI para el servidor, las herramientas de código abierto C2PA para la señal de procedencia, y una API de detección alojada para la señal del clasificador. Debido a que este es un proyecto de API, también diseñaremos el contrato del endpoint primero y usaremos Apidog para simularlo y probarlo, de modo que tu equipo de frontend pueda empezar a integrarse antes de que el código del backend esté terminado.
TL;DR
Construirás un servicio FastAPI que expone `POST /verify` que acepta una carga de imagen, extrae y valida su manifiesto de Credenciales de Contenido C2PA con la librería `c2pa-python`, llama a un clasificador de detección de IA alojado como una segunda señal independiente, y devuelve un único veredicto JSON (`likely_authentic`, `likely_ai` o `uncertain`) con una puntuación de confianza y los detalles de procedencia sin procesar. También diseñarás el esquema OpenAPI para el endpoint y usarás Apidog para generar un servidor simulado y ejecutar pruebas de endpoint contra él.
button
Por qué dos señales en lugar de una
Antes de cualquier código, ayuda tener claro qué estás detectando. No existe una única propiedad de un archivo que te diga "esto lo hizo un humano" o "esto lo hizo una IA". En cambio, hay pistas, y cada pista detecta un tipo diferente de imagen mientras omite otras.
La primera pista es la procedencia. C2PA, la Coalición para la Procedencia y Autenticidad del Contenido, es un estándar abierto que adjunta metadatos criptográficamente firmados y a prueba de manipulaciones a un archivo multimedia. Ese paquete de metadatos se llama manifiesto, y el nombre que ve el usuario es Credenciales de Contenido. Cuando una herramienta participante, una cámara, un editor o un generador de imágenes, crea o modifica una imagen, puede escribir un manifiesto que registra lo sucedido y lo firma con un certificado. Si puedes leer y validar ese manifiesto, obtienes una declaración fuerte y verificable sobre el historial de la imagen.
El problema: C2PA es opcional, y el manifiesto es frágil. Una captura de pantalla lo elimina. La recodificación a través de una aplicación de mensajería lo elimina. Muchas plataformas eliminan los metadatos al subirlos. Por lo tanto, un manifiesto ausente te dice casi nada; no significa que la imagen sea falsa, y no significa que sea real.
La segunda pista es un clasificador estadístico. Un modelo de detección se entrena con millones de imágenes reales y generadas y aprende los artefactos visuales que los generadores tienden a dejar. Funciona en cualquier imagen, con o sin metadatos, pero es probabilístico. Devuelve una probabilidad, no un hecho, y puede estar equivocado, especialmente en imágenes fuera de su distribución de entrenamiento o imágenes que han sido muy comprimidas.
Ninguna señal es suficiente por sí sola. La procedencia es precisa pero rara vez está presente. El clasificador siempre está disponible pero nunca es certero. Combínalos y obtendrás un veredicto que dice, en efecto, "aquí está lo que la criptografía prueba, aquí está lo que el modelo estima, y aquí está la confianza que nos da la combinación". Ese es el objetivo del diseño. Si deseas una visión más profunda de por qué los enfoques de una sola señal se quedan cortos, nuestro artículo sobre por qué falla la detección de imágenes de IA cubre los modos de falla en detalle.
Visión general de la arquitectura
El servicio es pequeño a propósito. Un solo endpoint, dos llamadas a servicios externos, una respuesta combinada.
┌─────────────────────────────┐
imagen ──▶ │ FastAPI POST /verify │
│ │
│ 1. validar la carga │
│ 2. ┌──────────────────┐ │
│ │ manifiesto C2PA │ │ señal de procedencia
│ │ (c2pa-python) │ │
│ └──────────────────┘ │
│ 3. ┌──────────────────┐ │
│ │ API del clasificador│ │ señal estadística
│ │ (detector alojado) │ │
│ └──────────────────┘ │
│ 4. combinar en un veredicto│
└─────────────────────────────┘
│
▼
Veredicto JSON + confianza
El paso 1 verifica que la carga sea una imagen real de un tipo compatible y dentro de un límite de tamaño. El paso 2 lee el manifiesto C2PA localmente; no hay llamada de red, solo análisis y validación de certificados. El paso 3 envía los bytes de la imagen a un clasificador alojado a través de HTTPS. El paso 4 fusiona los dos resultados con una pequeña función de reglas y devuelve el veredicto.
Los dos pasos de señal son independientes. Esto es importante para el manejo de errores: si el clasificador agota el tiempo de espera, aún puedes devolver un veredicto parcial de la señal de procedencia, y viceversa. Volveremos a eso en la sección de endurecimiento.
Para la pila, se requiere Python 3.10 o más reciente porque la librería C2PA lo necesita. Utilizarás FastAPI para la capa web, Uvicorn para ejecutarlo, `python-multipart` para las cargas de archivos, `httpx` para la llamada saliente del clasificador y `c2pa-python` para la procedencia.
pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
La señal C2PA
La Iniciativa de Autenticidad de Contenido, bajo la organización `contentauth` de GitHub, publica las herramientas C2PA de código abierto. Hay dos piezas de las que oirás hablar:
- `c2patool`, una herramienta de línea de comandos para mostrar y añadir manifiestos. Es útil para una inspección rápida desde una terminal. Ten en cuenta que su repositorio independiente ahora está archivado, y la CLI vive dentro del proyecto Rust `c2pa-rs`.
- `c2pa-python`, el enlace Python para la misma librería Rust subyacente (`c2pa-rs`). Esto es lo que usará tu servicio. Se publica en PyPI como `c2pa-python` y se instala con `pip install c2pa-python`.
La ruta de lectura de la librería se centra en un objeto `Reader`. Lo apuntas a una imagen y luego pides el almacén de manifiestos como JSON. Aquí está el núcleo del módulo de procedencia.
# provenance.py
import json
import c2pa
def read_provenance(image_path: str) -> dict:
"""
Lee y valida el manifiesto C2PA de una imagen.
Devuelve un diccionario normalizado que describe lo que se encontró.
"""
try:
with c2pa.Reader(image_path) as reader:
manifest_store = json.loads(reader.json())
except c2pa.C2paError as err:
# ManifestNotFound es el caso esperado para la mayoría de las imágenes.
if str(err).startswith("ManifestNotFound"):
return {
"has_manifest": False,
"validation": "none",
"detail": "No hay manifiesto C2PA presente en esta imagen.",
}
# Cualquier otro C2paError significa que el archivo tenía datos C2PA que no pudimos analizar.
return {
"has_manifest": True,
"validation": "error",
"detail": f"No se pudo analizar el manifiesto: {err}",
}
active_label = manifest_store.get("active_manifest")
manifests = manifest_store.get("manifests", {})
active = manifests.get(active_label, {})
# validation_status aparece solo cuando hay problemas de validación.
validation_status = manifest_store.get("validation_status", [])
validation = "valid" if not validation_status else "invalid"
claim_generator = active.get("claim_generator", "unknown")
signature_issuer = active.get("signature_info", {}).get("issuer", "unknown")
return {
"has_manifest": True,
"validation": validation,
"claim_generator": claim_generator,
"signature_issuer": signature_issuer,
"validation_status": validation_status,
"detail": "Manifiesto leído correctamente.",
}
Algunas notas sobre lo que hace el código. El `Reader` se utiliza como un gestor de contexto para que los recursos subyacentes se liberen. `reader.json()` devuelve el almacén completo del manifiesto como una cadena JSON; la librería también ofrece `reader.detailed_json()` si quieres el informe extenso con cada aserción e ingrediente. El resultado esperado para la mayoría de las cargas es un `C2paError` cuyo mensaje comienza con `ManifestNotFound`, porque la mayoría de las imágenes simplemente no tienen Credenciales de Contenido. Trata eso como un dato, no como un fallo.
Cuando un manifiesto está presente, dos campos son los más importantes para un veredicto. La cadena `claim_generator` te dice qué herramienta escribió el manifiesto, por ejemplo, una cadena de firmware de cámara o el nombre de una herramienta de imagen de IA. El array `validation_status` está vacío cuando la firma y los hashes son correctos, y se llena con códigos de error cuando no lo son. Un manifiesto inválido es una señal de alerta que vale la pena destacar; significa que el archivo reclama un historial que la criptografía no respalda.
Lo que esta señal no puede hacer: no puede darte un veredicto cuando no hay manifiesto, que es la mayoría de las veces. Por eso, exactamente, necesitas la segunda señal.
La señal del clasificador
El clasificador es una API alojada que evalúa la probabilidad de que una imagen haya sido generada por IA. Varios proveedores ofrecen esto. Este tutorial utiliza Sightengine porque su modelo de detección de IA tiene una API HTTP documentada y una forma de respuesta clara, pero el patrón es el mismo para cualquier proveedor; solo cambias la URL, los parámetros y el campo que lees. Si estás considerando opciones, nuestro resumen de las mejores APIs de detección de imágenes de IA compara la precisión, los precios y la cobertura entre proveedores.
El endpoint de verificación de Sightengine es `https://api.sightengine.com/1.0/check.json`. Envías la imagen como `media`, estableces `models` en `genai`, y pasas tu `api_user` y `api_secret`. La respuesta incluye `type.ai_generated`, una puntuación de 0 a 1 donde cuanto mayor es, más probable es que sea generada por IA.
# classifier.py
import httpx
SIGHTENGINE_URL = "https://api.sightengine.com/1.0/check.json"
async def classify_image(
image_bytes: bytes,
filename: str,
api_user: str,
api_secret: str,
timeout_seconds: float = 8.0,
) -> dict:
"""
Envía la imagen al detector alojado.
Devuelve un diccionario normalizado con la puntuación de IA generada.
"""
data = {
"models": "genai",
"api_user": api_user,
"api_secret": api_secret,
}
files = {"media": (filename, image_bytes)}
try:
async with httpx.AsyncClient(timeout=timeout_seconds) as client:
response = await client.post(SIGHTENGINE_URL, data=data, files=files)
response.raise_for_status()
payload = response.json()
except httpx.TimeoutException:
return {"available": False, "reason": "classifier_timeout"}
except httpx.HTTPStatusError as err:
return {
"available": False,
"reason": f"classifier_http_{err.response.status_code}",
}
except httpx.HTTPError as err:
return {"available": False, "reason": f"classifier_error: {err}"}
if payload.get("status") != "success":
return {
"available": False,
"reason": payload.get("error", {}).get("message", "unknown_error"),
}
ai_score = payload.get("type", {}).get("ai_generated")
if ai_score is None:
return {"available": False, "reason": "missing_score_in_response"}
return {"available": True, "ai_score": float(ai_score)}
La función es asíncrona para que un clasificador lento no bloquee el bucle de eventos. El tiempo de espera es explícito y corto; ocho segundos es un valor predeterminado sensato para un endpoint interactivo, y debes ajustarlo a la latencia real de tu proveedor. Cada ruta de fallo devuelve `available: False` con un motivo legible por máquina en lugar de generar una excepción. Esto es deliberado: una interrupción del clasificador debe degradar el veredicto, no bloquear la solicitud. La lógica del veredicto en la siguiente sección lee `available` y decide qué hacer.
Trata la puntuación como una estimación. Un 0.92 significa "el modelo está bastante seguro", no "esto está probado que es IA". Los proveedores actualizan sus modelos, y la precisión varía según el generador y la cantidad de compresión de la imagen antes de llegar a ti. Para una visión más amplia de cómo se comportan estas herramientas en la práctica, consulta nuestra guía sobre cómo verificar si una imagen es generada por IA.
Diseñando el contrato de /verify
Aquí es donde Apidog se gana su lugar. Antes de escribir el manejador de rutas, diseña la solicitud y la respuesta como un esquema OpenAPI. Hacer esto primero te proporciona tres cosas: una única fuente de verdad en la que ambos equipos están de acuerdo, un servidor simulado al que el frontend puede llamar de inmediato y un conjunto de pruebas que puedes ejecutar en el momento en que exista el backend.
La solicitud
`POST /verify` toma un cuerpo `multipart/form-data` con un campo, `image`, el archivo a verificar. Mantenlo así de simple. Los parámetros de consulta opcionales pueden venir después.
La respuesta
La respuesta es donde el trabajo de diseño da sus frutos. Debe mostrar el veredicto final, la confianza y ambas señales sin procesar para que un llamador pueda auditar la decisión. Aquí está la forma.
{
"verdict": "likely_ai",
"confidence": 0.86,
"signals": {
"provenance": {
"has_manifest": true,
"validation": "valid",
"claim_generator": "SomeImageTool/2.1",
"signature_issuer": "Some Issuing CA"
},
"classifier": {
"available": true,
"ai_score": 0.91
}
},
"explanation": "Un manifiesto C2PA válido nombra una herramienta de imagen de IA, y el clasificador puntuó la imagen como probablemente generada por IA.",
"checked_at": "2026-05-21T09:30:00Z"
}
`verdict` es uno de tres valores de cadena: `likely_authentic`, `likely_ai`, o `uncertain`. Tres valores, no dos, porque la honestidad importa; cuando las señales no concuerdan o ambas son débiles, "incierto" es la respuesta correcta. `confidence` es un flotante de 0 a 1 que describe cuán fuertemente las señales respaldan ese veredicto. `signals` contiene ambas entradas sin procesar para que el llamador pueda mostrar su propia interfaz de usuario o aplicar su propia política. `explanation` es una frase legible por humanos para el personal de soporte y los registros.
Expresar esto como un esquema OpenAPI es sencillo. Aquí está el componente de respuesta que pondrías en tu especificación.
components:
schemas:
VerifyResponse:
type: object
required: [verdict, confidence, signals, checked_at]
properties:
verdict:
type: string
enum: [likely_authentic, likely_ai, uncertain]
confidence:
type: number
format: float
minimum: 0
maximum: 1
signals:
type: object
properties:
provenance:
type: object
properties:
has_manifest: { type: boolean }
validation:
type: string
enum: [valid, invalid, error, none]
claim_generator: { type: string }
signature_issuer: { type: string }
classifier:
type: object
properties:
available: { type: boolean }
ai_score:
type: number
format: float
explanation: { type: string }
checked_at: { type: string, format: date-time }
Puedes crear este esquema directamente en el diseñador visual de Apidog o importar un archivo OpenAPI existente. Diseñar la API antes de la implementación es un flujo de trabajo que vale la pena adoptar en general; nuestro tutorial de modo spec-first muestra cómo hacerlo de principio a fin en Apidog.
Recorrido del código
Ahora las piezas encajan. A continuación se muestra la aplicación FastAPI: validación de entrada, ambas llamadas de señal, la función combinadora y la ruta.
Combinando las dos señales
La función de veredicto es el corazón del servicio. Codifica tu política. La procedencia, cuando es válida y está presente, es la señal más fuerte porque es criptográfica; el clasificador es un desempate y un respaldo. Aquí tienes una versión clara y conservadora.
# verdict.py
def combine_signals(provenance: dict, classifier: dict) -> dict:
"""Combina las señales de procedencia y del clasificador en un veredicto."""
has_manifest = provenance.get("has_manifest", False)
validation = provenance.get("validation", "none")
generator = (provenance.get("claim_generator") or "").lower()
classifier_ok = classifier.get("available", False)
ai_score = classifier.get("ai_score")
# Heurística: las herramientas de IA conocidas tienden a identificarse en el manifiesto.
ai_keywords = ("firefly", "dall-e", "dalle", "midjourney", "stable",
"gpt", "gemini", "imagen", "generat")
generator_looks_ai = any(k in generator for k in ai_keywords)
# Caso 1: un manifiesto válido que nombra a un generador de IA. Señal de IA fuerte.
if has_manifest and validation == "valid" and generator_looks_ai:
return _verdict("likely_ai", 0.95,
"Un manifiesto C2PA válido nombra una herramienta de imagen de IA.")
# Caso 2: un manifiesto válido de una cámara o editor no-IA. Señal auténtica fuerte.
if has_manifest and validation == "valid" and not generator_looks_ai:
if classifier_ok and ai_score is not None and ai_score > 0.85:
return _verdict("uncertain", 0.55,
"El manifiesto parece auténtico pero el clasificador "
"no está de acuerdo; las señales entran en conflicto.")
return _verdict("likely_authentic", 0.9,
"Hay presente un manifiesto C2PA válido de una herramienta no-IA.")
# Caso 3: un manifiesto que falla la validación. Tratar como sospechoso.
if has_manifest and validation in ("invalid", "error"):
return _verdict("uncertain", 0.6,
"La imagen contiene un manifiesto C2PA que falló la "
"validación; su historial declarado no está verificado.")
# Caso 4: sin manifiesto. Recurrir completamente al clasificador.
if classifier_ok and ai_score is not None:
if ai_score >= 0.7:
return _verdict("likely_ai", round(ai_score, 2),
"Sin datos de procedencia; el clasificador puntuó la "
"imagen como probablemente generada por IA.")
if ai_score <= 0.3:
return _verdict("likely_authentic", round(1 - ai_score, 2),
"Sin datos de procedencia; el clasificador puntuó la "
"imagen como probablemente auténtica.")
return _verdict("uncertain", 0.5,
"Sin datos de procedencia y la puntuación del clasificador es "
"inconcluyente.")
# Caso 5: sin manifiesto y sin clasificador. Realmente no podemos decir.
return _verdict("uncertain", 0.0,
"Sin datos de procedencia y el clasificador no estaba disponible.")
def _verdict(verdict: str, confidence: float, explanation: str) -> dict:
return {"verdict": verdict, "confidence": confidence,
"explanation": explanation}
Lee los cinco casos y podrás ver la política. Un manifiesto válido domina. Un manifiesto fallido es una advertencia, no una prueba de falsificación, por lo que se considera "incierto". Un conflicto entre un manifiesto limpio y una puntuación alta del clasificador también se considera "incierto" en lugar de tomar partido. Y cuando ambas señales faltan, el servicio lo dice honestamente con confianza cero en lugar de adivinar. Ajustarás estos umbrales según tu propia tolerancia al riesgo; una plataforma de contenido y una sala de redacción establecerían los límites de manera diferente.
La aplicación FastAPI
# main.py
import os
import tempfile
from datetime import datetime, timezone
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse
from provenance import read_provenance
from classifier import classify_image
from verdict import combine_signals
app = FastAPI(title="AI Image Detector API", version="1.0.0")
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_BYTES = 12 * 1024 * 1024 # 12 MB
SIGHTENGINE_USER = os.environ.get("SIGHTENGINE_API_USER", "")
SIGHTENGINE_SECRET = os.environ.get("SIGHTENGINE_API_SECRET", "")
@app.post("/verify")
async def verify(image: UploadFile = File(...)):
# 1. Validar la carga.
if image.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=415,
detail=f"Tipo no compatible {image.content_type}. "
f"Envía JPEG, PNG o WebP.",
)
image_bytes = await image.read()
if len(image_bytes) == 0:
raise HTTPException(status_code=400, detail="Archivo vacío.")
if len(image_bytes) > MAX_BYTES:
raise HTTPException(status_code=413, detail="El archivo excede el límite de 12 MB.")
# 2. Señal de procedencia. El lector C2PA necesita una ruta de archivo,
# así que escribe en un archivo temporal y límpialo después.
suffix = os.path.splitext(image.filename or "")[1] or ".img"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(image_bytes)
tmp_path = tmp.name
try:
provenance = read_provenance(tmp_path)
finally:
os.unlink(tmp_path)
# 3. Señal del clasificador. Los fallos devuelven available: False, no excepciones.
if SIGHTENGINE_USER and SIGHTENGINE_SECRET:
classifier = await classify_image(
image_bytes, image.filename or "upload",
SIGHTENGINE_USER, SIGHTENGINE_SECRET,
)
else:
classifier = {"available": False, "reason": "classifier_not_configured"}
# 4. Combinar y responder.
result = combine_signals(provenance, classifier)
return JSONResponse({
"verdict": result["verdict"],
"confidence": result["confidence"],
"signals": {
"provenance": {
k: provenance.get(k) for k in
("has_manifest", "validation", "claim_generator",
"signature_issuer")
},
"classifier": {
"available": classifier.get("available", False),
"ai_score": classifier.get("ai_score"),
},
},
"explanation": result["explanation"],
"checked_at": datetime.now(timezone.utc).isoformat(),
})
Ejecútalo localmente con `uvicorn main:app --reload` y el endpoint estará activo en `http://127.0.0.1:8000/verify`. El lector C2PA espera una ruta de archivo, por lo que el controlador escribe la carga en un archivo temporal y lo elimina en un bloque `finally`; el clasificador funciona directamente con los bytes. Observa que la solicitud nunca falla por un manifiesto ausente o un clasificador no disponible; ambos son estados normales que la función de veredicto maneja.
Un backend diseñado de esta manera, un servicio enfocado con un contrato limpio, encaja en la tendencia más amplia de productos que exponen su capacidad central a través de una API. Si esa idea te interesa, nuestro ensayo sobre el software que se vuelve "headless" vale la pena leerlo.
Pruebas y simulación con Apidog
Aquí está el problema del flujo de trabajo: tu equipo de frontend quiere construir la interfaz de usuario de carga y el panel de resultados ahora, pero el backend anterior tarda unos días en terminar, obtener las claves y desplegarse. No quieres que se queden bloqueados. Para esto sirven los servidores simulados, y es donde el diseño del esquema primero da sus frutos.
Generar un servidor simulado a partir del esquema
Importa el esquema OpenAPI en Apidog, o construye el endpoint `/verify` en el diseñador visual. Apidog lee el esquema de respuesta y genera un servidor simulado automáticamente. Debido a que el esquema define tipos de campos y enumeraciones, el simulador devuelve datos con la misma forma que el endpoint real: un `verdict` que es uno de los tres valores enumerados, un flotante `confidence` entre 0 y 1, un objeto `signals` poblado. El frontend apunta su llamada `fetch` a la URL simulada de Apidog y obtiene respuestas realistas desde el primer día. Cuando se entrega el backend real, cambian una URL base.
El simulador también es donde ejercitas los casos difíciles antes de que exista cualquier código real. Define respuestas de ejemplo para los veredictos que importan:
- una respuesta `likely_authentic` con un manifiesto de cámara válido,
- una respuesta `likely_ai` con una herramienta de IA nombrada en el manifiesto,
- una respuesta `uncertain` donde el clasificador no estaba disponible,
- las respuestas de error `415` y `413`.
El frontend puede construir y estilizar cada estado, incluidos los estados de error, contra el simulador. Así es como se envía una interfaz de usuario y una API en paralelo en lugar de en secuencia.
Ejecutar pruebas de endpoint en Apidog
Una vez que el backend esté funcionando, crea una solicitud en Apidog para `POST /verify`. Configura el método, apúntalo a tu URL local, y en la pestaña Body elige `form-data`, añade el campo `image`, establece su tipo en Archivo y selecciona una imagen de prueba del disco.
Envíala, y Apidog mostrará la respuesta JSON. Ahora añade aserciones para que esto se convierta en una verificación repetible en lugar de un clic único:
- asegura que el estado de la respuesta sea `200`,
- asegura que `verdict` existe y es una de las tres cadenas permitidas,
- asegura que `confidence` es un número entre 0 y 1,
- asegura que `signals.provenance.has_manifest` es un booleano.
Crea un pequeño escenario de prueba que ejecute varias cargas en secuencia: una imagen con Credenciales de Contenido, un JPEG simple sin manifiesto, un archivo sobredimensionado y un archivo que no es una imagen renombrado con una extensión `.jpg`. Cada uno verifica una rama diferente de tu lógica de veredicto y tu validación de entrada. Guarda el escenario y podrás volver a ejecutar todo el conjunto después de cada cambio, o conectarlo a la CI para que una regresión en la función de veredicto falle la compilación. Probar un endpoint de carga manualmente con `curl` se vuelve tedioso rápidamente; un escenario guardado no lo hace.
Robustecimiento y casos extremos
El camino feliz es el 80 por ciento fácil. Un servicio de verificación vive o muere por el otro 20 por ciento, porque las entradas son, por naturaleza, adversarias; alguien está tratando de hacer pasar una imagen por algo que no es.
Archivos corruptos o truncados. Un archivo puede tener un tipo MIME de imagen y aun así ser basura. El lector C2PA genera un `C2paError` si no puede analizar los datos, y la función `read_provenance` ya lo convierte en un resultado limpio en lugar de un 500. Para mayor seguridad, puedes decodificar la imagen con una librería como Pillow antes de procesar y rechazarla con un `400` si la decodificación falla. Esto también bloquea el truco de los archivos de texto renombrados.
Manifiesto ausente. Cubierto, pero vale la pena reiterarlo porque es el caso más común y el más fácil de malinterpretar. Un manifiesto ausente no es un error ni un veredicto. La librería lo señala con un `C2paError` cuyo mensaje comienza con `ManifestNotFound`; el servicio lo intercepta específicamente y pasa al clasificador. Nunca permitas que un manifiesto ausente produzca un 500 o un veredicto "falso".
Tiempo de espera o interrupción del clasificador. El clasificador es una dependencia de red de terceros, por lo que asume que fallará a veces. La función `classify_image` usa un tiempo de espera `httpx` explícito y devuelve `available: False` ante cualquier tiempo de espera o error HTTP. La función de veredicto entonces recurre solo a la procedencia, o devuelve `uncertain` con confianza cero si tampoco hay manifiesto. El endpoint sigue respondiendo con un `200` y un veredicto honesto; un proveedor lento no puede derribar tu servicio.
Manifiestos falsificados. Un manifiesto puede estar presente pero ser inválido, firmado con un certificado incorrecto o con hashes que no coinciden con los píxeles. Este es el caso que la gente olvida. Comprueba siempre `validation_status`; un array vacío significa que el manifiesto se verificó, uno poblado significa que no. La función de veredicto trata un manifiesto fallido como una advertencia que cae en `uncertain`, nunca como prueba. Confiar en un manifiesto no validado es peor que no tener ningún manifiesto.
Archivos grandes y abuso. Limita el tamaño de la carga, el ejemplo usa 12 MB, y rechaza cualquier cosa más grande con un `413` antes de leer todo el cuerpo en memoria. Pon un límite de tasa delante del endpoint; la validación criptográfica y una llamada a la API saliente por cada solicitud no son gratuitas, y un endpoint de verificación abierto es un objetivo tentador.
Privacidad. Estás recibiendo imágenes de usuarios. Procesalas en memoria o en un archivo temporal que eliminas inmediatamente, como hace el ejemplo, y no registres los bytes de las imágenes. Si estás enviando imágenes a un clasificador de terceros, indícalo en tu política de privacidad y asegúrate de que esté permitido para tu caso de uso.
Lo que cada señal detecta y omite
Esta tabla es el modelo mental a mantener. Es la razón por la que el servicio usa ambos.
| Escenario | Señal de procedencia C2PA | Señal del clasificador |
|---|---|---|
| Imagen de IA de una herramienta que escribe Credenciales de Contenido | La detecta: el manifiesto nombra al generador | Normalmente la detecta: artefactos presentes |
| Imagen de IA con metadatos eliminados (captura de pantalla, re-subida) | La omite: no hay manifiesto que leer | La detecta: funciona con píxeles, no necesita metadatos |
| Foto real de una cámara que firma Credenciales de Contenido | La confirma: manifiesto válido, generador no-IA | Puede dar falso positivo por compresión o ediciones fuertes |
| Foto real sin metadatos en absoluto | Sin señal: nada que validar | Solo la mejor suposición: probabilística, puede ser errónea |
| Imagen con un manifiesto falsificado o manipulado | La detecta: `validation_status` marca el fallo | Puede o no detectarla, depende de los píxeles |
| Generador novedoso en el que el clasificador no fue entrenado | La detecta solo si la herramienta escribe un manifiesto | A menudo la omite: fuera de la distribución de entrenamiento |
| Foto real fuertemente editada (retoque de IA sobre una base real) | El manifiesto, si está presente, registra el historial de edición | Ambiguo: parcialmente sintético, la puntuación se sitúa a mitad de rango |
Lee cualquier fila y verás la misma historia: donde una señal es ciega, la otra a menudo no lo es. La procedencia es exacta pero escasa; el clasificador es universal pero difuso. El veredicto combinado es más confiable que cualquiera de las columnas por sí sola, y el valor honesto de `uncertain` existe para las filas donde ambas señales son débiles.
Casos de uso en el mundo real
Este patrón no es académico. Algunos lugares donde encaja directamente:
- Plataformas de contenido generado por usuarios. Un mercado o una aplicación social pueden pasar las cargas por `/verify` y etiquetar o poner en cola para revisión cualquier cosa que se considere probablemente IA, o cualquier cosa que lleve un manifiesto que falle la validación. El veredicto de tres valores se asigna limpiamente a "permitir", "marcar" y "enviar a un humano".
- Salas de redacción y verificación de hechos. Un editor que verifica una imagen viral obtiene tanto la procedencia criptográfica, si la hay, como una estimación de modelo independiente en una sola llamada, con una frase explicativa que pueden citar en sus notas.
- Tramitación de seguros y reclamaciones. Cuando un cliente presenta pruebas fotográficas, un paso de verificación levanta una bandera en las imágenes que parecen generadas o llevan un manifiesto manipulado, antes de que un ajustador humano les dedique tiempo.
- Pipelines internos de activos. Un equipo que necesita mantener las imágenes generadas por IA fuera de, o claramente etiquetadas en, una biblioteca de stock puede restringir la ingesta en `/verify`.
- Publicación consciente de la procedencia. A medida que más cámaras y editores adopten las Credenciales de Contenido, un CMS puede leer el manifiesto al subirlo y mostrar una insignia verificada, recurriendo al clasificador cuando no hay manifiesto presente.
El hilo conductor: quieres una primera pasada rápida y automatizada que sea honesta sobre su propia incertidumbre, para que la atención humana se dirija donde realmente se necesita.
Conclusión
Detectar bien las imágenes generadas por IA no consiste en encontrar una prueba perfecta. Se trata de combinar señales independientes y ser honesto sobre la confianza.
- Las Credenciales de Contenido C2PA te brindan una señal de procedencia sólida y criptográficamente verificable, pero el manifiesto es opcional y se elimina fácilmente, por lo que a menudo está ausente.
- Un clasificador alojado te proporciona una señal universal pero probabilística que funciona en cualquier imagen, con o sin metadatos.
- Combinarlos en un pequeño servicio FastAPI produce un veredicto de tres valores, `likely_authentic`, `likely_ai` o `uncertain`, con una puntuación de confianza y ambas señales sin procesar adjuntas para auditoría.
- Diseñar el contrato OpenAPI primero te permite simular el endpoint en Apidog para que el frontend se construya en paralelo, y luego ejecutar escenarios de prueba guardados contra el backend real.
- Ningún detector es perfecto. Dos señales aumentan la confianza; no eliminan la incertidumbre, por lo que el veredicto `uncertain` es una característica, no una brecha.
Para construir esto de verdad, diseña el esquema `/verify`, genera un servidor simulado y ejecuta tus pruebas de endpoint en un solo lugar. Descarga Apidog para diseñar, simular y probar la API mientras la construyes, luego pasa del simulador al backend real con un solo cambio de URL base.
button
