สร้าง API ตรวจจับภาพ AI ด้วย C2PA และตัวจำแนกประเภท

Ashley Innocent

Ashley Innocent

21 May 2026

สร้าง API ตรวจจับภาพ AI ด้วย C2PA และตัวจำแนกประเภท

Apidog สำหรับองค์กร

ติดตั้งภายในองค์กร

SSO & RBAC

รองรับ SOC 2

สำรวจ Apidog Enterprise

มีคนอัปโหลดรูปภาพลงในผลิตภัณฑ์ของคุณแล้วอ้างว่ารูปนั้นถ่ายจากกล้อง แบ็กเอนด์ของคุณสามารถพิสูจน์หรือหักล้างเรื่องนั้นได้หรือไม่? ตอนนี้โปรแกรมสร้างภาพสามารถสร้างผลลัพธ์ที่ดูสมจริงสำหรับผู้ตรวจสอบที่เป็นมนุษย์ได้แล้ว ดังนั้นการ "เชื่อสายตา" จึงใช้ไม่ได้ผลมานานแล้ว ข่าวดีก็คือคุณไม่จำเป็นต้องฝึกโมเดลของคุณเองเพื่อสร้างคำตอบที่เป็นประโยชน์ คุณสามารถรวมสัญญาณอิสระสองอย่างเข้าด้วยกัน ได้แก่ รายการระบุแหล่งที่มาทางวิทยาการเข้ารหัส และตัวแยกประเภทการเรียนรู้ของเครื่อง ให้เป็นคำตัดสินเดียวที่น่าเชื่อถือกว่าสัญญาณใดสัญญาณหนึ่งเพียงอย่างเดียว

บทช่วยสอนนี้จะแนะนำการสร้างแบ็กเอนด์ดังกล่าวให้เป็นบริการเดียวที่มีเอนด์พอยต์ POST /verify คุณจะส่งรูปภาพไปให้ แล้วมันจะส่งคำตัดสินในรูปแบบ JSON กลับมาพร้อมคะแนนความเชื่อมั่นและรายละเอียดแหล่งที่มาที่พบ เราจะใช้ Python และ FastAPI สำหรับเซิร์ฟเวอร์, เครื่องมือ C2PA แบบโอเพนซอร์สสำหรับสัญญาณแหล่งที่มา, และ API ตรวจจับแบบโฮสต์สำหรับสัญญาณตัวแยกประเภท เนื่องจากนี่เป็นโปรเจกต์ API เราจะออกแบบสัญญาเอนด์พอยต์ก่อนและใช้ Apidog เพื่อจำลองและทดสอบมัน เพื่อให้ทีมฟรอนต์เอนด์ของคุณสามารถเริ่มรวมระบบได้ก่อนที่โค้ดแบ็กเอนด์จะเสร็จสิ้น

สรุป (TL;DR)

คุณจะสร้างบริการ FastAPI ที่เปิดเผย POST /verify ซึ่งรับการอัปโหลดรูปภาพ, แยกและตรวจสอบ manifest Content Credentials ของ C2PA ด้วยไลบรารี c2pa-python, เรียกใช้ตัวแยกประเภทการตรวจจับ AI แบบโฮสต์เป็นสัญญาณอิสระที่สอง, และส่งคืนคำตัดสินในรูปแบบ JSON เดียว (likely_authentic, likely_ai, หรือ uncertain) พร้อมคะแนนความเชื่อมั่นและรายละเอียดแหล่งที่มาดิบ นอกจากนี้ คุณยังจะออกแบบสคีมา OpenAPI สำหรับเอนด์พอยต์และใช้ Apidog เพื่อสร้างเซิร์ฟเวอร์จำลองและรันการทดสอบเอนด์พอยต์กับมัน

button

ทำไมต้องสองสัญญาณแทนที่จะเป็นหนึ่งเดียว

ก่อนที่จะเขียนโค้ดใดๆ สิ่งสำคัญคือต้องเข้าใจอย่างชัดเจนว่าคุณกำลังตรวจจับอะไรอยู่ ไม่มีคุณสมบัติเดียวของไฟล์ที่บอกคุณว่า "มนุษย์สร้างสิ่งนี้" หรือ "AI สร้างสิ่งนี้" แต่มีเบาะแส และเบาะแสแต่ละอย่างจะจับภาพประเภทที่แตกต่างกันในขณะที่พลาดประเภทอื่นๆ

เบาะแสแรกคือแหล่งที่มา (provenance) C2PA (Coalition for Content Provenance and Authenticity) เป็นมาตรฐานเปิดที่แนบข้อมูลเมตาที่ป้องกันการปลอมแปลงและลงนามด้วยวิทยาการเข้ารหัสกับไฟล์สื่อ ข้อมูลเมตาที่รวมกันนี้เรียกว่า manifest และชื่อที่ผู้ใช้รู้จักคือ Content Credentials เมื่อเครื่องมือที่เข้าร่วม เช่น กล้อง, โปรแกรมแก้ไข, หรือโปรแกรมสร้างภาพ สร้างหรือเปลี่ยนแปลงภาพ มันสามารถเขียน manifest ที่บันทึกสิ่งที่เกิดขึ้นและลงนามด้วยใบรับรอง หากคุณสามารถอ่านและตรวจสอบ manifest นั้น คุณจะได้รับคำยืนยันที่แข็งแกร่งและตรวจสอบได้เกี่ยวกับประวัติของภาพ

ข้อเสียคือ C2PA เป็นแบบเลือกเข้าร่วม (opt-in) และ manifest นั้นเปราะบาง การจับภาพหน้าจอจะลบออก การเข้ารหัสใหม่ผ่านแอปส่งข้อความจะลบออก แพลตฟอร์มหลายแห่งจะลบข้อมูลเมตาเมื่ออัปโหลด ดังนั้นการไม่มี manifest จึงแทบไม่ได้บอกอะไรคุณเลย ไม่ได้หมายความว่าภาพนั้นเป็นของปลอม และไม่ได้หมายความว่าภาพนั้นเป็นของจริง

เบาะแสที่สองคือตัวแยกประเภททางสถิติ (statistical classifier) โมเดลการตรวจจับได้รับการฝึกฝนด้วยรูปภาพจริงและรูปภาพที่สร้างขึ้นนับล้านภาพ และเรียนรู้สิ่งผิดปกติทางสายตาที่โปรแกรมสร้างภาพมักจะทิ้งไว้เบื้องหลัง มันทำงานกับรูปภาพใดๆ ไม่ว่าจะมีข้อมูลเมตาหรือไม่ก็ตาม แต่มันเป็นแบบความน่าจะเป็น มันส่งคืนความน่าจะเป็น ไม่ใช่ข้อเท็จจริง และอาจผิดพลาดได้ โดยเฉพาะอย่างยิ่งกับรูปภาพที่อยู่นอกการกระจายการฝึกอบรมหรือรูปภาพที่ถูกบีบอัดอย่างมาก

สัญญาณใดสัญญาณหนึ่งไม่เพียงพอต่อการตัดสิน Provenance นั้นแม่นยำแต่แทบจะไม่มีอยู่จริง ตัวแยกประเภทสามารถใช้งานได้เสมอแต่ไม่เคยแน่นอน เมื่อรวมเข้าด้วยกัน คุณจะได้คำตัดสินที่พูดได้ว่า "นี่คือสิ่งที่วิทยาการเข้ารหัสพิสูจน์ นี่คือสิ่งที่โมเดลประมาณการ และนี่คือความมั่นใจที่การรวมกันทำให้เรา" นั่นคือเป้าหมายการออกแบบ หากคุณต้องการเจาะลึกว่าทำไมวิธีการแบบสัญญาณเดียวจึงล้มเหลว บทความของเราเกี่ยวกับ ทำไมการตรวจจับภาพ AI จึงล้มเหลว ครอบคลุมรูปแบบความล้มเหลวโดยละเอียด

ภาพรวมสถาปัตยกรรม

บริการนี้มีขนาดเล็กโดยตั้งใจ เอนด์พอยต์เดียว การเรียกใช้งานดาวน์สตรีมสองรายการ การตอบสนองที่รวมกันหนึ่งรายการ

                ┌─────────────────────────────┐
   image  ──▶   │   FastAPI  POST /verify      │
                │                              │
                │   1. ตรวจสอบการอัปโหลด       │
                │   2. ┌──────────────────┐    │
                │      │ C2PA manifest     │    │  สัญญาณแหล่งที่มา
                │      │ (c2pa-python)     │    │
                │      └──────────────────┘    │
                │   3. ┌──────────────────┐    │
                │      │ classifier API    │    │  สัญญาณทางสถิติ
                │      │ (hosted detector) │    │
                │      └──────────────────┘    │
                │   4. รวมเป็นคำตัดสิน        │
                └─────────────────────────────┘
                              │
                              ▼
                   คำตัดสิน JSON + ความเชื่อมั่น

ขั้นตอนที่ 1 ตรวจสอบว่าไฟล์ที่อัปโหลดเป็นรูปภาพจริงในรูปแบบที่รองรับและอยู่ในขีดจำกัดขนาด ขั้นตอนที่ 2 อ่าน C2PA manifest ภายในเครื่อง ไม่มีการเรียกเครือข่าย เพียงแค่แยกวิเคราะห์และตรวจสอบใบรับรอง ขั้นตอนที่ 3 ส่งไบต์รูปภาพไปยังตัวแยกประเภทที่โฮสต์ผ่าน HTTPS ขั้นตอนที่ 4 รวมผลลัพธ์ทั้งสองด้วยฟังก์ชันกฎเล็กๆ และส่งคืนคำตัดสิน

ขั้นตอนสัญญาณทั้งสองเป็นอิสระต่อกัน สิ่งนี้มีความสำคัญต่อการจัดการข้อผิดพลาด: หากตัวแยกประเภทหมดเวลา คุณยังสามารถส่งคืนคำตัดสินบางส่วนจากสัญญาณแหล่งที่มาได้ และในทางกลับกัน เราจะกลับมาดูเรื่องนี้ในส่วนการเสริมความแข็งแกร่ง

สำหรับสแต็ก จำเป็นต้องใช้ Python 3.10 หรือใหม่กว่าเนื่องจากไลบรารี C2PA ต้องการสิ่งนี้ คุณจะใช้ FastAPI สำหรับเลเยอร์เว็บ, Uvicorn เพื่อรัน, python-multipart สำหรับการอัปโหลดไฟล์, httpx สำหรับการเรียกตัวแยกประเภทขาออก, และ c2pa-python สำหรับแหล่งที่มา

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

สัญญาณ C2PA

Content Authenticity Initiative ภายใต้องค์กร GitHub contentauth เผยแพร่เครื่องมือ C2PA แบบโอเพนซอร์ส มีสองส่วนที่คุณจะได้ยิน:

เส้นทางการอ่านของไลบรารีมีศูนย์กลางอยู่ที่วัตถุ Reader คุณชี้ไปที่รูปภาพ จากนั้นขอ manifest store เป็น JSON นี่คือส่วนสำคัญของโมดูล provenance

# provenance.py
import json
import c2pa


def read_provenance(image_path: str) -> dict:
    """
    อ่านและตรวจสอบ C2PA manifest จากรูปภาพ
    ส่งคืน dict ที่ถูกทำให้เป็นมาตรฐานซึ่งอธิบายสิ่งที่พบ
    """
    try:
        with c2pa.Reader(image_path) as reader:
            manifest_store = json.loads(reader.json())
    except c2pa.C2paError as err:
        # ManifestNotFound เป็นกรณีที่คาดไว้สำหรับรูปภาพส่วนใหญ่
        if str(err).startswith("ManifestNotFound"):
            return {
                "has_manifest": False,
                "validation": "none",
                "detail": "ไม่พบ C2PA manifest ในรูปภาพนี้",
            }
        # C2paError อื่นๆ หมายความว่าไฟล์มีข้อมูล C2PA ที่เราไม่สามารถแยกวิเคราะห์ได้
        return {
            "has_manifest": True,
            "validation": "error",
            "detail": f"ไม่สามารถแยกวิเคราะห์ 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 สำเร็จแล้ว",
    }

ข้อสังเกตบางประการเกี่ยวกับสิ่งที่โค้ดทำ: Reader ถูกใช้เป็นตัวจัดการบริบทเพื่อให้ทรัพยากรพื้นฐานถูกปล่อย reader.json() ส่งคืน manifest store แบบเต็มในรูปแบบสตริง JSON; ไลบรารียังมี reader.detailed_json() หากคุณต้องการรายงานแบบยาวพร้อมกับการยืนยันและส่วนประกอบทุกอย่าง ผลลัพธ์ที่คาดหวังสำหรับการอัปโหลดส่วนใหญ่คือ C2paError ซึ่งข้อความเริ่มต้นด้วย ManifestNotFound เพราะรูปภาพส่วนใหญ่ไม่มี Content Credentials ถือว่าเป็นข้อมูล ไม่ใช่ความล้มเหลว

เมื่อมี manifest สองฟิลด์ที่สำคัญที่สุดสำหรับการตัดสินคือ claim_generator ซึ่งเป็นสตริงที่บอกคุณว่าเครื่องมือใดเขียน manifest ตัวอย่างเช่น สตริงเฟิร์มแวร์กล้องหรือชื่อเครื่องมือภาพ AI ส่วนอาร์เรย์ validation_status จะว่างเปล่าเมื่อลายเซ็นและแฮชตรวจสอบถูกต้อง และจะถูกเติมด้วยรหัสข้อผิดพลาดเมื่อไม่ถูกต้อง Manifest ที่ไม่ถูกต้องถือเป็นธงแดงที่ควรแจ้งเตือนอย่างชัดเจน ซึ่งหมายความว่าไฟล์อ้างอิงประวัติที่การเข้ารหัสไม่สามารถยืนยันได้

สิ่งที่สัญญาณนี้ไม่สามารถทำได้: ไม่สามารถให้คำตัดสินแก่คุณเมื่อไม่มี manifest ซึ่งเป็นกรณีส่วนใหญ่ นั่นคือเหตุผลที่คุณต้องการสัญญาณที่สอง

สัญญาณตัวจำแนก

ตัวจำแนกเป็น API ที่โฮสต์ซึ่งให้คะแนนความน่าจะเป็นที่ภาพจะถูกสร้างโดย AI มีผู้ให้บริการหลายรายเสนอสิ่งนี้ บทช่วยสอนนี้ใช้ Sightengine เนื่องจากโมเดลการตรวจจับ AI มี HTTP API ที่มีการบันทึกและรูปแบบการตอบสนองที่ชัดเจน แต่รูปแบบนี้เหมือนกันสำหรับผู้ให้บริการรายใดก็ได้ คุณเพียงแค่เปลี่ยน URL, พารามิเตอร์ และฟิลด์ที่คุณอ่าน หากคุณกำลังพิจารณาตัวเลือกต่างๆ บทสรุปของเราเกี่ยวกับ API การตรวจจับภาพ AI ที่ดีที่สุด เปรียบเทียบความแม่นยำ ราคา และขอบเขตการทำงานของผู้ให้บริการต่างๆ

เอนด์พอยต์ตรวจสอบของ Sightengine คือ https://api.sightengine.com/1.0/check.json คุณ POST รูปภาพเป็น media ตั้งค่า models เป็น genai และส่ง api_user และ api_secret ของคุณ การตอบสนองจะรวม type.ai_generated ซึ่งเป็นคะแนนจาก 0 ถึง 1 โดยที่ค่าที่สูงกว่าหมายถึงแนวโน้มที่จะถูกสร้างโดย AI มากขึ้น

# 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:
    """
    ส่งรูปภาพไปยังตัวตรวจจับที่โฮสต์ไว้
    ส่งคืน dict ที่ถูกทำให้เป็นมาตรฐานพร้อมคะแนนที่สร้างโดย AI
    """
    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)}

ฟังก์ชันนี้เป็นแบบ async ดังนั้นตัวจำแนกที่ทำงานช้าจะไม่บล็อกอีเวนต์ลูป การหมดเวลานั้นชัดเจนและสั้น; แปดวินาทีเป็นค่าเริ่มต้นที่เหมาะสมสำหรับเอนด์พอยต์แบบโต้ตอบ และคุณควรปรับให้เข้ากับเวลาแฝงจริงของผู้ให้บริการของคุณ ทุกเส้นทางความล้มเหลวจะคืนค่า available: False พร้อมเหตุผลที่เครื่องอ่านได้แทนที่จะส่งข้อยกเว้น นั่นเป็นความตั้งใจ: การขัดข้องของตัวจำแนกควรทำให้คำตัดสินลดลง ไม่ใช่ทำให้คำขอขัดข้อง ตรรกะการตัดสินในส่วนถัดไปจะอ่าน available และตัดสินใจว่าจะทำอย่างไร

ให้ถือว่าคะแนนเป็นค่าประมาณ ค่า 0.92 คือ "โมเดลค่อนข้างแน่ใจ" ไม่ใช่ "สิ่งนี้ได้รับการพิสูจน์แล้วว่าเป็น AI" ผู้ขายจะอัปเดตโมเดลของตน และความแม่นยำจะแตกต่างกันไปตามเครื่องมือสร้างและตามปริมาณการบีบอัดภาพก่อนที่จะถึงคุณ สำหรับมุมมองที่กว้างขึ้นเกี่ยวกับพฤติกรรมของเครื่องมือเหล่านี้ในทางปฏิบัติ โปรดดูคู่มือของเราเกี่ยวกับ วิธีตรวจสอบว่าภาพถูกสร้างโดย AI หรือไม่

การออกแบบสัญญา /verify

นี่คือส่วนที่ Apidog ได้รับความนิยม ก่อนที่จะเขียนตัวจัดการเส้นทาง ให้ออกแบบคำขอและคำตอบเป็นสคีมา OpenAPI การทำสิ่งนี้ก่อนจะให้สามสิ่งแก่คุณ: แหล่งความจริงเดียวที่ทั้งสองทีมตกลงกัน, เซิร์ฟเวอร์จำลองที่ฟรอนต์เอนด์สามารถเรียกใช้ได้ทันที, และชุดการทดสอบที่คุณสามารถรันได้ทันทีที่แบ็กเอนด์มีอยู่จริง

คำขอ

POST /verify รับเนื้อหา multipart/form-data ที่มีฟิลด์เดียวคือ image ซึ่งเป็นไฟล์ที่จะตรวจสอบ ทำให้ง่ายเข้าไว้ พารามิเตอร์การสอบถามที่ไม่บังคับสามารถเพิ่มเข้ามาได้ภายหลัง

การตอบสนอง

การตอบสนองคือจุดที่งานออกแบบจะให้ผลลัพธ์ที่ดี มันจะต้องแสดงคำตัดสินสุดท้าย ความเชื่อมั่น และสัญญาณดิบทั้งสองเพื่อให้ผู้เรียกสามารถตรวจสอบการตัดสินใจได้ นี่คือรูปแบบ

{
  "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": "A valid C2PA manifest names an AI image tool, and the classifier scored the image as likely AI-generated.",
  "checked_at": "2026-05-21T09:30:00Z"
}

verdict คือค่าสตริงหนึ่งในสามค่า: likely_authentic, likely_ai, หรือ uncertain มีสามค่า ไม่ใช่สอง เพราะความซื่อสัตย์เป็นสิ่งสำคัญ; เมื่อสัญญาณขัดแย้งกันหรืออ่อนแอทั้งคู่ "uncertain" คือคำตอบที่ถูกต้อง confidence คือค่าทศนิยม 0 ถึง 1 ที่อธิบายว่าสัญญาณสนับสนุนคำตัดสินนั้นมากน้อยเพียงใด signals มีข้อมูลดิบทั้งสองเพื่อให้ผู้เรียกสามารถแสดง UI ของตนเองหรือใช้ policy ของตนเองได้ 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's visual designer หรือนำเข้าไฟล์ OpenAPI ที่มีอยู่ การออกแบบ API ก่อนการนำไปใช้งานเป็นเวิร์กโฟลว์ที่ควรนำมาใช้โดยทั่วไป; คู่มือการใช้งานโหมด spec-first ของเรา แสดงวิธีทำตั้งแต่ต้นจนจบใน Apidog

การแนะนำโค้ด

ตอนนี้ชิ้นส่วนต่างๆ มารวมกัน ด้านล่างนี้คือแอป FastAPI: การตรวจสอบอินพุต, การเรียกสัญญาณทั้งสอง, ฟังก์ชันการรวม, และเส้นทาง

การรวมสองสัญญาณเข้าด้วยกัน

ฟังก์ชันการตัดสิน (verdict function) เป็นหัวใจของบริการ มันเข้ารหัส policy ของคุณ แหล่งที่มา (provenance) เมื่อถูกต้องและมีอยู่ จะเป็นสัญญาณที่แข็งแกร่งกว่าเพราะเป็นแบบวิทยาการเข้ารหัส; ตัวจำแนก (classifier) เป็นตัวตัดสินในกรณีที่ผลลัพธ์ไม่ชัดเจน และเป็นทางเลือกสำรอง นี่คือเวอร์ชันที่ชัดเจนและอนุรักษ์นิยม

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

    # Heuristic: เครื่องมือ AI ที่รู้จักมักจะระบุตัวเองใน 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)

    # กรณีที่ 1: manifest ที่ถูกต้องซึ่งระบุเครื่องมือสร้าง AI สัญญาณ AI แข็งแกร่ง
    if has_manifest and validation == "valid" and generator_looks_ai:
        return _verdict("likely_ai", 0.95,
                        "C2PA manifest ที่ถูกต้องระบุว่าเป็นเครื่องมือสร้างภาพ AI")

    # กรณีที่ 2: manifest ที่ถูกต้องจากกล้องหรือโปรแกรมแก้ไขที่ไม่ใช่ 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 ดูเป็นของแท้แต่ตัวจำแนกไม่เห็นด้วย; สัญญาณขัดแย้งกัน")
        return _verdict("likely_authentic", 0.9,
                        "พบ C2PA manifest ที่ถูกต้องจากเครื่องมือที่ไม่ใช่ AI")

    # กรณีที่ 3: manifest ที่ตรวจสอบไม่ผ่าน ถือว่าน่าสงสัย
    if has_manifest and validation in ("invalid", "error"):
        return _verdict("uncertain", 0.6,
                        "ภาพมี C2PA manifest ที่ตรวจสอบไม่ผ่าน; ประวัติที่อ้างสิทธิ์ไม่ได้รับการยืนยัน")

    # กรณีที่ 4: ไม่มี manifest กลับไปใช้ตัวจำแนกทั้งหมด
    if classifier_ok and ai_score is not None:
        if ai_score >= 0.7:
            return _verdict("likely_ai", round(ai_score, 2),
                            "ไม่มีข้อมูลแหล่งที่มา; ตัวจำแนกให้คะแนนภาพว่าน่าจะสร้างโดย AI")
        if ai_score <= 0.3:
            return _verdict("likely_authentic", round(1 - ai_score, 2),
                            "ไม่มีข้อมูลแหล่งที่มา; ตัวจำแนกให้คะแนนภาพว่าน่าจะเป็นของแท้")
        return _verdict("uncertain", 0.5,
                        "ไม่มีข้อมูลแหล่งที่มาและคะแนนตัวจำแนกไม่สามารถสรุปได้")

    # กรณีที่ 5: ไม่มี manifest และไม่มีตัวจำแนก เราไม่สามารถบอกได้จริงๆ
    return _verdict("uncertain", 0.0,
                    "ไม่มีข้อมูลแหล่งที่มาและตัวจำแนกไม่พร้อมใช้งาน")


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

อ่านกรณีทั้งห้าแล้วคุณจะเห็น policy Manifest ที่ถูกต้องจะมีความสำคัญกว่า Manifest ที่ล้มเหลวเป็นคำเตือน ไม่ใช่หลักฐานของการปลอมแปลง ดังนั้นจึงถูกจัดอยู่ในประเภท "uncertain" การขัดแย้งกันระหว่าง manifest ที่สะอาดกับคะแนนตัวจำแนกที่สูงก็จัดอยู่ในประเภท "uncertain" เช่นกัน แทนที่จะเลือกข้าง และเมื่อไม่มีสัญญาณทั้งสอง บริการจะแจ้งอย่างตรงไปตรงมาด้วยความมั่นใจเป็นศูนย์แทนที่จะเดา คุณจะปรับค่า threshold เหล่านี้ตามระดับความเสี่ยงที่คุณยอมรับได้; แพลตฟอร์มเนื้อหาและห้องข่าวจะกำหนดเส้นแบ่งที่แตกต่างกัน

แอป 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. ตรวจสอบการอัปโหลด
    if image.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail=f"ประเภท {image.content_type} ไม่รองรับ "
                   f"โปรดส่ง JPEG, PNG, หรือ WebP",
        )

    image_bytes = await image.read()
    if len(image_bytes) == 0:
        raise HTTPException(status_code=400, detail="ไฟล์ว่างเปล่า")
    if len(image_bytes) > MAX_BYTES:
        raise HTTPException(status_code=413, detail="ไฟล์เกินขีดจำกัด 12 MB")

    # 2. สัญญาณแหล่งที่มา ตัวอ่าน C2PA ต้องการเส้นทางไฟล์
    #    ดังนั้นจึงเขียนลงในไฟล์ชั่วคราวแล้วลบออกในภายหลัง
    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.filename or "upload",
            SIGHTENGINE_USER, 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(),
    })

รันในเครื่องด้วย uvicorn main:app --reload แล้วเอนด์พอยต์จะพร้อมใช้งานที่ http://127.0.0.1:8000/verify ตัวอ่าน C2PA คาดหวังเส้นทางไฟล์ ดังนั้นตัวจัดการจะเขียนไฟล์ที่อัปโหลดลงในไฟล์ชั่วคราวและลบออกในบล็อก finally; ตัวจำแนกทำงานได้โดยตรงจากไบต์ สังเกตว่าคำขอไม่เคยล้มเหลวเนื่องจาก manifest หายไปหรือตัวจำแนกไม่ทำงาน; ทั้งสองเป็นสถานะปกติที่ฟังก์ชันการตัดสินจัดการ

แบ็กเอนด์ที่ออกแบบด้วยวิธีนี้ เป็นบริการที่เน้นงานเฉพาะที่มีสัญญาที่ชัดเจน เหมาะกับแนวโน้มที่กว้างขึ้นของผลิตภัณฑ์ที่เปิดเผยความสามารถหลักผ่าน API หากแนวคิดนี้ทำให้คุณสนใจ บทความของเราเกี่ยวกับ ซอฟต์แวร์ที่เปลี่ยนเป็นแบบ Headless คุ้มค่าที่จะอ่าน

การทดสอบและการจำลองด้วย Apidog

นี่คือปัญหาของเวิร์กโฟลว์: ทีมฟรอนต์เอนด์ของคุณต้องการสร้าง UI การอัปโหลดและแผงผลลัพธ์ตอนนี้ แต่แบ็กเอนด์ข้างต้นใช้เวลาหลายวันกว่าจะเสร็จ ได้คีย์ และปรับใช้ คุณไม่ต้องการให้พวกเขาติดขัด นี่คือสิ่งที่เซิร์ฟเวอร์จำลองมีไว้ใช้ และนี่คือจุดที่การออกแบบสคีมาตั้งแต่แรกเริ่มให้ผลตอบแทน

สร้างเซิร์ฟเวอร์จำลองจากสคีมา

นำเข้า OpenAPI schema เข้าสู่ Apidog หรือสร้าง endpoint /verify ใน visual designer ของ Apidog Apidog จะอ่าน response schema และสร้าง mock server โดยอัตโนมัติ เนื่องจาก schema กำหนดประเภทฟิลด์และ enum, mock จะส่งคืนข้อมูลที่มีโครงสร้างเหมือนกับ endpoint จริงทุกประการ: verdict ที่เป็นหนึ่งในสามค่า enum, confidence float ระหว่าง 0 ถึง 1, และออบเจ็กต์ signals ที่มีข้อมูลครบถ้วน Frontend จะชี้การเรียก fetch ไปที่ Apidog mock URL และรับ response ที่สมจริงได้ตั้งแต่วันแรก เมื่อ backend จริงถูกส่งมอบ พวกเขาก็แค่เปลี่ยน base URL เดียว

เซิร์ฟเวอร์จำลองยังเป็นที่ที่คุณสามารถทดสอบกรณีที่ยากลำบากก่อนที่โค้ดจริงจะมีอยู่ กำหนดตัวอย่างการตอบสนองสำหรับคำตัดสินที่สำคัญ:

ฟรอนต์เอนด์สามารถสร้างและจัดรูปแบบทุกสถานะ รวมถึงสถานะข้อผิดพลาด โดยใช้โมเดลจำลอง นี่คือวิธีที่คุณจัดส่ง UI และ API พร้อมกันแทนที่จะเรียงตามลำดับ

รันการทดสอบเอนด์พอยต์ใน Apidog

เมื่อแบ็กเอนด์ทำงานแล้ว ให้สร้างคำขอใน Apidog สำหรับ POST /verify กำหนด method ชี้ไปที่ URL โลคัลของคุณ และในแท็บ Body เลือก form-data เพิ่มฟิลด์ image กำหนดประเภทเป็น File และเลือกรูปภาพทดสอบจากดิสก์

ส่งไป แล้ว Apidog จะแสดงการตอบกลับในรูปแบบ JSON ตอนนี้เพิ่ม assertion เพื่อให้สิ่งนี้กลายเป็นการตรวจสอบที่ทำซ้ำได้ แทนที่จะเป็นเพียงการคลิกครั้งเดียว:

สร้างสถานการณ์การทดสอบขนาดเล็กที่รันการอัปโหลดหลายรายการต่อเนื่องกัน: รูปภาพที่มี Content Credentials, รูปภาพ JPEG ธรรมดาที่ไม่มี manifest, ไฟล์ขนาดใหญ่เกินไป, และไฟล์ที่ไม่ใช่รูปภาพที่เปลี่ยนชื่อด้วยนามสกุล .jpg แต่ละรายการจะตรวจสอบส่วนที่แตกต่างกันของตรรกะการตัดสินและการตรวจสอบอินพุตของคุณ บันทึกสถานการณ์และคุณสามารถรันชุดการทดสอบทั้งหมดซ้ำได้หลังจากการเปลี่ยนแปลงทุกครั้ง หรือเชื่อมโยงเข้ากับ CI เพื่อให้การถดถอยในฟังก์ชันการตัดสินทำให้ build ล้มเหลว การทดสอบเอนด์พอยต์การอัปโหลดด้วยตนเองด้วย curl นั้นจะล้าสมัยอย่างรวดเร็ว; สถานการณ์ที่บันทึกไว้ไม่เป็นเช่นนั้น

การเสริมความแข็งแกร่งและกรณีขอบ

เส้นทางปกติเป็น 80 เปอร์เซ็นต์ที่ง่าย บริการยืนยันตัวตนจะอยู่รอดหรือล้มเหลวที่ 20 เปอร์เซ็นต์ที่เหลือ เพราะข้อมูลอินพุตมีลักษณะที่เป็นปฏิปักษ์โดยธรรมชาติ มีคนพยายามปลอมแปลงภาพให้ดูเหมือนเป็นอย่างอื่น

ไฟล์เสียหายหรือไฟล์ที่ถูกตัด. ไฟล์อาจมีประเภท MIME ของรูปภาพ แต่ยังคงเป็นไฟล์ขยะ ตัวอ่าน C2PA จะส่ง C2paError หากข้อมูลไม่สามารถแยกวิเคราะห์ได้ และฟังก์ชัน read_provenance จะเปลี่ยนให้เป็นผลลัพธ์ที่สะอาดแทนที่จะเป็น 500 สำหรับการป้องกันสองชั้น คุณสามารถถอดรหัสรูปภาพด้วยไลบรารีอย่าง Pillow ก่อนการประมวลผล และปฏิเสธด้วย 400 หากการถอดรหัสล้มเหลว ซึ่งยังบล็อกเทคนิคการเปลี่ยนชื่อไฟล์ข้อความด้วย

Manifest หายไป. ได้รับการครอบคลุมแล้ว แต่ควรกล่าวซ้ำเพราะเป็นกรณีที่พบบ่อยที่สุดและผิดพลาดง่ายที่สุด การไม่มี manifest ไม่ใช่ข้อผิดพลาดและไม่ใช่คำตัดสิน ไลบรารีจะส่งสัญญาณด้วย C2paError ที่มีข้อความขึ้นต้นด้วย ManifestNotFound; บริการจะจับสิ่งนั้นโดยเฉพาะและดำเนินการต่อไปยังตัวจำแนก อย่าให้ manifest ที่หายไปทำให้เกิด 500 หรือคำตัดสินว่า "ของปลอม"

ตัวจำแนกหมดเวลาหรือหยุดทำงาน. ตัวจำแนกเป็นส่วนหนึ่งของเครือข่ายบุคคลที่สาม ดังนั้นสมมติว่าจะต้องมีข้อผิดพลาดบางครั้ง ฟังก์ชัน classify_image ใช้การหมดเวลา httpx ที่ชัดเจน และคืนค่า available: False เมื่อเกิดการหมดเวลาหรือข้อผิดพลาด HTTP ใดๆ จากนั้นฟังก์ชันการตัดสินจะย้อนกลับไปใช้แหล่งที่มาเพียงอย่างเดียว หรือคืนค่า uncertain ด้วยความมั่นใจเป็นศูนย์ หากไม่มี manifest เอนด์พอยต์ยังคงตอบกลับด้วย 200 และคำตัดสินที่ซื่อสัตย์; ผู้ขายที่ทำงานช้าไม่สามารถทำให้บริการของคุณหยุดทำงานได้

Manifest ปลอมแปลง. Manifest อาจมีอยู่แต่ไม่ถูกต้อง ลงนามด้วยใบรับรองที่ไม่ดี หรือมีค่าแฮชที่ไม่ตรงกับพิกเซล นี่คือกรณีที่คนมักลืม ตรวจสอบ validation_status เสมอ; อาร์เรย์ว่างเปล่าหมายถึง manifest ได้รับการตรวจสอบแล้ว ส่วนอาร์เรย์ที่มีข้อมูลหมายถึงไม่ผ่าน ฟังก์ชันการตัดสินจะถือว่า manifest ที่ล้มเหลวเป็นคำเตือนที่นำไปสู่ uncertain ไม่ใช่หลักฐาน การเชื่อ manifest ที่ไม่ได้รับการตรวจสอบนั้นแย่กว่าการไม่มี manifest เลย

ไฟล์ขนาดใหญ่และการใช้งานที่ไม่เหมาะสม. กำหนดขีดจำกัดขนาดการอัปโหลด ตัวอย่างใช้ 12 MB และปฏิเสธไฟล์ที่ใหญ่กว่าด้วย 413 ก่อนที่จะอ่านเนื้อหาทั้งหมดเข้าสู่หน่วยความจำ ใส่การจำกัดอัตรา (rate limit) ไว้หน้าเอนด์พอยต์; การตรวจสอบความถูกต้องด้วยการเข้ารหัสและการเรียก API ภายนอกต่อคำขอไม่ได้ไม่มีค่าใช้จ่าย และเอนด์พอยต์การยืนยันที่เปิดกว้างเป็นเป้าหมายที่น่าสนใจ

ความเป็นส่วนตัว. คุณกำลังรับรูปภาพจากผู้ใช้ ประมวลผลในหน่วยความจำหรือในไฟล์ชั่วคราวที่คุณลบออกทันทีตามตัวอย่าง และอย่าบันทึกไบต์รูปภาพ หากคุณกำลังส่งรูปภาพไปยังตัวจำแนกบุคคลที่สาม ให้ระบุในนโยบายความเป็นส่วนตัวของคุณและตรวจสอบให้แน่ใจว่าได้รับอนุญาตสำหรับการใช้งานของคุณ

แต่ละสัญญาณจับอะไรได้และพลาดอะไรไป

ตารางนี้คือโมเดลความคิดที่ต้องเก็บไว้ นั่นคือเหตุผลที่บริการใช้ทั้งสองอย่าง

สถานการณ์ สัญญาณแหล่งที่มา C2PA สัญญาณตัวจำแนก
ภาพ AI จากเครื่องมือที่เขียน Content Credentials จับได้: manifest ระบุชื่อเครื่องมือสร้าง มักจะจับได้: มีสิ่งผิดปกติ
ภาพ AI ที่ข้อมูลเมตาถูกลบออก (จับภาพหน้าจอ, อัปโหลดซ้ำ) พลาด: ไม่มี manifest ให้อ่าน จับได้: ทำงานบนพิกเซล ไม่ต้องใช้ข้อมูลเมตา
ภาพจริงจากกล้องที่ลงนาม Content Credentials ยืนยัน: manifest ถูกต้อง, ไม่ใช่เครื่องมือสร้าง AI อาจผิดพลาดกับภาพที่มีการบีบอัดมากหรือถูกแก้ไข
ภาพจริงที่ไม่มีข้อมูลเมตาเลย ไม่มีสัญญาณ: ไม่มีอะไรให้ตรวจสอบ คาดเดาเท่านั้น: เป็นความน่าจะเป็น อาจผิดพลาดได้
ภาพที่มี manifest ปลอมแปลงหรือถูกแก้ไข จับได้: validation_status แจ้งความผิดพลาด อาจจะจับได้หรือไม่ก็ได้ ขึ้นอยู่กับพิกเซล
เครื่องมือสร้างใหม่ที่ตัวจำแนกไม่เคยถูกฝึก จับได้เฉพาะเมื่อเครื่องมือเขียน manifest มักจะพลาด: อยู่นอกขอบเขตการฝึก
ภาพจริงที่ถูกแก้ไขอย่างหนัก (รีทัช AI บนฐานจริง) Manifest หากมีอยู่ จะบันทึกประวัติการแก้ไข คลุมเครือ: สังเคราะห์บางส่วน, คะแนนอยู่กลางๆ

อ่านในแต่ละแถวแล้วคุณจะเห็นเรื่องราวเดียวกัน: เมื่อสัญญาณหนึ่งตาบอด อีกสัญญาณหนึ่งมักจะไม่Provenence นั้นแม่นยำแต่มีน้อย; ตัวจำแนกนั้นเป็นสากลแต่คลุมเครือ คำตัดสินที่รวมกันนั้นน่าเชื่อถือกว่าคอลัมน์ใดคอลัมน์หนึ่งเพียงลำพัง และค่า uncertain ที่ซื่อสัตย์นั้นมีอยู่สำหรับแถวที่สัญญาณทั้งสองอ่อนแอ

กรณีการใช้งานในโลกจริง

รูปแบบนี้ไม่ใช่วิชาการ มีบางแห่งที่เหมาะสมโดยตรง:

จุดร่วมคือ: คุณต้องการการตรวจสอบเบื้องต้นที่รวดเร็ว อัตโนมัติ และซื่อสัตย์เกี่ยวกับความไม่แน่นอนของตนเอง เพื่อให้ความสนใจของมนุษย์ไปในที่ที่จำเป็นจริงๆ

สรุป

การตรวจจับภาพที่สร้างโดย AI อย่างมีประสิทธิภาพไม่ได้เกี่ยวกับการหาการทดสอบที่สมบูรณ์แบบเพียงหนึ่งเดียว แต่เกี่ยวกับการรวมสัญญาณอิสระและมีความซื่อสัตย์เกี่ยวกับความเชื่อมั่น

ในการสร้างสิ่งนี้จริงๆ ให้คุณออกแบบ schema /verify, สร้าง mock server, และเรียกใช้การทดสอบเอนด์พอยต์ของคุณในที่เดียว ดาวน์โหลด Apidog เพื่อออกแบบ, จำลอง, และทดสอบ API ในขณะที่คุณสร้างมัน จากนั้นย้ายจาก mock ไปสู่แบ็กเอนด์จริงด้วยการเปลี่ยน base-URL เพียงครั้งเดียว

button

ฝึกการออกแบบ API แบบ Design-first ใน Apidog

ค้นพบวิธีที่ง่ายขึ้นในการสร้างและใช้ API