spideyhead commited on
Commit
9b75405
Β·
verified Β·
1 Parent(s): 3f87d55

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +27 -0
  2. main.py +296 -0
  3. requirements.txt +12 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Install system dependencies: OpenCV + ffmpeg for audio extraction
4
+ RUN apt-get update && apt-get install -y \
5
+ libglib2.0-0 \
6
+ libsm6 \
7
+ libxext6 \
8
+ libxrender-dev \
9
+ ffmpeg \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Set up a non-root user for Hugging Face Spaces
13
+ RUN useradd -m -u 1000 user
14
+ USER user
15
+ ENV PATH="/home/user/.local/bin:$PATH"
16
+
17
+ WORKDIR /app
18
+
19
+ # Copy requirements and install
20
+ COPY --chown=user requirements.txt .
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Copy the app
24
+ COPY --chown=user . .
25
+
26
+ # Run the app on port 7860 (Hugging Face Spaces default)
27
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import torch
4
+ import shutil
5
+ import subprocess
6
+ import numpy as np
7
+ from fastapi import FastAPI, UploadFile, File
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from transformers import (
10
+ VideoMAEImageProcessor, VideoMAEForVideoClassification,
11
+ AutoFeatureExtractor, AutoModelForAudioClassification
12
+ )
13
+ from facenet_pytorch import MTCNN
14
+ from PIL import Image
15
+
16
+ app = FastAPI()
17
+
18
+ # Enable CORS for the React frontend
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"],
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ os.makedirs("temp", exist_ok=True)
28
+
29
+ # ─── Load Models on Startup ────────────────────────────────────────────────────
30
+ print("Loading MTCNN Face Detector...")
31
+ mtcnn = MTCNN(keep_all=False, select_largest=True, post_process=False)
32
+
33
+ print("Loading VideoMAE Temporal Deepfake Detector...")
34
+ video_model_name = "Ammar2k/videomae-base-finetuned-deepfake-subset"
35
+ video_processor = VideoMAEImageProcessor.from_pretrained(video_model_name)
36
+ video_model = VideoMAEForVideoClassification.from_pretrained(video_model_name)
37
+ video_model.eval()
38
+
39
+ print("Loading Wav2Vec2 Audio Deepfake Detector...")
40
+ audio_model_name = "MelodyMachine/Deepfake-audio-detection-V2"
41
+ audio_extractor = AutoFeatureExtractor.from_pretrained(audio_model_name)
42
+ audio_model = AutoModelForAudioClassification.from_pretrained(audio_model_name)
43
+ audio_model.eval()
44
+
45
+ print("All models loaded successfully!")
46
+
47
+ # ─── Video Analysis ─────────────────────────────────────────────────────────
48
+ def extract_face_sequence(video_path, num_frames=32):
49
+ """Extract a continuous face-tracked sequence of N frames from the video."""
50
+ cap = cv2.VideoCapture(video_path)
51
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
+
53
+ if total_frames == 0:
54
+ cap.release()
55
+ return []
56
+
57
+ # Start from the middle of the video to get the best face-visible region
58
+ start_frame = max(0, (total_frames // 2) - (num_frames // 2))
59
+ cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
60
+
61
+ faces = []
62
+ last_box = None
63
+
64
+ for _ in range(num_frames):
65
+ ret, frame = cap.read()
66
+ if not ret:
67
+ break
68
+
69
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
70
+ pil_img = Image.fromarray(frame_rgb)
71
+
72
+ boxes, _ = mtcnn.detect(pil_img)
73
+
74
+ # Bounding-box smoothing: use last known good box if detection fails
75
+ if boxes is not None and len(boxes) > 0:
76
+ last_box = boxes[0]
77
+
78
+ if last_box is None:
79
+ continue
80
+
81
+ box = last_box
82
+ w = box[2] - box[0]
83
+ h = box[3] - box[1]
84
+ pad_w = int(w * 0.3)
85
+ pad_h = int(h * 0.3)
86
+ x1 = max(0, int(box[0]) - pad_w)
87
+ y1 = max(0, int(box[1]) - pad_h)
88
+ x2 = min(pil_img.width, int(box[2]) + pad_w)
89
+ y2 = min(pil_img.height, int(box[3]) + pad_h)
90
+
91
+ if x2 > x1 and y2 > y1:
92
+ face_crop = pil_img.crop((x1, y1, x2, y2))
93
+ faces.append(face_crop)
94
+
95
+ cap.release()
96
+
97
+ if not faces:
98
+ return []
99
+
100
+ # Pad to ensure we always have exactly num_frames
101
+ while len(faces) < num_frames:
102
+ faces.append(faces[-1])
103
+
104
+ return faces[:num_frames]
105
+
106
+
107
+ def analyze_video_temporal(faces):
108
+ """Run VideoMAE on two 16-frame batches and average the results."""
109
+ half = len(faces) // 2
110
+ batch_a = faces[:half]
111
+ batch_b = faces[half:]
112
+
113
+ scores = []
114
+ for batch in [batch_a, batch_b]:
115
+ if len(batch) < 16:
116
+ while len(batch) < 16:
117
+ batch.append(batch[-1])
118
+ batch = batch[:16]
119
+
120
+ inputs = video_processor(list(batch), return_tensors="pt")
121
+ with torch.no_grad():
122
+ outputs = video_model(**inputs)
123
+ probs = torch.nn.functional.softmax(outputs.logits[0], dim=-1)
124
+ predicted_idx = probs.argmax(-1).item()
125
+ label = video_model.config.id2label[predicted_idx].lower()
126
+ is_fake = 'fake' in label
127
+ fake_prob = probs[predicted_idx].item() if is_fake else (1.0 - probs[predicted_idx].item())
128
+ scores.append(fake_prob)
129
+
130
+ avg_fake_prob = sum(scores) / len(scores)
131
+ is_fake = avg_fake_prob > 0.5
132
+ confidence = round((avg_fake_prob if is_fake else (1.0 - avg_fake_prob)) * 100, 2)
133
+ return is_fake, confidence, avg_fake_prob
134
+
135
+
136
+ # ─── Audio Analysis ─────────────────────────────────────────────────────────
137
+ def extract_audio(video_path):
138
+ """Extract audio from video using ffmpeg. Returns numpy array or None."""
139
+ audio_path = video_path.replace(os.path.splitext(video_path)[1], "_audio.wav")
140
+ try:
141
+ result = subprocess.run(
142
+ ["ffmpeg", "-y", "-i", video_path, "-ar", "16000", "-ac", "1", "-f", "wav", audio_path],
143
+ stdout=subprocess.PIPE,
144
+ stderr=subprocess.PIPE,
145
+ timeout=30
146
+ )
147
+ if result.returncode != 0 or not os.path.exists(audio_path):
148
+ return None, None
149
+
150
+ import soundfile as sf
151
+ audio_array, sample_rate = sf.read(audio_path)
152
+ os.remove(audio_path)
153
+ return audio_array.astype(np.float32), sample_rate
154
+
155
+ except Exception as e:
156
+ print(f"Audio extraction failed: {e}")
157
+ if os.path.exists(audio_path):
158
+ os.remove(audio_path)
159
+ return None, None
160
+
161
+
162
+ def analyze_audio(audio_array, sample_rate):
163
+ """Run Wav2Vec2 audio deepfake detector on the audio array."""
164
+ try:
165
+ # Use max 10 seconds of audio for speed
166
+ max_samples = 16000 * 10
167
+ if len(audio_array) > max_samples:
168
+ audio_array = audio_array[:max_samples]
169
+
170
+ inputs = audio_extractor(
171
+ audio_array,
172
+ sampling_rate=16000,
173
+ return_tensors="pt",
174
+ padding=True
175
+ )
176
+
177
+ with torch.no_grad():
178
+ outputs = audio_model(**inputs)
179
+
180
+ probs = torch.nn.functional.softmax(outputs.logits[0], dim=-1)
181
+ predicted_idx = probs.argmax(-1).item()
182
+ label = audio_model.config.id2label[predicted_idx].lower()
183
+ is_fake = 'fake' in label or 'spoof' in label
184
+ fake_prob = probs[predicted_idx].item() if is_fake else (1.0 - probs[predicted_idx].item())
185
+ confidence = round((fake_prob if is_fake else (1.0 - fake_prob)) * 100, 2)
186
+ return is_fake, confidence, fake_prob
187
+
188
+ except Exception as e:
189
+ print(f"Audio analysis failed: {e}")
190
+ return None, None, None
191
+
192
+
193
+ # ─── API Endpoint ────────────────────────────────────────────────────────────
194
+ @app.post("/api/analyze")
195
+ async def analyze_video(file: UploadFile = File(...)):
196
+ print(f"Received file: {file.filename}")
197
+
198
+ temp_video_path = os.path.join("temp", file.filename)
199
+ with open(temp_video_path, "wb") as buffer:
200
+ shutil.copyfileobj(file.file, buffer)
201
+
202
+ try:
203
+ # ── Video Analysis ──────────────────────────────────────────────────
204
+ print("Extracting 32-frame face sequence...")
205
+ faces = extract_face_sequence(temp_video_path, num_frames=32)
206
+
207
+ if not faces:
208
+ return {
209
+ "isFake": False,
210
+ "confidence": 0,
211
+ "explanation": "Could not detect a clear face in the video. Please ensure the subject's face is clearly visible.",
212
+ "details": []
213
+ }
214
+
215
+ print(f"Running dual-batch VideoMAE temporal inference ({len(faces)} frames)...")
216
+ video_is_fake, video_confidence, video_fake_prob = analyze_video_temporal(faces)
217
+
218
+ # ── Audio Analysis ──────────────────────────────────────────────────
219
+ print("Extracting audio track...")
220
+ audio_array, sample_rate = extract_audio(temp_video_path)
221
+
222
+ audio_is_fake = None
223
+ audio_confidence = None
224
+ audio_fake_prob = None
225
+ has_audio = False
226
+
227
+ if audio_array is not None and len(audio_array) > 1600:
228
+ has_audio = True
229
+ print("Running Wav2Vec2 audio deepfake analysis...")
230
+ audio_is_fake, audio_confidence, audio_fake_prob = analyze_audio(audio_array, sample_rate)
231
+
232
+ # ── Ensemble Score Merger ───────────────────────────────────────────
233
+ if has_audio and audio_fake_prob is not None:
234
+ # Weight: 60% video, 40% audio
235
+ ensemble_fake_prob = (video_fake_prob * 0.6) + (audio_fake_prob * 0.4)
236
+ is_fake = ensemble_fake_prob > 0.5
237
+ confidence = round(ensemble_fake_prob * 100, 2) if is_fake else round((1 - ensemble_fake_prob) * 100, 2)
238
+ analysis_type = "Ensemble (Video + Audio)"
239
+ else:
240
+ is_fake = video_is_fake
241
+ confidence = video_confidence
242
+ analysis_type = "Video-Only (No audio track detected)"
243
+
244
+ print(f"Final Result: isFake={is_fake}, confidence={confidence}%, type={analysis_type}")
245
+
246
+ # ── Build Detailed Report ───────────────────────────────────────────
247
+ explanation = (
248
+ "Our ensemble AI analyzed both facial motion and voice patterns. "
249
+ "Significant temporal artifacts and/or synthetic voice characteristics were detected."
250
+ if is_fake else
251
+ "Our ensemble AI analyzed both facial motion and voice patterns. "
252
+ "Natural micro-expressions, consistent facial flow, and authentic voice characteristics were found."
253
+ )
254
+
255
+ details = [
256
+ {
257
+ "title": "🎬 Video Temporal Analysis",
258
+ "desc": f"VideoMAE 3D Transformer analyzed two 16-frame clips. Verdict: {'FAKE' if video_is_fake else 'REAL'} ({video_confidence}% confidence)."
259
+ },
260
+ ]
261
+
262
+ if has_audio and audio_is_fake is not None:
263
+ details.append({
264
+ "title": "πŸŽ™οΈ Audio Voice Analysis",
265
+ "desc": f"Wav2Vec2 SSL model analyzed the voice track for synthetic cloning. Verdict: {'FAKE' if audio_is_fake else 'REAL'} ({audio_confidence}% confidence)."
266
+ })
267
+ else:
268
+ details.append({
269
+ "title": "πŸŽ™οΈ Audio Voice Analysis",
270
+ "desc": "No audio track was detected in this video file. Analysis based on video only."
271
+ })
272
+
273
+ details.append({
274
+ "title": "🧠 Ensemble Score",
275
+ "desc": f"Final combined confidence: {confidence}%. Analysis type: {analysis_type}."
276
+ })
277
+
278
+ return {
279
+ "isFake": is_fake,
280
+ "confidence": confidence,
281
+ "explanation": explanation,
282
+ "details": details
283
+ }
284
+
285
+ except Exception as e:
286
+ print(f"Error analyzing video: {e}")
287
+ return {"error": str(e)}
288
+
289
+ finally:
290
+ if os.path.exists(temp_video_path):
291
+ os.remove(temp_video_path)
292
+
293
+
294
+ @app.get("/")
295
+ def health_check():
296
+ return {"status": "Ensemble Deepfake Detection Backend is running!"}
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ opencv-python-headless
5
+ torch
6
+ torchvision
7
+ torchaudio
8
+ transformers
9
+ facenet-pytorch
10
+ Pillow
11
+ librosa
12
+ soundfile