DEV Community

Cover image for بناء واجهة برمجة تطبيقات كشف الصور بالذكاء الاصطناعي باستخدام C2PA وتصنيف الصور
Yusuf Khalidd
Yusuf Khalidd

Posted on • Originally published at apidog.com

بناء واجهة برمجة تطبيقات كشف الصور بالذكاء الاصطناعي باستخدام C2PA وتصنيف الصور

يقوم مستخدم برفع صورة إلى منتجك ويدّعي أنها التُقطت بكاميرا. المطلوب من الـ backend ليس “تخمينًا بصريًا”، بل حكمًا قابلًا للتفسير يعتمد على إشارتين مستقلتين: مصدر تشفيري عبر C2PA، ومصنف كشف صور مولّدة بالذكاء الاصطناعي.

جرّب Apidog اليوم

في هذا الدليل ستبني خدمة FastAPI واحدة تحتوي على نقطة النهاية POST /verify. تستقبل صورة، تفحص بيانات اعتماد المحتوى C2PA إن وجدت، تستدعي API كشف مستضافة، ثم تعيد JSON يحتوي على الحكم النهائي، درجة الثقة، والإشارات الخام. سنصمم العقد أولًا باستخدام Apidog حتى يستطيع فريق الواجهة الأمامية استخدام mock server قبل اكتمال تنفيذ الـ backend.

ما الذي ستبنيه

الخدمة ستنفذ التدفق التالي:

  1. استقبال صورة عبر multipart/form-data.
  2. التحقق من نوع وحجم الملف.
  3. قراءة بيان C2PA باستخدام c2pa-python.
  4. إرسال الصورة إلى مصنف مستضاف للكشف عن الصور المولّدة بالذكاء الاصطناعي.
  5. دمج الإشارتين في حكم واحد:
    • likely_authentic
    • likely_ai
    • uncertain
  6. إرجاع JSON قابل للاستهلاك من الواجهة الأمامية أو أنظمة المراجعة.

لماذا تحتاج إشارتين؟

لا توجد خاصية واحدة في الملف تخبرك بشكل قاطع أن الصورة “حقيقية” أو “مولّدة بالذكاء الاصطناعي”.

إشارة المصدر: C2PA

C2PA هو معيار مفتوح يضيف بيانات وصفية موقعة ومقاومة للتلاعب إلى ملفات الوسائط. هذه البيانات تسمى Content Credentials، وتصف تاريخ الصورة: من أنشأها، ما الأداة المستخدمة، وما التعديلات التي حدثت.

الميزة: إذا كان البيان موجودًا وصالحًا، فهو دليل قوي.

المشكلة: C2PA اختياري ويمكن فقدانه بسهولة عند:

  • أخذ لقطة شاشة.
  • إعادة ضغط الصورة.
  • رفعها إلى منصة تزيل metadata.
  • إرسالها عبر تطبيقات مراسلة.

لذلك غياب البيان لا يعني أن الصورة مزيفة أو حقيقية.

إشارة المصنف

المصنف الإحصائي يعمل على البكسلات نفسها. لا يحتاج إلى metadata، لكنه يعطي احتمالًا وليس حقيقة. قد يخطئ مع الصور المضغوطة، الصور المعدلة بشدة، أو المولدات الجديدة خارج نطاق تدريبه.

لهذا السبب نجمع الإشارتين. المصدر قوي لكنه غالبًا غائب، والمصنف متاح غالبًا لكنه احتمالي. إذا أردت فهمًا أعمق لفشل الأساليب أحادية الإشارة، راجع مقال لماذا تفشل أنظمة الكشف عن صور الذكاء الاصطناعي.

البنية العامة

                ┌─────────────────────────────┐
   image  ──▶   │   FastAPI  POST /verify      │
                │                              │
                │   1. validate upload         │
                │   2. ┌──────────────────┐    │
                │      │ C2PA manifest     │    │  provenance signal
                │      │ c2pa-python       │    │
                │      └──────────────────┘    │
                │   3. ┌──────────────────┐    │
                │      │ classifier API    │    │  statistical signal
                │      │ hosted detector   │    │
                │      └──────────────────┘    │
                │   4. combine into verdict    │
                └─────────────────────────────┘
                              │
                              ▼
                   JSON verdict + confidence
Enter fullscreen mode Exit fullscreen mode

ثبت الحزم المطلوبة:

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

ستحتاج إلى Python 3.10 أو أحدث.

تصميم عقد POST /verify أولًا

قبل تنفيذ الكود، صمم عقد API. هذا يجعل الواجهة الأمامية قادرة على التكامل مع mock server، ويجعل الاختبار لاحقًا أسهل.

الطلب

POST /verify

Body من نوع multipart/form-data يحتوي على حقل واحد:

الحقل النوع الوصف
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": "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"
}
Enter fullscreen mode Exit fullscreen mode

مكوّن 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، ثم توليد mock server مباشرة. بذلك يستطيع فريق الواجهة الأمامية بناء شاشة الرفع والنتائج قبل تشغيل الخدمة الفعلية.

قراءة إشارة C2PA

استخدم c2pa-python لقراءة بيان C2PA من الصورة.

أنشئ ملف provenance.py:

# provenance.py
import json
import c2pa


def read_provenance(image_path: str) -> dict:
    """
    اقرأ وتحقق من بيان C2PA داخل الصورة.
    تعيد الدالة كائنًا موحدًا يمكن استخدامه في منطق الحكم.
    """
    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.",
            }

        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 = 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

نقاط مهمة:

  • ManifestNotFound حالة طبيعية وليست خطأ في الخدمة.
  • validation_status الفارغ يعني أن التحقق نجح.
  • بيان موجود لكنه غير صالح يجب أن يُعامل كإشارة تحذير.
  • لا تعتمد على C2PA وحده لأن معظم الصور لن تحتوي على بيان.

استدعاء مصنف كشف الصور المولّدة

سنستخدم Sightengine كمثال لأنه يوفر API موثقًا. النمط نفسه ينطبق على أي مزود آخر. إذا كنت تقارن بين الخيارات، راجع أفضل واجهات برمجة تطبيقات الكشف عن صور الذكاء الاصطناعي.

أنشئ ملف 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:
    """
    أرسل الصورة إلى المصنف المستضاف.
    تعيد الدالة درجة احتمالية أن تكون الصورة مولّدة بالذكاء الاصطناعي.
    """
    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

لاحظ أن الدالة لا ترمي استثناء عند فشل المصنف. بدلًا من ذلك تعيد:

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

هذا مهم لأن تعطل مزود خارجي لا يجب أن يسقط endpoint بالكامل.

دمج الإشارتين في حكم واحد

أنشئ ملف 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(keyword in generator for keyword in ai_keywords)

    # 1. بيان صالح ويذكر أداة ذكاء اصطناعي
    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. بيان صالح من أداة لا تبدو كمولد ذكاء اصطناعي
    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

هذه السياسة محافظة:

  • البيان الصالح أقوى من المصنف.
  • البيان غير الصالح لا يعني “مزيف”، لكنه يجعل النتيجة غير مؤكدة.
  • عند تعارض الإشارات، أرجع uncertain.
  • عند غياب كل شيء، أرجع uncertain بثقة 0.0.

تنفيذ FastAPI

أنشئ ملف 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."
            ),
        )

    # 2. اقرأ الملف وتحقق من الحجم
    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.",
        )

    # 3. 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)

    # 4. استدعاء المصنف إذا كانت المفاتيح متوفرة
    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. دمج الإشارات وإرجاع الاستجابة
    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

شغّل الخدمة محليًا:

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

ستكون نقطة النهاية متاحة على:

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

اختبار سريع باستخدام curl:

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

إعداد متغيرات البيئة

إذا كنت تستخدم Sightengine، عرّف المفاتيح قبل التشغيل:

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

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

إذا لم تكن المفاتيح موجودة، ستعمل الخدمة، لكن إشارة المصنف ستكون:

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

المحاكاة والاختبار باستخدام Apidog

1. أنشئ endpoint في Apidog

أنشئ endpoint باسم:

POST /verify
Enter fullscreen mode Exit fullscreen mode

واستخدم:

  • Body: multipart/form-data
  • Field: image
  • Type: File

ثم أضف مخطط الاستجابة السابق.

2. شغّل mock server

بعد تعريف المخطط، يستطيع Apidog إنشاء mock server يعيد استجابات مطابقة للعقد.

أنشئ أمثلة استجابة للحالات التالية:

  • likely_authentic مع بيان C2PA صالح.
  • likely_ai مع أداة ذكاء اصطناعي مذكورة في البيان.
  • uncertain عند تعارض المصنف مع C2PA.
  • uncertain عند فشل المصنف.
  • خطأ 415 عند نوع ملف غير مدعوم.
  • خطأ 413 عند ملف كبير.

بهذا يستطيع فريق الواجهة الأمامية بناء الواجهة دون انتظار تنفيذ الخدمة.

3. اختبر الخدمة الحقيقية

بعد تشغيل FastAPI، أنشئ طلبًا في Apidog إلى:

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

ثم أضف assertions مثل:

  • حالة الاستجابة تساوي 200.
  • verdict واحدة من:
    • likely_authentic
    • likely_ai
    • uncertain
  • confidence رقم بين 0 و 1.
  • signals.provenance.has_manifest قيمة boolean.
  • checked_at موجود.

4. أنشئ سيناريو اختبار

استخدم عدة ملفات اختبار:

  1. صورة تحتوي على C2PA صالح.
  2. صورة عادية بدون metadata.
  3. ملف كبير يتجاوز الحد.
  4. ملف نصي تمت إعادة تسميته إلى .jpg.
  5. صورة يتعذر على المصنف تحليلها أو ينتهي وقت طلبها.

احفظ السيناريو وشغّله بعد كل تعديل على منطق الحكم.

حالات يجب التعامل معها بوضوح

بيان C2PA مفقود

هذا هو الوضع الأكثر شيوعًا. لا تعامله كخطأ.

الصحيح:

{
  "has_manifest": false,
  "validation": "none"
}
Enter fullscreen mode Exit fullscreen mode

ثم انتقل إلى المصنف.

بيان موجود لكنه غير صالح

إذا كان validation_status غير فارغ، لا تثق بالبيان. أرجع uncertain بدلًا من إصدار حكم قاطع.

المصنف انتهت مهلته

لا تجعل timeout يؤدي إلى 500.

الصحيح أن تعيد:

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

ثم تستخدم C2PA إن وجد، أو تعيد uncertain.

الملفات الكبيرة

ضع حدًا واضحًا مثل 12 MB وأعد 413.

الأنواع غير المدعومة

اقبل فقط الأنواع التي تدعمها:

ALLOWED_TYPES = {
    "image/jpeg",
    "image/png",
    "image/webp",
}
Enter fullscreen mode Exit fullscreen mode

وأعد 415 عند غير ذلك.

الخصوصية

أنت تستقبل صور مستخدمين، لذلك:

  • لا تسجل محتوى الصور.
  • احذف الملفات المؤقتة فورًا.
  • وضّح في سياسة الخصوصية إذا كنت ترسل الصور إلى مزود خارجي.
  • لا تحتفظ بالصور إلا إذا كان ذلك ضروريًا ومصرحًا به.

ما الذي تلتقطه كل إشارة؟

السيناريو C2PA المصنف
صورة مولّدة من أداة تكتب Content Credentials يلتقطها إذا ذكر البيان الأداة غالبًا يلتقطها
صورة مولّدة مع metadata محذوفة لا توجد إشارة يعمل على البكسلات
صورة حقيقية من كاميرا توقع C2PA يؤكدها إذا كان البيان صالحًا قد يخطئ عند الضغط أو التعديل
صورة حقيقية بدون metadata لا توجد إشارة يعطي تقديرًا احتماليًا
بيان مزور أو معدل validation_status يكشف الفشل قد يلتقطها أو لا
مولد جديد لم يتدرب عليه المصنف يلتقطها فقط إذا كتب بيانًا قد يفشل
صورة حقيقية معدلة بالذكاء الاصطناعي البيان قد يسجل تاريخ التحرير غالبًا تكون النتيجة متوسطة

أين يفيد هذا النمط؟

  • منصات المحتوى الذي ينشئه المستخدمون.
  • غرف الأخبار ومراجعة الحقائق.
  • استقبال مطالبات التأمين.
  • إدارة مكتبات الصور الداخلية.
  • أنظمة إدارة المحتوى التي تريد عرض شارات تحقق عند وجود C2PA.

الفكرة ليست استبدال المراجعة البشرية دائمًا، بل إعطاء مرور أولي سريع وصادق بشأن درجة اليقين.

الخلاصة

لا يوجد كاشف مثالي للصور المولّدة بالذكاء الاصطناعي. الحل العملي هو دمج إشارات مستقلة:

  • C2PA يعطي مصدرًا تشفيريًا قويًا عندما يكون موجودًا.
  • المصنف يعطي تقديرًا احتماليًا يعمل حتى عند غياب metadata.
  • FastAPI يجعل التغليف كـ API بسيطًا.
  • Apidog يساعدك على تصميم العقد، محاكاة endpoint، واختبارها قبل وبعد التنفيذ.

ابدأ بتصميم /verify في OpenAPI، أنشئ mock server، ثم اربط التنفيذ الحقيقي بنفس العقد. بهذه الطريقة تبني الواجهة والـ backend بالتوازي، وتحتفظ بحكم واضح وقابل للتدقيق بدل الاعتماد على إشارة واحدة غير كافية.

Top comments (0)