File size: 3,142 Bytes
bb5ec1b
 
 
 
 
 
 
 
5ccd8c3
bb5ec1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ccd8c3
 
 
 
 
 
 
 
 
 
bb5ec1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ccd8c3
bb5ec1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import io

from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image, UnidentifiedImageError
from pydantic import BaseModel

from .config import get_settings
from .detector import NoFaceDetectedError, Prediction, get_detector

settings = get_settings()

app = FastAPI(
    title="DeepVerify detection service",
    description=(
        "Image deepfake detection using the pre-trained yermandy/deepfake-detection "
        "checkpoint (CLIP ViT-L/14 + LN-tuning, MIT). No model is trained here."
    ),
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=[o.strip() for o in settings.allowed_origins.split(",")],
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)


def run_prediction(image: Image.Image) -> Prediction:
    """Detect and score, in-process.

    A seam, not indirection: on ZeroGPU the forward pass has to happen inside a
    GPU-scoped function, so the Space replaces this with its own implementation
    and the route below stays identical in both deployments.
    """
    return get_detector().predict(image)


class Health(BaseModel):
    status: str
    model_id: str
    weights_sha256: str
    device: str
    threshold: float


class DetectionResponse(BaseModel):
    is_deepfake: bool
    label: str
    fake_probability: float
    real_probability: float
    threshold: float
    face_box: tuple[int, int, int, int]
    face_confidence: float
    model_id: str


@app.get("/health", response_model=Health)
def health() -> Health:
    detector = get_detector()
    return Health(
        status="ok",
        model_id=detector.model_id,
        weights_sha256=detector.weights_sha256,
        device=str(detector.device),
        threshold=detector.threshold,
    )


@app.post("/v1/detect", response_model=DetectionResponse)
async def detect(file: UploadFile = File(...)) -> DetectionResponse:
    contents = await file.read()
    if len(contents) > settings.max_upload_bytes:
        raise HTTPException(
            status_code=413,
            detail=f"file exceeds {settings.max_upload_bytes // (1024 * 1024)}MB limit",
        )

    try:
        image = Image.open(io.BytesIO(contents))
        image.load()
    except (UnidentifiedImageError, OSError):
        raise HTTPException(status_code=400, detail="file is not a readable image")

    detector = get_detector()
    try:
        prediction = run_prediction(image)
    except NoFaceDetectedError:
        raise HTTPException(
            status_code=422,
            detail=(
                "no face detected. This detector is trained on face crops, so a score "
                "on a face-free image would be meaningless."
            ),
        )

    return DetectionResponse(
        is_deepfake=prediction.is_deepfake,
        label=prediction.label,
        fake_probability=prediction.fake_probability,
        real_probability=prediction.real_probability,
        threshold=prediction.threshold,
        face_box=prediction.face_box,
        face_confidence=prediction.face_confidence,
        model_id=detector.model_id,
    )