惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Hacker News: Front Page
博客园 - 【当耐特】
量子位
博客园 - 聂微东
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The Register - Security
The Register - Security
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tailwind CSS Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
雷峰网
雷峰网
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
Jina AI
Jina AI
The Cloudflare Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
AWS News Blog
AWS News Blog
L
LINUX DO - 最新话题
酷 壳 – CoolShell
酷 壳 – CoolShell
The GitHub Blog
The GitHub Blog
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security @ Cisco Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Stack Overflow Blog
Stack Overflow Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Engineering at Meta
Engineering at Meta
W
WeLiveSecurity
博客园 - 三生石上(FineUI控件)
Security Archives - TechRepublic
Security Archives - TechRepublic
Hugging Face - Blog
Hugging Face - Blog
T
Troy Hunt's Blog
C
CERT Recently Published Vulnerability Notes
N
News and Events Feed by Topic
S
SegmentFault 最新的问题
美团技术团队
C
CXSECURITY Database RSS Feed - CXSecurity.com
人人都是产品经理
人人都是产品经理
SecWiki News
SecWiki News
N
News and Events Feed by Topic
C
Check Point Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
宝玉的分享
宝玉的分享
Schneier on Security
Schneier on Security
阮一峰的网络日志
阮一峰的网络日志
Cisco Talos Blog
Cisco Talos Blog
T
Threat Research - Cisco Blogs
J
Java Code Geeks

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Bangun API Pendeteksi Gambar AI dengan C2PA + Klasifikasi
Walse · 2026-05-21 · via DEV Community

Seseorang mengunggah foto ke produk Anda dan mengklaim foto itu diambil dengan kamera. Backend Anda tidak bisa lagi hanya “percaya pada mata”, karena generator gambar sekarang dapat menghasilkan gambar yang tampak nyata. Pendekatan yang lebih aman adalah menggabungkan dua sinyal independen: manifes asal-usul kriptografis dari C2PA dan skor pengklasifikasi deteksi gambar AI.

Coba Apidog hari ini

Dalam tutorial ini, kita akan membangun layanan FastAPI dengan endpoint POST /verify. Endpoint ini menerima gambar, membaca Kredensial Konten C2PA jika tersedia, memanggil API deteksi gambar AI sebagai sinyal kedua, lalu mengembalikan putusan JSON. Karena ini proyek API, kita juga akan mendesain kontrak OpenAPI terlebih dahulu dan mengujinya dengan Apidog.

TL;DR

Anda akan membuat layanan FastAPI yang:

  1. menerima unggahan gambar lewat POST /verify,
  2. membaca dan memvalidasi manifes C2PA dengan c2pa-python,
  3. memanggil pengklasifikasi deteksi AI yang di-host,
  4. menggabungkan dua sinyal menjadi salah satu putusan:
    • likely_authentic
    • likely_ai
    • uncertain
  5. mengembalikan skor kepercayaan dan detail sinyal mentah,
  6. mendesain kontrak OpenAPI dan menjalankan mock/test endpoint dengan Apidog.

Mengapa Menggunakan Dua Sinyal?

Tidak ada satu properti file yang bisa membuktikan “gambar ini dibuat manusia” atau “gambar ini dibuat AI”. Yang tersedia adalah sinyal.

1. Sinyal asal-usul C2PA

C2PA adalah standar terbuka untuk menyertakan metadata yang ditandatangani secara kriptografis pada file media. Metadata ini disebut manifes, dan nama yang biasa dilihat pengguna adalah Kredensial Konten.

Jika kamera, editor, atau generator gambar mendukung C2PA, alat tersebut dapat menulis riwayat pembuatan atau pengeditan gambar dan menandatanganinya dengan sertifikat.

Kelemahannya:

  • C2PA bersifat opt-in.
  • Screenshot biasanya menghapus metadata.
  • Aplikasi pesan atau platform upload sering menghapus metadata.
  • Tidak adanya manifes bukan berarti gambar palsu atau asli.

2. Sinyal pengklasifikasi AI

Pengklasifikasi deteksi AI melihat piksel gambar dan mengembalikan kemungkinan bahwa gambar tersebut dihasilkan AI.

Kelemahannya:

  • hasilnya probabilistik,
  • bisa false positive,
  • bisa false negative,
  • akurasi dapat turun pada gambar yang dikompresi berat atau berasal dari generator baru.

Jadi strategi yang lebih jujur adalah:

“Inilah yang bisa dibuktikan secara kriptografis, inilah perkiraan model, dan inilah tingkat keyakinan gabungannya.”

Jika Anda ingin memahami mode kegagalan pendekatan satu sinyal, baca juga artikel tentang mengapa deteksi gambar AI gagal.

Arsitektur Layanan

Layanan ini hanya memiliki satu endpoint dan dua sinyal downstream.

                ┌─────────────────────────────┐
   gambar ──▶   │   FastAPI POST /verify       │
                │                              │
                │   1. Validasi unggahan       │
                │   2. Baca manifes C2PA       │
                │   3. Panggil classifier AI   │
                │   4. Gabungkan putusan       │
                └─────────────────────────────┘
                              │
                              ▼
                   JSON verdict + confidence

Enter fullscreen mode Exit fullscreen mode

Stack yang digunakan:

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

Instal dependensi:

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

Enter fullscreen mode Exit fullscreen mode

Membaca Sinyal C2PA

Pustaka c2pa-python adalah binding Python untuk pustaka Rust c2pa-rs. Kita akan menggunakannya untuk membaca manifes C2PA dari file gambar.

Buat file provenance.py:

# provenance.py
import json
import c2pa


def read_provenance(image_path: str) -> dict:
    """
    Baca dan validasi manifes C2PA dari gambar.
    Mengembalikan dict yang dinormalisasi.
    """
    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": "Tidak ada manifes C2PA di gambar ini.",
            }

        return {
            "has_manifest": True,
            "validation": "error",
            "detail": f"Tidak dapat mengurai manifes: {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": "Manifes berhasil dibaca.",
    }

Enter fullscreen mode Exit fullscreen mode

Hal penting:

  • ManifestNotFound adalah kondisi normal.
  • Manifes hilang tidak boleh dianggap error.
  • validation_status kosong berarti tanda tangan dan hash valid.
  • validation_status berisi data berarti manifes gagal validasi.
  • claim_generator dapat membantu mengidentifikasi apakah alat pembuatnya kamera, editor, atau generator AI.

Memanggil Pengklasifikasi Deteksi AI

Untuk sinyal kedua, gunakan API deteksi AI yang di-host. Contoh ini memakai Sightengine karena endpoint dan responsnya terdokumentasi dengan jelas. Polanya sama untuk vendor lain: ganti URL, parameter, dan field respons yang dibaca.

Endpoint Sightengine:

https://api.sightengine.com/1.0/check.json

Enter fullscreen mode Exit fullscreen mode

Buat file 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:
    """
    Kirim gambar ke detektor AI yang di-host.
    Mengembalikan skor AI-generated yang dinormalisasi.
    """
    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

Prinsip implementasinya:

  • Gunakan timeout eksplisit.
  • Jangan biarkan kegagalan vendor menjatuhkan endpoint.
  • Kembalikan available: false jika classifier timeout atau error.
  • Perlakukan skor sebagai probabilitas, bukan fakta.

Untuk perbandingan vendor, lihat daftar API deteksi gambar AI terbaik. Untuk pendekatan manual dan teknis lainnya, lihat panduan cara memeriksa apakah gambar dihasilkan oleh AI.

Mendesain Kontrak POST /verify

Sebelum menulis route FastAPI, desain kontrak endpoint. Ini membuat frontend bisa mulai integrasi lewat mock server sebelum backend selesai.

Dengan Apidog, Anda bisa:

  • mendesain endpoint secara visual,
  • mengimpor/menulis OpenAPI,
  • membuat mock server,
  • menyimpan skenario test endpoint,
  • menjalankan test terhadap backend asli.

Request

Endpoint menerima multipart/form-data dengan satu field:

image: file

Enter fullscreen mode Exit fullscreen mode

Response

Contoh response:

{
  "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": "Manifes C2PA yang valid menyebutkan alat gambar AI, dan pengklasifikasi menilai gambar tersebut kemungkinan dihasilkan AI.",
  "checked_at": "2026-05-21T09:30:00Z"
}

Enter fullscreen mode Exit fullscreen mode

Nilai verdict hanya boleh salah satu dari:

likely_authentic
likely_ai
uncertain

Enter fullscreen mode Exit fullscreen mode

Gunakan uncertain ketika sinyal lemah, hilang, atau bertentangan.

Skema OpenAPI

Tambahkan komponen response berikut ke spesifikasi OpenAPI Anda:

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

Jika Anda ingin menerapkan workflow spec-first, lihat panduan mode spec-first di Apidog.

Menggabungkan Dua Sinyal

Buat file verdict.py.

Logika berikut konservatif:

  • manifes C2PA valid lebih kuat daripada classifier,
  • manifes gagal validasi menghasilkan uncertain,
  • classifier digunakan saat manifes tidak tersedia,
  • konflik sinyal menghasilkan uncertain.
# verdict.py

def combine_signals(provenance: dict, classifier: dict) -> dict:
    """
    Gabungkan sinyal asal-usul dan classifier menjadi satu putusan.
    """
    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. Manifes valid menyebut alat AI.
    if has_manifest and validation == "valid" and generator_looks_ai:
        return _verdict(
            "likely_ai",
            0.95,
            "Manifes C2PA yang valid menyebutkan alat gambar AI.",
        )

    # 2. Manifes valid dari alat non-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,
                "Manifes terlihat otentik tetapi pengklasifikasi tidak setuju; sinyal bertentangan.",
            )

        return _verdict(
            "likely_authentic",
            0.9,
            "Manifes C2PA yang valid dari alat non-AI tersedia.",
        )

    # 3. Manifes ada tetapi gagal validasi.
    if has_manifest and validation in ("invalid", "error"):
        return _verdict(
            "uncertain",
            0.6,
            "Gambar membawa manifes C2PA yang gagal divalidasi.",
        )

    # 4. Tidak ada manifes, gunakan classifier.
    if classifier_ok and ai_score is not None:
        if ai_score >= 0.7:
            return _verdict(
                "likely_ai",
                round(ai_score, 2),
                "Tidak ada data asal-usul; pengklasifikasi menilai gambar kemungkinan dihasilkan AI.",
            )

        if ai_score <= 0.3:
            return _verdict(
                "likely_authentic",
                round(1 - ai_score, 2),
                "Tidak ada data asal-usul; pengklasifikasi menilai gambar kemungkinan otentik.",
            )

        return _verdict(
            "uncertain",
            0.5,
            "Tidak ada data asal-usul dan skor pengklasifikasi tidak meyakinkan.",
        )

    # 5. Tidak ada manifes dan classifier tidak tersedia.
    return _verdict(
        "uncertain",
        0.0,
        "Tidak ada data asal-usul dan pengklasifikasi tidak tersedia.",
    )


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

Enter fullscreen mode Exit fullscreen mode

Ambang batas 0.7, 0.3, dan 0.85 bukan angka universal. Sesuaikan berdasarkan toleransi risiko produk Anda.

Membuat Aplikasi FastAPI

Buat file 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 Detektor Gambar AI",
    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. Validasi content type.
    if image.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail=(
                f"Tipe {image.content_type} tidak didukung. "
                "Kirim JPEG, PNG, atau WebP."
            ),
        )

    # 2. Baca byte gambar.
    image_bytes = await image.read()

    if len(image_bytes) == 0:
        raise HTTPException(
            status_code=400,
            detail="File kosong.",
        )

    if len(image_bytes) > MAX_BYTES:
        raise HTTPException(
            status_code=413,
            detail="File melebihi batas 12 MB.",
        )

    # 3. Baca C2PA.
    # c2pa.Reader membutuhkan path file, jadi gunakan file sementara.
    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. Panggil classifier jika credential tersedia.
    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. Gabungkan sinyal.
    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(),
        }
    )

Enter fullscreen mode Exit fullscreen mode

Jalankan server lokal:

uvicorn main:app --reload

Enter fullscreen mode Exit fullscreen mode

Endpoint aktif di:

http://127.0.0.1:8000/verify

Enter fullscreen mode Exit fullscreen mode

Contoh request dengan curl:

curl -X POST http://127.0.0.1:8000/verify \
  -F "image=@sample.jpg"

Enter fullscreen mode Exit fullscreen mode

Jika Anda menggunakan Sightengine, set environment variable terlebih dahulu:

export SIGHTENGINE_API_USER="your_user"
export SIGHTENGINE_API_SECRET="your_secret"

Enter fullscreen mode Exit fullscreen mode

Desain seperti ini cocok untuk layanan API kecil yang fokus pada satu kapabilitas. Untuk konteks lebih luas tentang pola produk berbasis API, baca artikel tentang software yang menjadi headless.

Mocking dan Testing dengan Apidog

Frontend tidak perlu menunggu backend selesai. Setelah kontrak OpenAPI tersedia, Anda bisa membuat mock server di Apidog.

1. Buat mock server

Di Apidog:

  1. Buat project baru.
  2. Tambahkan endpoint POST /verify.
  3. Set body sebagai multipart/form-data.
  4. Tambahkan field image bertipe file.
  5. Tambahkan response schema VerifyResponse.
  6. Buat contoh response untuk beberapa skenario.

Contoh skenario mock yang sebaiknya dibuat:

  • likely_authentic dengan manifes kamera valid,
  • likely_ai dengan manifes dari alat AI,
  • uncertain ketika classifier tidak tersedia,
  • error 415 untuk tipe file tidak didukung,
  • error 413 untuk file terlalu besar.

Frontend dapat mengarahkan fetch ke URL mock Apidog. Saat backend asli siap, ganti base URL saja.

2. Jalankan test endpoint

Setelah backend lokal berjalan:

  1. Buat request POST /verify di Apidog.
  2. Set URL ke http://127.0.0.1:8000/verify.
  3. Di Body, pilih form-data.
  4. Tambahkan field image.
  5. Set tipe field ke File.
  6. Pilih gambar uji.
  7. Kirim request.

Tambahkan assertion:

  • status response adalah 200,
  • verdict ada,
  • verdict adalah salah satu dari likely_authentic, likely_ai, uncertain,
  • confidence angka antara 0 dan 1,
  • signals.provenance.has_manifest bertipe boolean,
  • signals.classifier.available bertipe boolean.

Buat test suite kecil dengan beberapa file:

  • gambar dengan Kredensial Konten,
  • JPEG biasa tanpa manifes,
  • file terlalu besar,
  • file non-gambar yang diganti ekstensi menjadi .jpg,
  • gambar yang memicu timeout/mock classifier unavailable.

Dengan cara ini, perubahan pada fungsi putusan bisa langsung diuji ulang.

Penguatan Implementasi

Endpoint verifikasi menerima input yang bisa bersifat adversarial. Tambahkan perlindungan berikut sebelum production.

Validasi file sebenarnya

Content-Type bisa dipalsukan. Untuk validasi lebih kuat, decode gambar dengan Pillow:

pip install pillow

Enter fullscreen mode Exit fullscreen mode

Contoh validasi:

from io import BytesIO
from PIL import Image, UnidentifiedImageError


def validate_image_bytes(image_bytes: bytes) -> None:
    try:
        with Image.open(BytesIO(image_bytes)) as img:
            img.verify()
    except UnidentifiedImageError:
        raise ValueError("File bukan gambar valid.")

Enter fullscreen mode Exit fullscreen mode

Lalu panggil sebelum membaca C2PA atau classifier.

Jangan anggap manifes hilang sebagai error

Kasus paling umum adalah gambar tanpa manifes. Ini bukan 500, bukan bukti palsu, dan bukan bukti asli.

Respons yang benar biasanya tetap 200 dengan sinyal:

{
  "has_manifest": false,
  "validation": "none"
}

Enter fullscreen mode Exit fullscreen mode

Tangani timeout classifier

Classifier adalah dependency jaringan. Gunakan timeout pendek dan perlakukan kegagalan sebagai sinyal tidak tersedia.

{
  "available": false,
  "reason": "classifier_timeout"
}

Enter fullscreen mode Exit fullscreen mode

Jangan biarkan vendor yang lambat menjatuhkan endpoint Anda.

Waspadai manifes palsu atau rusak

Manifes yang ada belum tentu valid. Selalu cek validation_status.

  • kosong: valid,
  • berisi data: gagal validasi.

Manifes gagal validasi harus menghasilkan uncertain, bukan likely_authentic.

Batasi ukuran file

Contoh kode memakai batas 12 MB. Untuk production:

  • batasi ukuran request di reverse proxy,
  • batasi ukuran di aplikasi,
  • gunakan rate limiting,
  • logging minimal,
  • hindari menyimpan gambar pengguna lebih lama dari yang dibutuhkan.

Perhatikan privasi

Anda menerima gambar pengguna dan mungkin mengirimkannya ke vendor pihak ketiga. Pastikan:

  • tidak mencatat byte gambar,
  • file sementara dihapus,
  • kebijakan privasi menjelaskan pemrosesan pihak ketiga,
  • penggunaan vendor sesuai dengan kebutuhan produk Anda.

Apa yang Ditangkap dan Dilewatkan Tiap Sinyal

Skenario Sinyal asal-usul C2PA Sinyal pengklasifikasi
Gambar AI dari alat yang menulis Kredensial Konten Menangkapnya: manifes menyebutkan generator Biasanya menangkapnya: artefak visual hadir
Gambar AI dengan metadata dihapus Melewatkannya: tidak ada manifes Menangkapnya: bekerja pada piksel
Foto asli dari kamera yang menandatangani Kredensial Konten Memverifikasi: manifes valid dan generator non-AI Bisa false positive pada kompresi/edit berat
Foto asli tanpa metadata Tidak ada sinyal Hanya estimasi probabilistik
Gambar dengan manifes palsu atau dirusak Menangkapnya lewat validation_status Mungkin menangkap, mungkin tidak
Generator baru yang belum ada di data training Menangkap hanya jika alat menulis manifes Bisa melewatkan karena out-of-distribution
Foto asli dengan retouch AI Jika ada, manifes mencatat riwayat edit Ambigu; skor bisa berada di tengah

Kesimpulannya: C2PA kuat tetapi tidak selalu ada. Classifier selalu bisa dijalankan pada piksel, tetapi hasilnya tidak pasti. Gabungan keduanya lebih berguna daripada salah satu saja.

Kasus Penggunaan

Pola POST /verify ini cocok untuk:

  • Platform konten buatan pengguna

    Tandai gambar yang kemungkinan AI atau memiliki manifes gagal validasi.

  • Ruang berita dan fact-checking

    Berikan editor sinyal asal-usul dan skor classifier dalam satu respons.

  • Asuransi dan klaim

    Flag bukti foto yang terlihat sintetis atau memiliki metadata rusak.

  • Pipeline aset internal

    Cegah gambar AI masuk ke library tanpa label.

  • CMS sadar provenance

    Tampilkan badge terverifikasi ketika Kredensial Konten valid tersedia.

Kesimpulan

Deteksi gambar AI yang baik bukan tentang menemukan satu tes sempurna. Yang lebih realistis adalah menggabungkan sinyal independen dan menyatakan ketidakpastian secara eksplisit.

Ringkasnya:

  • C2PA memberi sinyal kriptografis yang kuat, tetapi sering tidak tersedia.
  • Classifier memberi sinyal universal, tetapi probabilistik.
  • FastAPI cukup untuk membangun layanan POST /verify kecil dan fokus.
  • Putusan tiga nilai lebih jujur daripada boolean.
  • OpenAPI + Apidog membantu frontend dan backend bekerja paralel lewat mock server dan test suite.

Langkah berikutnya: desain skema /verify, buat mock server di Apidog, jalankan test endpoint, lalu ganti mock URL ke backend asli saat implementasi siap.