誰かがあなたの製品に写真をアップロードし、それがカメラで撮影されたものだと主張しています。あなたのバックエンドはそれを証明できますか、それとも反証できますか?画像生成AIは今や人間が見ても本物に見える結果を生み出すため、「目視で信頼する」方法はかなり前から機能しなくなりました。幸いなことに、有用な回答を提供するために独自のモデルをトレーニングする必要はありません。暗号化された来歴マニフェストと機械学習分類器という2つの独立したシグナルを組み合わせることで、どちらか一方のシグナルよりも信頼性の高い判断を下すことができます。 このチュートリアルでは、`POST /verify` エンドポイントを持つ単一のサービスとして、そのバックエンドを構築する手順を説明します。画像を渡すと、信頼度スコアと検出された来歴の詳細を含むJSON判定が返されます。サーバーにはPythonとFastAPI、来歴シグナルにはオープンソースのC2PAツール、分類器シグナルにはホスト型検出APIを使用します。これはAPIプロジェクトであるため、最初にエンドポイントの契約を設計し、Apidog を使用してモックとテストを行い、バックエンドコードが完成する前にフロントエンドチームが統合を開始できるようにします。
TL;DR(要するに)
画像アップロードを受け入れ、`c2pa-python` ライブラリを使用してC2PA Content Credentialsマニフェストを抽出し検証し、2つ目の独立したシグナルとしてホスト型AI検出分類器を呼び出し、信頼度スコアと生の来歴詳細を含む単一のJSON判定(`likely_authentic`、`likely_ai`、または`uncertain`)を返す`POST /verify`を公開するFastAPIサービスを構築します。また、エンドポイントのOpenAPIスキーマを設計し、Apidogを使用してモックサーバーを生成し、それに対してエンドポイントテストを実行します。
なぜ1つではなく2つのシグナルなのか
コードを書く前に、何を検出するのかを明確にしておくことが役立ちます。ファイルには「人間が作成した」または「AIが作成した」と明確に告げる単一のプロパティはありません。代わりに手がかりがあり、それぞれの手がかりは異なる種類の画像を捉えつつ、他の画像を見逃します。 最初のヒントは来歴(プロビナンス)です。C2PA(Content Provenance and Authenticity連合)は、メディアファイルに改ざん検知可能で暗号的に署名されたメタデータを付加するオープン標準です。このメタデータバンドルはマニフェストと呼ばれ、ユーザー向けの名称はコンテンツクレデンシャル(Content Credentials)です。参加ツール(カメラ、エディター、画像生成ツールなど)が画像を作成または変更する際、発生した事柄を記録し証明書で署名されたマニフェストを書き込むことができます。そのマニフェストを読み取り検証できれば、画像の履歴に関する強力で検証可能な声明を得ることができます。 欠点:C2PAはオプトインであり、マニフェストは脆弱です。スクリーンショットはそれを剥ぎ取ります。メッセージングアプリを介した再エンコードも剥ぎ取ります。多くのプラットフォームはアップロード時にメタデータを削除します。そのため、マニフェストがないことはほとんど何も教えてくれません。画像が偽物であることも意味しませんし、本物であることも意味しません。 2つ目の手がかりは統計的分類器です。検出モデルは何百万もの実写画像と生成画像でトレーニングされており、生成AIが残す傾向のある視覚的なアーティファクトを学習します。メタデータの有無にかかわらず、あらゆる画像で機能しますが、確率的です。事実ではなく可能性を返し、特にトレーニング分布外の画像や、高度に圧縮された画像では間違っている可能性があります。 どちらのシグナルも単独では不十分です。来歴は正確ですが、めったに存在しません。分類器は常に利用可能ですが、決して確実ではありません。これらを組み合わせることで、「暗号技術が証明すること、モデルが推定すること、そしてその組み合わせによってどれだけ確信が持てるか」を事実上示す判断が得られます。これが設計目標です。単一シグナルアプローチがなぜ不十分なのかを詳しく知りたい場合は、AI画像検出が失敗する理由に関する当社の記事で、その失敗モードについて詳しく説明しています。
アーキテクチャの概要
サービスは意図的に小規模に設計されています。1つのエンドポイント、2つのダウンストリーム呼び出し、1つの結合された応答。
┌─────────────────────────────┐
image ──▶ │ FastAPI POST /verify │
│ │
│ 1. validate upload │
│ 2. ┌──────────────────┐ │
│ │ C2PA manifest │ │ 来歴シグナル
│ │ (c2pa-python) │ │
│ └──────────────────┘ │
│ 3. ┌──────────────────┐ │
│ │ classifier API │ │ 統計的シグナル
│ │ (hosted detector) │ │
│ └──────────────────┘ │
│ 4. combine into verdict │
└─────────────────────────────┘
│
▼
JSON verdict + confidence
ステップ1では、アップロードされた画像がサポートされている種類の本物の画像であり、サイズ制限内にあることを確認します。ステップ2では、C2PAマニフェストをローカルで読み取ります。ネットワーク呼び出しはなく、解析と証明書の検証のみです。ステップ3では、画像のバイトデータをHTTPS経由でホスト型分類器に送信します。ステップ4では、小さなルール関数で2つの結果をマージし、判断を返します。 2つのシグナル処理ステップは独立しています。これはエラー処理にとって重要です。分類器がタイムアウトした場合でも、来歴シグナルからの部分的な判断を返すことができますし、その逆も可能です。これについては、堅牢化のセクションで改めて説明します。 スタックには、C2PAライブラリが必要なためPython 3.10以降が必要です。WebレイヤーにはFastAPI、実行にはUvicorn、ファイルアップロードには`python-multipart`、外部分類器呼び出しには`httpx`、来歴には`c2pa-python`を使用します。
pip install fastapi "uvicorn[standard]" python-multipart httpx c2pa-python
C2PAシグナル
Content Authenticity Initiativeは、`contentauth` GitHub組織の下でオープンソースのC2PAツールを公開しています。そこには、次の2つのツールがあります。
- `c2patool`:マニフェストを表示および追加するためのコマンドラインツール。ターミナルからの迅速な検査に便利です。そのスタンドアロンリポジトリは現在アーカイブされており、CLIは`c2pa-rs` Rustプロジェクト内に存在することに注意してください。
- `c2pa-python`:同じ基盤となるRustライブラリ(`c2pa-rs`)のPythonバインディング。これがあなたのサービスで使用するものです。PyPIで`c2pa-python`として公開されており、`pip install c2pa-python`でインストールできます。
ライブラリの読み取りパスは`Reader`オブジェクトを中心に構成されています。画像を指し示し、JSON形式でマニフェストストアを要求します。以下は、プロビナンスモジュールの核心部分です。
# 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:
# ManifestNotFoundは、ほとんどの画像で想定されるケースです。
if str(err).startswith("ManifestNotFound"):
return {
"has_manifest": False,
"validation": "none",
"detail": "この画像にはC2PAマニフェストが存在しません。",
}
# その他のC2paErrorは、ファイルに解析できなかったC2PAデータがあったことを意味します。
return {
"has_manifest": True,
"validation": "error",
"detail": f"マニフェストを解析できませんでした: {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": "マニフェストが正常に読み取られました。",
}
コードの動作についていくつか注意点があります。`Reader`はコンテキストマネージャーとして使用されるため、基盤となるリソースが解放されます。`reader.json()`は完全なマニフェストストアをJSON文字列として返します。すべての主張と要素を含む詳細なレポートが必要な場合は、`reader.detailed_json()`もライブラリで提供されています。ほとんどのアップロードで予想される結果は、メッセージが`ManifestNotFound`で始まる`C2paError`です。これは、ほとんどの画像にコンテンツクレデンシャルがないためです。これを失敗としてではなく、データとして扱います。 マニフェストが存在する場合、判断に最も重要な2つのフィールドがあります。`claim_generator`文字列は、マニフェストを書き込んだツール(例:カメラのファームウェア文字列やAI画像ツールの名前)を示します。`validation_status`配列は、署名とハッシュが一致する場合は空になり、一致しない場合はエラーコードが格納されます。無効なマニフェストは、声高に表明すべき危険信号です。それは、ファイルが暗号技術で裏付けられない履歴を主張していることを意味します。 このシグナルができないこと:マニフェストがない場合(ほとんどの場合)、判断を下すことはできません。だからこそ、2つ目のシグナルが必要なのです。
分類器シグナル
分類器は、画像がAI生成である可能性をスコアリングするホスト型APIです。いくつかのベンダーがこれを提供しています。このチュートリアルではSightengineを使用します。そのAI検出モデルには文書化されたHTTP APIと明確な応答形式があるためです。ただし、どのプロバイダーでもパターンは同じです。URL、パラメータ、読み取るフィールドを交換するだけです。選択肢を検討している場合は、最高のAI画像検出APIのまとめで、ベンダー間の精度、価格設定、カバレッジを比較しています。 Sightengineのチェックエンドポイントは`https://api.sightengine.com/1.0/check.json`です。画像を`media`としてPOSTし、`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:
"""
画像をホスト型検出器に送信します。
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)}
この関数は非同期であるため、遅い分類器がイベントループをブロックすることはありません。タイムアウトは明示的かつ短く設定されています。8秒はインタラクティブなエンドポイントにとって妥当なデフォルト値であり、プロバイダーの実際のレイテンシーに合わせて調整する必要があります。すべての失敗パスは、例外を発生させるのではなく、機械で読み取り可能な理由とともに`available: False`を返します。これは意図的なものです。分類器の停止は、リクエストをクラッシュさせるのではなく、判定の質を低下させるべきだからです。次のセクションの判定ロジックは`available`を読み取り、何をすべきかを決定します。 スコアは推定値として扱ってください。0.92は「モデルはかなり確信している」という意味であり、「これがAIによって生成されたと証明された」という意味ではありません。ベンダーはモデルを更新し、生成元や画像があなたに届くまでにどれだけ圧縮されたかによって精度は異なります。これらのツールが実際にどのように動作するかをより広範に理解するには、画像がAI生成であるかを確認する方法に関するガイドを参照してください。
/verify コントラクトの設計
ここがApidogが真価を発揮する部分です。ルートハンドラーを記述する前に、リクエストとレスポンスをOpenAPIスキーマとして設計します。これを最初に行うことで、以下の3つの利点が得られます。両チームが合意する唯一の真実のソース、フロントエンドがすぐに呼び出せるモックサーバー、そしてバックエンドが存在した瞬間に実行できるテストスイートです。
リクエスト
`POST /verify`は、検査対象のファイルである`image`という1つのフィールドを持つ`multipart/form-data`ボディを受け取ります。これをシンプルに保ちます。オプションのクエリパラメータは後で追加できます。
レスポンス
レスポンスは、設計作業が報われる部分です。最終的な判断、信頼度、および両方の生シグナルを示す必要があり、これにより呼び出し元は決定を監査できます。以下にその形式を示します。
{
"verdict": "likely_ai",
"confidence": 0.86,
"signals": {
"provenance": {
"has_manifest": true,
"validation": "valid",
"claim_generator": "SomeImageTool/2.1",
"signature_issuer": "Some Issuing CA"
},
"classifier": {
"available": true,
"ai_score": 0.91
}
},
"explanation": "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`の3つの文字列値のいずれかです。2つではなく3つの値なのは、正直さが重要だからです。シグナルが食い違ったり、両方とも弱い場合は、「不確実(uncertain)」が正しい回答です。`confidence`は、シグナルがその判定をどれだけ強く支持するかを示す0から1の浮動小数点数です。`signals`は両方の生入力を保持しているため、呼び出し元は独自のUIを表示したり、独自のポリシーを適用したりできます。`explanation`は、サポートスタッフやログ用の人間が読める文章です。 これをOpenAPIスキーマとして表現するのは簡単です。以下に、仕様に含める応答コンポーネントを示します。
components:
schemas:
VerifyResponse:
type: object
required: [verdict, confidence, signals, checked_at]
properties:
verdict:
type: string
enum: [likely_authentic, likely_ai, uncertain]
confidence:
type: number
format: float
minimum: 0
maximum: 1
signals:
type: object
properties:
provenance:
type: object
properties:
has_manifest: { type: boolean }
validation:
type: string
enum: [valid, invalid, error, none]
claim_generator: { type: string }
signature_issuer: { type: string }
classifier:
type: object
properties:
available: { type: boolean }
ai_score:
type: number
format: float
explanation: { type: string }
checked_at: { type: string, format: date-time }
このスキーマは、Apidogのビジュアルデザイナーで直接作成することも、既存のOpenAPIファイルをインポートすることもできます。実装の前にAPIを設計することは、一般的に採用する価値のあるワークフローです。仕様先行モードのウォークスルーでは、Apidogでこれをエンドツーエンドで実行する方法を示しています。
コードのウォークスルー
ここで各要素がまとまります。以下はFastAPIアプリケーションです。入力検証、両方のシグナル呼び出し、結合関数、およびルートが含まれています。
2つのシグナルを結合する
判定関数はサービスの核心です。それはあなたのポリシーをエンコードします。来歴は、有効で存在する場合、暗号的であるためより強力なシグナルです。分類器はタイブレーカーであり、フォールバックです。ここに、明確で保守的なバージョンを示します。
# verdict.py
def combine_signals(provenance: dict, classifier: dict) -> dict:
"""来歴と分類器のシグナルを1つの判定に結合します。"""
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ツールはマニフェスト内で自身を識別する傾向があります。
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生成元を示す有効なマニフェスト。強いAIシグナル。
if has_manifest and validation == "valid" and generator_looks_ai:
return _verdict("likely_ai", 0.95,
"有効なC2PAマニフェストがAI画像ツールを示しています。")
# ケース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,
"マニフェストは本物に見えますが、分類器が"
"異議を唱えています。シグナルが競合しています。")
return _verdict("likely_authentic", 0.9,
"非AIツールからの有効なC2PAマニフェストが存在します。")
# ケース3:検証に失敗したマニフェスト。疑わしいものとして扱います。
if has_manifest and validation in ("invalid", "error"):
return _verdict("uncertain", 0.6,
"画像には検証に失敗したC2PAマニフェストが含まれています。"
"主張された履歴は未検証です。")
# ケース4:マニフェストがない場合。完全に分類器にフォールバックします。
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:マニフェストも分類器も利用できない場合。判断できません。
return _verdict("uncertain", 0.0,
"来歴データがなく、分類器も利用できませんでした。")
def _verdict(verdict: str, confidence: float, explanation: str) -> dict:
return {"verdict": verdict, "confidence": confidence,
"explanation": explanation}
5つのケースを読み進めれば、ポリシーがわかります。有効なマニフェストが優先されます。失敗したマニフェストは警告であり、偽造の証拠ではないため、「不確実(uncertain)」と判断されます。クリーンなマニフェストと高い分類器スコアが競合する場合も、一方を選ぶのではなく「不確実(uncertain)」と判断されます。そして、両方のシグナルがない場合は、推測するのではなく、信頼度ゼロで正直にその旨をサービスが伝えます。これらの閾値は、あなたのリスク許容度に合わせて調整することになります。コンテンツプラットフォームと報道機関では、その線引きが異なるでしょう。
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="ファイルが12MBの制限を超えています。")
# 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`ブロックで削除します。分類器はバイトデータから直接動作します。マニフェストがない場合や分類器がない場合でもリクエストが決してクラッシュしないことに注目してください。どちらも判定関数が処理する正常な状態です。 このように設計されたバックエンド、つまりクリーンな契約を持つ集約されたサービスは、製品がそのコア機能をAPIを通じて公開するというより広範なトレンドに合致しています。このアイデアに興味がある場合は、ソフトウェアのヘッドレス化に関する当社の記事を読む価値があります。
Apidogを使ったテストとモック
ここでワークフローの問題が発生します。フロントエンドチームはアップロードUIと結果パネルをすぐに構築したいと考えていますが、上記のバックエンドは完成し、キーを取得し、デプロイするまでに数日かかります。彼らの作業をブロックしたくありません。これがモックサーバーの目的であり、スキーマを最初に設計することが報われる場所です。
スキーマからモックサーバーを生成する
OpenAPIスキーマをApidogにインポートするか、ビジュアルデザイナーで`/verify`エンドポイントを構築します。Apidogは応答スキーマを読み取り、モックサーバーを自動的に生成します。スキーマがフィールドタイプと列挙型を定義しているため、モックは実際のAPIエンドポイントとまったく同じ形状のデータを返します。たとえば、3つの列挙値のいずれかである`verdict`、0から1の間の浮動小数点数である`confidence`、およびデータが入力された`signals`オブジェクトなどです。フロントエンドは`fetch`呼び出しをApidogのモックURLに向け、初日から現実的な応答を得ることができます。実際のバックエンドが出荷される際には、ベースURLを1つ変更するだけです。 モックは、実際のコードが存在する前に困難なケースを試す場所でもあります。重要な判定の例の応答を定義します。
- 有効なカメラマニフェストを持つ`likely_authentic`応答、
- マニフェストにAIツールの名前が記載された`likely_ai`応答、
- 分類器が利用できなかった場合の`uncertain`応答、
- `415`および`413`エラー応答。
フロントエンドは、モックに対して、エラー状態を含むあらゆる状態を構築し、スタイルを適用できます。これが、UIとAPIを順次ではなく並行して出荷する方法です。
Apidogでエンドポイントテストを実行する
バックエンドが実行されたら、Apidogで`POST /verify`のリクエストを作成します。メソッドを設定し、ローカルURLを指し、Bodyタブで`form-data`を選択し、`image`フィールドを追加し、そのタイプをFileに設定し、ディスクからテスト画像を選択します。 それを送信すると、ApidogがJSON応答を表示します。これで、一度だけのクリックではなく、繰り返し可能なチェックにするためにアサーションを追加します。
- 応答ステータスが`200`であることをアサートする、
- `verdict`が存在し、許可された3つの文字列のいずれかであることをアサートする、
- `confidence`が0から1の間の数値であることをアサートする、
- `signals.provenance.has_manifest`がブール値であることをアサートする。
複数のアップロードを順次実行する小さなテストシナリオを構築します。コンテンツクレデンシャルを持つ画像、マニフェストのない通常のJPEG、サイズ超過のファイル、`.jpg`拡張子にリネームされた非画像ファイルなどです。それぞれが、判定ロジックと入力検証の異なるブランチをチェックします。このシナリオを保存すれば、変更があるたびにスイート全体を再実行できます。または、CIに組み込んで、判定関数のリグレッションがビルドを失敗させるようにすることも可能です。`curl`でアップロードエンドポイントを手動でテストするのはすぐに飽きますが、保存されたシナリオはそうではありません。
堅牢化とエッジケース
ハッピーパスは簡単な80%です。検証サービスは残りの20%で成否が決まります。なぜなら、入力は本質的に敵対的であり、誰かが画像を偽って提示しようとしているからです。
破損または切り詰められたファイル。
ファイルが画像MIMEタイプを持っていても、実際にはゴミデータであることがあります。C2PAリーダーは解析できないデータに対して`C2paError`を発生させ、`read_provenance`関数はそれを500エラーではなくクリーンな結果に変換します。より確実にするために、処理前にPillowのようなライブラリで画像をデコードし、デコードに失敗した場合は`400`で拒否することができます。これにより、リネームされたテキストファイルのトリックも防げます。
マニフェストの欠落。
既に触れましたが、これは最も一般的なケースであり、誤解されやすいため改めて言及する価値があります。マニフェストがないことはエラーでも判断でもありません。ライブラリはメッセージが`ManifestNotFound`で始まる`C2paError`でそれを通知します。サービスはそれを特別にキャッチし、分類器に進みます。マニフェストの欠落が500エラーや「偽物」という判断を引き起こすことは決してあってはなりません。
分類器のタイムアウトまたは停止。
分類器はサードパーティのネットワーク依存であるため、時には失敗すると仮定してください。`classify_image`関数は明示的な`httpx`タイムアウトを使用し、タイムアウトまたはHTTPエラーが発生した場合は`available: False`を返します。その後、判定関数は来歴のみにフォールバックするか、マニフェストも存在しない場合は信頼度ゼロの`uncertain`を返します。エンドポイントは`200`と正直な判定で応答し続けます。遅いベンダーがあなたのサービスを停止させることはありません。
スプーフィングされたマニフェスト。
マニフェストが存在しても無効である場合、つまり不正な証明書で署名されているか、ハッシュがピクセルと一致しない場合があります。これは人々が見落としがちなケースです。常に`validation_status`をチェックしてください。空の配列はマニフェストが検証されたことを意味し、要素がある場合は検証されなかったことを意味します。判定関数は、検証に失敗したマニフェストを「不確実(uncertain)」と判断する警告として扱い、決して証拠とはみなしません。検証されていないマニフェストを信頼することは、マニフェストがまったくないよりも悪いことです。
大容量ファイルと悪用。
アップロードサイズを制限します(例では12MBを使用)。それより大きいものは、全体をメモリに読み込む前に`413`で拒否します。エンドポイントの前にレートリミットを設けてください。リクエストごとの暗号検証と外部API呼び出しは無料ではなく、開かれた検証エンドポイントは狙われやすいターゲットです。
プライバシー。
あなたはユーザーの画像を受け取ります。例のように、それらをメモリまたはすぐに削除する一時ファイルで処理し、画像バイトをログに記録しないでください。画像をサードパーティの分類器に送信する場合は、プライバシーポリシーにその旨を明記し、あなたのユースケースでそれが許可されていることを確認してください。
各シグナルが捉えるものと見逃すもの
この表は、常に心に留めておくべきメンタルモデルです。サービスが両方を使用する理由でもあります。
| シナリオ | C2PA来歴シグナル | 分類器シグナル |
|---|---|---|
| コンテンツクレデンシャルを書き込むツールからのAI画像 | 検出:マニフェストが生成元を名指し | 通常検出:アーティファクトが存在 |
| メタデータが削除されたAI画像(スクリーンショット、再アップロード) | 見逃す:読み取るマニフェストなし | 検出:ピクセルに基づいて動作、メタデータ不要 |
| コンテンツクレデンシャルに署名するカメラからの実写写真 | 確認:有効なマニフェスト、非AI生成元 | 高圧縮や編集で誤検知の可能性あり |
| メタデータがまったくない実写写真 | シグナルなし:検証するものなし | 最善の推測のみ:確率的、間違っている可能性あり |
| 偽造または改ざんされたマニフェストを持つ画像 | 検出:validation_statusが失敗をフラグ |
検出できる場合とできない場合あり、ピクセルに依存 |
| 分類器がトレーニングされていない新規生成元 | ツールがマニフェストを書き込む場合にのみ検出 | しばしば見逃す:トレーニング分布外 |
| 大幅に編集された実写写真(実写ベースのAIレタッチ) | マニフェストがあれば、編集履歴を記録 | 曖昧:部分的に合成、スコアは中程度の範囲 |
どの行を横に読んでも同じ話が見えてきます。あるシグナルが盲点である場合、別のシグナルはそうでないことが多いです。来歴は正確ですが、めったにありません。分類器は普遍的ですが、曖昧です。結合された判定は、どちらか一方の列だけよりも信頼性が高く、両方のシグナルが弱い行については、正直な`uncertain`値が存在します。
実世界のユースケース
このパターンは学術的なものではありません。直接当てはまるいくつかの場所を挙げます。
- ユーザー生成コンテンツプラットフォーム。マーケットプレイスやソーシャルアプリは、`/verify`を通じてアップロードを実行し、AI生成の可能性が高いとスコア付けされたものや、検証に失敗したマニフェストを持つものにラベルを付けたり、レビュー待ちにしたりできます。3値の判定は、「許可」、「フラグを立てる」、「人間に送る」にきれいにマッピングされます。
- 報道機関とファクトチェック。バイラル画像をチェックする編集者は、暗号化された来歴(もしあれば)と独立したモデルの推定値を1回の呼び出しで取得でき、メモに引用できる説明文も得られます。
- 保険と請求受付。顧客が写真の証拠を提出する際、検証ステップは、人間のアジャスターが時間を費やす前に、生成されたように見える画像や改ざんされたマニフェストを持つ画像にフラグを立てます。
- 内部アセットパイプライン。AI生成画像をストックライブラリから除外したり、明確にラベル付けしたりする必要があるチームは、`/verify`で取り込みを制御できます。
- 来歴を意識した出版。より多くのカメラとエディターがコンテンツクレデンシャルを採用するにつれて、CMSはアップロード時にマニフェストを読み取り、検証済みバッジを表示できます。マニフェストが存在しない場合は、分類器にフォールバックします。
共通のテーマ:自身の不確実性について正直である、高速で自動化された一次審査を望んでおり、これにより人間の注意を本当に必要な場所に集中させることができます。
結論
AI生成画像を適切に検出することは、1つの完璧なテストを見つけることではありません。それは独立したシグナルを組み合わせ、信頼度について正直であることです。
- C2PAコンテンツクレデンシャルは、強力で暗号的に検証可能な来歴シグナルを提供しますが、マニフェストはオプトインであり、簡単に剥ぎ取られるため、存在しないことがよくあります。
- ホスト型分類器は、メタデータの有無にかかわらず、あらゆる画像で機能する普遍的ですが確率的なシグナルを提供します。
- これらを小さなFastAPIサービスで組み合わせることで、監査のために信頼度スコアと両方の生シグナルが添付された、`likely_authentic`、`likely_ai`、`uncertain`の3値の判定が生成されます。
- OpenAPI契約を最初に設計することで、Apidogでエンドポイントをモックでき、フロントエンドは並行して構築でき、その後、実際のバックエンドに対して保存されたテストシナリオを実行できます。
- 完璧な検出器はありません。2つのシグナルは信頼度を高めますが、不確実性を排除するものではありません。だからこそ、`uncertain`という判定は機能であり、欠陥ではありません。
これを実際に構築するには、`/verify`スキーマを設計し、モックサーバーを生成し、エンドポイントテストを1か所で実行します。Apidogをダウンロードして、APIを構築しながら設計、モック、テストを行い、その後、ベースURLを一度変更するだけでモックから実際のバックエンドに移行できます。
