CC commited on
Commit
198f874
·
1 Parent(s): 928ffea

Deploy DeepFake video classifier to Hugging Face Spaces

Browse files

- FastAPI backend for video deepfake detection
- EfficientNet model for classification
- Face detection preprocessing with OpenCV
- Git LFS for model file (50MB)
- Configured for HF Spaces on port 7860

.dockerignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ .git
4
+ .gitignore
5
+ *.pth
6
+ models/
7
+ temp/
8
+ *.log
9
+ .DS_Store
10
+ .env
11
+ *.backup
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ libsm6 \
9
+ libxext6 \
10
+ libxrender-dev \
11
+ libgomp1 \
12
+ wget \
13
+ curl \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ COPY app ./app
20
+
21
+ RUN mkdir -p /tmp/uploads
22
+
23
+ ENV PYTHONPATH=/app
24
+ ENV ENVIRONMENT=production
25
+ ENV DEVICE=cpu
26
+ ENV HF_HOME=/tmp/huggingface-cache
27
+ ENV SPACE_AUTHOR=oo01
28
+
29
+ EXPOSE 7860
30
+
31
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
app/__init__.py ADDED
File without changes
app/__pycache__/config.cpython-310.pyc ADDED
Binary file (1.23 kB). View file
 
app/__pycache__/main.cpython-310.pyc ADDED
Binary file (4.83 kB). View file
 
app/__pycache__/model.cpython-310.pyc ADDED
Binary file (3.41 kB). View file
 
app/__pycache__/utils.cpython-310.pyc ADDED
Binary file (3.72 kB). View file
 
app/config.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ import torch
4
+ from dotenv import load_dotenv
5
+ import tempfile
6
+
7
+ load_dotenv()
8
+
9
+ # Model configuration - Use temp directory for HF Spaces
10
+ if os.environ.get("SPACE_AUTHOR"):
11
+ MODEL_PATH = os.environ.get(
12
+ "MODEL_PATH",
13
+ str(Path(tempfile.gettempdir()) / "models" / "ffpp_efficientnet_best.pth")
14
+ )
15
+ else:
16
+ MODEL_PATH = os.environ.get(
17
+ "MODEL_PATH",
18
+ str(Path("models") / "ffpp_efficientnet_best.pth")
19
+ )
20
+
21
+ if os.environ.get("FORCE_CPU", "false").lower() == "true":
22
+ DEVICE = "cpu"
23
+ elif torch.cuda.is_available():
24
+ DEVICE = "cuda"
25
+ elif torch.backends.mps.is_available():
26
+ DEVICE = "mps"
27
+ else:
28
+ DEVICE = "cpu"
29
+
30
+ PREDICTION_THRESHOLD = float(os.environ.get("PREDICTION_THRESHOLD", 0.5))
31
+
32
+ FRAMES_PER_CLIP = int(os.environ.get("FRAMES_PER_CLIP", 16))
33
+ IMG_SIZE = int(os.environ.get("IMG_SIZE", 224))
34
+ MAX_FILE_SIZE = int(os.environ.get("MAX_FILE_SIZE", 50 * 1024 * 1024))
35
+ ALLOWED_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv"}
36
+
37
+ ALLOWED_ORIGINS = os.environ.get(
38
+ "ALLOWED_ORIGINS",
39
+ "http://localhost:5173,http://localhost:3000,https://your-frontend.onrender.com"
40
+ ).split(",")
41
+
42
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
43
+ IMAGENET_STD = [0.229, 0.224, 0.225]
44
+
45
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
46
+ IS_PRODUCTION = os.environ.get("ENVIRONMENT", "development") == "production"
47
+
48
+ RATE_LIMIT_PER_MINUTE = int(os.environ.get("RATE_LIMIT_PER_MINUTE", 10))
app/config.py.backup ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ import torch
4
+ from dotenv import load_dotenv
5
+ import os
6
+
7
+ # Load environment variables
8
+ load_dotenv()
9
+
10
+ # Model configuration - Use environment variable for model path
11
+ MODEL_PATH = os.environ.get(
12
+ "MODEL_PATH",
13
+ str( Path("models") / "ffpp_efficientnet_best.pth")
14
+ )
15
+
16
+ # Device configuration with fallback
17
+ if os.environ.get("FORCE_CPU", "false").lower() == "true":
18
+ DEVICE = "cpu"
19
+ elif torch.cuda.is_available():
20
+ DEVICE = "cuda"
21
+ elif torch.backends.mps.is_available():
22
+ DEVICE = "mps"
23
+ else:
24
+ DEVICE = "cpu"
25
+
26
+ # Prediction threshold (can be overridden by env)
27
+ PREDICTION_THRESHOLD = float(os.environ.get("PREDICTION_THRESHOLD", 0.5))
28
+
29
+ # Video processing
30
+ FRAMES_PER_CLIP = int(os.environ.get("FRAMES_PER_CLIP", 16))
31
+ IMG_SIZE = int(os.environ.get("IMG_SIZE", 224))
32
+ MAX_FILE_SIZE = int(os.environ.get("MAX_FILE_SIZE", 50 * 1024 * 1024)) # 50MB
33
+ ALLOWED_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv"}
34
+
35
+ # CORS - Get from environment
36
+ ALLOWED_ORIGINS = os.environ.get(
37
+ "ALLOWED_ORIGINS",
38
+ "http://localhost:5173,http://localhost:3000,https://your-frontend.onrender.com"
39
+ ).split(",")
40
+
41
+ # ImageNet normalization
42
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
43
+ IMAGENET_STD = [0.229, 0.224, 0.225]
44
+
45
+ # Logging configuration
46
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
47
+ IS_PRODUCTION = os.environ.get("ENVIRONMENT", "development") == "production"
48
+
49
+ # Rate limiting (optional)
50
+ RATE_LIMIT_PER_MINUTE = int(os.environ.get("RATE_LIMIT_PER_MINUTE", 10))
51
+
52
+
53
+ # import os
54
+ # from pathlib import Path
55
+ # import torch
56
+
57
+ # # Model configuration
58
+ # MODEL_PATH = Path(__file__).parent.parent / "models" / "ffpp_efficientnet_best.pth"
59
+
60
+ # if torch.cuda.is_available():
61
+ # DEVICE = "cuda"
62
+ # elif torch.backends.mps.is_available():
63
+ # DEVICE = "mps"
64
+ # else:
65
+ # DEVICE = "cpu"
66
+
67
+ # # Prediction threshold (based on notebook testing - 0.5 works well)
68
+ # PREDICTION_THRESHOLD = 0.5
69
+
70
+ # # Video processing
71
+ # FRAMES_PER_CLIP = 16
72
+ # IMG_SIZE = 224
73
+ # MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
74
+ # ALLOWED_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv"}
75
+
76
+ # # ImageNet normalization
77
+ # IMAGENET_MEAN = [0.485, 0.456, 0.406]
78
+ # IMAGENET_STD = [0.229, 0.224, 0.225]
app/download_model.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ def download_model():
6
+ """Copy model file from various possible locations"""
7
+
8
+ # Destination path in temp directory
9
+ dest_path = Path("/tmp/models/ffpp_efficientnet_best.pth")
10
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
11
+
12
+ # Check if model already exists
13
+ if dest_path.exists():
14
+ print(f"✓ Model already exists at {dest_path}")
15
+ return str(dest_path)
16
+
17
+ # Check different possible source locations
18
+ possible_sources = []
19
+
20
+ # If running on HF Space, check if we have a models folder
21
+ if os.environ.get("SPACE_AUTHOR"):
22
+ possible_sources.append(Path("/app/models/ffpp_efficientnet_best.pth"))
23
+
24
+ # Check local models folder
25
+ possible_sources.append(Path("models/ffpp_efficientnet_best.pth"))
26
+
27
+ # Check parent backend folder (for local dev)
28
+ possible_sources.append(Path("../backend/models/ffpp_efficientnet_best.pth"))
29
+
30
+ # Try each source
31
+ for src_path in possible_sources:
32
+ if src_path.exists():
33
+ print(f"✓ Found model at {src_path}")
34
+ shutil.copy2(src_path, dest_path)
35
+ print(f"✓ Model copied to {dest_path}")
36
+ return str(dest_path)
37
+
38
+ # Model not found - will cause error but let the app handle it
39
+ print("⚠️ WARNING: Model file not found! DeepFake detection will not work.")
40
+ return None
41
+
42
+ # Run when imported
43
+ if __name__ == "__main__":
44
+ download_model()
45
+ else:
46
+ download_model()
app/main.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import JSONResponse
4
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
5
+ import tempfile
6
+ import shutil
7
+ from pathlib import Path
8
+ import logging
9
+ import time
10
+ import os
11
+ from contextlib import asynccontextmanager
12
+
13
+ from .download_model import download_model
14
+
15
+ # Download model at startup
16
+ download_model()
17
+
18
+ from .config import (
19
+ MODEL_PATH, DEVICE, FRAMES_PER_CLIP, IMG_SIZE,
20
+ MAX_FILE_SIZE, ALLOWED_EXTENSIONS, PREDICTION_THRESHOLD,
21
+ ALLOWED_ORIGINS, LOG_LEVEL, IS_PRODUCTION
22
+ )
23
+ from .model import DeepFakeModel
24
+ from .utils import video_to_tensor, save_uploaded_video
25
+
26
+ # Setup logging
27
+ logging.basicConfig(
28
+ level=getattr(logging, LOG_LEVEL),
29
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Global model variable
34
+ model = None
35
+
36
+ @asynccontextmanager
37
+ async def lifespan(app: FastAPI):
38
+ """Lifespan context manager for startup/shutdown events"""
39
+ global model
40
+ # Startup
41
+ logger.info("Starting up...")
42
+ try:
43
+ if not Path(MODEL_PATH).exists():
44
+ logger.error(f"Model file not found at {MODEL_PATH}")
45
+ raise FileNotFoundError(f"Model not found at {MODEL_PATH}")
46
+
47
+ model = DeepFakeModel(MODEL_PATH, DEVICE)
48
+ logger.info(f"Model loaded successfully on {DEVICE}")
49
+ except Exception as e:
50
+ logger.error(f"Failed to load model: {e}")
51
+ model = None
52
+
53
+ yield
54
+
55
+ # Shutdown
56
+ logger.info("Shutting down...")
57
+
58
+ # Initialize FastAPI with lifespan
59
+ app = FastAPI(
60
+ title="DeepFake Detection API",
61
+ version="1.0.0",
62
+ lifespan=lifespan
63
+ )
64
+
65
+ # Add security middleware in production
66
+ if IS_PRODUCTION:
67
+ app.add_middleware(
68
+ TrustedHostMiddleware,
69
+ allowed_hosts=os.environ.get("ALLOWED_HOSTS", "*").split(",")
70
+ )
71
+
72
+ # CORS middleware
73
+ app.add_middleware(
74
+ CORSMiddleware,
75
+ allow_origins=ALLOWED_ORIGINS,
76
+ allow_credentials=True,
77
+ allow_methods=["GET", "POST", "OPTIONS"],
78
+ allow_headers=["*"],
79
+ max_age=3600,
80
+ )
81
+
82
+ # Rate limiting middleware (simple version)
83
+ request_counts = {}
84
+
85
+ @app.middleware("http")
86
+ async def rate_limit_middleware(request: Request, call_next):
87
+ if IS_PRODUCTION:
88
+ client_ip = request.client.host
89
+ current_minute = int(time.time() / 60)
90
+ key = f"{client_ip}:{current_minute}"
91
+
92
+ from .config import RATE_LIMIT_PER_MINUTE
93
+ request_counts[key] = request_counts.get(key, 0) + 1
94
+
95
+ if request_counts[key] > RATE_LIMIT_PER_MINUTE:
96
+ return JSONResponse(
97
+ status_code=429,
98
+ content={"detail": "Rate limit exceeded. Please try again later."}
99
+ )
100
+
101
+ # Clean old entries
102
+ if len(request_counts) > 1000:
103
+ old_keys = [k for k in request_counts.keys()
104
+ if int(k.split(':')[1]) < current_minute - 1]
105
+ for k in old_keys:
106
+ del request_counts[k]
107
+
108
+ response = await call_next(request)
109
+ return response
110
+
111
+ @app.get("/")
112
+ async def root():
113
+ """Root endpoint with API info."""
114
+ return {
115
+ "name": "DeepFake Detection API",
116
+ "version": "1.0.0",
117
+ "status": "running",
118
+ "endpoints": [
119
+ "/health - Health check",
120
+ "/predict - Upload video for detection",
121
+ "/predict-batch - Batch prediction"
122
+ ]
123
+ }
124
+
125
+ @app.get("/health")
126
+ async def health_check():
127
+ """Health check endpoint for Render."""
128
+ return {
129
+ "status": "healthy" if model else "degraded",
130
+ "device": DEVICE,
131
+ "model_loaded": model is not None,
132
+ "threshold": PREDICTION_THRESHOLD,
133
+ "environment": "production" if IS_PRODUCTION else "development"
134
+ }
135
+
136
+ @app.post("/predict")
137
+ async def predict(file: UploadFile = File(...)):
138
+ """Predict if uploaded video is REAL or FAKE."""
139
+ if model is None:
140
+ raise HTTPException(status_code=503, detail="Model not loaded")
141
+
142
+ # Validate file extension
143
+ file_ext = Path(file.filename).suffix.lower()
144
+ if file_ext not in ALLOWED_EXTENSIONS:
145
+ raise HTTPException(
146
+ status_code=400,
147
+ detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
148
+ )
149
+
150
+ # Validate file size
151
+ file.file.seek(0, 2)
152
+ file_size = file.file.tell()
153
+ file.file.seek(0)
154
+
155
+ if file_size > MAX_FILE_SIZE:
156
+ raise HTTPException(
157
+ status_code=400,
158
+ detail=f"File too large. Max size: {MAX_FILE_SIZE // (1024*1024)}MB"
159
+ )
160
+
161
+ temp_dir = tempfile.mkdtemp()
162
+
163
+ try:
164
+ # Save uploaded file
165
+ video_path = save_uploaded_video(file, temp_dir)
166
+ logger.info(f"Processing video: {file.filename} (size: {file_size} bytes)")
167
+
168
+ # Convert video to tensor
169
+ video_tensor = video_to_tensor(
170
+ video_path,
171
+ num_frames=FRAMES_PER_CLIP,
172
+ img_size=IMG_SIZE
173
+ )
174
+
175
+ # Make prediction with configured threshold
176
+ result = model.predict(video_tensor, threshold=PREDICTION_THRESHOLD)
177
+ result["filename"] = file.filename
178
+
179
+ logger.info(f"Prediction for {file.filename}: {result['prediction']} (conf={result['confidence']})")
180
+
181
+ return JSONResponse(content=result)
182
+
183
+ except ValueError as e:
184
+ raise HTTPException(status_code=400, detail=str(e))
185
+ except Exception as e:
186
+ logger.error(f"Error processing video: {e}")
187
+ raise HTTPException(status_code=500, detail=f"Error processing video: {str(e)}")
188
+
189
+ finally:
190
+ # Cleanup
191
+ shutil.rmtree(temp_dir, ignore_errors=True)
192
+
193
+
194
+
195
+ # from fastapi import FastAPI, UploadFile, File, HTTPException
196
+ # from fastapi.middleware.cors import CORSMiddleware
197
+ # from fastapi.responses import JSONResponse
198
+ # import tempfile
199
+ # import shutil
200
+ # from pathlib import Path
201
+ # import logging
202
+ # # /opt/anaconda3/envs/deepfake/bin/python -m uvicorn app.main:app --reload
203
+ # from .config import MODEL_PATH, DEVICE, FRAMES_PER_CLIP, IMG_SIZE, MAX_FILE_SIZE, ALLOWED_EXTENSIONS, PREDICTION_THRESHOLD
204
+ # from .model import DeepFakeModel
205
+ # from .utils import video_to_tensor, save_uploaded_video
206
+
207
+ # # Setup logging
208
+ # logging.basicConfig(level=logging.INFO)
209
+ # logger = logging.getLogger(__name__)
210
+
211
+ # # Initialize FastAPI
212
+ # app = FastAPI(title="DeepFake Detection API", version="1.0.0")
213
+
214
+ # # CORS middleware
215
+ # app.add_middleware(
216
+ # CORSMiddleware,
217
+ # allow_origins=["http://localhost:5173", "http://localhost:3000"], # React dev servers
218
+ # allow_credentials=True,
219
+ # allow_methods=["*"],
220
+ # allow_headers=["*"],
221
+ # )
222
+
223
+ # # Load model (with error handling)
224
+ # model = None
225
+
226
+ # @app.on_event("startup")
227
+ # async def load_model():
228
+ # global model
229
+ # try:
230
+ # if not MODEL_PATH.exists():
231
+ # logger.error(f"Model file not found at {MODEL_PATH}")
232
+ # raise FileNotFoundError(f"Model not found at {MODEL_PATH}")
233
+
234
+ # model = DeepFakeModel(str(MODEL_PATH), DEVICE)
235
+ # logger.info("Model loaded successfully")
236
+ # except Exception as e:
237
+ # logger.error(f"Failed to load model: {e}")
238
+ # model = None
239
+
240
+
241
+ # @app.get("/health")
242
+ # async def health_check():
243
+ # """Health check endpoint."""
244
+ # return {
245
+ # "status": "healthy" if model else "model_not_loaded",
246
+ # "device": DEVICE,
247
+ # "model_loaded": model is not None,
248
+ # "threshold": PREDICTION_THRESHOLD
249
+ # }
250
+
251
+
252
+ # @app.post("/predict")
253
+ # async def predict(file: UploadFile = File(...)):
254
+ # """
255
+ # Predict if uploaded video is REAL or FAKE.
256
+
257
+ # Args:
258
+ # file: Video file (mp4, avi, mov, mkv)
259
+
260
+ # Returns:
261
+ # Prediction result with confidence scores
262
+ # """
263
+ # if model is None:
264
+ # raise HTTPException(status_code=503, detail="Model not loaded")
265
+
266
+ # # Validate file extension
267
+ # file_ext = Path(file.filename).suffix.lower()
268
+ # if file_ext not in ALLOWED_EXTENSIONS:
269
+ # raise HTTPException(
270
+ # status_code=400,
271
+ # detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
272
+ # )
273
+
274
+ # # Validate file size
275
+ # file.file.seek(0, 2)
276
+ # file_size = file.file.tell()
277
+ # file.file.seek(0)
278
+
279
+ # if file_size > MAX_FILE_SIZE:
280
+ # raise HTTPException(
281
+ # status_code=400,
282
+ # detail=f"File too large. Max size: {MAX_FILE_SIZE // (1024*1024)}MB"
283
+ # )
284
+
285
+ # temp_dir = tempfile.mkdtemp()
286
+
287
+ # try:
288
+ # # Save uploaded file
289
+ # video_path = save_uploaded_video(file, temp_dir)
290
+ # logger.info(f"Processing video: {file.filename}")
291
+
292
+ # # Convert video to tensor
293
+ # video_tensor = video_to_tensor(
294
+ # video_path,
295
+ # num_frames=FRAMES_PER_CLIP,
296
+ # img_size=IMG_SIZE
297
+ # )
298
+
299
+ # # Make prediction with configured threshold
300
+ # result = model.predict(video_tensor, threshold=PREDICTION_THRESHOLD)
301
+ # result["filename"] = file.filename
302
+
303
+ # logger.info(f"Prediction for {file.filename}: {result['prediction']} (conf={result['confidence']})")
304
+
305
+ # return JSONResponse(content=result)
306
+
307
+ # except ValueError as e:
308
+ # raise HTTPException(status_code=400, detail=str(e))
309
+ # except Exception as e:
310
+ # logger.error(f"Error processing video: {e}")
311
+ # raise HTTPException(status_code=500, detail=f"Error processing video: {str(e)}")
312
+
313
+ # finally:
314
+ # # Cleanup
315
+ # shutil.rmtree(temp_dir, ignore_errors=True)
316
+
317
+
318
+ # @app.post("/predict-batch")
319
+ # async def predict_batch(files: list[UploadFile] = File(...)):
320
+ # """
321
+ # Predict for multiple videos.
322
+ # """
323
+ # if model is None:
324
+ # raise HTTPException(status_code=503, detail="Model not loaded")
325
+
326
+ # results = []
327
+
328
+ # for file in files:
329
+ # file_ext = Path(file.filename).suffix.lower()
330
+ # if file_ext not in ALLOWED_EXTENSIONS:
331
+ # results.append({
332
+ # "filename": file.filename,
333
+ # "error": f"Unsupported file type: {file_ext}"
334
+ # })
335
+ # continue
336
+
337
+ # temp_dir = tempfile.mkdtemp()
338
+
339
+ # try:
340
+ # video_path = save_uploaded_video(file, temp_dir)
341
+ # video_tensor = video_to_tensor(video_path, FRAMES_PER_CLIP, IMG_SIZE)
342
+ # result = model.predict(video_tensor, threshold=PREDICTION_THRESHOLD)
343
+ # result["filename"] = file.filename
344
+ # results.append(result)
345
+ # except Exception as e:
346
+ # results.append({
347
+ # "filename": file.filename,
348
+ # "error": str(e)
349
+ # })
350
+ # finally:
351
+ # shutil.rmtree(temp_dir, ignore_errors=True)
352
+
353
+ # return JSONResponse(content={"results": results})
354
+
355
+
356
+ # # Optional: Endpoint to test with custom threshold
357
+ # @app.post("/predict-custom")
358
+ # async def predict_custom(
359
+ # file: UploadFile = File(...),
360
+ # threshold: float = PREDICTION_THRESHOLD
361
+ # ):
362
+ # """
363
+ # Predict with custom threshold.
364
+
365
+ # Args:
366
+ # file: Video file (mp4, avi, mov, mkv)
367
+ # threshold: Custom threshold between 0 and 1 (default: 0.4)
368
+ # """
369
+ # if model is None:
370
+ # raise HTTPException(status_code=503, detail="Model not loaded")
371
+
372
+ # # Validate threshold
373
+ # if threshold < 0 or threshold > 1:
374
+ # raise HTTPException(
375
+ # status_code=400,
376
+ # detail="Threshold must be between 0 and 1"
377
+ # )
378
+
379
+ # # Validate file extension
380
+ # file_ext = Path(file.filename).suffix.lower()
381
+ # if file_ext not in ALLOWED_EXTENSIONS:
382
+ # raise HTTPException(
383
+ # status_code=400,
384
+ # detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
385
+ # )
386
+
387
+ # # Validate file size
388
+ # file.file.seek(0, 2)
389
+ # file_size = file.file.tell()
390
+ # file.file.seek(0)
391
+
392
+ # if file_size > MAX_FILE_SIZE:
393
+ # raise HTTPException(
394
+ # status_code=400,
395
+ # detail=f"File too large. Max size: {MAX_FILE_SIZE // (1024*1024)}MB"
396
+ # )
397
+
398
+ # temp_dir = tempfile.mkdtemp()
399
+
400
+ # try:
401
+ # # Save uploaded file
402
+ # video_path = save_uploaded_video(file, temp_dir)
403
+ # logger.info(f"Processing video: {file.filename}")
404
+
405
+ # # Convert video to tensor
406
+ # video_tensor = video_to_tensor(
407
+ # video_path,
408
+ # num_frames=FRAMES_PER_CLIP,
409
+ # img_size=IMG_SIZE
410
+ # )
411
+
412
+ # # Make prediction with custom threshold
413
+ # result = model.predict(video_tensor, threshold=threshold)
414
+ # result["filename"] = file.filename
415
+
416
+ # logger.info(f"Prediction for {file.filename}: {result['prediction']} (conf={result['confidence']}, threshold={threshold})")
417
+
418
+ # return JSONResponse(content=result)
419
+
420
+ # except ValueError as e:
421
+ # raise HTTPException(status_code=400, detail=str(e))
422
+ # except Exception as e:
423
+ # logger.error(f"Error processing video: {e}")
424
+ # raise HTTPException(status_code=500, detail=f"Error processing video: {str(e)}")
425
+
426
+ # finally:
427
+ # # Cleanup
428
+ # shutil.rmtree(temp_dir, ignore_errors=True)
app/main.py.backup ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import JSONResponse
4
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
5
+ import tempfile
6
+ import shutil
7
+ from pathlib import Path
8
+ import logging
9
+ import time
10
+ import os
11
+ from contextlib import asynccontextmanager
12
+
13
+ from .config import (
14
+ MODEL_PATH, DEVICE, FRAMES_PER_CLIP, IMG_SIZE,
15
+ MAX_FILE_SIZE, ALLOWED_EXTENSIONS, PREDICTION_THRESHOLD,
16
+ ALLOWED_ORIGINS, LOG_LEVEL, IS_PRODUCTION
17
+ )
18
+ from .model import DeepFakeModel
19
+ from .utils import video_to_tensor, save_uploaded_video
20
+
21
+ # Setup logging
22
+ logging.basicConfig(
23
+ level=getattr(logging, LOG_LEVEL),
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Global model variable
29
+ model = None
30
+
31
+ @asynccontextmanager
32
+ async def lifespan(app: FastAPI):
33
+ """Lifespan context manager for startup/shutdown events"""
34
+ global model
35
+ # Startup
36
+ logger.info("Starting up...")
37
+ try:
38
+ if not Path(MODEL_PATH).exists():
39
+ logger.error(f"Model file not found at {MODEL_PATH}")
40
+ raise FileNotFoundError(f"Model not found at {MODEL_PATH}")
41
+
42
+ model = DeepFakeModel(MODEL_PATH, DEVICE)
43
+ logger.info(f"Model loaded successfully on {DEVICE}")
44
+ except Exception as e:
45
+ logger.error(f"Failed to load model: {e}")
46
+ model = None
47
+
48
+ yield
49
+
50
+ # Shutdown
51
+ logger.info("Shutting down...")
52
+
53
+ # Initialize FastAPI with lifespan
54
+ app = FastAPI(
55
+ title="DeepFake Detection API",
56
+ version="1.0.0",
57
+ lifespan=lifespan
58
+ )
59
+
60
+ # Add security middleware in production
61
+ if IS_PRODUCTION:
62
+ app.add_middleware(
63
+ TrustedHostMiddleware,
64
+ allowed_hosts=os.environ.get("ALLOWED_HOSTS", "*").split(",")
65
+ )
66
+
67
+ # CORS middleware
68
+ app.add_middleware(
69
+ CORSMiddleware,
70
+ allow_origins=ALLOWED_ORIGINS,
71
+ allow_credentials=True,
72
+ allow_methods=["GET", "POST", "OPTIONS"],
73
+ allow_headers=["*"],
74
+ max_age=3600,
75
+ )
76
+
77
+ # Rate limiting middleware (simple version)
78
+ request_counts = {}
79
+
80
+ @app.middleware("http")
81
+ async def rate_limit_middleware(request: Request, call_next):
82
+ if IS_PRODUCTION:
83
+ client_ip = request.client.host
84
+ current_minute = int(time.time() / 60)
85
+ key = f"{client_ip}:{current_minute}"
86
+
87
+ from .config import RATE_LIMIT_PER_MINUTE
88
+ request_counts[key] = request_counts.get(key, 0) + 1
89
+
90
+ if request_counts[key] > RATE_LIMIT_PER_MINUTE:
91
+ return JSONResponse(
92
+ status_code=429,
93
+ content={"detail": "Rate limit exceeded. Please try again later."}
94
+ )
95
+
96
+ # Clean old entries
97
+ if len(request_counts) > 1000:
98
+ old_keys = [k for k in request_counts.keys()
99
+ if int(k.split(':')[1]) < current_minute - 1]
100
+ for k in old_keys:
101
+ del request_counts[k]
102
+
103
+ response = await call_next(request)
104
+ return response
105
+
106
+ @app.get("/")
107
+ async def root():
108
+ """Root endpoint with API info."""
109
+ return {
110
+ "name": "DeepFake Detection API",
111
+ "version": "1.0.0",
112
+ "status": "running",
113
+ "endpoints": [
114
+ "/health - Health check",
115
+ "/predict - Upload video for detection",
116
+ "/predict-batch - Batch prediction"
117
+ ]
118
+ }
119
+
120
+ @app.get("/health")
121
+ async def health_check():
122
+ """Health check endpoint for Render."""
123
+ return {
124
+ "status": "healthy" if model else "degraded",
125
+ "device": DEVICE,
126
+ "model_loaded": model is not None,
127
+ "threshold": PREDICTION_THRESHOLD,
128
+ "environment": "production" if IS_PRODUCTION else "development"
129
+ }
130
+
131
+ @app.post("/predict")
132
+ async def predict(file: UploadFile = File(...)):
133
+ """Predict if uploaded video is REAL or FAKE."""
134
+ if model is None:
135
+ raise HTTPException(status_code=503, detail="Model not loaded")
136
+
137
+ # Validate file extension
138
+ file_ext = Path(file.filename).suffix.lower()
139
+ if file_ext not in ALLOWED_EXTENSIONS:
140
+ raise HTTPException(
141
+ status_code=400,
142
+ detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
143
+ )
144
+
145
+ # Validate file size
146
+ file.file.seek(0, 2)
147
+ file_size = file.file.tell()
148
+ file.file.seek(0)
149
+
150
+ if file_size > MAX_FILE_SIZE:
151
+ raise HTTPException(
152
+ status_code=400,
153
+ detail=f"File too large. Max size: {MAX_FILE_SIZE // (1024*1024)}MB"
154
+ )
155
+
156
+ temp_dir = tempfile.mkdtemp()
157
+
158
+ try:
159
+ # Save uploaded file
160
+ video_path = save_uploaded_video(file, temp_dir)
161
+ logger.info(f"Processing video: {file.filename} (size: {file_size} bytes)")
162
+
163
+ # Convert video to tensor
164
+ video_tensor = video_to_tensor(
165
+ video_path,
166
+ num_frames=FRAMES_PER_CLIP,
167
+ img_size=IMG_SIZE
168
+ )
169
+
170
+ # Make prediction with configured threshold
171
+ result = model.predict(video_tensor, threshold=PREDICTION_THRESHOLD)
172
+ result["filename"] = file.filename
173
+
174
+ logger.info(f"Prediction for {file.filename}: {result['prediction']} (conf={result['confidence']})")
175
+
176
+ return JSONResponse(content=result)
177
+
178
+ except ValueError as e:
179
+ raise HTTPException(status_code=400, detail=str(e))
180
+ except Exception as e:
181
+ logger.error(f"Error processing video: {e}")
182
+ raise HTTPException(status_code=500, detail=f"Error processing video: {str(e)}")
183
+
184
+ finally:
185
+ # Cleanup
186
+ shutil.rmtree(temp_dir, ignore_errors=True)
187
+
188
+
189
+
190
+ # from fastapi import FastAPI, UploadFile, File, HTTPException
191
+ # from fastapi.middleware.cors import CORSMiddleware
192
+ # from fastapi.responses import JSONResponse
193
+ # import tempfile
194
+ # import shutil
195
+ # from pathlib import Path
196
+ # import logging
197
+ # # /opt/anaconda3/envs/deepfake/bin/python -m uvicorn app.main:app --reload
198
+ # from .config import MODEL_PATH, DEVICE, FRAMES_PER_CLIP, IMG_SIZE, MAX_FILE_SIZE, ALLOWED_EXTENSIONS, PREDICTION_THRESHOLD
199
+ # from .model import DeepFakeModel
200
+ # from .utils import video_to_tensor, save_uploaded_video
201
+
202
+ # # Setup logging
203
+ # logging.basicConfig(level=logging.INFO)
204
+ # logger = logging.getLogger(__name__)
205
+
206
+ # # Initialize FastAPI
207
+ # app = FastAPI(title="DeepFake Detection API", version="1.0.0")
208
+
209
+ # # CORS middleware
210
+ # app.add_middleware(
211
+ # CORSMiddleware,
212
+ # allow_origins=["http://localhost:5173", "http://localhost:3000"], # React dev servers
213
+ # allow_credentials=True,
214
+ # allow_methods=["*"],
215
+ # allow_headers=["*"],
216
+ # )
217
+
218
+ # # Load model (with error handling)
219
+ # model = None
220
+
221
+ # @app.on_event("startup")
222
+ # async def load_model():
223
+ # global model
224
+ # try:
225
+ # if not MODEL_PATH.exists():
226
+ # logger.error(f"Model file not found at {MODEL_PATH}")
227
+ # raise FileNotFoundError(f"Model not found at {MODEL_PATH}")
228
+
229
+ # model = DeepFakeModel(str(MODEL_PATH), DEVICE)
230
+ # logger.info("Model loaded successfully")
231
+ # except Exception as e:
232
+ # logger.error(f"Failed to load model: {e}")
233
+ # model = None
234
+
235
+
236
+ # @app.get("/health")
237
+ # async def health_check():
238
+ # """Health check endpoint."""
239
+ # return {
240
+ # "status": "healthy" if model else "model_not_loaded",
241
+ # "device": DEVICE,
242
+ # "model_loaded": model is not None,
243
+ # "threshold": PREDICTION_THRESHOLD
244
+ # }
245
+
246
+
247
+ # @app.post("/predict")
248
+ # async def predict(file: UploadFile = File(...)):
249
+ # """
250
+ # Predict if uploaded video is REAL or FAKE.
251
+
252
+ # Args:
253
+ # file: Video file (mp4, avi, mov, mkv)
254
+
255
+ # Returns:
256
+ # Prediction result with confidence scores
257
+ # """
258
+ # if model is None:
259
+ # raise HTTPException(status_code=503, detail="Model not loaded")
260
+
261
+ # # Validate file extension
262
+ # file_ext = Path(file.filename).suffix.lower()
263
+ # if file_ext not in ALLOWED_EXTENSIONS:
264
+ # raise HTTPException(
265
+ # status_code=400,
266
+ # detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
267
+ # )
268
+
269
+ # # Validate file size
270
+ # file.file.seek(0, 2)
271
+ # file_size = file.file.tell()
272
+ # file.file.seek(0)
273
+
274
+ # if file_size > MAX_FILE_SIZE:
275
+ # raise HTTPException(
276
+ # status_code=400,
277
+ # detail=f"File too large. Max size: {MAX_FILE_SIZE // (1024*1024)}MB"
278
+ # )
279
+
280
+ # temp_dir = tempfile.mkdtemp()
281
+
282
+ # try:
283
+ # # Save uploaded file
284
+ # video_path = save_uploaded_video(file, temp_dir)
285
+ # logger.info(f"Processing video: {file.filename}")
286
+
287
+ # # Convert video to tensor
288
+ # video_tensor = video_to_tensor(
289
+ # video_path,
290
+ # num_frames=FRAMES_PER_CLIP,
291
+ # img_size=IMG_SIZE
292
+ # )
293
+
294
+ # # Make prediction with configured threshold
295
+ # result = model.predict(video_tensor, threshold=PREDICTION_THRESHOLD)
296
+ # result["filename"] = file.filename
297
+
298
+ # logger.info(f"Prediction for {file.filename}: {result['prediction']} (conf={result['confidence']})")
299
+
300
+ # return JSONResponse(content=result)
301
+
302
+ # except ValueError as e:
303
+ # raise HTTPException(status_code=400, detail=str(e))
304
+ # except Exception as e:
305
+ # logger.error(f"Error processing video: {e}")
306
+ # raise HTTPException(status_code=500, detail=f"Error processing video: {str(e)}")
307
+
308
+ # finally:
309
+ # # Cleanup
310
+ # shutil.rmtree(temp_dir, ignore_errors=True)
311
+
312
+
313
+ # @app.post("/predict-batch")
314
+ # async def predict_batch(files: list[UploadFile] = File(...)):
315
+ # """
316
+ # Predict for multiple videos.
317
+ # """
318
+ # if model is None:
319
+ # raise HTTPException(status_code=503, detail="Model not loaded")
320
+
321
+ # results = []
322
+
323
+ # for file in files:
324
+ # file_ext = Path(file.filename).suffix.lower()
325
+ # if file_ext not in ALLOWED_EXTENSIONS:
326
+ # results.append({
327
+ # "filename": file.filename,
328
+ # "error": f"Unsupported file type: {file_ext}"
329
+ # })
330
+ # continue
331
+
332
+ # temp_dir = tempfile.mkdtemp()
333
+
334
+ # try:
335
+ # video_path = save_uploaded_video(file, temp_dir)
336
+ # video_tensor = video_to_tensor(video_path, FRAMES_PER_CLIP, IMG_SIZE)
337
+ # result = model.predict(video_tensor, threshold=PREDICTION_THRESHOLD)
338
+ # result["filename"] = file.filename
339
+ # results.append(result)
340
+ # except Exception as e:
341
+ # results.append({
342
+ # "filename": file.filename,
343
+ # "error": str(e)
344
+ # })
345
+ # finally:
346
+ # shutil.rmtree(temp_dir, ignore_errors=True)
347
+
348
+ # return JSONResponse(content={"results": results})
349
+
350
+
351
+ # # Optional: Endpoint to test with custom threshold
352
+ # @app.post("/predict-custom")
353
+ # async def predict_custom(
354
+ # file: UploadFile = File(...),
355
+ # threshold: float = PREDICTION_THRESHOLD
356
+ # ):
357
+ # """
358
+ # Predict with custom threshold.
359
+
360
+ # Args:
361
+ # file: Video file (mp4, avi, mov, mkv)
362
+ # threshold: Custom threshold between 0 and 1 (default: 0.4)
363
+ # """
364
+ # if model is None:
365
+ # raise HTTPException(status_code=503, detail="Model not loaded")
366
+
367
+ # # Validate threshold
368
+ # if threshold < 0 or threshold > 1:
369
+ # raise HTTPException(
370
+ # status_code=400,
371
+ # detail="Threshold must be between 0 and 1"
372
+ # )
373
+
374
+ # # Validate file extension
375
+ # file_ext = Path(file.filename).suffix.lower()
376
+ # if file_ext not in ALLOWED_EXTENSIONS:
377
+ # raise HTTPException(
378
+ # status_code=400,
379
+ # detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
380
+ # )
381
+
382
+ # # Validate file size
383
+ # file.file.seek(0, 2)
384
+ # file_size = file.file.tell()
385
+ # file.file.seek(0)
386
+
387
+ # if file_size > MAX_FILE_SIZE:
388
+ # raise HTTPException(
389
+ # status_code=400,
390
+ # detail=f"File too large. Max size: {MAX_FILE_SIZE // (1024*1024)}MB"
391
+ # )
392
+
393
+ # temp_dir = tempfile.mkdtemp()
394
+
395
+ # try:
396
+ # # Save uploaded file
397
+ # video_path = save_uploaded_video(file, temp_dir)
398
+ # logger.info(f"Processing video: {file.filename}")
399
+
400
+ # # Convert video to tensor
401
+ # video_tensor = video_to_tensor(
402
+ # video_path,
403
+ # num_frames=FRAMES_PER_CLIP,
404
+ # img_size=IMG_SIZE
405
+ # )
406
+
407
+ # # Make prediction with custom threshold
408
+ # result = model.predict(video_tensor, threshold=threshold)
409
+ # result["filename"] = file.filename
410
+
411
+ # logger.info(f"Prediction for {file.filename}: {result['prediction']} (conf={result['confidence']}, threshold={threshold})")
412
+
413
+ # return JSONResponse(content=result)
414
+
415
+ # except ValueError as e:
416
+ # raise HTTPException(status_code=400, detail=str(e))
417
+ # except Exception as e:
418
+ # logger.error(f"Error processing video: {e}")
419
+ # raise HTTPException(status_code=500, detail=f"Error processing video: {str(e)}")
420
+
421
+ # finally:
422
+ # # Cleanup
423
+ # shutil.rmtree(temp_dir, ignore_errors=True)
app/model.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import timm
4
+ from pathlib import Path
5
+ import logging
6
+ import os
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class EfficientNetDeepFakeDetector(nn.Module):
13
+ """Frame-level EfficientNet-B0 with temporal mean-pooling."""
14
+
15
+ FEAT_DIM = 1280
16
+
17
+ def __init__(self, dropout: float = 0.4):
18
+ super().__init__()
19
+
20
+ # Backbone
21
+ backbone = timm.create_model(
22
+ 'efficientnet_b0',
23
+ pretrained=False,
24
+ num_classes=0,
25
+ global_pool='avg'
26
+ )
27
+
28
+ # Freeze BatchNorm layers
29
+ for m in backbone.modules():
30
+ if isinstance(m, (nn.BatchNorm2d, nn.SyncBatchNorm)):
31
+ m.eval()
32
+ for p in m.parameters():
33
+ p.requires_grad = False
34
+
35
+ self.backbone = backbone
36
+
37
+ # Classifier head
38
+ self.head = nn.Sequential(
39
+ nn.LayerNorm(self.FEAT_DIM),
40
+ nn.Dropout(dropout),
41
+ nn.Linear(self.FEAT_DIM, 256),
42
+ nn.GELU(),
43
+ nn.Dropout(dropout * 0.5),
44
+ nn.Linear(256, 1)
45
+ )
46
+
47
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
48
+ B, T, C, H, W = x.shape
49
+ x = x.view(B * T, C, H, W)
50
+ feat = self.backbone(x)
51
+ feat = feat.view(B, T, self.FEAT_DIM)
52
+ feat = feat.mean(dim=1)
53
+ logit = self.head(feat).squeeze(-1)
54
+ return logit
55
+
56
+
57
+ class DeepFakeModel:
58
+ def __init__(self, model_path: str, device: str = "cpu"):
59
+ self.device = torch.device(device)
60
+ self.model = EfficientNetDeepFakeDetector(dropout=0.4).to(self.device)
61
+ self._load_model(model_path)
62
+ self.model.eval()
63
+ logger.info(f"Model loaded on {self.device}")
64
+
65
+ def _load_model(self, model_path: str):
66
+ checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
67
+ self.model.load_state_dict(checkpoint['model_state_dict'])
68
+ logger.info(f"Loaded checkpoint from epoch {checkpoint.get('epoch', 'unknown')}")
69
+
70
+ @torch.no_grad()
71
+ def predict(self, video_tensor: torch.Tensor, threshold: float = 0.4) -> dict:
72
+ """
73
+ Predict if video is real or fake.
74
+
75
+ Args:
76
+ video_tensor: Tensor of shape (T, 3, H, W) or (1, T, 3, H, W)
77
+ threshold: Decision threshold (default: 0.4 from notebook testing)
78
+
79
+ Returns:
80
+ dict with prediction, confidence, and probabilities
81
+ """
82
+ if video_tensor.dim() == 4:
83
+ video_tensor = video_tensor.unsqueeze(0)
84
+
85
+ video_tensor = video_tensor.to(self.device)
86
+ logit = self.model(video_tensor)
87
+ prob = torch.sigmoid(logit).item()
88
+
89
+ # prob = P(REAL), because training used label 1=REAL, 0=FAKE
90
+ prediction = "REAL" if prob >= threshold else "FAKE"
91
+ confidence = prob if prediction == "REAL" else 1 - prob
92
+
93
+ return {
94
+ "prediction": prediction,
95
+ "confidence": round(confidence, 4),
96
+ "probability_real": round(prob, 4),
97
+ "probability_fake": round(1 - prob, 4),
98
+ "threshold": threshold
99
+ }
app/utils.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import torch
4
+ from PIL import Image
5
+ import tempfile
6
+ import os
7
+ from pathlib import Path
8
+ import logging
9
+
10
+ # logger = logging.getLogger(__name__)
11
+
12
+ # # ImageNet normalization constants
13
+ # MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
14
+ # STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
15
+
16
+
17
+ def save_uploaded_video(upload_file, temp_dir: str) -> str:
18
+ """Save uploaded video to temporary file and return path."""
19
+ file_path = os.path.join(temp_dir, upload_file.filename)
20
+ with open(file_path, "wb") as buffer:
21
+ buffer.write(upload_file.file.read())
22
+ return file_path
23
+
24
+
25
+ # def extract_frames(video_path: str, num_frames: int = 16) -> list:
26
+ # """Extract evenly spaced frames from video."""
27
+ # cap = cv2.VideoCapture(video_path)
28
+ # total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
29
+
30
+ # if total_frames <= 0:
31
+ # cap.release()
32
+ # return []
33
+
34
+ # indices = np.linspace(0, total_frames - 1, num=min(num_frames, total_frames), dtype=int)
35
+ # frames = []
36
+
37
+ # for idx in indices:
38
+ # cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
39
+ # ret, frame = cap.read()
40
+ # if ret:
41
+ # frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
42
+ # frames.append(frame_rgb)
43
+
44
+ # cap.release()
45
+ # return frames
46
+ # utils.py — replace extract_frames + preprocess_frame with these
47
+
48
+ import cv2
49
+ import numpy as np
50
+ import torch
51
+ from PIL import Image
52
+ import os
53
+ import logging
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+ MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
58
+ STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
59
+
60
+ # Load OpenCV's face detector (ships with opencv-python, no extra install)
61
+ _face_cascade = cv2.CascadeClassifier(
62
+ cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
63
+ )
64
+
65
+ def _crop_face(frame_bgr: np.ndarray, margin: float = 0.3) -> np.ndarray:
66
+ """
67
+ Detect and crop the largest face in a BGR frame.
68
+ Returns the face crop, or the full frame if no face found.
69
+ """
70
+ gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
71
+ faces = _face_cascade.detectMultiScale(
72
+ gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60)
73
+ )
74
+
75
+ if len(faces) == 0:
76
+ # Fall back to centre crop (better than full frame)
77
+ h, w = frame_bgr.shape[:2]
78
+ size = min(h, w)
79
+ y0 = (h - size) // 2
80
+ x0 = (w - size) // 2
81
+ return frame_bgr[y0:y0+size, x0:x0+size]
82
+
83
+ # Pick the largest detected face
84
+ x, y, fw, fh = max(faces, key=lambda f: f[2] * f[3])
85
+
86
+ # Add margin
87
+ mx = int(fw * margin)
88
+ my = int(fh * margin)
89
+ H, W = frame_bgr.shape[:2]
90
+ x1 = max(0, x - mx)
91
+ y1 = max(0, y - my)
92
+ x2 = min(W, x + fw + mx)
93
+ y2 = min(H, y + fh + my)
94
+
95
+ return frame_bgr[y1:y2, x1:x2]
96
+
97
+
98
+ def extract_frames(video_path: str, num_frames: int = 16) -> list:
99
+ """Extract evenly spaced frames from video, with face crop."""
100
+ cap = cv2.VideoCapture(video_path)
101
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
102
+
103
+ if total_frames <= 0:
104
+ cap.release()
105
+ return []
106
+
107
+ indices = np.linspace(0, total_frames - 1, num=min(num_frames, total_frames), dtype=int)
108
+ frames = []
109
+
110
+ for idx in indices:
111
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
112
+ ret, frame = cap.read()
113
+ if ret:
114
+ face = _crop_face(frame) # <-- crop face
115
+ frame_rgb = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
116
+ frames.append(frame_rgb)
117
+
118
+ cap.release()
119
+ return frames
120
+
121
+ def preprocess_frame(frame: np.ndarray, target_size: int = 224) -> torch.Tensor:
122
+ """Preprocess a single frame for model input."""
123
+ # Convert to PIL and resize
124
+ pil_img = Image.fromarray(frame).resize((target_size, target_size), Image.BILINEAR)
125
+
126
+ # Convert to tensor and normalize to [0, 1]
127
+ tensor = torch.from_numpy(np.array(pil_img)).float().permute(2, 0, 1) / 255.0
128
+
129
+ # Normalize with ImageNet stats
130
+ tensor = (tensor - MEAN) / STD
131
+ tensor = torch.nan_to_num(tensor, nan=0.0, posinf=5.0, neginf=-5.0)
132
+
133
+ return tensor
134
+
135
+
136
+ def video_to_tensor(video_path: str, num_frames: int = 16, img_size: int = 224) -> torch.Tensor:
137
+ """Convert video to tensor of shape (num_frames, 3, img_size, img_size)."""
138
+ frames = extract_frames(video_path, num_frames)
139
+
140
+ if not frames:
141
+ raise ValueError("Could not extract frames from video")
142
+
143
+ tensors = []
144
+ for frame in frames:
145
+ tensor = preprocess_frame(frame, img_size)
146
+ tensors.append(tensor)
147
+
148
+ # Pad if needed
149
+ if len(tensors) < num_frames:
150
+ last_tensor = tensors[-1]
151
+ while len(tensors) < num_frames:
152
+ tensors.append(last_tensor.clone())
153
+
154
+ return torch.stack(tensors)
models/ffpp_efficientnet_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a562ddf6f7f5f63318b2481690515370c1a1a9a404eb638a16b1bd867e83d18f
3
+ size 52139766
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ torch==2.1.0
4
+ torchvision==0.16.0
5
+ timm==0.9.12
6
+ opencv-python-headless==4.8.1.78
7
+ numpy==1.24.3
8
+ python-multipart==0.0.6
9
+ Pillow==10.1.0
10
+ python-dotenv==1.0.0
11
+ python-decouple==3.8
12
+ gunicorn==21.2.0