C2PA 및 분류기를 사용한 AI 이미지 감지기 API 구축

Ashley Innocent

Ashley Innocent

21 May 2026

C2PA 및 분류기를 사용한 AI 이미지 감지기 API 구축

누군가 귀하의 제품에 사진을 업로드하고 카메라로 촬영했다고 주장합니다. 귀하의 백엔드는 이를 증명하거나 반증할 수 있습니까? 이미지 생성기가 이제 인간 검토자에게는 진짜처럼 보이는 결과물을 생성하므로, "눈으로 믿는 것"은 오래 전에 효과가 없어졌습니다. 다행히도 유용한 답변을 제공하기 위해 자체 모델을 훈련할 필요는 없습니다. 암호화된 출처 매니페스트와 기계 학습 분류기라는 두 가지 독립적인 신호를 결합하여, 어느 한 신호만으로는 얻을 수 없는 더 정직한 판정을 내릴 수 있습니다.

이 튜토리얼은 POST /verify 엔드포인트를 가진 단일 서비스로 백엔드를 구축하는 과정을 안내합니다. 이미지를 제공하면, 신뢰 점수와 찾은 출처 세부 정보가 포함된 JSON 판정을 반환합니다. 서버에는 Python과 FastAPI를 사용하고, 출처 신호에는 오픈 소스 C2PA 툴링을, 분류기 신호에는 호스팅된 탐지 API를 사용할 것입니다. 이 프로젝트는 API 프로젝트이므로, 먼저 엔드포인트 계약을 설계하고 Apidog를 사용하여 이를 모의(mock)하고 테스트하여, 백엔드 코드가 완성되기 전에 프론트엔드 팀이 통합 작업을 시작할 수 있도록 할 것입니다.

요약 (TL;DR)

이미지 업로드를 허용하는 POST /verify를 노출하는 FastAPI 서비스를 구축할 것입니다. 이 서비스는 c2pa-python 라이브러리를 사용하여 C2PA 콘텐츠 자격 증명 매니페스트를 추출하고 검증하고, 두 번째 독립적인 신호로 호스팅된 AI 탐지 분류기를 호출하며, 신뢰 점수와 원시 출처 세부 정보가 포함된 단일 JSON 판정(likely_authentic, likely_ai 또는 uncertain)을 반환합니다. 또한 엔드포인트에 대한 OpenAPI 스키마를 설계하고 Apidog를 사용하여 모의 서버를 생성하고 이에 대해 엔드포인트 테스트를 실행할 것입니다.

버튼

하나의 신호 대신 두 개의 신호를 사용하는 이유

코드를 작성하기 전에 무엇을 탐지하는지 명확히 이해하는 것이 도움이 됩니다. 파일에는 "인간이 만들었다" 또는 "AI가 만들었다"고 알려주는 단일 속성이 없습니다. 대신 단서들이 있으며, 각 단서는 다른 종류의 이미지를 포착하면서 다른 이미지는 놓칠 수 있습니다.

첫 번째 단서는 출처입니다. C2PA(콘텐츠 출처 및 진위 연합)는 미디어 파일에 변조 방지 기능이 있는 암호화 서명 메타데이터를 첨부하는 오픈 표준입니다. 이 메타데이터 번들을 매니페스트라고 하며, 사용자에게는 콘텐츠 자격 증명으로 알려져 있습니다. 참여 도구(카메라, 편집기, 이미지 생성기 등)가 이미지를 생성하거나 변경할 때, 발생한 일을 기록하고 인증서로 서명하는 매니페스트를 작성할 수 있습니다. 해당 매니페스트를 읽고 유효성을 검사할 수 있다면, 이미지의 역사에 대한 강력하고 검증 가능한 진술을 얻을 수 있습니다.

문제는 C2PA는 옵트인 방식이며, 매니페스트는 취약하다는 점입니다. 스크린샷은 이를 제거합니다. 메시징 앱을 통한 재인코딩도 이를 제거합니다. 많은 플랫폼은 업로드 시 메타데이터를 제거합니다. 따라서 매니페스트가 없다는 것은 거의 아무것도 알려주지 않습니다. 이미지가 가짜라는 의미도 아니고, 진짜라는 의미도 아닙니다.

두 번째 단서는 통계적 분류기입니다. 탐지 모델은 수백만 개의 실제 이미지와 생성된 이미지로 훈련되어, 생성기가 남기는 시각적 아티팩트를 학습합니다. 메타데이터 유무에 관계없이 모든 이미지에서 작동하지만, 확률적입니다. 사실이 아닌 가능성을 반환하며, 특히 훈련 분포를 벗어나는 이미지나 심하게 압축된 이미지에서는 틀릴 수 있습니다.

어떤 신호도 단독으로는 충분하지 않습니다. 출처는 정확하지만 거의 존재하지 않습니다. 분류기는 항상 사용 가능하지만 결코 확실하지 않습니다. 이들을 결합하면 "암호화가 증명하는 것은 이것이고, 모델이 추정하는 것은 이것이며, 이 조합이 우리를 얼마나 확신하게 만드는지"라는 효과적인 판정을 얻게 됩니다. 이것이 설계 목표입니다. 단일 신호 접근 방식이 왜 실패하는지 더 깊이 알고 싶다면, AI 이미지 탐지가 실패하는 이유에 대한 저희 글이 실패 모드를 자세히 다루고 있습니다.

아키텍처 개요

이 서비스는 의도적으로 작게 설계되었습니다. 하나의 엔드포인트, 두 개의 다운스트림 호출, 하나의 결합된 응답입니다.

                ┌─────────────────────────────┐
   이미지  ──▶   │   FastAPI  POST /verify      │
                │                              │
                │   1. 업로드 검증             │
                │   2. ┌──────────────────┐    │
                │      │ C2PA 매니페스트     │    │  출처 신호
                │      │ (c2pa-python)     │    │
                │      └──────────────────┘    │
                │   3. ┌──────────────────┐    │
                │      │ 분류기 API          │    │  통계적 신호
                │      │ (호스팅된 탐지기)   │    │
                │      └──────────────────┘    │
                │   4. 판정으로 결합             │
                └─────────────────────────────┘
                              │
                              ▼
                   JSON 판정 + 신뢰도

1단계는 업로드가 지원되는 유형의 실제 이미지이며 크기 제한 내에 있는지 확인합니다. 2단계는 C2PA 매니페스트를 로컬에서 읽습니다. 네트워크 호출 없이 구문 분석 및 인증서 유효성 검사만 수행합니다. 3단계는 이미지 바이트를 호스팅된 분류기에 HTTPS를 통해 보냅니다. 4단계는 작은 규칙 함수로 두 결과를 병합하고 판정을 반환합니다.

두 신호 단계는 독립적입니다. 이는 오류 처리에서 중요합니다. 분류기가 타임아웃되면, 출처 신호에서 부분적인 판정을 반환할 수 있으며, 그 반대도 마찬가지입니다. 이는 강화 섹션에서 다시 다룰 것입니다.

스택의 경우, C2PA 라이브러리가 필요하므로 Python 3.10 이상이 필요합니다. 웹 계층에는 FastAPI, 실행에는 Uvicorn, 파일 업로드에는 python-multipart, 외부 분류기 호출에는 httpx, 출처에는 c2pa-python을 사용할 것입니다.

pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python

C2PA 신호

contentauth GitHub 조직 산하의 콘텐츠 진위 이니셔티브는 오픈 소스 C2PA 툴링을 게시합니다. 두 가지 주요 구성 요소가 있습니다.

라이브러리의 읽기 경로는 Reader 객체를 중심으로 합니다. 이미지에 연결한 다음 JSON으로 매니페스트 저장소를 요청합니다. 다음은 출처 모듈의 핵심입니다.

# 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": "No C2PA manifest present in this image.",
            }
        # Any other C2paError means the file had C2PA data we could not parse.
        return {
            "has_manifest": True,
            "validation": "error",
            "detail": f"Could not parse manifest: {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": "Manifest read successfully.",
    }

코드의 작동 방식에 대한 몇 가지 참고 사항입니다. Reader는 컨텍스트 관리자로 사용되어 기본 리소스가 해제됩니다. reader.json()은 전체 매니페스트 저장소를 JSON 문자열로 반환합니다. 모든 어설션과 재료가 포함된 자세한 보고서를 원하면 라이브러리는 reader.detailed_json()도 제공합니다. 대부분의 업로드에서 예상되는 결과는 메시지가 ManifestNotFound로 시작하는 C2paError입니다. 왜냐하면 대부분의 이미지에는 단순히 콘텐츠 자격 증명이 없기 때문입니다. 이를 실패가 아닌 데이터로 취급하십시오.

매니페스트가 존재할 때, 판정에 가장 중요한 두 필드는 claim_generator 문자열과 validation_status 배열입니다. claim_generator 문자열은 카메라 펌웨어 문자열이나 AI 이미지 도구의 이름과 같이 매니페스트를 작성한 도구를 알려줍니다. validation_status 배열은 서명과 해시가 확인될 때는 비어 있고, 그렇지 않을 때는 오류 코드로 채워집니다. 유효하지 않은 매니페스트는 시끄럽게 알려야 할 위험 신호입니다. 이는 파일이 암호화로 뒷받침되지 않는 이력을 주장한다는 의미입니다.

이 신호가 할 수 없는 것은 대부분의 경우 매니페스트가 없을 때 판정을 내릴 수 없다는 것입니다. 이것이 바로 두 번째 신호가 필요한 이유입니다.

분류기 신호

분류기는 이미지가 AI 생성일 가능성을 점수화하는 호스팅된 API입니다. 여러 공급업체가 이를 제공합니다. 이 튜토리얼은 Sightengine의 AI 탐지 모델이 문서화된 HTTP API와 명확한 응답 형태를 가지고 있기 때문에 이를 사용하지만, 패턴은 어떤 공급업체에서도 동일합니다. URL, 매개변수 및 읽을 필드만 바꾸면 됩니다. 옵션을 고려 중이라면, 최고의 AI 이미지 탐지 API에 대한 저희의 종합 검토가 공급업체별 정확도, 가격 및 적용 범위를 비교합니다.

Sightengine의 확인 엔드포인트는 https://api.sightengine.com/1.0/check.json입니다. 이미지를 media로 POST하고, modelsgenai로 설정하며, api_userapi_secret을 전달합니다. 응답에는 type.ai_generated가 포함되며, 이는 AI 생성일 가능성이 높을수록 높은 0에서 1 사이의 점수입니다.

# 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)}

함수는 비동기식이므로 느린 분류기가 이벤트 루프를 차단하지 않습니다. 타임아웃은 명시적이고 짧습니다. 8초는 상호작용형 엔드포인트에 대한 합리적인 기본값이며, 공급업체의 실제 지연 시간에 따라 조정해야 합니다. 모든 실패 경로는 예외를 발생시키는 대신 기계가 읽을 수 있는 이유와 함께 available: False를 반환합니다. 이는 의도적인 것입니다. 분류기 장애는 판정을 저하시켜야지 요청을 중단시켜서는 안 됩니다. 다음 섹션의 판정 로직은 available을 읽고 무엇을 할지 결정합니다.

점수를 추정치로 취급하십시오. 0.92는 "모델이 꽤 확신한다"는 의미이지 "이것이 AI로 입증되었다"는 의미가 아닙니다. 공급업체는 모델을 업데이트하며, 정확도는 생성기와 이미지가 귀하에게 도달하기 전에 얼마나 압축되었는지에 따라 달라집니다. 이러한 도구들이 실제로 어떻게 작동하는지에 대한 더 넓은 시야를 얻으려면 AI 생성 이미지 확인 방법에 대한 저희 가이드를 참조하십시오.

/verify 계약 설계

이것이 Apidog가 제 역할을 하는 부분입니다. 라우트 핸들러를 작성하기 전에 OpenAPI 스키마로 요청과 응답을 설계하십시오. 이렇게 먼저 하면 세 가지를 얻을 수 있습니다. 즉, 양 팀이 동의하는 단일 진실 소스, 프론트엔드가 즉시 호출할 수 있는 모의 서버, 그리고 백엔드가 존재하는 순간 실행할 수 있는 테스트 스위트입니다.

요청

POST /verify는 확인할 파일인 image 필드 하나를 가진 multipart/form-data 본문을 받습니다. 간단하게 유지하십시오. 선택적 쿼리 매개변수는 나중에 추가할 수 있습니다.

응답

응답은 설계 작업이 결실을 맺는 부분입니다. 최종 판정, 신뢰도, 그리고 호출자가 결정을 감사할 수 있도록 두 가지 원시 신호를 모두 보여주어야 합니다. 형태는 다음과 같습니다.

{
  "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": "유효한 C2PA 매니페스트가 AI 이미지 도구를 명시하며, 분류기가 해당 이미지를 AI 생성으로 추정합니다.",
  "checked_at": "2026-05-21T09:30:00Z"
}

verdict는 세 가지 문자열 값 중 하나입니다: likely_authentic (진품으로 추정), likely_ai (AI로 추정), 또는 uncertain (불확실). 세 가지 값이지 두 가지가 아닌 이유는 정직성이 중요하기 때문입니다. 신호가 일치하지 않거나 둘 다 약할 때, "불확실"이 올바른 답변입니다. confidence는 해당 판정을 신호가 얼마나 강력하게 뒷받침하는지를 나타내는 0에서 1 사이의 부동 소수점 값입니다. signals는 호출자가 자체 UI를 표시하거나 자체 정책을 적용할 수 있도록 두 가지 원시 입력을 모두 전달합니다. explanation은 지원 직원 및 로그를 위한 사람이 읽을 수 있는 문장입니다.

이를 OpenAPI 스키마로 표현하는 것은 간단합니다. 다음은 스펙에 넣을 응답 구성 요소입니다.

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 }

이 스키마는 Apidog의 시각적 디자이너에서 직접 작성하거나 기존 OpenAPI 파일을 가져올 수 있습니다. 구현 전에 API를 설계하는 것은 일반적으로 채택할 가치가 있는 워크플로입니다. 스펙 우선 모드 둘러보기에서는 Apidog에서 이를 처음부터 끝까지 수행하는 방법을 보여줍니다.

코드 둘러보기

이제 조각들이 합쳐집니다. 아래는 FastAPI 앱입니다: 입력 유효성 검사, 두 신호 호출, 결합 함수, 그리고 라우트입니다.

두 신호 결합

판정 함수는 서비스의 핵심입니다. 이는 귀하의 정책을 인코딩합니다. 유효하고 존재하는 경우 출처는 암호화 방식이므로 더 강력한 신호입니다. 분류기는 동점 해결 장치이자 대체 수단입니다. 다음은 명확하고 보수적인 버전입니다.

# verdict.py


def combine_signals(provenance: dict, classifier: dict) -> dict:
    """Merge the provenance and classifier signals into one 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")

    # Heuristic: known AI tools tend to identify themselves in the manifest.
    ai_keywords = ("firefly", "dall-e", "dalle", "midjourney", "stable",
                   "gpt", "gemini", "imagen", "generat")
    generator_looks_ai = any(k in generator for k in ai_keywords)

    # Case 1: a valid manifest naming an AI generator. Strong AI signal.
    if has_manifest and validation == "valid" and generator_looks_ai:
        return _verdict("likely_ai", 0.95,
                        "A valid C2PA manifest names an AI image tool.")

    # Case 2: a valid manifest from a camera or non-AI editor. Strong authentic signal.
    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,
                            "Manifest looks authentic but the classifier "
                            "disagrees; signals conflict.")
        return _verdict("likely_authentic", 0.9,
                        "A valid C2PA manifest from a non-AI tool is present.")

    # Case 3: a manifest that fails validation. Treat as suspicious.
    if has_manifest and validation in ("invalid", "error"):
        return _verdict("uncertain", 0.6,
                        "The image carries a C2PA manifest that failed "
                        "validation; its claimed history is unverified.")

    # Case 4: no manifest. Fall back entirely to the classifier.
    if classifier_ok and ai_score is not None:
        if ai_score >= 0.7:
            return _verdict("likely_ai", round(ai_score, 2),
                            "No provenance data; the classifier scored the "
                            "image as likely AI-generated.")
        if ai_score <= 0.3:
            return _verdict("likely_authentic", round(1 - ai_score, 2),
                            "No provenance data; the classifier scored the "
                            "image as likely authentic.")
        return _verdict("uncertain", 0.5,
                        "No provenance data and the classifier score is "
                        "inconclusive.")

    # Case 5: no manifest and no classifier. We genuinely cannot say.
    return _verdict("uncertain", 0.0,
                    "No provenance data and the classifier was unavailable.")


def _verdict(verdict: str, confidence: float, explanation: str) -> dict:
    return {"verdict": verdict, "confidence": confidence,
            "explanation": explanation}
```

다섯 가지 사례를 읽어보면 정책을 알 수 있습니다. 유효한 매니페스트가 지배적입니다. 실패한 매니페스트는 위조의 증거가 아닌 경고이므로 "불확실"로 분류됩니다. 깨끗한 매니페스트와 높은 분류기 점수 간의 충돌 또한 한쪽을 선택하는 대신 "불확실"로 분류됩니다. 그리고 두 신호가 모두 없을 때는 서비스는 솔직하게 0의 신뢰도로 불확실하다고 말하며 추측하지 않습니다. 귀하는 귀하의 위험 허용치에 따라 이러한 임계값을 조정할 것입니다. 콘텐츠 플랫폼과 뉴스룸은 다르게 선을 그을 것입니다.

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. Validate the upload.
    if image.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail=f"Unsupported type {image.content_type}. "
                   f"Send JPEG, PNG, or WebP.",
        )

    image_bytes = await image.read()
    if len(image_bytes) == 0:
        raise HTTPException(status_code=400, detail="Empty file.")
    if len(image_bytes) > MAX_BYTES:
        raise HTTPException(status_code=413, detail="File exceeds 12 MB limit.")

    # 2. Provenance signal. The C2PA reader needs a file path,
    #    so write to a temp file and clean up afterward.
    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. Classifier signal. Failures return available: False, not 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. Combine and respond.
    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(),
    })

uvicorn main:app --reload로 로컬에서 실행하면 엔드포인트는 http://127.0.0.1:8000/verify에서 활성화됩니다. C2PA 리더는 파일 경로를 필요로 하므로, 핸들러는 업로드된 파일을 임시 파일에 쓰고 finally 블록에서 삭제합니다. 분류기는 바이트에서 직접 작동합니다. 요청은 매니페스트 누락이나 분류기 부재로 인해 절대 중단되지 않습니다. 둘 다 판정 함수가 처리하는 정상 상태입니다.

이러한 방식으로 설계된 백엔드, 즉 깔끔한 계약을 가진 집중된 서비스는 API를 통해 핵심 기능을 노출하는 제품의 광범위한 트렌드에 부합합니다. 이 아이디어가 흥미롭다면, 헤드리스(headless)화되는 소프트웨어에 대한 저희의 에세이를 읽어볼 가치가 있습니다.

Apidog로 테스트 및 모의(Mocking)

여기 워크플로우 문제가 있습니다. 프론트엔드 팀은 지금 업로드 UI와 결과 패널을 구축하고 싶지만, 위에 설명된 백엔드는 완성하고 키를 얻고 배포하는 데 며칠이 걸립니다. 그들이 지연되기를 원하지 않을 것입니다. 이것이 모의 서버의 용도이며, 스키마를 먼저 설계하는 것이 보상을 받는 지점입니다.

스키마에서 모의 서버 생성

OpenAPI 스키마를 Apidog로 가져오거나 시각적 디자이너에서 /verify 엔드포인트를 구축하십시오. Apidog는 응답 스키마를 읽고 자동으로 모의 서버를 생성합니다. 스키마가 필드 유형과 열거형을 정의하므로, 모의 서버는 실제 엔드포인트와 정확히 동일한 형태로 데이터를 반환합니다. 즉, 세 가지 열거형 값 중 하나인 verdict, 0에서 1 사이의 confidence 부동 소수점, 채워진 signals 객체 등입니다. 프론트엔드는 fetch 호출을 Apidog 모의 URL로 지정하고 첫날부터 현실적인 응답을 받습니다. 실제 백엔드가 배포되면 기본 URL 하나만 변경하면 됩니다.

모의 서버는 실제 코드가 존재하기 전에 어려운 경우를 연습하는 곳이기도 합니다. 중요한 판정에 대한 예시 응답을 정의하십시오.

프론트엔드는 오류 상태를 포함한 모든 상태를 모의 서버에 대해 구축하고 스타일을 지정할 수 있습니다. 이것이 UI와 API를 순차적으로가 아닌 병렬로 배포하는 방법입니다.

Apidog에서 엔드포인트 테스트 실행

백엔드가 실행되면 Apidog에서 POST /verify 요청을 생성하십시오. 메서드를 설정하고 로컬 URL을 지정한 다음, 본문 탭에서 form-data를 선택하고 image 필드를 추가하고 유형을 파일로 설정한 다음 디스크에서 테스트 이미지를 선택하십시오.

이를 전송하면 Apidog는 JSON 응답을 보여줍니다. 이제 이것이 일회성 클릭이 아닌 반복 가능한 검사가 되도록 어설션을 추가하십시오.

콘텐츠 자격 증명이 있는 이미지, 매니페스트가 없는 일반 JPEG, 너무 큰 파일, .jpg 확장자로 이름이 변경된 비이미지 파일 등 여러 업로드를 순서대로 실행하는 작은 테스트 시나리오를 구축하십시오. 각 시나리오는 판정 로직과 입력 유효성 검사의 다른 분기를 확인합니다. 시나리오를 저장하면 모든 변경 후 전체 스위트를 다시 실행하거나, CI에 연결하여 판정 함수에서 회귀가 발생하면 빌드가 실패하도록 할 수 있습니다. curl로 업로드 엔드포인트를 수동으로 테스트하는 것은 금방 지루해지지만, 저장된 시나리오는 그렇지 않습니다.

강화 및 엣지 케이스

해피 패스는 쉬운 80%입니다. 검증 서비스는 나머지 20%에 따라 성패가 갈립니다. 입력은 본질적으로 적대적이기 때문입니다. 누군가는 이미지를 실제와 다르게 속이려고 합니다.

손상되거나 잘린 파일. 파일은 이미지 MIME 유형을 가질 수 있지만 여전히 쓰레기일 수 있습니다. C2PA 리더는 구문 분석할 수 없는 데이터에 대해 C2paError를 발생시키고, read_provenance 함수는 이미 이를 500 오류 대신 깔끔한 결과로 변환합니다. 만약을 대비하여 Pillow와 같은 라이브러리로 이미지를 디코딩한 다음 처리하고, 디코딩이 실패하면 400으로 거부할 수 있습니다. 이는 이름이 변경된 텍스트 파일 트릭도 차단합니다.

매니페스트 누락. 다루어졌지만, 가장 흔한 경우이고 가장 쉽게 잘못될 수 있으므로 다시 언급할 가치가 있습니다. 매니페스트가 없다는 것은 오류도 아니고 판정도 아닙니다. 라이브러리는 메시지가 ManifestNotFound로 시작하는 C2paError로 이를 신호하며, 서비스는 이를 특별히 포착하여 분류기로 넘어갑니다. 매니페스트 누락이 500 오류나 "가짜" 판정을 생성하게 하지 마십시오.

분류기 타임아웃 또는 중단. 분류기는 타사 네트워크 종속성이므로 가끔 실패할 것이라고 가정하십시오. classify_image 함수는 명시적인 httpx 타임아웃을 사용하며, 모든 타임아웃 또는 HTTP 오류에 대해 available: False를 반환합니다. 판정 함수는 그 다음 출처만으로 대체하거나, 매니페스트도 없을 경우 0의 신뢰도로 uncertain을 반환합니다. 엔드포인트는 여전히 200과 정직한 판정으로 응답합니다. 느린 공급업체가 귀하의 서비스를 중단시킬 수는 없습니다.

변조된 매니페스트. 매니페스트가 존재하지만 유효하지 않을 수 있습니다. 잘못된 인증서로 서명되었거나 해시가 픽셀과 일치하지 않을 수 있습니다. 이것은 사람들이 잊어버리는 경우입니다. 항상 validation_status를 확인하십시오. 빈 배열은 매니페스트가 검증되었음을 의미하고, 채워진 배열은 그렇지 않음을 의미합니다. 판정 함수는 실패한 매니페스트를 증거가 아닌 경고로 취급하여 uncertain으로 분류합니다. 유효성 검사를 거치지 않은 매니페스트를 신뢰하는 것은 매니페스트가 없는 것보다 나쁩니다.

대용량 파일 및 오용. 업로드 크기를 제한하십시오. 예시는 12MB를 사용하며, 그보다 큰 파일은 전체 본문을 메모리로 읽기 전에 413으로 거부할 수 있습니다. 엔드포인트 앞에 속도 제한을 두십시오. 암호화 검증 및 요청당 외부 API 호출은 무료가 아니며, 개방형 검증 엔드포인트는 매력적인 표적입니다.

개인 정보 보호. 사용자 이미지를 수신하고 있습니다. 예시처럼 이미지를 메모리나 즉시 삭제되는 임시 파일에서 처리하고, 이미지 바이트를 기록하지 마십시오. 타사 분류기로 이미지를 보내는 경우, 개인 정보 보호 정책에 명시하고 귀하의 사용 사례에 허용되는지 확인하십시오.

각 신호가 포착하고 놓치는 것

이 표는 염두에 두어야 할 정신적 모델입니다. 서비스가 두 가지를 모두 사용하는 이유입니다.

시나리오 C2PA 출처 신호 분류기 신호
콘텐츠 자격 증명을 작성하는 도구로 만든 AI 이미지 포착: 매니페스트에 생성기 명시 대개 포착: 아티팩트 존재
메타데이터가 제거된 AI 이미지 (스크린샷, 재업로드) 놓침: 읽을 매니페스트 없음 포착: 픽셀로 작동, 메타데이터 불필요
콘텐츠 자격 증명을 서명하는 카메라로 찍은 실제 사진 확인: 유효한 매니페스트, 비 AI 생성기 심한 압축 또는 편집 시 오탐할 수 있음
메타데이터가 전혀 없는 실제 사진 신호 없음: 검증할 것 없음 최고의 추정치만: 확률적이며, 틀릴 수 있음
위조되거나 변조된 매니페스트가 있는 이미지 포착: validation_status가 실패를 표시 포착할 수도 안 할 수도 있음, 픽셀에 따라 다름
분류기가 훈련되지 않은 새로운 생성기 도구가 매니페스트를 작성하는 경우에만 포착 종종 놓침: 훈련 분포를 벗어남
심하게 편집된 실제 사진 (실제 기반 위에 AI 리터칭) 매니페스트가 있는 경우, 편집 이력을 기록 모호함: 부분적으로 합성, 점수가 중간 범위

어떤 행이든 가로로 읽으면 같은 이야기를 볼 수 있습니다. 한 신호가 맹점인 곳에서 다른 신호는 종종 그렇지 않습니다. 출처는 정확하지만 희소하며, 분류기는 보편적이지만 모호합니다. 결합된 판정은 어느 한쪽 열만 있는 것보다 더 신뢰할 수 있으며, 정직한 uncertain 값은 두 신호가 모두 약한 행을 위해 존재합니다.

실제 사용 사례

이 패턴은 학문적이지 않습니다. 직접 적용할 수 있는 몇 가지 사례는 다음과 같습니다.

공통적인 주제는 다음과 같습니다. 불확실성에 대해 정직하게 빠른 자동화된 첫 번째 통과를 원하므로, 사람의 관심이 실제로 필요한 곳으로 향하게 됩니다.

결론

AI 생성 이미지를 잘 탐지하는 것은 하나의 완벽한 테스트를 찾는 것이 아닙니다. 독립적인 신호를 결합하고 신뢰도에 대해 정직해지는 것입니다.

이를 실제로 구축하려면 /verify 스키마를 설계하고, 모의 서버를 생성하고, 엔드포인트 테스트를 한 곳에서 실행하십시오. Apidog를 다운로드하여 API를 구축하면서 설계, 모의 및 테스트한 다음, 기본 URL 변경 한 번으로 모의에서 실제 백엔드로 전환하십시오.

버튼

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요