bithal26 commited on
Commit
3f51b6d
·
verified ·
1 Parent(s): a04071f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +328 -0
app.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import torch
4
+ import numpy as np
5
+ from PIL import Image
6
+ import gradio as gr
7
+ from gradio_client import Client, handle_file
8
+ from torchvision.transforms import Normalize
9
+ from facenet_pytorch.models.mtcnn import MTCNN
10
+ import concurrent.futures
11
+ import tempfile
12
+
13
+ # ==========================================
14
+ # 1. API ROUTER CONFIGURATION
15
+ # ==========================================
16
+ # These must match your exact Hugging Face Worker Space names
17
+ WORKER_SPACES = [
18
+ "bithal26/DeepFake-Worker-1",
19
+ "bithal26/DeepFake-Worker-2",
20
+ "bithal26/DeepFake-Worker-3",
21
+ "bithal26/DeepFake-Worker-4",
22
+ "bithal26/DeepFake-Worker-5",
23
+ "bithal26/DeepFake-Worker-6",
24
+ "bithal26/DeepFake-Worker-7"
25
+ ]
26
+
27
+ # Note: If your worker spaces are PRIVATE, you must add your HF_TOKEN
28
+ # to this UI Space's Secrets for the Client to connect successfully.
29
+ clients = []
30
+ print("Initializing connections to 7 API Workers...")
31
+ for space in WORKER_SPACES:
32
+ try:
33
+ clients.append(Client(space))
34
+ except Exception as e:
35
+ print(f"Warning: Could not connect to {space}. Is it private/sleeping? Error: {e}")
36
+
37
+ # ==========================================
38
+ # 2. MTCNN PREPROCESSING ENGINE
39
+ # ==========================================
40
+ mean = [0.485, 0.456, 0.406]
41
+ std = [0.229, 0.224, 0.225]
42
+ normalize_transform = Normalize(mean, std)
43
+ device = torch.device('cpu')
44
+
45
+ def isotropically_resize_image(img, size, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC):
46
+ h, w = img.shape[:2]
47
+ if max(w, h) == size: return img
48
+ scale = size / w if w > h else size / h
49
+ w, h = w * scale, h * scale
50
+ interpolation = interpolation_up if scale > 1 else interpolation_down
51
+ return cv2.resize(img, (int(w), int(h)), interpolation=interpolation)
52
+
53
+ def put_to_center(img, input_size):
54
+ img = img[:input_size, :input_size]
55
+ image = np.zeros((input_size, input_size, 3), dtype=np.uint8)
56
+ start_w = (input_size - img.shape[1]) // 2
57
+ start_h = (input_size - img.shape[0]) // 2
58
+ image[start_h:start_h + img.shape[0], start_w: start_w + img.shape[1], :] = img
59
+ return image
60
+
61
+ class VideoReader:
62
+ def read_frames(self, path, num_frames):
63
+ capture = cv2.VideoCapture(path)
64
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
65
+ if frame_count <= 0: return None
66
+ frame_idxs = np.linspace(0, frame_count - 1, num_frames, endpoint=True, dtype=np.int32)
67
+
68
+ frames, idxs_read = [], []
69
+ for frame_idx in range(frame_idxs[0], frame_idxs[-1] + 1):
70
+ ret = capture.grab()
71
+ if not ret: break
72
+ current = len(idxs_read)
73
+ if frame_idx == frame_idxs[current]:
74
+ ret, frame = capture.retrieve()
75
+ if not ret or frame is None: break
76
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
77
+ frames.append(frame)
78
+ idxs_read.append(frame_idx)
79
+ capture.release()
80
+ return np.stack(frames), idxs_read if len(frames) > 0 else None
81
+
82
+ class FaceExtractor:
83
+ def __init__(self):
84
+ self.video_reader = VideoReader()
85
+ self.detector = MTCNN(margin=0, thresholds=[0.7, 0.8, 0.8], device=device)
86
+
87
+ def process_video(self, video_path, frames_per_video=16):
88
+ result = self.video_reader.read_frames(video_path, num_frames=frames_per_video)
89
+ if result is None: return []
90
+ my_frames, my_idxs = result
91
+ results = []
92
+ for frame in my_frames:
93
+ img = Image.fromarray(frame.astype(np.uint8))
94
+ img = img.resize(size=[s // 2 for s in img.size])
95
+ batch_boxes, probs = self.detector.detect(img, landmarks=False)
96
+ faces = []
97
+ if batch_boxes is not None:
98
+ for bbox in batch_boxes:
99
+ if bbox is not None:
100
+ xmin, ymin, xmax, ymax = [int(b * 2) for b in bbox]
101
+ w, h = xmax - xmin, ymax - ymin
102
+ p_h, p_w = h // 3, w // 3
103
+ crop = frame[max(ymin - p_h, 0):ymax + p_h, max(xmin - p_w, 0):xmax + p_w]
104
+ faces.append(crop)
105
+ if faces:
106
+ results.append({"faces": faces})
107
+ return results
108
+
109
+ face_extractor = FaceExtractor()
110
+
111
+ def confident_strategy(pred, t=0.8):
112
+ pred = np.array(pred)
113
+ sz = len(pred)
114
+ if sz == 0: return 0.0
115
+ fakes = np.count_nonzero(pred > t)
116
+ if fakes > sz // 2.5 and fakes > 11:
117
+ return np.mean(pred[pred > t])
118
+ elif np.count_nonzero(pred < 0.2) > 0.9 * sz:
119
+ return np.mean(pred[pred < 0.2])
120
+ else:
121
+ return np.mean(pred)
122
+
123
+ # ==========================================
124
+ # 3. PARALLEL API EXECUTION
125
+ # ==========================================
126
+ def call_worker(client, tensor_filepath):
127
+ """Pings a single Hugging Face API Worker"""
128
+ try:
129
+ result = client.predict(tensor_file=handle_file(tensor_filepath), api_name="/predict")
130
+ # Result should be a dictionary: {"predictions": [...]}
131
+ preds = result.get("predictions", [])
132
+ if not preds:
133
+ return 0.5 # Default middle ground if error
134
+ return confident_strategy(preds)
135
+ except Exception as e:
136
+ print(f"API Call Failed: {e}")
137
+ return 0.5
138
+
139
+ def analyze_video(video_path):
140
+ if not video_path:
141
+ return "<div style='color:var(--red); font-family:Syne;'>Please upload a video file.</div>"
142
+
143
+ # 1. Extract Faces locally
144
+ input_size = 380
145
+ faces = face_extractor.process_video(video_path, frames_per_video=16)
146
+
147
+ if len(faces) == 0:
148
+ return "<div style='color:var(--amber); font-family:Syne; padding:20px;'>No faces detected. Please upload a clear video.</div>"
149
+
150
+ x = []
151
+ for frame_data in faces:
152
+ for face in frame_data["faces"]:
153
+ resized_face = isotropically_resize_image(face, input_size)
154
+ resized_face = put_to_center(resized_face, input_size)
155
+ x.append(resized_face)
156
+ if len(x) >= 16 * 4:
157
+ break
158
+
159
+ x = np.array(x, dtype=np.uint8)
160
+ x = torch.tensor(x, device=device).float()
161
+ x = x.permute((0, 3, 1, 2))
162
+ for i in range(len(x)):
163
+ x[i] = normalize_transform(x[i] / 255.)
164
+
165
+ # 2. Save the math to a temporary file
166
+ temp_dir = tempfile.gettempdir()
167
+ tensor_path = os.path.join(temp_dir, "batch_tensor.pt")
168
+ torch.save(x, tensor_path)
169
+
170
+ # 3. Ping all 7 Workers in parallel
171
+ worker_scores = []
172
+ with concurrent.futures.ThreadPoolExecutor(max_workers=7) as executor:
173
+ futures = [executor.submit(call_worker, client, tensor_path) for client in clients]
174
+ for future in concurrent.futures.as_completed(futures):
175
+ worker_scores.append(future.result())
176
+
177
+ # 4. Aggregate results
178
+ final_score = np.mean(worker_scores)
179
+ is_fake = final_score > 0.5
180
+ display_score = (final_score * 100) if is_fake else ((1 - final_score) * 100)
181
+
182
+ # Format the individual scores for the UI
183
+ model_bars_html = ""
184
+ for i, score in enumerate(worker_scores):
185
+ percentage = score * 100
186
+ color = "var(--red)" if percentage > 50 else "var(--green)"
187
+ model_bars_html += f"""
188
+ <div class="metric-row">
189
+ <div class="metric-header"><span class="metric-name">EfficientNet Node {i+1}</span><span class="metric-value">{percentage:.1f}%</span></div>
190
+ <div class="metric-bar"><div class="metric-fill" style="width:{percentage}%; background:{color}"></div></div>
191
+ </div>
192
+ """
193
+
194
+ # 5. Inject into your Custom HTML Template
195
+ verdict_color = "var(--red)" if is_fake else "var(--green)"
196
+ verdict_text = "DEEPFAKE DETECTED" if is_fake else "AUTHENTIC CONTENT"
197
+ verdict_desc = "High confidence manipulation detected. Neural forensics indicate spatial anomalies and blending artifacts typical of synthetic face-swapping." if is_fake else "No significant facial manipulation detected. Spatial forensics are within normal parameters. Content appears to be authentic media."
198
+
199
+ # Calculate a proxy for "Face Anomaly" vs "Temporal" based on the raw score to fill your template's visual metrics
200
+ face_anomaly_score = (final_score * 100) if is_fake else (final_score * 100)
201
+
202
+ html_report = f"""
203
+ <div class="report-layout">
204
+ <div class="report-card accent">
205
+ <div class="card-title"><span class="dot"></span>Forensic Analysis Report</div>
206
+ <div style="margin-top:8px">
207
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px">
208
+ <div>
209
+ <div style="font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:2px;color:var(--text-faint);text-transform:uppercase">Verdict</div>
210
+ <div style="font-family:'Bebas Neue',sans-serif;font-size:32px;color:{verdict_color};margin-top:4px">{verdict_text}</div>
211
+ </div>
212
+ <div style="text-align:right">
213
+ <div style="font-family:'Bebas Neue',sans-serif;font-size:48px;color:{verdict_color};text-shadow:0 0 20px {verdict_color};line-height:1">{display_score:.1f}%</div>
214
+ <div style="font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:2px;color:{verdict_color};text-transform:uppercase">Confidence</div>
215
+ </div>
216
+ </div>
217
+ <p style="color:var(--text-dim); font-size:14px; line-height:1.6; margin-bottom:20px;">{verdict_desc}</p>
218
+ <ul class="forensic-list">
219
+ <li class="forensic-item">
220
+ <div class="forensic-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 1 0-16 0"/></svg></div>
221
+ <span class="forensic-name">Spatial Artifact Detection</span>
222
+ <span class="forensic-status {'alert' if is_fake else 'pass'}">{'Anomaly' if is_fake else 'Pass'}</span>
223
+ </li>
224
+ <li class="forensic-item">
225
+ <div class="forensic-icon"><svg viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg></div>
226
+ <span class="forensic-name">Feature Extraction Integrity</span>
227
+ <span class="forensic-status {'alert' if face_anomaly_score > 60 else 'pass'}">{'Fail' if face_anomaly_score > 60 else 'Normal'}</span>
228
+ </li>
229
+ </ul>
230
+ </div>
231
+ </div>
232
+
233
+ <div style="display:flex;flex-direction:column;gap:2px">
234
+ <div class="report-card" style="flex:1">
235
+ <div class="card-title"><span class="dot"></span>Ensemble Node Breakdown</div>
236
+ <div style="margin-top:16px">
237
+ {model_bars_html}
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ """
243
+ return html_report
244
+
245
+ # ==========================================
246
+ # 4. MASTER UI - NETFLIX HTML INTEGRATION
247
+ # ==========================================
248
+ # We pull your exact CSS variables and styling directly from your deepfake-detector.html
249
+ css = """
250
+ @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@300;400;500&display=swap');
251
+
252
+ :root {
253
+ --bg: #030508;
254
+ --bg2: #070c12;
255
+ --panel: rgba(8, 18, 30, 0.85);
256
+ --border: rgba(0, 210, 255, 0.12);
257
+ --border-bright: rgba(0, 210, 255, 0.45);
258
+ --cyan: #00d2ff;
259
+ --red: #ff2d55;
260
+ --green: #00ff88;
261
+ --amber: #ffb800;
262
+ --text: #e8f4ff;
263
+ --text-dim: rgba(232, 244, 255, 0.5);
264
+ --text-faint: rgba(232, 244, 255, 0.25);
265
+ }
266
+
267
+ body, .gradio-container { background-color: var(--bg) !important; color: var(--text) !important; font-family: 'Syne', sans-serif !important; }
268
+ .gr-panel { background: var(--panel) !important; border: 1px solid var(--border) !important; border-radius: 4px !important; }
269
+
270
+ /* Dashboard Titles */
271
+ .veridex-title { font-family: 'Bebas Neue', sans-serif; font-size: 60px; letter-spacing: 4px; color: var(--text); text-align: center; margin-top: 40px;}
272
+ .veridex-title span { color: var(--cyan); }
273
+ .veridex-sub { font-family: 'JetBrains Mono', monospace; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--cyan); text-align: center; margin-bottom: 40px; }
274
+
275
+ /* Custom HTML injected classes from your design */
276
+ .report-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 20px; }
277
+ .report-card { background: var(--panel); border: 1px solid var(--border); padding: 30px; }
278
+ .report-card.accent { border-color: rgba(0,210,255,0.2); background: rgba(0, 210, 255, 0.04); }
279
+ .card-title { font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 3px; text-transform: uppercase; color: var(--cyan); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
280
+ .card-title .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }
281
+
282
+ .forensic-list { list-style: none; display: flex; flex-direction: column; gap: 12px; padding:0; }
283
+ .forensic-item { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid var(--border); }
284
+ .forensic-icon { width: 32px; height: 32px; border: 1px solid var(--border-bright); display: flex; align-items: center; justify-content: center; }
285
+ .forensic-icon svg { width: 14px; height: 14px; stroke: var(--cyan); fill: none; stroke-width: 2; }
286
+ .forensic-name { font-size: 13px; font-weight: 600; flex: 1; font-family: 'Syne', sans-serif;}
287
+ .forensic-status { font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: 2px; text-transform: uppercase; padding: 3px 8px; }
288
+ .forensic-status.pass { color: var(--green); border: 1px solid rgba(0,255,136,0.3); background: rgba(0,255,136,0.05); }
289
+ .forensic-status.alert { color: var(--red); border: 1px solid rgba(255,45,85,0.3); background: rgba(255,45,85,0.05); }
290
+
291
+ .metric-row { margin-bottom: 14px; }
292
+ .metric-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
293
+ .metric-name { font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-dim); }
294
+ .metric-value { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text); }
295
+ .metric-bar { height: 3px; background: rgba(255,255,255,0.06); width: 100%; overflow: hidden; }
296
+ .metric-fill { height: 100%; transition: width 1s ease; }
297
+
298
+ @media (max-width: 900px) { .report-layout { grid-template-columns: 1fr; } }
299
+ """
300
+
301
+ with gr.Blocks(css=css, theme=gr.themes.Default(neutral_hue="slate", primary_hue="cyan")) as app:
302
+ gr.HTML("""
303
+ <div class="veridex-title">VERI<span>DEX</span></div>
304
+ <div class="veridex-sub">Neural Detection Engine v4.2 // Distributed Architecture</div>
305
+ """)
306
+
307
+ with gr.Row():
308
+ with gr.Column(scale=1):
309
+ gr.Markdown("### 1. Ingest Video Evidence")
310
+ video_in = gr.Video(label="Upload Media (.mp4, .avi)")
311
+ analyze_btn = gr.Button("Run Distributed Ensemble Analysis", variant="primary", size="lg")
312
+
313
+ gr.HTML("""
314
+ <div style="margin-top:20px; font-family:'JetBrains Mono'; font-size:10px; color:var(--text-faint); line-height:1.8;">
315
+ › Local MTCNN Node Active<br>
316
+ › 7 Parallel EfficientNet Endpoints Linked<br>
317
+ › Awaiting input...
318
+ </div>
319
+ """)
320
+
321
+ with gr.Column(scale=2):
322
+ gr.Markdown("### 2. Forensic Output")
323
+ report_out = gr.HTML(value="<div style='color:var(--text-dim); padding:40px; text-align:center; border:1px dashed var(--border);'>Awaiting video analysis...</div>")
324
+
325
+ analyze_btn.click(fn=analyze_video, inputs=video_in, outputs=report_out)
326
+
327
+ if __name__ == "__main__":
328
+ app.launch()