NOBODY204 commited on
Commit
c58a8ee
·
verified ·
1 Parent(s): 60de469

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +319 -78
app.py CHANGED
@@ -7,134 +7,375 @@ import json
7
  import os
8
 
9
  # ═══════════════════════════════════════════════════════════
10
- # ÉTAPE 1 : RESTAURATION (RESTAURATION S2T)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # ═══════════════════════════════════════════════════════════
12
  def restore_roi(roi):
13
- """ Améliore la qualité pour l'affichage (Denoising + Sharpening) """
14
- if roi is None or roi.size == 0: return roi
15
  denoised = cv2.fastNlMeansDenoisingColored(roi, None, 10, 10, 7, 21)
16
  gaussian = cv2.GaussianBlur(denoised, (0, 0), 2.0)
17
  restored = cv2.addWeighted(denoised, 1.5, gaussian, -0.5, 0)
18
  return restored
19
 
20
  # ═══════════════════════════════════════════════════════════
21
- # ÉTAPE 2 : MOTEURS D'ANALYSE (Inspirés GitHub & DeepSafe)
22
  # ═══════════════════════════════════════════════════════════
23
 
24
  def test_localised_boundaries(roi):
25
- """
26
- Inspiré de 'Localised-Deepfake-Detection'.
27
- Cherche les discontinuités aux bords du visage (Face-swap).
28
- """
29
  gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
30
  edges = cv2.Canny(gray, 100, 200)
31
- # Analyse de la densité des contours sur les bords du masque
32
  h, w = edges.shape
33
  border_mask = np.zeros((h, w), dtype=np.uint8)
34
- cv2.rectangle(border_mask, (0,0), (w,h), 255, 2)
35
  edge_density = np.sum(cv2.bitwise_and(edges, border_mask))
36
  return 0.90 if edge_density < 500 else 0.30
37
 
38
  def test_noise_coherence(roi, frame):
39
- """
40
- Inspiré de 'DeepSafe'.
41
- Vérifie si le grain du visage matche avec le décor (vidéo Rzan vs Stallone).
42
- """
43
  gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
44
  gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
45
  var_roi = cv2.Laplacian(gray_roi, cv2.CV_32F).var()
46
  var_bg = cv2.Laplacian(gray_frame, cv2.CV_32F).var()
47
  ratio = var_roi / (var_bg + 1e-6)
48
- # Un ratio proche de 1.0 est signe d'authenticité (Rzan)
49
- if 0.5 < ratio < 1.7: return 0.95
50
- return 0.25 # Trop lisse (Deepfake) ou trop bruité (Injection)
51
 
52
  def test_fft_frequency(roi):
53
- """ Détection fréquentielle (Signatures IA) """
54
  gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY).astype(np.float32)
55
  fshift = np.fft.fftshift(np.fft.fft2(gray))
56
  mag = 20 * np.log(np.abs(fshift) + 1)
57
  h, w = mag.shape
58
- inner = mag[h//3:2*h//3, w//3:2*w//3].mean()
59
  outer = mag.mean()
60
- return 0.90 if (inner/outer) < 1.5 else 0.40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  # ═══════════════════════════════════════════════════════════
63
- # ÉTAPE 3 : LOGIQUE DE VERDICT & RAPPORT IASA
64
  # ═══════════════════════════════════════════════════════════
65
 
66
  def get_verdict(score_pct):
67
- if score_pct >= 75:
68
- return "✅ AUTHENTIQUE", "Validation conforme aux standards mobiles S2T."
69
- elif score_pct >= 55:
70
- return "⚠️ SUSPECT", "Incohérences de texture localisées détectées."
71
  else:
72
- return "🚨 DEEPFAKE DÉTECTÉ", "Anomalie majeure de structure (Face-Swap probable)."
 
 
 
 
73
 
74
  def analyze_video(video_path):
75
- if video_path is None: return "⚠️ Pas de vidéo.", "{}"
76
-
 
77
  cap = cv2.VideoCapture(video_path)
78
  frames = []
79
- # On analyse 16 frames réparties sur la durée
80
- for _ in range(16):
 
 
81
  ret, frame = cap.read()
82
- if ret: frames.append(frame)
 
83
  cap.release()
84
 
85
- face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
86
- all_scores = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- for frame in frames:
89
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
90
- faces = face_cascade.detectMultiScale(gray, 1.1, 5)
91
-
92
  for (x, y, w, h) in faces:
93
- roi_raw = frame[y:y+h, x:x+w]
94
-
95
- # Application des 3 moteurs
96
- s1 = test_localised_boundaries(roi_raw)
97
- s2 = test_noise_coherence(roi_raw, frame)
98
- s3 = test_fft_frequency(roi_raw)
99
-
100
- final_s = (s1 * 0.3) + (s2 * 0.5) + (s3 * 0.2)
101
- all_scores.append(final_s)
102
-
103
- if not all_scores: return "Aucun visage détecté.", "{}"
104
-
105
- global_score_pct = round(np.mean(all_scores) * 100, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  verdict, explication = get_verdict(global_score_pct)
107
 
108
- sep = "─" * 48
109
  rapport = (
110
- f"🛡️ VideoShield v4.0 — Rapport d'Authenticité\n{sep}\n"
111
- f"VERDICT : {verdict}\n"
112
- f"SCORE : {global_score_pct}%\n"
113
- f"ANALYSE : {explication}\n{sep}\n"
114
- f"Moteurs : Localised-Detection | DeepSafe | FFT\n"
115
- f"Standard : IASA TC-04 | S2T Tunisia 2026"
 
 
 
 
 
116
  )
117
-
118
- res_json = {"score": global_score_pct, "verdict": verdict, "timestamp": str(datetime.datetime.now())}
119
- return rapport, json.dumps(res_json, indent=2)
120
-
121
- # ═══════════════════════════════════════════════════════════
122
- # INTERFACE GRADIO ORIGINALE
123
- # ═══════════════════════════════════════════════════════════
124
-
125
- with gr.Blocks(title="VideoShield v4.0", theme=gr.themes.Soft()) as demo:
126
- gr.Markdown("# 🛡️ VideoShield v4.0 — Restauration & Authenticité S2T")
127
- gr.Markdown("Analyse forensique basée sur les standards IASA TC-04.")
128
-
129
- with gr.Row():
130
- with gr.Column():
131
- video_input = gr.Video(label="Vidéo Archive (Rzan, Chuck Norris, etc.)")
132
- btn = gr.Button("🔍 ANALYSER ET RESTAURER", variant="primary")
133
- with gr.Column():
134
- rapport_out = gr.Textbox(label="Rapport IASA TC-04", lines=12)
135
- json_out = gr.Code(label="Indexation JSON", language="json")
136
-
137
- btn.click(analyze_video, inputs=[video_input], outputs=[rapport_out, json_out])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  if __name__ == "__main__":
140
  demo.launch()
 
7
  import os
8
 
9
  # ═══════════════════════════════════════════════════════════
10
+ # CONFIG FFMPEG (local Windows + HuggingFace)
11
+ # ═══════════════════════════════════════════════════════════
12
+ os.environ["PATH"] = r"C:\Users\s.meddeb\ffmpeg\bin;" + os.environ.get("PATH", "")
13
+
14
+ # ═══════════════════════════════════════════════════════════
15
+ # ISCC SDK (optionnel — pip install iscc-sdk --user)
16
+ # ═══════════════════════════════════════════════════════════
17
+ try:
18
+ import iscc_sdk as idk
19
+ ISCC_AVAILABLE = True
20
+ except ImportError:
21
+ ISCC_AVAILABLE = False
22
+
23
+ # ═══════════════════════════════════════════════════════════
24
+ # ÉTAPE 1 : RESTAURATION
25
  # ═══════════════════════════════════════════════════════════
26
  def restore_roi(roi):
27
+ if roi is None or roi.size == 0:
28
+ return roi
29
  denoised = cv2.fastNlMeansDenoisingColored(roi, None, 10, 10, 7, 21)
30
  gaussian = cv2.GaussianBlur(denoised, (0, 0), 2.0)
31
  restored = cv2.addWeighted(denoised, 1.5, gaussian, -0.5, 0)
32
  return restored
33
 
34
  # ═══════════════════════════════════════════════════════════
35
+ # ÉTAPE 2 : MOTEURS V4 (hérités)
36
  # ═══════════════════════════════════════════════════════════
37
 
38
  def test_localised_boundaries(roi):
 
 
 
 
39
  gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
40
  edges = cv2.Canny(gray, 100, 200)
 
41
  h, w = edges.shape
42
  border_mask = np.zeros((h, w), dtype=np.uint8)
43
+ cv2.rectangle(border_mask, (0, 0), (w, h), 255, 2)
44
  edge_density = np.sum(cv2.bitwise_and(edges, border_mask))
45
  return 0.90 if edge_density < 500 else 0.30
46
 
47
  def test_noise_coherence(roi, frame):
 
 
 
 
48
  gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
49
  gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
50
  var_roi = cv2.Laplacian(gray_roi, cv2.CV_32F).var()
51
  var_bg = cv2.Laplacian(gray_frame, cv2.CV_32F).var()
52
  ratio = var_roi / (var_bg + 1e-6)
53
+ if 0.5 < ratio < 1.7:
54
+ return 0.95
55
+ return 0.25
56
 
57
  def test_fft_frequency(roi):
 
58
  gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY).astype(np.float32)
59
  fshift = np.fft.fftshift(np.fft.fft2(gray))
60
  mag = 20 * np.log(np.abs(fshift) + 1)
61
  h, w = mag.shape
62
+ inner = mag[h // 3:2 * h // 3, w // 3:2 * w // 3].mean()
63
  outer = mag.mean()
64
+ return 0.90 if (inner / outer) < 1.5 else 0.40
65
+
66
+ # ═══════════════════════════════════════════════════════════
67
+ # ÉTAPE 3 : NOUVEAUX MOTEURS V5 (Sora 2.0 / Runway / Kling)
68
+ # ═══════════════════════════════════════════════════════════
69
+
70
+ def test_optical_flow(frames):
71
+ """
72
+ Flux optique Farneback entre frames consécutives.
73
+ Sora 2.0 et Runway Gen-3 produisent un mouvement trop lisse (ratio std/mean < 0.15)
74
+ ou incohérent par à-coups. Les vraies vidéos ont une variance naturelle.
75
+ """
76
+ if len(frames) < 2:
77
+ return 0.50
78
+
79
+ scores = []
80
+ for i in range(min(len(frames) - 1, 8)):
81
+ g1 = cv2.cvtColor(frames[i], cv2.COLOR_BGR2GRAY)
82
+ g2 = cv2.cvtColor(frames[i + 1], cv2.COLOR_BGR2GRAY)
83
+ flow = cv2.calcOpticalFlowFarneback(
84
+ g1, g2, None, 0.5, 3, 15, 3, 5, 1.2, 0
85
+ )
86
+ magnitude, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
87
+ mag_std = np.std(magnitude)
88
+ mag_mean = np.mean(magnitude)
89
+
90
+ if mag_mean < 0.01:
91
+ scores.append(0.60) # Vidéo quasi-statique → neutre
92
+ else:
93
+ ratio = mag_std / mag_mean
94
+ if 0.30 < ratio < 2.50:
95
+ scores.append(0.88) # Mouvement naturel ✅
96
+ elif ratio < 0.15:
97
+ scores.append(0.18) # Trop lisse → IA 🚨
98
+ elif ratio > 4.0:
99
+ scores.append(0.22) # Saccades → injection 🚨
100
+ else:
101
+ scores.append(0.50)
102
+
103
+ return float(np.mean(scores)) if scores else 0.50
104
+
105
+
106
+ def test_eye_region(frame):
107
+ """
108
+ Analyse de la région oculaire via cascade Haar.
109
+ Sora 2.0 / Kling peinent encore sur la symétrie et la texture de l'iris.
110
+ """
111
+ eye_cascade = cv2.CascadeClassifier(
112
+ cv2.data.haarcascades + "haarcascade_eye.xml"
113
+ )
114
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
115
+ eyes = eye_cascade.detectMultiScale(gray, 1.1, 4, minSize=(20, 20))
116
+
117
+ if len(eyes) == 0:
118
+ return 0.38 # Aucun œil détecté → suspect
119
+
120
+ if len(eyes) == 2:
121
+ (x1, y1, w1, h1) = eyes[0]
122
+ (x2, y2, w2, h2) = eyes[1]
123
+ height_diff = abs(y1 - y2)
124
+ size_diff = abs(w1 - w2)
125
+ if height_diff < 20 and size_diff < 15:
126
+ return 0.90 # Symétrie naturelle ✅
127
+ return 0.52
128
+
129
+ return 0.58 # 1 ou 3+ yeux → ambigu
130
+
131
+
132
+ def test_color_coherence(roi, frame):
133
+ """
134
+ Cohérence colorimétrique en espace LAB entre visage et fond.
135
+ Les deepfakes modernes présentent une légère discordance de température
136
+ (delta-E > 30) car le générateur composite deux espaces couleur distincts.
137
+ """
138
+ if roi.size == 0 or frame.size == 0:
139
+ return 0.50
140
+
141
+ lab_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB).astype(np.float32)
142
+ lab_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB).astype(np.float32)
143
+
144
+ mean_roi = np.mean(lab_roi, axis=(0, 1))
145
+ mean_frame = np.mean(lab_frame, axis=(0, 1))
146
+
147
+ delta_E = float(np.sqrt(np.sum((mean_roi - mean_frame) ** 2)))
148
+
149
+ if delta_E < 22:
150
+ return 0.88 # Cohérent ✅
151
+ elif delta_E < 40:
152
+ return 0.55 # Légèrement suspect
153
+ else:
154
+ return 0.22 # Incohérence forte → IA 🚨
155
+
156
+
157
+ def test_texture_lbp(roi):
158
+ """
159
+ Approximation LBP (Local Binary Pattern) via numpy sans scikit.
160
+ Les visages IA ont une texture trop uniforme (variance LBP faible).
161
+ """
162
+ if roi.size == 0:
163
+ return 0.50
164
+
165
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY).astype(np.float32)
166
+
167
+ # Approximation LBP : comparaison pixel central avec 8 voisins
168
+ shifted = [
169
+ np.roll(np.roll(gray, dy, axis=0), dx, axis=1)
170
+ for dy, dx in [(-1,-1),(-1,0),(-1,1),(0,1),(1,1),(1,0),(1,-1),(0,-1)]
171
+ ]
172
+ lbp = np.zeros_like(gray)
173
+ for i, s in enumerate(shifted):
174
+ lbp += (gray >= s).astype(np.float32) * (2 ** i)
175
+
176
+ hist, _ = np.histogram(lbp, bins=64, range=(0, 256))
177
+ hist = hist / (hist.sum() + 1e-6)
178
+ variance = float(np.var(hist))
179
+
180
+ if variance > 0.0003:
181
+ return 0.88 # Texture riche et naturelle ✅
182
+ elif variance > 0.0001:
183
+ return 0.55
184
+ else:
185
+ return 0.22 # Texture trop uniforme → synthétique 🚨
186
 
187
  # ═══════════════════════════════════════════════════════════
188
+ # ÉTAPE 4 : VERDICT CALIBRÉ V5
189
  # ═══════════════════════════════════════════════════════════
190
 
191
  def get_verdict(score_pct):
192
+ if score_pct >= 72:
193
+ return "✅ AUTHENTIQUE", "Cohérence temporelle, colorimétrique et texturale conforme."
194
+ elif score_pct >= 58:
195
+ return "⚠️ SUSPECT", "Incohérences détectées vérification manuelle recommandée."
196
  else:
197
+ return "🚨 DEEPFAKE DÉTECTÉ", "Anomalie majeure (IA générative : Sora/Runway/Kling détectés)."
198
+
199
+ # ═══════════════════════════════════════════════════════════
200
+ # ÉTAPE 5 : PIPELINE PRINCIPAL
201
+ # ═══════════════════════════════════════════════════════════
202
 
203
  def analyze_video(video_path):
204
+ if video_path is None:
205
+ return "⚠️ Pas de vidéo fournie.", "{}", ""
206
+
207
  cap = cv2.VideoCapture(video_path)
208
  frames = []
209
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
210
+ step = max(1, total // 16)
211
+ for i in range(16):
212
+ cap.set(cv2.CAP_PROP_POS_FRAMES, i * step)
213
  ret, frame = cap.read()
214
+ if ret:
215
+ frames.append(frame)
216
  cap.release()
217
 
218
+ if not frames:
219
+ return "❌ Impossible de lire la vidéo.", "{}", ""
220
+
221
+ face_cascade = cv2.CascadeClassifier(
222
+ cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
223
+ )
224
+
225
+ # ── Analyse temporelle globale (flux optique sur toutes les frames) ──
226
+ flow_score = test_optical_flow(frames)
227
+
228
+ per_face_scores = []
229
+ engine_log = []
230
+
231
+ for idx, frame in enumerate(frames):
232
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
233
+ faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(60, 60))
234
 
 
 
 
 
235
  for (x, y, w, h) in faces:
236
+ roi = frame[y:y+h, x:x+w]
237
+
238
+ s1 = test_localised_boundaries(roi)
239
+ s2 = test_noise_coherence(roi, frame)
240
+ s3 = test_fft_frequency(roi)
241
+ s5 = test_eye_region(frame)
242
+ s6 = test_color_coherence(roi, frame)
243
+ s7 = test_texture_lbp(roi)
244
+
245
+ # Pondération V5 — flux optique global intégré
246
+ final = (
247
+ s1 * 0.10 + # Boundary edges
248
+ s2 * 0.18 + # Noise coherence
249
+ s3 * 0.12 + # FFT frequency
250
+ flow_score * 0.25 + # Optical flow temporel ← clé Sora
251
+ s5 * 0.10 + # Eye region
252
+ s6 * 0.15 + # Color LAB coherence
253
+ s7 * 0.10 # LBP texture
254
+ )
255
+ per_face_scores.append(final)
256
+ engine_log.append({
257
+ "frame": idx,
258
+ "boundary": round(s1, 2),
259
+ "noise": round(s2, 2),
260
+ "fft": round(s3, 2),
261
+ "flow": round(flow_score, 2),
262
+ "eye": round(s5, 2),
263
+ "color_lab":round(s6, 2),
264
+ "lbp": round(s7, 2),
265
+ "score": round(final * 100, 1)
266
+ })
267
+
268
+ if not per_face_scores:
269
+ # Aucun visage → analyse temporelle seule (Sora paysage, etc.)
270
+ global_score_pct = round(flow_score * 100, 1)
271
+ note = "Aucun visage détecté — verdict basé sur flux optique uniquement."
272
+ else:
273
+ global_score_pct = round(float(np.mean(per_face_scores)) * 100, 1)
274
+ note = f"{len(per_face_scores)} région(s) de visage analysée(s) sur {len(frames)} frames."
275
+
276
  verdict, explication = get_verdict(global_score_pct)
277
 
278
+ sep = "─" * 52
279
  rapport = (
280
+ f"🛡️ VideoShield v5.0 — Rapport d'Authenticité\n{sep}\n"
281
+ f"VERDICT : {verdict}\n"
282
+ f"SCORE : {global_score_pct}%\n"
283
+ f"ANALYSE : {explication}\n"
284
+ f"NOTE : {note}\n"
285
+ f"{sep}\n"
286
+ f"Moteurs : Boundary | Noise | FFT | Optical Flow\n"
287
+ f" : Eye Region | Color LAB | LBP Texture\n"
288
+ f"Cibles : Sora 2.0 | Runway Gen-3 | Kling | Pika 2\n"
289
+ f"Standard : IASA TC-04 | ACoNum Tunisia 2026\n"
290
+ f"{sep}"
291
  )
292
+
293
+ res_json = {
294
+ "version": "VideoShield v5.0",
295
+ "score": global_score_pct,
296
+ "verdict": verdict,
297
+ "flow_global": round(flow_score, 3),
298
+ "engines_detail": engine_log[:5], # 5 premières entrées pour lisibilité
299
+ "timestamp": str(datetime.datetime.now())
300
+ }
301
+
302
+ return rapport, json.dumps(res_json, indent=2), ""
303
+
304
+
305
+ def generate_iscc(video_path):
306
+ """Génère l'empreinte ISCC du fichier vidéo (onglet séparé)."""
307
+ if not ISCC_AVAILABLE:
308
+ return "❌ iscc-sdk non installé.\nFaire : pip install iscc-sdk --user"
309
+ if video_path is None:
310
+ return "⚠️ Aucun fichier fourni."
311
+
312
+ try:
313
+ ext = os.path.splitext(video_path)[1].lower()
314
+ if ext in [".wav", ".mp3", ".flac", ".aac", ".ogg"]:
315
+ meta = idk.code_audio(video_path)
316
+ else:
317
+ meta = idk.code_video(video_path)
318
+
319
+ iscc_code = meta.get("iscc", "N/A")
320
+ content_hash = meta.get("content", "N/A")
321
+ data_hash = meta.get("data", "N/A")
322
+ structure = meta.get("structure", "N/A")
323
+
324
+ sep = "─" * 52
325
+ return (
326
+ f"🔏 EMPREINTE ISCC — {os.path.basename(video_path)}\n{sep}\n"
327
+ f"Code ISCC : {iscc_code}\n"
328
+ f"Content Hash: {content_hash} ← stable après ré-encodage\n"
329
+ f"Data Hash : {data_hash}\n"
330
+ f"Structure : {structure}\n"
331
+ f"{sep}\n"
332
+ f"✅ Généré localement — conforme IASA TC-04 / ACoNum 2026"
333
+ )
334
+ except Exception as e:
335
+ return f"❌ Erreur ISCC : {str(e)}"
336
+
337
+
338
+ # ═══════════════════════════════════════════════════════════
339
+ # INTERFACE GRADIO V5
340
+ # ═══════════════════════════════════════════════════════════
341
+
342
+ with gr.Blocks(title="VideoShield v5.0", theme=gr.themes.Soft()) as demo:
343
+
344
+ gr.Markdown("# 🛡️ VideoShield v5.0 — Authenticité Vidéo IA")
345
+ gr.Markdown(
346
+ "Détection de deepfakes génératifs (Sora 2.0, Runway Gen-3, Kling, Pika 2) "
347
+ "via 7 moteurs forensiques OpenCV. Standard IASA TC-04 · ACoNum Tunisia 2026."
348
+ )
349
+
350
+ with gr.Tab("🔍 Analyse Deepfake"):
351
+ with gr.Row():
352
+ with gr.Column():
353
+ video_input = gr.Video(label="Vidéo à analyser (.mp4 / .mkv / .avi)")
354
+ btn_analyze = gr.Button("🔍 ANALYSER", variant="primary")
355
+ with gr.Column():
356
+ rapport_out = gr.Textbox(label="Rapport IASA TC-04", lines=14)
357
+ json_out = gr.Code(label="Indexation JSON", language="json")
358
+
359
+ btn_analyze.click(
360
+ analyze_video,
361
+ inputs=[video_input],
362
+ outputs=[rapport_out, json_out, gr.Textbox(visible=False)]
363
+ )
364
+
365
+ with gr.Tab("🔏 ISCC Fingerprint"):
366
+ gr.Markdown(
367
+ "Génère l'**empreinte de contenu ISCC** du fichier. "
368
+ "Le `content_hash` reste stable même après ré-encodage — "
369
+ "idéal pour le suivi d'authenticité TC-04."
370
+ )
371
+ with gr.Row():
372
+ with gr.Column():
373
+ video_iscc = gr.Video(label="Fichier vidéo ou audio")
374
+ btn_iscc = gr.Button("🔏 GÉNÉRER EMPREINTE ISCC", variant="secondary")
375
+ with gr.Column():
376
+ iscc_out = gr.Textbox(label="Résultat ISCC", lines=12)
377
+
378
+ btn_iscc.click(generate_iscc, inputs=[video_iscc], outputs=[iscc_out])
379
 
380
  if __name__ == "__main__":
381
  demo.launch()