Quelqu'un télécharge une photo sur votre produit et affirme qu'un appareil photo l'a prise. Votre backend peut-il prouver ou réfuter cela ? Les générateurs d'images produisent désormais des résultats qui semblent réels à un examinateur humain, donc « faire confiance à ses yeux » a cessé de fonctionner il y a déjà un certain temps. La bonne nouvelle est que vous n'avez pas besoin d'entraîner votre propre modèle pour fournir une réponse utile. Vous pouvez combiner deux signaux indépendants, un manifeste de provenance cryptographique et un classificateur d'apprentissage automatique, en un seul verdict plus honnête que chaque signal pris isolément.
Ce tutoriel explique comment construire ce backend en tant que service unique avec un endpoint POST /verify. Vous lui donnez une image, il renvoie un verdict JSON avec un score de confiance et les détails de provenance qu'il a trouvés. Nous utiliserons Python et FastAPI pour le serveur, les outils open-source C2PA pour le signal de provenance, et une API de détection hébergée pour le signal du classificateur. Puisqu'il s'agit d'un projet d'API, nous concevrons également d'abord le contrat de l'endpoint et utiliserons Apidog pour le simuler et le tester, afin que votre équipe frontend puisse commencer l'intégration avant que le code backend ne soit terminé.
TL;DR
Vous construirez un service FastAPI exposant POST /verify qui accepte le téléchargement d'une image, extrait et valide son manifeste de Content Credentials C2PA avec la bibliothèque c2pa-python, appelle un classificateur de détection d'IA hébergé comme second signal indépendant, et renvoie un verdict JSON unique (likely_authentic, likely_ai, ou uncertain) avec un score de confiance et les détails de provenance bruts. Vous concevrez également le schéma OpenAPI pour l'endpoint et utiliserez Apidog pour générer un serveur simulé et exécuter des tests d'endpoint contre celui-ci.
Pourquoi deux signaux au lieu d'un
Avant tout code, il est utile d'être clair sur ce que vous détectez. Il n'y a pas une seule propriété d'un fichier qui vous dit « un humain a fait cela » ou « une IA a fait cela ». Il y a plutôt des indices, et chaque indice détecte un type d'image différent tout en en manquant d'autres.
Le premier indice est la provenance. C2PA, la Coalition pour la Provenance et l'Authenticité du Contenu, est une norme ouverte qui attache des métadonnées infalsifiables et cryptographiquement signées à un fichier multimédia. Cet ensemble de métadonnées est appelé un manifeste, et son nom visible par l'utilisateur est Content Credentials. Lorsqu'un outil participant, un appareil photo, un éditeur ou un générateur d'images, crée ou modifie une image, il peut écrire un manifeste qui enregistre ce qui s'est passé et le signe avec un certificat. Si vous pouvez lire et valider ce manifeste, vous obtenez une déclaration solide et vérifiable sur l'historique de l'image.
Le piège : C2PA est opt-in, et le manifeste est fragile. Une capture d'écran l'enlève. Le réencodage via une application de messagerie l'enlève. De nombreuses plateformes suppriment les métadonnées lors du téléchargement. Donc un manifeste manquant ne vous dit presque rien ; cela ne signifie pas que l'image est fausse, et cela ne signifie pas qu'elle est réelle.
Le deuxième indice est un classificateur statistique. Un modèle de détection est entraîné sur des millions d'images réelles et générées et apprend les artefacts visuels que les générateurs ont tendance à laisser derrière eux. Il fonctionne sur n'importe quelle image, avec ou sans métadonnées, mais il est probabiliste. Il renvoie une probabilité, pas un fait, et il peut se tromper, surtout sur des images en dehors de sa distribution d'entraînement ou des images fortement compressées.
Aucun signal n'est suffisant à lui seul. La provenance est précise mais rarement présente. Le classificateur est toujours disponible mais jamais certain. Combinez-les et vous obtenez un verdict qui dit, en substance, « voici ce que la cryptographie prouve, voici ce que le modèle estime, et voici la confiance que la combinaison nous inspire ». C'est l'objectif de conception. Si vous souhaitez un examen plus approfondi des raisons pour lesquelles les approches à signal unique échouent, notre article sur les raisons de l'échec de la détection d'images par IA couvre les modes de défaillance en détail.
Vue d'ensemble de l'architecture
Le service est volontairement petit. Un endpoint, deux appels en aval, une réponse combinée.
┌─────────────────────────────┐
image ──▶ │ FastAPI POST /verify │
│ │
│ 1. validate upload │
│ 2. ┌──────────────────┐ │
│ │ C2PA manifest │ │ signal de provenance
│ │ (c2pa-python) │ │
│ └──────────────────┘ │
│ 3. ┌──────────────────┐ │
│ │ classifier API │ │ signal statistique
│ │ (hosted detector) │ │
│ └──────────────────┘ │
│ 4. combine into verdict │
└─────────────────────────────┘
│
▼
Verdict JSON + confiance
L'étape 1 vérifie que le fichier téléchargé est une image réelle d'un type pris en charge et respectant une limite de taille. L'étape 2 lit le manifeste C2PA localement ; pas d'appel réseau, juste l'analyse et la validation du certificat. L'étape 3 envoie les octets de l'image à un classificateur hébergé via HTTPS. L'étape 4 fusionne les deux résultats avec une petite fonction de règles et renvoie le verdict.
Les deux étapes de signal sont indépendantes. C'est important pour la gestion des erreurs : si le classificateur expire, vous pouvez toujours renvoyer un verdict partiel à partir du signal de provenance, et vice-versa. Nous y reviendrons dans la section sur le durcissement.
Pour la pile technologique, Python 3.10 ou plus récent est requis car la bibliothèque C2PA en a besoin. Vous utiliserez FastAPI pour la couche web, Uvicorn pour l'exécuter, python-multipart pour les téléchargements de fichiers, httpx pour l'appel sortant du classificateur, et c2pa-python pour la provenance.
pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
Le signal C2PA
La Content Authenticity Initiative, sous l'organisation GitHub contentauth, publie les outils open-source C2PA. Il y a deux éléments dont vous entendrez parler :
c2patool, un outil en ligne de commande pour afficher et ajouter des manifestes. Il est pratique pour une inspection rapide depuis un terminal. Notez que son dépôt autonome est maintenant archivé, et la CLI réside au sein du projet Rustc2pa-rs.c2pa-python, le binding Python pour la même bibliothèque Rust sous-jacente (c2pa-rs). C'est ce que votre service utilisera. Il est publié sur PyPI sous le nomc2pa-pythonet s'installe avecpip install c2pa-python.
Le chemin de lecture de la bibliothèque est centré sur un objet Reader. Vous le dirigez vers une image, puis demandez le magasin de manifestes en JSON. Voici le cœur du module de provenance.
# provenance.py
import json
import c2pa
def read_provenance(image_path: str) -> dict:
"""
Read and validate the C2PA manifest from an image.
Returns a normalized dict describing what was found.
"""
try:
with c2pa.Reader(image_path) as reader:
manifest_store = json.loads(reader.json())
except c2pa.C2paError as err:
# ManifestNotFound is the expected case for most images.
if str(err).startswith("ManifestNotFound"):
return {
"has_manifest": False,
"validation": "none",
"detail": "Aucun manifeste C2PA présent dans cette image.",
}
# Any other C2paError means the file had C2PA data we could not parse.
return {
"has_manifest": True,
"validation": "error",
"detail": f"Impossible d'analyser le manifeste : {err}",
}
active_label = manifest_store.get("active_manifest")
manifests = manifest_store.get("manifests", {})
active = manifests.get(active_label, {})
# validation_status appears only when there are validation problems.
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": "Manifeste lu avec succès.",
}
Quelques notes sur ce que fait le code. Le Reader est utilisé comme gestionnaire de contexte afin que les ressources sous-jacentes soient libérées. reader.json() renvoie le magasin complet du manifeste sous forme de chaîne JSON ; la bibliothèque offre également reader.detailed_json() si vous souhaitez le rapport détaillé avec chaque assertion et ingrédient. Le résultat attendu pour la plupart des téléchargements est une C2paError dont le message commence par ManifestNotFound, car la plupart des images n'ont tout simplement pas de Content Credentials. Considérez cela comme une donnée, et non comme un échec.
Lorsqu'un manifeste est présent, deux champs sont les plus importants pour un verdict. La chaîne claim_generator vous indique quel outil a écrit le manifeste, par exemple une chaîne de firmware d'appareil photo ou le nom d'un outil d'image IA. Le tableau validation_status est vide lorsque la signature et les hachages sont corrects, et rempli de codes d'erreur lorsqu'ils ne le sont pas. Un manifeste invalide est un signal d'alarme qui mérite d'être signalé bruyamment ; cela signifie que le fichier revendique un historique que la cryptographie ne confirme pas.
Ce que ce signal ne peut pas faire : il ne peut pas vous donner un verdict lorsqu'il n'y a pas de manifeste, ce qui est la plupart du temps. C'est précisément pourquoi vous avez besoin du deuxième signal.
Le signal du classificateur
Le classificateur est une API hébergée qui évalue la probabilité qu'une image soit générée par IA. Plusieurs fournisseurs proposent cela. Ce tutoriel utilise Sightengine car son modèle de détection d'IA a une API HTTP documentée et une forme de réponse claire, mais le modèle est le même pour tout fournisseur ; vous échangez l'URL, les paramètres et le champ que vous lisez. Si vous évaluez les options, notre récapitulatif des meilleures API de détection d'images IA compare la précision, les prix et la couverture des fournisseurs.
L'endpoint de vérification de Sightengine est https://api.sightengine.com/1.0/check.json. Vous POSTez l'image en tant que media, définissez models sur genai, et passez votre api_user et api_secret. La réponse inclut type.ai_generated, un score de 0 à 1 où un score plus élevé signifie une probabilité plus forte d'être généré par 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:
"""
Send the image to the hosted detector.
Returns a normalized dict with the AI-generated score.
"""
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 fonction est asynchrone afin qu'un classificateur lent ne bloque pas la boucle d'événements. Le timeout est explicite et court ; huit secondes est une valeur par défaut raisonnable pour un endpoint interactif, et vous devriez l'ajuster à la latence réelle de votre fournisseur. Chaque chemin d'échec renvoie available: False avec une raison lisible par machine au lieu de lever une exception. C'est délibéré : une panne du classificateur doit dégrader le verdict, et non planter la requête. La logique de verdict de la section suivante lit available et décide quoi faire.
Traitez le score comme une estimation. Un score de 0.92 signifie « le modèle est assez sûr », pas « ceci est prouvé par l'IA ». Les fournisseurs mettent à jour leurs modèles, et la précision varie selon le générateur et le degré de compression de l'image avant qu'elle ne vous parvienne. Pour une vue plus large du comportement de ces outils en pratique, consultez notre guide sur comment vérifier si une image est générée par IA.
Concevoir le contrat /verify
Voici la partie où Apidog gagne sa place. Avant d'écrire le gestionnaire de route, concevez la requête et la réponse comme un schéma OpenAPI. Faire cela en premier vous offre trois choses : une source unique de vérité sur laquelle les deux équipes s'accordent, un serveur simulé que le frontend peut appeler immédiatement, et une suite de tests que vous pouvez exécuter dès que le backend existe.
La requête
POST /verify prend un corps multipart/form-data avec un seul champ, image, le fichier à vérifier. Gardez cela simple. Les paramètres de requête optionnels peuvent venir plus tard.
La réponse
C'est dans la réponse que le travail de conception porte ses fruits. Elle doit afficher le verdict final, la confiance, et les deux signaux bruts afin qu'un appelant puisse auditer la décision. Voici la forme.
{
"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 manifeste C2PA valide nomme un outil d'image IA, et le classificateur a estimé que l'image était probablement générée par IA.",
"checked_at": "2026-05-21T09:30:00Z"
}
verdict est l'une des trois valeurs de chaîne : likely_authentic, likely_ai, ou uncertain. Trois valeurs, pas deux, car l'honnêteté compte ; lorsque les signaux ne concordent pas ou sont tous deux faibles, « uncertain » est la bonne réponse. confidence est un flottant de 0 à 1 décrivant la force avec laquelle les signaux soutiennent ce verdict. signals contient les deux entrées brutes afin que l'appelant puisse afficher sa propre UI ou appliquer sa propre politique. explanation est une phrase lisible par un humain pour le personnel de support et les journaux.
Exprimer cela comme un schéma OpenAPI est simple. Voici le composant de réponse que vous mettriez dans votre spécification.
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 }
Vous pouvez rédiger ce schéma directement dans le concepteur visuel d'Apidog ou importer un fichier OpenAPI existant. Concevoir l'API avant l'implémentation est un flux de travail qui mérite d'être adopté en général ; notre guide du mode spec-first montre comment le faire de bout en bout dans Apidog.
Explication détaillée du code
Maintenant, les pièces s'assemblent. Voici l'application FastAPI : validation des entrées, appels des deux signaux, la fonction de combinaison, et la route.
Combiner les deux signaux
La fonction de verdict est le cœur du service. Elle encode votre politique. La provenance, lorsqu'elle est valide et présente, est le signal le plus fort car elle est cryptographique ; le classificateur est un facteur décisif et un mécanisme de repli. Voici une version claire et conservatrice.
# verdict.py
def combine_signals(provenance: dict, classifier: dict) -> dict:
"""Fusionne les signaux de provenance et du classificateur en un seul verdict."""
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")
# Heuristique : les outils d'IA connus ont tendance à s'identifier dans le manifeste.
ai_keywords = ("firefly", "dall-e", "dalle", "midjourney", "stable",
"gpt", "gemini", "imagen", "generat")
generator_looks_ai = any(k in generator for k in ai_keywords)
# Cas 1 : un manifeste valide nommant un générateur d'IA. Signal IA fort.
if has_manifest and validation == "valid" and generator_looks_ai:
return _verdict("likely_ai", 0.95,
"Un manifeste C2PA valide nomme un outil d'image IA.")
# Cas 2 : un manifeste valide d'un appareil photo ou d'un éditeur non-IA. Signal authentique fort.
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,
"Le manifeste semble authentique mais le classificateur "
"n'est pas d'accord ; les signaux sont en conflit.")
return _verdict("likely_authentic", 0.9,
"Un manifeste C2PA valide provenant d'un outil non-IA est présent.")
# Cas 3 : un manifeste qui échoue à la validation. Traité comme suspect.
if has_manifest and validation in ("invalid", "error"):
return _verdict("uncertain", 0.6,
"L'image contient un manifeste C2PA dont la validation a échoué ; "
"son historique revendiqué n'est pas vérifié.")
# Cas 4 : pas de manifeste. Revenir entièrement au classificateur.
if classifier_ok and ai_score is not None:
if ai_score >= 0.7:
return _verdict("likely_ai", round(ai_score, 2),
"Pas de données de provenance ; le classificateur a estimé que l'image "
"était probablement générée par IA.")
if ai_score <= 0.3:
return _verdict("likely_authentic", round(1 - ai_score, 2),
"Pas de données de provenance ; le classificateur a estimé que l'image "
"était probablement authentique.")
return _verdict("uncertain", 0.5,
"Pas de données de provenance et le score du classificateur est peu concluant.")
# Cas 5 : pas de manifeste et pas de classificateur. Nous ne pouvons vraiment pas dire.
return _verdict("uncertain", 0.0,
"Pas de données de provenance et le classificateur était indisponible.")
def _verdict(verdict: str, confidence: float, explanation: str) -> dict:
return {"verdict": verdict, "confidence": confidence,
"explanation": explanation}
Lisez les cinq cas et vous pourrez voir la politique. Un manifeste valide domine. Un manifeste échoué est un avertissement, pas une preuve de falsification, il aboutit donc à « uncertain ». Un conflit entre un manifeste propre et un score de classificateur élevé aboutit également à « uncertain » plutôt qu'à choisir un camp. Et lorsque les deux signaux sont manquants, le service le dit honnêtement avec une confiance nulle plutôt que de deviner. Vous ajusterez ces seuils en fonction de votre propre tolérance au risque ; une plateforme de contenu et une salle de rédaction traceraient les lignes différemment.
L'application 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. Valider le téléchargement.
if image.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=415,
detail=f"Type non pris en charge {image.content_type}. "
f"Envoyez JPEG, PNG ou WebP.",
)
image_bytes = await image.read()
if len(image_bytes) == 0:
raise HTTPException(status_code=400, detail="Fichier vide.")
if len(image_bytes) > MAX_BYTES:
raise HTTPException(status_code=413, detail="Le fichier dépasse la limite de 12 Mo.")
# 2. Signal de provenance. Le lecteur C2PA a besoin d'un chemin de fichier,
# donc écrivez dans un fichier temporaire et nettoyez aprè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. Signal du classificateur. Les échecs renvoient available: False, pas des exceptions.
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. Combiner et répondre.
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(),
})
Exécutez-le localement avec uvicorn main:app --reload et l'endpoint sera actif sur http://127.0.0.1:8000/verify. Le lecteur C2PA attend un chemin de fichier, donc le gestionnaire écrit le téléchargement dans un fichier temporaire et le supprime dans un bloc finally ; le classificateur fonctionne directement à partir des octets. Remarquez que la requête ne plante jamais en cas de manifeste manquant ou de classificateur absent ; ce sont deux états normaux gérés par la fonction de verdict.
Un backend conçu de cette manière, un service ciblé avec un contrat clair, s'inscrit dans la tendance plus large des produits exposant leur capacité principale via une API. Si cette idée vous intéresse, notre essai sur les logiciels devenant "headless" vaut la peine d'être lu.
Test et simulation avec Apidog
Voici le problème de workflow : votre équipe frontend veut construire l'interface utilisateur de téléchargement et le panneau de résultats maintenant, mais le backend ci-dessus prend quelques jours à terminer, à obtenir les clés et à déployer. Vous ne voulez pas qu'ils soient bloqués. C'est à cela que servent les serveurs simulés, et c'est là que la conception du schéma en premier lieu porte ses fruits.
Générer un serveur simulé à partir du schéma
Importez le schéma OpenAPI dans Apidog, ou construisez l'endpoint /verify dans le concepteur visuel. Apidog lit le schéma de réponse et génère un serveur simulé automatiquement. Parce que le schéma définit les types de champs et les énumérations, la simulation renvoie des données structurées exactement comme le véritable endpoint : un verdict qui est l'une des trois valeurs d'énumération, un flottant confidence entre 0 et 1, un objet signals rempli. Le frontend pointe son appel fetch vers l'URL de simulation Apidog et obtient des réponses réalistes dès le premier jour. Lorsque le backend réel est livré, ils ne changent qu'une seule URL de base.
La simulation est également l'endroit où vous exercez les cas difficiles avant qu'aucun code réel n'existe. Définissez des exemples de réponses pour les verdicts importants :
- une réponse
likely_authenticavec un manifeste d'appareil photo valide, - une réponse
likely_aiavec un outil d'IA nommé dans le manifeste, - une réponse
uncertainlorsque le classificateur n'était pas disponible, - les réponses d'erreur
415et413.
Le frontend peut construire et styliser chaque état, y compris les états d'erreur, par rapport à la simulation. C'est ainsi que vous livrez une UI et une API en parallèle au lieu de séquentiellement.
Exécuter des tests d'endpoint dans Apidog
Une fois le backend en cours d'exécution, créez une requête dans Apidog pour POST /verify. Définissez la méthode, pointez-la vers votre URL locale, et dans l'onglet Body, choisissez form-data, ajoutez le champ image, définissez son type sur Fichier, et sélectionnez une image de test sur le disque.
Envoyez-le, et Apidog affiche la réponse JSON. Ajoutez maintenant des assertions pour que cela devienne une vérification répétable plutôt qu'un clic unique :
- affirmer que le statut de la réponse est
200, - affirmer que
verdictexiste et est l'une des trois chaînes autorisées, - affirmer que
confidenceest un nombre entre 0 et 1, - affirmer que
signals.provenance.has_manifestest un booléen.
Construisez un petit scénario de test qui exécute plusieurs téléchargements en séquence : une image avec des Content Credentials, un JPEG simple sans manifeste, un fichier surdimensionné, et un fichier non-image renommé avec une extension .jpg. Chacun vérifie une branche différente de votre logique de verdict et de votre validation d'entrée. Enregistrez le scénario et vous pourrez réexécuter toute la suite après chaque modification, ou l'intégrer dans le CI afin qu'une régression dans la fonction de verdict fasse échouer la build. Tester un endpoint de téléchargement manuellement avec curl devient vite fastidieux ; un scénario enregistré non.
Durcissement et cas limites
Le chemin heureux représente les 80 % faciles. Un service de vérification vit ou meurt sur les 20 % restants, car les entrées sont par nature adverses ; quelqu'un essaie de faire passer une image pour ce qu'elle n'est pas.
Fichiers corrompus ou tronqués. Un fichier peut avoir un type MIME d'image et être quand même une poubelle. Le lecteur C2PA lève une C2paError sur des données qu'il ne peut pas analyser, et la fonction read_provenance transforme déjà cela en un résultat propre au lieu d'une erreur 500. Par précaution, vous pouvez décoder l'image avec une bibliothèque comme Pillow avant le traitement et la rejeter avec un 400 si le décodage échoue. Cela bloque également l'astuce du fichier texte renommé.
Manifeste manquant. Couvert, mais il est bon de le répéter car c'est le cas le plus courant et le plus facile à mal interpréter. L'absence de manifeste n'est pas une erreur et n'est pas un verdict. La bibliothèque le signale avec une C2paError dont le message commence par ManifestNotFound ; le service l'intercepte spécifiquement et passe au classificateur. Ne laissez jamais un manifeste manquant produire une erreur 500 ou un verdict « faux ».
Délai d'attente ou panne du classificateur. Le classificateur est une dépendance réseau tierce, alors supposez qu'il échouera parfois. La fonction classify_image utilise un délai d'attente httpx explicite et renvoie available: False en cas de délai d'attente ou d'erreur HTTP. La fonction de verdict se rabat alors sur la provenance seule, ou renvoie uncertain avec une confiance nulle s'il n'y a pas non plus de manifeste. L'endpoint répond toujours avec un 200 et un verdict honnête ; un fournisseur lent ne peut pas faire tomber votre service.
Manifestes falsifiés. Un manifeste peut être présent mais invalide, signé avec un mauvais certificat ou avec des hachages qui ne correspondent pas aux pixels. C'est le cas que les gens oublient. Vérifiez toujours validation_status ; un tableau vide signifie que le manifeste a été vérifié, un tableau rempli signifie qu'il ne l'a pas été. La fonction de verdict traite un manifeste échoué comme un avertissement qui aboutit à uncertain, jamais comme une preuve. Faire confiance à un manifeste non validé est pire que n'avoir aucun manifeste du tout.
Fichiers volumineux et abus. Limitez la taille du téléchargement, l'exemple utilise 12 Mo, et rejetez tout ce qui est plus grand avec un 413 avant de lire tout le corps en mémoire. Placez une limite de débit devant l'endpoint ; la validation cryptographique et un appel API sortant par requête ne sont pas gratuits, et un endpoint de vérification ouvert est une cible invitante.
Confidentialité. Vous recevez des images d'utilisateurs. Traitez-les en mémoire ou dans un fichier temporaire que vous supprimez immédiatement, comme le fait l'exemple, et ne loguez pas les octets de l'image. Si vous envoyez des images à un classificateur tiers, mentionnez-le dans votre politique de confidentialité et assurez-vous que cela est autorisé pour votre cas d'utilisation.
Ce que chaque signal détecte et manque
Ce tableau est le modèle mental à garder. C'est pourquoi le service utilise les deux.
| Scénario | Signal de provenance C2PA | Signal du classificateur |
|---|---|---|
| Image IA d'un outil qui écrit des Content Credentials | Le détecte : le manifeste nomme le générateur | Le détecte généralement : artefacts présents |
| Image IA avec métadonnées supprimées (capture d'écran, re-téléchargement) | Le manque : pas de manifeste à lire | Le détecte : fonctionne sur les pixels, pas besoin de métadonnées |
| Vraie photo d'un appareil photo qui signe des Content Credentials | Le confirme : manifeste valide, générateur non-IA | Peut donner un faux positif en cas de forte compression ou de modifications |
| Vraie photo sans aucune métadonnée | Pas de signal : rien à valider | Meilleure estimation seulement : probabiliste, peut se tromper |
| Image avec un manifeste falsifié ou altéré | Le détecte : validation_status signale l'échec |
Peut ou non le détecter, dépend des pixels |
| Nouveau générateur sur lequel le classificateur n'a pas été entraîné | Le détecte seulement si l'outil écrit un manifeste | Le manque souvent : en dehors de la distribution d'entraînement |
| Vraie photo fortement modifiée (retouche IA sur une base réelle) | Le manifeste, s'il est présent, enregistre l'historique des modifications | Ambigu : partiellement synthétique, le score se situe dans la plage moyenne |
Lisez n'importe quelle ligne et vous verrez la même histoire : là où un signal est aveugle, l'autre ne l'est souvent pas. La provenance est exacte mais rare ; le classificateur est universel mais flou. Le verdict combiné est plus fiable que chaque colonne prise isolément, et la valeur honnête uncertain existe pour les lignes où les deux signaux sont faibles.
Cas d'utilisation réels
Ce modèle n'est pas académique. Voici quelques endroits où il s'adapte directement :
- Plateformes de contenu généré par les utilisateurs. Un marché ou une application sociale peut faire passer les téléchargements par
/verifyet étiqueter ou mettre en file d'attente pour examen tout ce qui est jugé susceptible d'être de l'IA, ou tout ce qui contient un manifeste dont la validation échoue. Le verdict à trois valeurs se traduit clairement par « autoriser », « signaler » et « envoyer à un humain ». - Salles de rédaction et vérification des faits. Un éditeur vérifiant une image virale obtient à la fois la provenance cryptographique, le cas échéant, et une estimation indépendée du modèle en un seul appel, avec une phrase d'explication qu'il peut citer dans ses notes.
- Assurance et réception des réclamations. Lorsqu'un client soumet des preuves photographiques, une étape de vérification signale les images qui semblent générées ou contiennent un manifeste altéré, avant qu'un expert en sinistres humain ne leur consacre du temps.
- Pipelines d'actifs internes. Une équipe qui doit exclure les images générées par IA d'une bibliothèque d'images, ou les y étiqueter clairement, peut filtrer l'ingestion sur
/verify. - Publication consciente de la provenance. Alors que de plus en plus d'appareils photo et d'éditeurs adoptent les Content Credentials, un CMS peut lire le manifeste lors du téléchargement et afficher un badge vérifié, en se rabattant sur le classificateur lorsqu'aucun manifeste n'est présent.
Le fil conducteur commun : vous voulez un premier passage rapide et automatisé, honnête quant à sa propre incertitude, afin que l'attention humaine soit dirigée là où elle est réellement nécessaire.
Conclusion
Bien détecter les images générées par IA ne consiste pas à trouver un test parfait. Il s'agit de combiner des signaux indépendants et d'être honnête quant à la confiance.
- Les Content Credentials C2PA vous donnent un signal de provenance fort, cryptographiquement vérifiable, mais le manifeste est opt-in et facilement supprimé, il est donc souvent absent.
- Un classificateur hébergé vous donne un signal universel mais probabiliste qui fonctionne sur n'importe quelle image, avec ou sans métadonnées.
- En les combinant dans un petit service FastAPI, on obtient un verdict à trois valeurs,
likely_authentic,likely_ai, ouuncertain, avec un score de confiance et les deux signaux bruts attachés pour l'audit. - Concevoir d'abord le contrat OpenAPI vous permet de simuler l'endpoint dans Apidog afin que le frontend se construise en parallèle, puis d'exécuter des scénarios de test enregistrés contre le backend réel.
- Aucun détecteur n'est parfait. Deux signaux augmentent la confiance ; ils n'éliminent pas l'incertitude, c'est pourquoi le verdict
uncertainest une caractéristique, et non une lacune.
Pour construire cela pour de vrai, concevez le schéma /verify, générez un serveur simulé, et exécutez vos tests d'endpoint en un seul endroit. Téléchargez Apidog pour concevoir, simuler et tester l'API au fur et à mesure que vous la construisez, puis passez du mock au backend réel avec un simple changement d'URL de base.
