NOBODY204 commited on
Commit
73be7d2
Β·
verified Β·
1 Parent(s): 178c70c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +384 -0
app.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ import hashlib
5
+ import datetime
6
+ import json
7
+ import os
8
+
9
+ # ═══════════════════════════════════════════════════════════
10
+ # Γ‰TAPE 1 β€” EXTRACTION DES FRAMES
11
+ # ═══════════════════════════════════════════════════════════
12
+
13
+ def extract_frames(video_path, max_seconds=20, n_frames=16):
14
+ """
15
+ Extrait n_frames images équidistantes dans les max_seconds premières secondes.
16
+ Retourne : liste de frames (numpy BGR), mΓ©tadonnΓ©es dict
17
+ """
18
+ cap = cv2.VideoCapture(video_path)
19
+ if not cap.isOpened():
20
+ raise ValueError("Impossible d'ouvrir la vidΓ©o.")
21
+
22
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
23
+ total_fr = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
24
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
25
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
26
+ duration = total_fr / fps
27
+
28
+ analyse_end = min(duration, max_seconds)
29
+ end_frame = int(analyse_end * fps)
30
+ indices = np.linspace(0, max(end_frame - 1, 0), n_frames, dtype=int)
31
+
32
+ frames = []
33
+ for idx in indices:
34
+ cap.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
35
+ ret, frame = cap.read()
36
+ if ret:
37
+ frames.append(frame)
38
+ cap.release()
39
+
40
+ meta = {
41
+ "fps": round(fps, 2),
42
+ "resolution": f"{width}x{height}",
43
+ "duree_totale_s": round(duration, 2),
44
+ "duree_analysee_s": round(analyse_end, 2),
45
+ "frames_extraites": len(frames),
46
+ }
47
+ return frames, meta
48
+
49
+
50
+ # ═══════════════════════════════════════════════════════════
51
+ # ÉTAPE 2 — DÉTECTION DE VISAGE
52
+ # ═══════════════════════════════════════════════════════════
53
+
54
+ face_cascade = cv2.CascadeClassifier(
55
+ cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
56
+ )
57
+
58
+ def detect_faces(frame):
59
+ """
60
+ Retourne la liste des ROI (Region Of Interest) des visages dΓ©tectΓ©s.
61
+ Chaque ROI = frame croppΓ©e sur le visage avec marge de 20%.
62
+ """
63
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
64
+ faces = face_cascade.detectMultiScale(
65
+ gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60)
66
+ )
67
+ rois = []
68
+ h_img, w_img = frame.shape[:2]
69
+ for (x, y, w, h) in faces:
70
+ margin = int(max(w, h) * 0.20)
71
+ x1 = max(0, x - margin)
72
+ y1 = max(0, y - margin)
73
+ x2 = min(w_img, x + w + margin)
74
+ y2 = min(h_img, y + h + margin)
75
+ rois.append(frame[y1:y2, x1:x2])
76
+ return rois
77
+
78
+
79
+ # ═══════════════════════════════════════════════════════════
80
+ # Γ‰TAPE 3A β€” TEST BRUIT (Noise Level)
81
+ # ═══════════════════════════════════════════════════════════
82
+
83
+ def test_noise(roi):
84
+ """
85
+ Analyse le niveau de bruit dans la ROI.
86
+ Une face deepfake a souvent un bruit anormalement bas (lissage GAN)
87
+ ou anormalement Γ©levΓ© sur certains canaux.
88
+
89
+ Retourne un score d'authenticitΓ© [0-1].
90
+ 1 = très probablement authentique
91
+ 0 = suspect (trop lisse ou trop bruitΓ©)
92
+ """
93
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY).astype(np.float32)
94
+
95
+ # Laplacien : mesure la variance du bruit
96
+ laplacian = cv2.Laplacian(gray, cv2.CV_32F)
97
+ variance = laplacian.var()
98
+
99
+ # Une variance très basse (<20) = GAN over-smoothing suspect
100
+ # Une variance normale = 50–500 pour une camΓ©ra rΓ©elle
101
+ if variance < 15:
102
+ return 0.25 # très lisse → suspect
103
+ elif variance < 40:
104
+ return 0.55 # légèrement lisse → incertain
105
+ elif variance < 600:
106
+ return 0.90 # plage normale β†’ authentique
107
+ else:
108
+ return 0.60 # trΓ¨s bruitΓ©e β†’ peut Γͺtre compression
109
+
110
+
111
+ # ═══════════════════════════════════════════════════════════
112
+ # ÉTAPE 3B — TEST FRÉQUENCES (FFT Artifacts)
113
+ # ═══════════════════════════════════════════════════════════
114
+
115
+ def test_fft(roi):
116
+ """
117
+ Analyse le spectre frΓ©quentiel via FFT.
118
+ Les GANs laissent des artefacts caractΓ©ristiques dans les hautes frΓ©quences
119
+ (pics rΓ©guliers dans le spectre = pattern artificiel).
120
+
121
+ Retourne un score d'authenticitΓ© [0-1].
122
+ """
123
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY).astype(np.float32)
124
+ f = np.fft.fft2(gray)
125
+ fshift = np.fft.fftshift(f)
126
+ mag = 20 * np.log(np.abs(fshift) + 1)
127
+
128
+ # Ratio Γ©nergie centre / pΓ©riphΓ©rie
129
+ h, w = mag.shape
130
+ cy, cx = h // 2, w // 2
131
+ r = min(h, w) // 6
132
+ center_mask = np.zeros_like(mag, dtype=bool)
133
+ for i in range(h):
134
+ for j in range(w):
135
+ if (i - cy)**2 + (j - cx)**2 < r**2:
136
+ center_mask[i, j] = True
137
+
138
+ center_energy = mag[center_mask].mean()
139
+ outer_energy = mag[~center_mask].mean()
140
+
141
+ if outer_energy == 0:
142
+ return 0.5
143
+
144
+ ratio = center_energy / outer_energy
145
+
146
+ # VidΓ©o rΓ©elle : ratio typiquement > 3.5
147
+ # GAN : distribue l'Γ©nergie diffΓ©remment β†’ ratio anormal
148
+ if ratio > 4.0:
149
+ return 0.92
150
+ elif ratio > 2.5:
151
+ return 0.70
152
+ elif ratio > 1.5:
153
+ return 0.45
154
+ else:
155
+ return 0.25
156
+
157
+
158
+ # ═══════════════════════════════════════════════════════════
159
+ # Γ‰TAPE 3C β€” TEST CONTOURS (Blending Mask / Bord du visage)
160
+ # ═══════════════════════════════════════════════════════════
161
+
162
+ def test_contours(roi):
163
+ """
164
+ Analyse la rΓ©gularitΓ© des contours autour du visage.
165
+ Un deepfake par face-swap laisse souvent une frontière artificielle
166
+ autour du visage (blending imparfait).
167
+
168
+ Retourne un score d'authenticitΓ© [0-1].
169
+ """
170
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
171
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
172
+ edges = cv2.Canny(blurred, 50, 150)
173
+
174
+ h, w = edges.shape
175
+ if h < 10 or w < 10:
176
+ return 0.5
177
+
178
+ # Zone de bordure = 15% du bord de la ROI
179
+ border = int(min(h, w) * 0.15)
180
+ border_region = np.zeros_like(edges)
181
+ border_region[:border, :] = edges[:border, :]
182
+ border_region[-border:, :] = edges[-border:, :]
183
+ border_region[:, :border] = edges[:, :border]
184
+ border_region[:, -border:] = edges[:, -border:]
185
+
186
+ center_region = edges[border:-border, border:-border]
187
+
188
+ border_density = border_region.mean()
189
+ center_density = center_region.mean() if center_region.size > 0 else 1
190
+
191
+ # Un deepfake a souvent plus de contours en bordure (blending visible)
192
+ if center_density == 0:
193
+ return 0.5
194
+
195
+ ratio = border_density / (center_density + 1e-5)
196
+
197
+ if ratio > 2.5:
198
+ return 0.30 # bords suspectes
199
+ elif ratio > 1.5:
200
+ return 0.60
201
+ else:
202
+ return 0.88 # bords naturels
203
+
204
+
205
+ # ═══════════════════════════════════════════════════════════
206
+ # Γ‰TAPE 4 β€” SCORE FINAL + VERDICT
207
+ # ═══════════════════════════════════════════════════════════
208
+
209
+ WEIGHTS = {
210
+ "bruit": 0.35,
211
+ "fft": 0.40,
212
+ "contours": 0.25,
213
+ }
214
+
215
+ def score_face(roi):
216
+ """Calcule le score d'authenticitΓ© pondΓ©rΓ© pour une face."""
217
+ s_bruit = test_noise(roi)
218
+ s_fft = test_fft(roi)
219
+ s_contours = test_contours(roi)
220
+
221
+ score = (
222
+ s_bruit * WEIGHTS["bruit"] +
223
+ s_fft * WEIGHTS["fft"] +
224
+ s_contours * WEIGHTS["contours"]
225
+ )
226
+ return round(score, 4), {
227
+ "bruit": round(s_bruit * 100, 1),
228
+ "fft": round(s_fft * 100, 1),
229
+ "contours": round(s_contours * 100, 1),
230
+ }
231
+
232
+ def get_verdict(score_pct):
233
+ if score_pct >= 80:
234
+ return "βœ… AUTHENTIQUE", "Aucun artefact deepfake dΓ©tectΓ©."
235
+ elif score_pct >= 55:
236
+ return "⚠️ SUSPECT", "Des incohérences ont été détectées. Vérification manuelle recommandée."
237
+ else:
238
+ return "🚨 DEEPFAKE DΓ‰TECTΓ‰", "Score d'authenticitΓ© trΓ¨s bas. Contenu probablement falsifiΓ©."
239
+
240
+
241
+ # ═══════════════════════════════════════════════════════════
242
+ # FONCTION PRINCIPALE GRADIO
243
+ # ═══════════════════════════════════════════════════════════
244
+
245
+ def analyze_deepfake(video_path):
246
+ if video_path is None:
247
+ return "⚠️ Veuillez charger un fichier vidéo.", "{}"
248
+
249
+ # ── Γ‰tape 1 : Extraction ─────────────────
250
+ try:
251
+ frames, meta = extract_frames(video_path, max_seconds=20, n_frames=16)
252
+ except Exception as e:
253
+ return f"❌ Erreur extraction : {e}", "{}"
254
+
255
+ if not frames:
256
+ return "❌ Aucune frame extraite. Format non supporté.", "{}"
257
+
258
+ # ── Γ‰tape 2 : DΓ©tection visages ──────────
259
+ all_face_scores = []
260
+ detail_scores = []
261
+ frames_with_face = 0
262
+
263
+ for i, frame in enumerate(frames):
264
+ rois = detect_faces(frame)
265
+ if not rois:
266
+ continue
267
+ frames_with_face += 1
268
+ for roi in rois:
269
+ if roi.size == 0:
270
+ continue
271
+ sc, details = score_face(roi)
272
+ all_face_scores.append(sc)
273
+ detail_scores.append(details)
274
+
275
+ # ── Γ‰tape 3 : Score global ───────────────
276
+ if not all_face_scores:
277
+ rapport = (
278
+ f"πŸ›‘οΈ VideoShield v3.0 β€” Rapport d'AuthenticitΓ©\n"
279
+ f"{'─'*48}\n"
280
+ f"⚠️ Aucun visage détecté dans la vidéo.\n"
281
+ f"La dΓ©tection deepfake nΓ©cessite un visage visible.\n"
282
+ f"{'─'*48}\n"
283
+ f"DurΓ©e analysΓ©e : {meta['duree_analysee_s']}s / {meta['duree_totale_s']}s\n"
284
+ f"Frames analysΓ©es: {meta['frames_extraites']}\n"
285
+ f"RΓ©solution : {meta['resolution']}\n"
286
+ )
287
+ json_data = {"statut": "Aucun visage dΓ©tectΓ©", **meta}
288
+ return rapport, json.dumps(json_data, indent=2, ensure_ascii=False)
289
+
290
+ global_score = np.mean(all_face_scores)
291
+ global_score_pct = round(global_score * 100, 1)
292
+ verdict, explication = get_verdict(global_score_pct)
293
+
294
+ # Moyennes des dΓ©tails
295
+ avg_details = {
296
+ "bruit_authenticite_%": round(np.mean([d["bruit"] for d in detail_scores]), 1),
297
+ "fft_authenticite_%": round(np.mean([d["fft"] for d in detail_scores]), 1),
298
+ "contours_authenticite_%": round(np.mean([d["contours"] for d in detail_scores]), 1),
299
+ }
300
+
301
+ # ── Rapport texte ────────────────────────
302
+ rapport = (
303
+ f"πŸ›‘οΈ VideoShield v3.0 β€” Rapport d'AuthenticitΓ©\n"
304
+ f"{'─'*48}\n"
305
+ f"VERDICT : {verdict}\n"
306
+ f"SCORE : {global_score_pct}%\n"
307
+ f"ANALYSE : {explication}\n"
308
+ f"{'─'*48}\n"
309
+ f"DÉTAIL DES TESTS :\n"
310
+ f" β€’ Analyse bruit (Laplacien) : {avg_details['bruit_authenticite_%']}%\n"
311
+ f" β€’ Analyse frΓ©q. (FFT) : {avg_details['fft_authenticite_%']}%\n"
312
+ f" β€’ Analyse contours (Canny) : {avg_details['contours_authenticite_%']}%\n"
313
+ f"{'─'*48}\n"
314
+ f"Visages analysΓ©s : {len(all_face_scores)} dΓ©tection(s) / {frames_with_face} frame(s)\n"
315
+ f"DurΓ©e analysΓ©e : {meta['duree_analysee_s']}s / {meta['duree_totale_s']}s\n"
316
+ f"RΓ©solution : {meta['resolution']} @ {meta['fps']} fps\n"
317
+ f"{'─'*48}\n"
318
+ f"Standard : IASA TC-04 | Trusted Sound 2026\n"
319
+ f"DΓ©veloppΓ© par S2T β€” Smart Tunisian Technoparks"
320
+ )
321
+
322
+ # ── JSON ─────────────────────────────────
323
+ json_data = {
324
+ "timestamp": datetime.datetime.now().isoformat(),
325
+ "verdict": verdict,
326
+ "score_global_%": global_score_pct,
327
+ "tests_detail": avg_details,
328
+ "visages_detectes": len(all_face_scores),
329
+ "metadata_video": meta,
330
+ "standard": "IASA TC-04 / C2PA v1.3"
331
+ }
332
+
333
+ return rapport, json.dumps(json_data, indent=2, ensure_ascii=False)
334
+
335
+
336
+ # ═══════════════════════════════════════════════════════════
337
+ # INTERFACE GRADIO
338
+ # ═══════════════════════════════════════════════════════════
339
+
340
+ with gr.Blocks(theme=gr.themes.Soft(), title="VideoShield v3.0") as demo:
341
+
342
+ gr.Markdown("""
343
+ # πŸ›‘οΈ VideoShield v3.0 β€” Deepfake Detection
344
+ **Analyse par Bruit Β· FFT Β· Contours β€” Aucun GPU requis**
345
+ > Projet *Trusted Sound 2026* β€” Creative Europe CREA-CULT-2026-COOP-1 | S2T Tunisia
346
+ """)
347
+
348
+ with gr.Row():
349
+ with gr.Column(scale=1):
350
+ video_input = gr.Video(label="πŸ“Ή Charger la vidΓ©o (MP4, MKV, AVI, MOV)")
351
+ submit_btn = gr.Button("πŸ” Analyser", variant="primary", size="lg")
352
+ gr.Markdown("""
353
+ **MΓ©thode :**
354
+ - 🎯 Extraction des 20 premières secondes
355
+ - πŸ‘€ DΓ©tection des visages (Haar Cascade)
356
+ - πŸ”¬ 3 tests : Bruit Β· FFT Β· Contours
357
+ - πŸ“Š Score global pondΓ©rΓ©
358
+ """)
359
+
360
+ with gr.Column(scale=1):
361
+ rapport_output = gr.Textbox(
362
+ label="πŸ“‹ Rapport d'AuthenticitΓ©",
363
+ lines=18,
364
+ show_copy_button=True
365
+ )
366
+ json_output = gr.Code(
367
+ label="πŸ“¦ DonnΓ©es JSON",
368
+ language="json"
369
+ )
370
+
371
+ gr.Markdown("""
372
+ ---
373
+ πŸ”— [Antigravity Shield β€” Audio Deepfake](https://huggingface.co/spaces/NOBODY204/Music) |
374
+ πŸ“ Standards : IASA TC-04 Β· C2PA v1.3
375
+ """)
376
+
377
+ submit_btn.click(
378
+ fn=analyze_deepfake,
379
+ inputs=[video_input],
380
+ outputs=[rapport_output, json_output]
381
+ )
382
+
383
+ if __name__ == "__main__":
384
+ demo.launch()