deepshield-api / backend /detectors /sightengine.py
vanshpatil16's picture
DeepShield API — 7-detector image deepfake detection
6be0044
Raw
History Blame Contribute Delete
4.57 kB
"""Sightengine AI image detector with multi-key failover support.
Docs: https://sightengine.com/docs/detect-ai-generated-images
Uses models: genai (AI-generated images) + deepfake (face swaps)
Supports multiple API keys for automatic failover on rate limits (HTTP 429).
Keys rotate round-robin to distribute load.
Retry policy: up to MAX_PASSES full sweeps through all keys before giving up.
Between passes a short back-off delay is applied so transient rate limits clear.
Response shape:
{
"type": {"ai_generated": 0.95},
"media": {...}
}
"""
from __future__ import annotations
import asyncio
import logging
import httpx
from backend.core.config import settings
from backend.core.schema import DetectionResult, Verdict
from .base import BaseDetector
log = logging.getLogger(__name__)
_ENDPOINT = "https://api.sightengine.com/1.0/check.json"
_MAX_PASSES = 3 # full sweeps through all keys
_PASS_DELAY = 2.0 # seconds to wait between passes
class SightengineDetector(BaseDetector):
name = "sightengine"
_current_key_index = 0 # Class-level state for round-robin
_lock = asyncio.Lock() # Protect index from concurrent access
async def _try_key(self, client: httpx.AsyncClient, image_path: str, user: str, secret: str):
"""Attempt one API call. Returns parsed data dict or raises."""
with open(image_path, "rb") as f:
resp = await client.post(
_ENDPOINT,
data={"models": "genai,deepfake", "api_user": user, "api_secret": secret},
files={"media": f},
)
if resp.status_code == 429:
raise Exception("rate_limited")
if resp.status_code != 200:
raise Exception(f"HTTP {resp.status_code}: {resp.text[:120]}")
return resp.json()
async def detect(self, image_path: str) -> DetectionResult:
pairs = settings.sightengine_pairs
if not pairs:
return self._error_result("SIGHTENGINE credentials not set")
last_error = "unknown"
async with httpx.AsyncClient(timeout=settings.request_timeout) as client:
for pass_num in range(_MAX_PASSES):
if pass_num > 0:
log.warning(
"Sightengine pass %d/%d after %.1fs delay (last error: %s)",
pass_num + 1, _MAX_PASSES, _PASS_DELAY, last_error,
)
await asyncio.sleep(_PASS_DELAY)
for _ in range(len(pairs)):
async with self._lock:
current_idx = self._current_key_index % len(pairs)
self._current_key_index = (current_idx + 1) % len(pairs)
user, secret = pairs[current_idx]
try:
data = await self._try_key(client, image_path, user, secret)
except Exception as exc:
last_error = str(exc)
log.debug("Sightengine key[%d] failed: %s", current_idx, last_error)
continue
# Success — parse and return
genai_score = float(data.get("type", {}).get("ai_generated", 0.5))
deepfake_score = (
float(data.get("faces", [{}])[0].get("deepfake", 0.0))
if data.get("faces") else 0.0
)
p_fake = max(genai_score, deepfake_score)
verdict = self._verdict_from_p_fake(p_fake)
evidence = [f"Sightengine genai score: {genai_score:.1%}"]
if deepfake_score > 0.1:
evidence.append(f"Sightengine deepfake score: {deepfake_score:.1%}")
generator = None
if genai_score > 0.5:
generator = data.get("type", {}).get("generator")
if pass_num > 0:
log.info("Sightengine succeeded on pass %d", pass_num + 1)
return DetectionResult(
detector=self.name,
p_fake=p_fake,
verdict=verdict,
confidence=abs(p_fake - 0.5) * 2,
evidence=evidence,
raw=data,
generator=generator,
)
return self._error_result(
f"Sightengine: all {len(pairs)} keys × {_MAX_PASSES} passes failed — last: {last_error}"
)