DEV Community

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

Posted on • Originally published at apidog.com

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

누군가 제품에 사진을 업로드하고 “카메라로 직접 촬영했다”고 주장합니다. 백엔드는 이를 검증할 수 있을까요? 이미지 생성기는 이미 사람 눈으로 구분하기 어려운 결과물을 만들고 있습니다. 따라서 “눈으로 봐서 진짜 같음”은 더 이상 검증 전략이 아닙니다. 대신 암호화된 출처 매니페스트(C2PA)와 머신러닝 분류기라는 두 가지 독립 신호를 결합하면, 어느 한쪽만 사용할 때보다 더 신뢰할 수 있는 판정을 만들 수 있습니다.

지금 Apidog 사용해 보기

이 글에서는 POST /verify 엔드포인트를 가진 FastAPI 서비스를 구현합니다. 이미지를 업로드하면 서비스는 C2PA 매니페스트를 읽고, 호스팅된 AI 이미지 탐지 API를 호출한 뒤, 신뢰도와 원시 신호를 포함한 JSON 판정을 반환합니다. 또한 구현 전에 OpenAPI 계약을 먼저 설계하고 Apidog에서 모의 서버와 테스트를 구성해 프론트엔드가 백엔드 완성 전에도 통합을 시작할 수 있게 합니다.

요약 (TL;DR)

구현할 서비스는 다음 흐름을 따릅니다.

  1. POST /verify에서 이미지 파일을 multipart/form-data로 받습니다.
  2. c2pa-python으로 C2PA 콘텐츠 자격 증명 매니페스트를 추출하고 검증합니다.
  3. Sightengine 같은 호스팅 AI 탐지 API를 호출해 AI 생성 가능성 점수를 받습니다.
  4. 두 신호를 결합해 다음 중 하나를 반환합니다.
    • likely_authentic
    • likely_ai
    • uncertain
  5. 응답에는 confidence, signals.provenance, signals.classifier, explanation을 포함합니다.
  6. OpenAPI 스키마를 먼저 작성하고 Apidog에서 모의 서버와 반복 가능한 테스트 시나리오를 만듭니다.

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

이미지 파일에는 “인간이 만들었다” 또는 “AI가 만들었다”를 알려주는 단일 속성이 없습니다. 대신 서로 다른 성격의 단서가 있습니다.

1. C2PA 출처 신호

C2PA(Content Provenance and Authenticity)는 미디어 파일에 암호화 서명된 메타데이터를 첨부하는 오픈 표준입니다. 이 메타데이터 묶음을 매니페스트라고 하며, 사용자에게는 콘텐츠 자격 증명(Content Credentials)으로 알려져 있습니다.

카메라, 편집기, 이미지 생성기 같은 도구가 C2PA를 지원하면 다음 정보를 기록할 수 있습니다.

  • 어떤 도구가 이미지를 만들었는지
  • 어떤 편집이 있었는지
  • 누가 서명했는지
  • 파일 내용이 서명 이후 변경되었는지

매니페스트가 유효하면 강력한 출처 신호가 됩니다.

하지만 C2PA는 옵트인 방식입니다. 스크린샷, 메신저 재인코딩, 플랫폼 업로드 과정에서 메타데이터가 제거될 수 있습니다. 따라서 매니페스트가 없다는 사실은 이미지가 가짜라는 뜻도, 진짜라는 뜻도 아닙니다.

2. AI 탐지 분류기 신호

분류기는 이미지 픽셀을 분석해 AI 생성 가능성을 0~1 점수로 반환합니다. 메타데이터가 없어도 동작하지만 확률적입니다.

예를 들어 0.92는 “모델이 AI 생성 가능성이 높다고 본다”는 의미이지, “AI 생성이 증명되었다”는 의미가 아닙니다.

따라서 설계 목표는 다음과 같습니다.

C2PA가 암호화로 증명하는 것과 분류기가 통계적으로 추정하는 것을 분리해서 보여주고, 두 신호를 조합해 보수적인 판정을 반환한다.

단일 신호 접근 방식의 실패 모드는 AI 이미지 탐지가 실패하는 이유에서 더 자세히 다룹니다.

아키텍처 개요

서비스는 작게 유지합니다.

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

사용할 스택은 다음과 같습니다.

  • Python 3.10+
  • FastAPI
  • Uvicorn
  • python-multipart
  • httpx
  • c2pa-python

설치합니다.

pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
Enter fullscreen mode Exit fullscreen mode

C2PA 신호 구현

콘텐츠 진위 이니셔티브(Content Authenticity Initiative)는 C2PA 관련 오픈 소스 도구를 제공합니다.

주요 도구는 다음과 같습니다.

  • c2patool: 터미널에서 매니페스트를 확인하거나 추가하는 CLI 도구
  • c2pa-python: Rust 기반 c2pa-rs 라이브러리의 Python 바인딩

서비스에서는 c2pa-python을 사용합니다.

provenance.py

# provenance.py
import json
import c2pa


def read_provenance(image_path: str) -> dict:
    """
    이미지에서 C2PA 매니페스트를 읽고 검증한다.
    결과는 서비스 내부에서 사용하기 쉬운 dict로 정규화한다.
    """
    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": "No C2PA manifest present in this image.",
            }

        # C2PA 데이터가 있지만 파싱 또는 검증에 실패한 경우
        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는 검증 문제가 있을 때 채워진다.
    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.",
    }
Enter fullscreen mode Exit fullscreen mode

핵심 포인트는 다음과 같습니다.

  • Reader는 컨텍스트 매니저로 사용해 리소스를 정리합니다.
  • reader.json()은 전체 매니페스트 저장소를 JSON 문자열로 반환합니다.
  • 더 자세한 보고서가 필요하면 reader.detailed_json()을 사용할 수 있습니다.
  • ManifestNotFound는 실패가 아니라 “출처 신호 없음”이라는 데이터로 처리합니다.
  • validation_status가 비어 있으면 검증 통과, 값이 있으면 검증 실패로 봅니다.

매니페스트가 없을 때는 판정할 수 없습니다. 그래서 두 번째 신호가 필요합니다.

분류기 신호 구현

분류기는 이미지가 AI 생성일 가능성을 점수화합니다. 이 글에서는 Sightengine의 AI 탐지 API를 예시로 사용합니다. 다른 공급업체를 사용해도 패턴은 같습니다.

  • 이미지 바이트를 API에 업로드
  • AI 생성 점수 필드를 읽음
  • 실패하면 예외 대신 available: False를 반환

AI 이미지 탐지 API를 비교하고 있다면 최고의 AI 이미지 탐지 API를 참고할 수 있습니다.

Sightengine 엔드포인트는 다음과 같습니다.

https://api.sightengine.com/1.0/check.json
Enter fullscreen mode Exit fullscreen mode

요청에는 다음 값을 보냅니다.

  • media: 이미지 파일
  • models: genai
  • api_user
  • api_secret

응답의 type.ai_generated 값을 AI 생성 점수로 사용합니다.

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:
    """
    이미지를 호스팅 탐지기에 전송한다.
    성공 시 AI 생성 점수를 반환하고,
    실패 시 available: False와 reason을 반환한다.
    """
    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)}
Enter fullscreen mode Exit fullscreen mode

이 함수는 비동기입니다. 느린 외부 API 호출이 FastAPI 이벤트 루프를 막지 않게 하기 위해서입니다.

운영에서는 다음을 조정하세요.

  • 타임아웃
  • 재시도 여부
  • 공급업체별 에러 매핑
  • API 키 관리 방식
  • 요청당 비용을 고려한 rate limit

점수는 항상 추정치로 다뤄야 합니다. 실제 동작 방식은 AI 생성 이미지 확인 방법에서도 다룹니다.

/verify 계약 설계

이제 구현 전에 API 계약을 정의합니다. 이 단계에서 Apidog를 사용하면 좋습니다.

스키마를 먼저 만들면 다음 이점이 있습니다.

  • 프론트엔드와 백엔드가 같은 계약을 공유합니다.
  • 백엔드 구현 전에도 모의 서버를 만들 수 있습니다.
  • 구현 후 같은 스키마 기반으로 테스트를 실행할 수 있습니다.

요청

POST /verify는 하나의 파일 필드를 받습니다.

POST /verify
Content-Type: multipart/form-data
Enter fullscreen mode Exit fullscreen mode

필드:

이름 타입 설명
image file 검증할 이미지

응답

응답은 최종 판정과 두 원시 신호를 모두 포함해야 합니다.

{
  "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"
}
Enter fullscreen mode Exit fullscreen mode

verdict는 세 가지 값 중 하나입니다.

의미
likely_authentic 진품으로 추정
likely_ai AI 생성으로 추정
uncertain 판단 불가 또는 신호 충돌

uncertain은 중요한 값입니다. 신호가 약하거나 충돌할 때 억지로 이진 분류하지 않기 위해 필요합니다.

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
Enter fullscreen mode Exit fullscreen mode

Apidog의 시각적 디자이너에서 직접 만들거나 기존 OpenAPI 파일을 가져올 수 있습니다. 스펙 우선 워크플로는 스펙 우선 모드 둘러보기에서 더 자세히 볼 수 있습니다.

판정 로직 구현

출처 신호와 분류기 신호를 결합하는 함수가 서비스의 핵심입니다.

정책은 보수적으로 설계합니다.

  • 유효한 C2PA 매니페스트는 강한 신호입니다.
  • 매니페스트가 AI 도구를 명시하면 likely_ai입니다.
  • 매니페스트가 비 AI 도구를 명시하지만 분류기가 강하게 반대하면 uncertain입니다.
  • 유효하지 않은 매니페스트는 uncertain입니다.
  • 매니페스트가 없으면 분류기 점수에 의존합니다.
  • 두 신호가 모두 없으면 uncertain입니다.

verdict.py

# verdict.py


def combine_signals(provenance: dict, classifier: dict) -> dict:
    """출처 신호와 분류기 신호를 하나의 판정으로 결합한다."""
    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(k in generator for k in ai_keywords)

    # 1. 유효한 매니페스트가 AI 생성 도구를 명시하는 경우
    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.",
        )

    # 2. 유효한 매니페스트가 카메라 또는 비 AI 도구를 명시하는 경우
    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.",
        )

    # 3. 매니페스트가 있지만 검증 실패한 경우
    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.",
        )

    # 4. 매니페스트가 없으면 분류기에 의존
    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.",
        )

    # 5. 두 신호 모두 없는 경우
    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,
    }
Enter fullscreen mode Exit fullscreen mode

임계값은 제품 정책에 맞게 조정해야 합니다. 예를 들어 뉴스룸, 보험 청구, 소셜 플랫폼은 서로 다른 위험 허용치를 가질 수 있습니다.

FastAPI 앱 구현

이제 /verify 엔드포인트를 구현합니다.

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="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. 업로드 검증
    if image.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail=(
                f"Unsupported type {image.content_type}. "
                "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. C2PA 출처 신호
    # c2pa.Reader는 파일 경로를 필요로 하므로 임시 파일을 사용한다.
    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. 분류기 신호
    # 분류기 실패는 예외가 아니라 available: False로 처리한다.
    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",
        }

    # 4. 신호 결합
    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(),
        }
    )
Enter fullscreen mode Exit fullscreen mode

로컬에서 실행합니다.

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

엔드포인트는 다음 주소에서 사용할 수 있습니다.

http://127.0.0.1:8000/verify
Enter fullscreen mode Exit fullscreen mode

환경 변수도 설정합니다.

export SIGHTENGINE_API_USER="your_api_user"
export SIGHTENGINE_API_SECRET="your_api_secret"
Enter fullscreen mode Exit fullscreen mode

이 백엔드는 작지만 API 제품의 핵심 패턴을 따릅니다. 명확한 계약을 가진 작은 서비스가 핵심 기능을 노출합니다. 이 관점은 헤드리스(headless)화되는 소프트웨어에서도 다룹니다.

Apidog로 모의 서버 만들기

프론트엔드는 업로드 UI와 결과 패널을 바로 만들고 싶지만, 실제 백엔드는 API 키 발급, 구현, 배포가 필요할 수 있습니다. 이때 스키마 기반 모의 서버가 유용합니다.

작업 흐름은 다음과 같습니다.

  1. OpenAPI 스키마를 Apidog에 가져옵니다.
  2. /verify 엔드포인트를 생성합니다.
  3. 요청 본문을 multipart/form-data로 설정합니다.
  4. image 필드를 파일 타입으로 정의합니다.
  5. 위의 VerifyResponse 스키마를 응답으로 연결합니다.
  6. Apidog 모의 서버를 생성합니다.
  7. 프론트엔드는 실제 백엔드 대신 모의 서버 URL을 호출합니다.

모의 응답 예시는 최소한 다음 케이스를 준비하세요.

  • 유효한 카메라 매니페스트가 있는 likely_authentic
  • AI 도구가 명시된 매니페스트가 있는 likely_ai
  • 분류기를 사용할 수 없는 uncertain
  • 지원하지 않는 파일 형식에 대한 415
  • 너무 큰 파일에 대한 413

이렇게 하면 백엔드 구현 전에도 UI 상태를 모두 만들 수 있습니다.

Apidog에서 엔드포인트 테스트하기

백엔드가 실행되면 Apidog에서 실제 요청을 만듭니다.

  1. 메서드: POST
  2. URL: http://127.0.0.1:8000/verify
  3. Body: form-data
  4. 필드 이름: image
  5. 필드 타입: File
  6. 테스트 이미지 선택
  7. 요청 전송

그다음 반복 가능한 테스트가 되도록 어설션을 추가합니다.

예시 어설션:

  • 응답 상태가 200인지 확인
  • verdict가 존재하는지 확인
  • verdictlikely_authentic, likely_ai, uncertain 중 하나인지 확인
  • confidence0 이상 1 이하인지 확인
  • signals.provenance.has_manifest가 boolean인지 확인
  • signals.classifier.available이 boolean인지 확인

추가로 다음 시나리오를 저장해두면 좋습니다.

  • C2PA 매니페스트가 있는 이미지
  • 매니페스트가 없는 일반 JPEG
  • 너무 큰 파일
  • .jpg로 이름만 바꾼 비이미지 파일
  • 분류기 API 키가 설정되지 않은 환경
  • 분류기 타임아웃 상황

수동 curl 테스트는 금방 번거로워집니다. 저장된 Apidog 시나리오는 변경 후 반복 실행하거나 CI에 연결하기 쉽습니다.

강화 및 엣지 케이스

이미지 검증 엔드포인트는 적대적 입력을 받는다고 가정해야 합니다.

손상되거나 잘린 파일

MIME 타입은 이미지처럼 보이지만 실제 내용은 깨져 있을 수 있습니다.

현재 예시는 C2PA 리더의 오류를 처리하지만, 운영에서는 Pillow 같은 라이브러리로 실제 이미지 디코딩까지 검증하는 것이 좋습니다.

예시:

from PIL import Image
from io import BytesIO

def validate_image_bytes(image_bytes: bytes) -> None:
    try:
        with Image.open(BytesIO(image_bytes)) as img:
            img.verify()
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid image file.")
Enter fullscreen mode Exit fullscreen mode

이 검사를 추가하려면 Pillow를 설치합니다.

pip install pillow
Enter fullscreen mode Exit fullscreen mode

매니페스트 누락

가장 흔한 케이스입니다.

매니페스트가 없다는 것은 다음을 의미하지 않습니다.

  • AI 생성이라는 뜻이 아님
  • 진짜 사진이라는 뜻도 아님
  • 서버 오류도 아님

그냥 “출처 신호 없음”입니다. 이 경우 분류기 신호로 넘어가야 합니다.

분류기 타임아웃

분류기는 외부 네트워크 의존성입니다. 실패할 수 있다고 가정해야 합니다.

classify_image()는 타임아웃이나 HTTP 오류를 예외로 전파하지 않고 다음 형태로 반환합니다.

{
  "available": false,
  "reason": "classifier_timeout"
}
Enter fullscreen mode Exit fullscreen mode

판정 함수는 이 상태를 읽고 가능한 경우 C2PA 신호만으로 판정하거나, 그것도 없으면 uncertain을 반환합니다.

변조된 매니페스트

매니페스트가 존재해도 유효하지 않을 수 있습니다.

예를 들어:

  • 서명이 잘못됨
  • 인증서 체인이 신뢰되지 않음
  • 이미지 해시가 매니페스트와 일치하지 않음

이때 validation_status가 채워집니다. 유효성 검증에 실패한 매니페스트는 신뢰하면 안 됩니다. 위 예시는 이를 uncertain으로 처리합니다.

대용량 파일과 남용

업로드 크기를 제한해야 합니다. 예시는 12MB를 사용합니다.

운영에서는 추가로 다음을 고려하세요.

  • API gateway 또는 reverse proxy에서 body size 제한
  • 사용자 또는 IP 기반 rate limit
  • 인증 필요 여부
  • 외부 분류기 호출 비용 제한
  • 요청 큐 또는 비동기 처리

개인 정보 보호

이미지는 민감한 데이터일 수 있습니다.

기본 원칙은 다음과 같습니다.

  • 이미지 바이트를 로그에 남기지 않습니다.
  • 임시 파일은 즉시 삭제합니다.
  • 필요하지 않으면 원본 이미지를 저장하지 않습니다.
  • 타사 분류기에 이미지를 전송한다는 사실을 개인정보 처리방침에 명시합니다.
  • 규제 산업에서는 외부 전송이 허용되는지 확인합니다.

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

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

핵심은 간단합니다.

  • C2PA는 정확하지만 항상 존재하지 않습니다.
  • 분류기는 거의 항상 적용할 수 있지만 확률적입니다.
  • 두 신호가 약하거나 충돌하면 uncertain이 올바른 답입니다.

실제 사용 사례

이 패턴은 다음 제품에 바로 적용할 수 있습니다.

사용자 생성 콘텐츠 플랫폼

마켓플레이스나 소셜 앱은 업로드된 이미지를 /verify에 전달하고, likely_ai 또는 검증 실패 매니페스트가 있는 이미지를 검토 대기열로 보낼 수 있습니다.

판정 값은 정책에 쉽게 매핑됩니다.

verdict 처리 예시
likely_authentic 허용
likely_ai 라벨링 또는 검토
uncertain 사람 검토

뉴스룸 및 팩트 체크

편집자는 바이럴 이미지에 대해 다음을 한 번에 얻을 수 있습니다.

  • C2PA 출처 정보
  • AI 탐지 점수
  • 사람이 읽을 수 있는 설명 문장
  • 감사 가능한 원시 신호

보험 및 청구 접수

고객이 제출한 사진 증거에 대해 생성 이미지 가능성이나 변조된 매니페스트를 사전에 플래그할 수 있습니다.

내부 자산 파이프라인

스톡 이미지, 캠페인 이미지, 제품 이미지 파이프라인에서 AI 생성 이미지를 자동으로 라벨링하거나 차단할 수 있습니다.

출처 인식 퍼블리싱

CMS는 업로드 시 매니페스트를 읽고 검증된 출처 배지를 표시할 수 있습니다. 매니페스트가 없으면 분류기 결과를 보조 신호로 사용할 수 있습니다.

결론

AI 생성 이미지 탐지는 완벽한 단일 테스트를 찾는 문제가 아닙니다. 독립적인 신호를 결합하고, 불확실성을 명시적으로 다루는 문제입니다.

구현 요약은 다음과 같습니다.

  • C2PA 콘텐츠 자격 증명은 암호화로 검증 가능한 강한 출처 신호입니다.
  • 하지만 매니페스트는 옵트인이고 쉽게 제거될 수 있습니다.
  • 호스팅 분류기는 메타데이터 없이도 동작하지만 확률적입니다.
  • FastAPI 서비스에서 두 신호를 결합하면 likely_authentic, likely_ai, uncertain 판정을 반환할 수 있습니다.
  • OpenAPI 계약을 먼저 설계하면 Apidog에서 모의 서버와 테스트를 먼저 만들 수 있습니다.
  • uncertain은 실패가 아니라 정직한 결과입니다.

실제로 시작하려면 /verify 스키마를 먼저 정의하고, Apidog에서 모의 서버를 만든 뒤, 백엔드 구현 후 같은 계약으로 엔드포인트 테스트를 실행하세요. 프론트엔드는 모의 URL로 먼저 통합하고, 배포 후 기본 URL만 실제 백엔드로 바꾸면 됩니다.

Top comments (0)