Alguém envia uma foto para o seu produto e afirma que ela veio de uma câmera. Seu backend consegue validar isso? Em vez de “confiar nos olhos”, construa um endpoint que combine dois sinais independentes: um manifesto C2PA/Content Credentials validado criptograficamente e um classificador de imagem gerada por IA. O resultado é um veredito mais útil e auditável do que qualquer sinal isolado.
Neste tutorial, você vai implementar um serviço FastAPI com POST /verify. Ele recebe uma imagem, tenta extrair e validar o manifesto C2PA, chama uma API hospedada de detecção de IA e retorna um JSON com veredito, confiança e sinais brutos. Como é uma API, também vamos desenhar o contrato OpenAPI primeiro e usar o Apidog para mockar e testar o endpoint antes do backend estar pronto.
TL;DR
Você vai construir:
- um endpoint
POST /verifycom uploadmultipart/form-data; - leitura de Content Credentials com
c2pa-python; - chamada a um classificador hospedado, como Sightengine;
- uma função de decisão que retorna:
-
provavelmente_autêntico; -
provavelmente_ia; -
incerto;
-
- um contrato OpenAPI para mock e testes no Apidog.
Por que usar dois sinais?
Não existe uma propriedade única em um arquivo que prove “foi feito por humano” ou “foi feito por IA”. Existem pistas.
Sinal 1: proveniência C2PA
C2PA, da Coalition for Content Provenance and Authenticity, é um padrão aberto para anexar metadados assinados criptograficamente a arquivos de mídia. Esses metadados são conhecidos para usuários finais como Content Credentials.
Quando uma câmera, editor ou gerador compatível cria ou altera uma imagem, ele pode escrever um manifesto com o histórico da mídia e assiná-lo. Se o manifesto estiver presente e válido, você tem uma declaração forte sobre a origem e as transformações da imagem.
Limitação: C2PA é opt-in e frágil. Screenshots, recodificações e uploads em algumas plataformas podem remover o manifesto. Portanto, ausência de manifesto não significa que a imagem é falsa nem que é verdadeira.
Sinal 2: classificador estatístico
Um classificador analisa os pixels e estima a probabilidade de a imagem ter sido gerada por IA. Ele funciona mesmo sem metadados, mas retorna probabilidade, não prova.
Limitação: pode errar em imagens comprimidas, editadas ou fora da distribuição de treinamento.
A combinação ideal é:
“O que a criptografia prova, o que o modelo estima e quão forte é a conclusão combinada.”
Para entender melhor falhas de abordagem de sinal único, veja por que a detecção de imagem de IA falha.
Arquitetura
┌─────────────────────────────┐
image ──▶ │ FastAPI POST /verify │
│ │
│ 1. validate upload │
│ 2. ┌──────────────────┐ │
│ │ C2PA manifest │ │ provenance signal
│ │ c2pa-python │ │
│ └──────────────────┘ │
│ 3. ┌──────────────────┐ │
│ │ classifier API │ │ statistical signal
│ │ hosted detector │ │
│ └──────────────────┘ │
│ 4. combine into verdict │
└─────────────────────────────┘
│
▼
JSON verdict + confidence
O fluxo:
- Validar tipo e tamanho do arquivo.
- Ler o manifesto C2PA localmente.
- Enviar a imagem para um classificador hospedado.
- Combinar os sinais em um veredito único.
Instale as dependências:
pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
Requisitos principais:
- Python 3.10+;
- FastAPI;
- Uvicorn;
-
python-multipart; -
httpx; -
c2pa-python.
Implementando o sinal C2PA
A Content Authenticity Initiative mantém ferramentas C2PA open source sob a organização contentauth.
Você verá dois nomes:
-
c2patool: CLI para inspecionar e adicionar manifestos. O repositório independente foi arquivado; a CLI vive no projeto Rustc2pa-rs. -
c2pa-python: binding Python da biblioteca Rustc2pa-rs. É o que vamos usar no serviço.
Crie provenance.py:
# provenance.py
import json
import c2pa
def read_provenance(image_path: str) -> dict:
"""
Lê e valida o manifesto C2PA de uma imagem.
Retorna um dict normalizado com o que foi encontrado.
"""
try:
with c2pa.Reader(image_path) as reader:
manifest_store = json.loads(reader.json())
except c2pa.C2paError as err:
if str(err).startswith("ManifestNotFound"):
return {
"has_manifest": False,
"validation": "none",
"detail": "Nenhum manifesto C2PA presente nesta imagem.",
}
return {
"has_manifest": True,
"validation": "error",
"detail": f"Não foi possível analisar o manifesto: {err}",
}
active_label = manifest_store.get("active_manifest")
manifests = manifest_store.get("manifests", {})
active = manifests.get(active_label, {})
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": "Manifesto lido com sucesso.",
}
Pontos importantes:
-
Readeré usado como context manager para liberar recursos corretamente. -
reader.json()retorna o manifesto completo em JSON. -
ManifestNotFoundé esperado na maioria das imagens. - Manifesto ausente não é erro.
-
validation_statusvazio indica validação bem-sucedida. -
validation_statuspreenchido indica problema de assinatura, hash ou integridade.
Implementando o classificador
Este exemplo usa Sightengine porque a API HTTP é documentada e retorna uma pontuação clara em type.ai_generated.
O padrão é o mesmo para outros fornecedores: trocar URL, parâmetros e campo de leitura.
Para comparar alternativas, veja melhores APIs de detecção de imagem de IA.
Crie classifier.py:
# 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:
"""
Envia a imagem para o detector hospedado.
Retorna um dict normalizado com a pontuação de IA.
"""
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),
}
Decisões de implementação:
- a função é assíncrona;
- o timeout é explícito;
- falhas retornam
available: False; - uma falha do classificador não deve derrubar o endpoint;
- a pontuação deve ser tratada como estimativa, não como prova.
Para contexto adicional, veja como verificar se uma imagem é gerada por IA.
Projetando o contrato POST /verify
Antes de escrever a rota, defina o contrato da API.
Com o Apidog, você pode:
- criar o endpoint visualmente;
- importar um arquivo OpenAPI;
- gerar um servidor mock;
- compartilhar o contrato com frontend;
- salvar cenários de teste.
Request
POST /verify recebe multipart/form-data com um campo:
image: File
Response
Exemplo de resposta:
{
"verdict": "provavelmente_ia",
"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": "Um manifesto C2PA válido nomeia uma ferramenta de imagem de IA, e o classificador pontuou a imagem como provavelmente gerada por IA.",
"checked_at": "2026-05-21T09:30:00Z"
}
verdict deve ser um destes valores:
-
provavelmente_autêntico; -
provavelmente_ia; -
incerto.
Use três estados. Quando os sinais discordam ou são fracos, incerto é o resultado correto.
Esquema OpenAPI
components:
schemas:
VerifyResponse:
type: object
required: [verdict, confidence, signals, checked_at]
properties:
verdict:
type: string
enum: [provavelmente_autêntico, provavelmente_ia, incerto]
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
Se você prefere um fluxo spec-first, veja o passo a passo do modo spec-first.
Combinando os sinais
Crie verdict.py:
# verdict.py
def combine_signals(provenance: dict, classifier: dict) -> dict:
"""
Combina proveniência e classificador em um único veredito.
"""
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")
ai_keywords = (
"firefly",
"dall-e",
"dalle",
"midjourney",
"stable",
"gpt",
"gemini",
"imagen",
"generat",
)
generator_looks_ai = any(keyword in generator for keyword in ai_keywords)
# 1. Manifesto válido que nomeia uma ferramenta de IA.
if has_manifest and validation == "valid" and generator_looks_ai:
return _verdict(
"provavelmente_ia",
0.95,
"Um manifesto C2PA válido nomeia uma ferramenta de imagem de IA.",
)
# 2. Manifesto válido de câmera ou ferramenta não-IA.
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(
"incerto",
0.55,
"O manifesto parece autêntico, mas o classificador discorda.",
)
return _verdict(
"provavelmente_autêntico",
0.9,
"Um manifesto C2PA válido de uma ferramenta não-IA está presente.",
)
# 3. Manifesto presente, mas inválido ou com erro.
if has_manifest and validation in ("invalid", "error"):
return _verdict(
"incerto",
0.6,
"A imagem possui um manifesto C2PA que falhou na validação.",
)
# 4. Sem manifesto: usar classificador.
if classifier_ok and ai_score is not None:
if ai_score >= 0.7:
return _verdict(
"provavelmente_ia",
round(ai_score, 2),
"Sem dados de proveniência; o classificador pontuou a imagem como provavelmente gerada por IA.",
)
if ai_score <= 0.3:
return _verdict(
"provavelmente_autêntico",
round(1 - ai_score, 2),
"Sem dados de proveniência; o classificador pontuou a imagem como provavelmente autêntica.",
)
return _verdict(
"incerto",
0.5,
"Sem dados de proveniência e a pontuação do classificador é inconclusiva.",
)
# 5. Sem manifesto e sem classificador.
return _verdict(
"incerto",
0.0,
"Sem dados de proveniência e o classificador estava indisponível.",
)
def _verdict(verdict: str, confidence: float, explanation: str) -> dict:
return {
"verdict": verdict,
"confidence": confidence,
"explanation": explanation,
}
Essa política é conservadora:
- manifesto válido tem peso alto;
- manifesto inválido não prova falsidade, mas gera alerta;
- conflito entre sinais vira
incerto; - sem sinais, a confiança é
0.0.
Ajuste os thresholds conforme seu risco. Uma rede social, uma seguradora e uma redação provavelmente terão políticas diferentes.
Criando o app FastAPI
Crie main.py:
# 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="API de Detecção de Imagens de IA",
version="1.0.0",
)
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_BYTES = 12 * 1024 * 1024
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 tipo.
if image.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=415,
detail=f"Tipo não suportado {image.content_type}. Envie JPEG, PNG ou WebP.",
)
# 2. Ler e validar tamanho.
image_bytes = await image.read()
if len(image_bytes) == 0:
raise HTTPException(status_code=400, detail="Arquivo vazio.")
if len(image_bytes) > MAX_BYTES:
raise HTTPException(
status_code=413,
detail="O arquivo excede o limite de 12 MB.",
)
# 3. Ler proveniência.
# c2pa-python precisa de um caminho de arquivo.
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)
# 4. Chamar classificador.
if SIGHTENGINE_USER and SIGHTENGINE_SECRET:
classifier = await classify_image(
image_bytes=image_bytes,
filename=image.filename or "upload",
api_user=SIGHTENGINE_USER,
api_secret=SIGHTENGINE_SECRET,
)
else:
classifier = {
"available": False,
"reason": "classifier_not_configured",
}
# 5. Combinar sinais.
result = combine_signals(provenance, classifier)
return JSONResponse(
{
"verdict": result["verdict"],
"confidence": result["confidence"],
"signals": {
"provenance": {
key: provenance.get(key)
for key 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(),
}
)
Execute localmente:
uvicorn main:app --reload
Endpoint:
http://127.0.0.1:8000/verify
Configure as credenciais do classificador:
export SIGHTENGINE_API_USER="seu_usuario"
export SIGHTENGINE_API_SECRET="seu_secret"
Esse design segue um padrão comum de produto API-first: uma capacidade central exposta por uma API pequena e bem definida. Para contexto, veja software headless.
Testando com curl
Teste com uma imagem local:
curl -X POST http://127.0.0.1:8000/verify \
-F "image=@./foto.jpg"
Resposta esperada:
{
"verdict": "incerto",
"confidence": 0.5,
"signals": {
"provenance": {
"has_manifest": false,
"validation": "none",
"claim_generator": null,
"signature_issuer": null
},
"classifier": {
"available": true,
"ai_score": 0.48
}
},
"explanation": "Sem dados de proveniência e a pontuação do classificador é inconclusiva.",
"checked_at": "2026-05-21T09:30:00+00:00"
}
Teste erro de tipo:
curl -X POST http://127.0.0.1:8000/verify \
-F "image=@./arquivo.txt"
Se o content_type for detectado como não suportado, o endpoint retorna 415.
Mockando o endpoint no Apidog
O frontend não precisa esperar o backend ficar pronto.
Fluxo recomendado:
- Crie o endpoint
POST /verifyno Apidog. - Defina o body como
multipart/form-data. - Adicione o campo
imagecomo arquivo. - Defina o schema
VerifyResponse. - Crie exemplos de resposta.
- Gere o mock server.
- Entregue a URL mock para o frontend.
Crie exemplos para:
-
provavelmente_autênticocom manifesto válido de câmera; -
provavelmente_iacom manifesto que nomeia ferramenta de IA; -
incertocom classificador indisponível; - erro
413para arquivo grande; - erro
415para tipo não suportado.
Assim, o frontend consegue implementar upload, loading, estados de erro e painel de resultado antes do backend real existir.
Quando o backend for lançado, basta trocar a base URL.
Testando o backend real no Apidog
Depois que o serviço estiver rodando:
- Crie uma request
POST /verify. - Aponte para
http://127.0.0.1:8000/verify. - Em Body, escolha
form-data. - Adicione
image. - Defina o tipo como
File. - Selecione uma imagem local.
- Envie.
Adicione asserções:
- status HTTP é
200; -
verdictexiste; -
verdictestá entreprovavelmente_autêntico,provavelmente_iaeincerto; -
confidenceé número entre0e1; -
signals.provenance.has_manifesté booleano; -
signals.classifier.availableé booleano.
Monte um cenário com múltiplos uploads:
- imagem com Content Credentials;
- JPEG comum sem manifesto;
- arquivo grande;
- arquivo não-imagem renomeado como
.jpg; - imagem com classificador indisponível.
Isso transforma testes manuais em uma suíte repetível.
Reforço e casos limite
Arquivos corrompidos
Um arquivo pode declarar image/jpeg e ainda assim ser inválido. Para reforçar, decodifique a imagem antes de processar.
Exemplo com Pillow:
pip install pillow
from PIL import Image, UnidentifiedImageError
from io import BytesIO
def validate_image_bytes(image_bytes: bytes) -> None:
try:
with Image.open(BytesIO(image_bytes)) as img:
img.verify()
except UnidentifiedImageError:
raise ValueError("Arquivo não é uma imagem válida.")
Você pode chamar essa função antes da etapa C2PA e retornar 400 se falhar.
Manifesto ausente
Esse é o caso mais comum.
Não trate como erro. Não retorne 500. Não conclua que a imagem é falsa.
Siga para o classificador.
Classificador indisponível
Assuma que a dependência externa falhará.
Boas práticas:
- timeout curto;
- tratamento de erro HTTP;
- retorno
available: False; - veredito degradado para
incertoquando necessário.
Manifesto inválido
Manifesto presente não significa manifesto confiável.
Sempre verifique validation_status.
- array vazio: manifesto validado;
- array preenchido: falha de validação.
Um manifesto inválido deve gerar alerta, não prova automática.
Arquivos grandes e abuso
Aplique:
- limite de tamanho;
- rate limiting;
- autenticação se o endpoint não for público;
- observabilidade por status e motivo de falha;
- limites de timeout em chamadas externas.
O exemplo usa 12 MB:
MAX_BYTES = 12 * 1024 * 1024
Privacidade
Você está processando imagens de usuários.
Evite:
- logar bytes da imagem;
- persistir arquivos temporários;
- enviar imagens a terceiros sem consentimento ou base legal;
- esconder o uso de classificador externo da política de privacidade.
O que cada sinal detecta e perde
| Cenário | Sinal de proveniência C2PA | Sinal do classificador |
|---|---|---|
| Imagem de IA de ferramenta que escreve Content Credentials | Detecta: manifesto nomeia o gerador | Geralmente detecta artefatos |
| Imagem de IA com metadados removidos | Perde: nenhum manifesto | Detecta pelos pixels |
| Foto real de câmera que assina Content Credentials | Confirma manifesto válido | Pode dar falso positivo |
| Foto real sem metadados | Sem sinal | Melhor palpite probabilístico |
| Imagem com manifesto forjado ou adulterado | Detecta via validation_status
|
Pode ou não detectar |
| Gerador novo fora do treino do classificador | Detecta apenas se houver manifesto | Pode falhar |
| Foto real fortemente editada com IA | Manifesto pode registrar histórico | Pode ficar ambíguo |
A proveniência é precisa, mas esparsa. O classificador é amplo, mas probabilístico. O veredito combinado é mais útil do que qualquer coluna sozinha.
Casos de uso
Plataformas de conteúdo gerado por usuário
Use /verify no upload e mapeie resultados:
-
provavelmente_autêntico: permitir; -
provavelmente_ia: rotular ou revisar; -
incerto: enviar para revisão humana.
Redações e fact-checking
Um editor pode obter em uma única chamada:
- manifesto C2PA, se houver;
- validação criptográfica;
- pontuação do classificador;
- explicação legível.
Seguros e sinistros
Antes de um analista humano revisar evidências fotográficas, o sistema pode sinalizar:
- imagens provavelmente geradas;
- manifestos adulterados;
- ausência de sinais fortes.
Pipelines internos de assets
Equipes que precisam manter imagens de IA fora de bibliotecas internas podem usar o endpoint como gate de ingestão.
CMS com proveniência
À medida que mais câmeras e editores adotam Content Credentials, um CMS pode exibir selos de proveniência verificada e recorrer ao classificador quando não houver manifesto.
Conclusão
Detectar imagens geradas por IA não exige um teste perfeito. Exige combinar sinais independentes e comunicar incerteza.
Neste tutorial, você implementou:
- leitura e validação de manifesto C2PA;
- chamada a classificador hospedado;
- função de decisão com três vereditos;
- endpoint FastAPI
POST /verify; - contrato OpenAPI;
- mock e testes com Apidog.
O ponto principal: incerto não é falha do produto. É uma resposta honesta quando os sinais não sustentam uma conclusão forte.
Para construir isso com menos bloqueio entre backend e frontend, modele o contrato /verify, gere um mock server e salve cenários de teste no Apidog.
Top comments (0)