oyabun-dev commited on
Commit
5f01c8d
Β·
0 Parent(s):

deploy: 2026-03-29T18:02:04Z

Browse files
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ ffmpeg \
5
+ libgl1 \
6
+ libglib2.0-0 \
7
+ libsm6 \
8
+ libxext6 \
9
+ libxrender-dev \
10
+ curl \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ WORKDIR /app
14
+
15
+ COPY requirements.txt .
16
+ # Install torch CPU-only first (~250MB vs ~2GB for the default CUDA build)
17
+ RUN \
18
+ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
19
+ RUN \
20
+ pip install --timeout 300 -r requirements.txt
21
+
22
+ COPY app/ ./app/
23
+ COPY scripts/ ./scripts/
24
+
25
+ EXPOSE 8000
26
+
27
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=300s --retries=3 \
28
+ CMD curl -f http://localhost:8000/health || exit 1
29
+
30
+ # Models download on first cold start and are cached by HF Spaces persistently.
31
+ # preload_models.py runs before uvicorn so the API is ready when /health passes.
32
+ CMD ["sh", "-c", "python scripts/preload_models.py && uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1"]
README.md ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: KAMY Vision AI
3
+ emoji: πŸ›‘οΈ
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 8000
8
+ pinned: false
9
+ ---
10
+
11
+ # AuthenticVision β€” DΓ©tection Deepfake & IA GΓ©nΓ©rative
12
+
13
+ Outil complet de dΓ©tection de contenus synthΓ©tiques (images, vidΓ©os, audio) gΓ©nΓ©rΓ©s par IA.
14
+ Disponible en API REST, CLI et interface web.
15
+
16
+ ---
17
+
18
+ ## Architecture v3.0
19
+
20
+ ```
21
+ AuthenticVision
22
+ β”œβ”€β”€ api.py β€” API FastAPI (images + vidΓ©os + audio)
23
+ β”œβ”€β”€ video_analyzer.py β€” Module d'analyse vidΓ©o multi-couches
24
+ β”œβ”€β”€ cli.py β€” Interface ligne de commande
25
+ β”œβ”€β”€ verify_robustness.py β€” Script de benchmark avec mΓ©triques
26
+ └── frontend/ β€” Interface web HTML/CSS/JS
27
+ ```
28
+
29
+ ### Modèles utilisés
30
+
31
+ **Images (ensemble de 3 modèles ViT fusionnés) :**
32
+ | Modèle | Rôle | Poids |
33
+ |--------|------|-------|
34
+ | `prithivMLmods/Deep-Fake-Detector-Model` | Deepfake faces | 35% |
35
+ | `prithivMLmods/AI-vs-Deepfake-vs-Real` | 3 classes : AI / Deepfake / Real | 40% |
36
+ | `Ateeqq/ai-vs-human-image-detector` | AI vs Humain (120k images) | 25% |
37
+
38
+ **Audio :** `MelodyMachine/Deepfake-audio-detection-V2`
39
+
40
+ **Optionnel :** `openai/clip-vit-base-patch32` (analyse sΓ©mantique, activable via `?use_clip=true`)
41
+
42
+ ### Couches forensiques β€” Images
43
+ - Ensemble 3 modèles ViT (score fusionné pondéré)
44
+ - Analyse EXIF Γ©tendue (19 sources IA dΓ©tectΓ©es : Gemini, DALL-E, Firefly, Flux, SynthID...)
45
+ - Spectre FFT (dΓ©tection sur-lissage et pics GAN)
46
+ - Texture & Bruit (uniformitΓ© anormale)
47
+ - Palette chromatique (entropie couleur artificielle)
48
+ - DΓ©tection filtre social (Snapchat/Instagram) avec seuils adaptatifs
49
+
50
+ ### Couches forensiques β€” VidΓ©o
51
+ - Ensemble modèles sur frames extraites (crop visage prioritaire)
52
+ - CohΓ©rence temporelle inter-frames (variation anormalement faible = deepfake)
53
+ - CohΓ©rence de teinte de peau entre visages (incohΓ©rence = manipulation)
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ # DΓ©pendances de base
61
+ pip install fastapi uvicorn torch transformers pillow piexif librosa python-multipart
62
+
63
+ # Support vidΓ©o (requis pour analyser des vidΓ©os)
64
+ pip install opencv-python-headless
65
+
66
+ # Support HEIC (photos iPhone)
67
+ pip install pillow-heif
68
+ ```
69
+
70
+ ### Lancer l'API
71
+
72
+ ```bash
73
+ python api.py
74
+ # API disponible sur http://localhost:8000
75
+ ```
76
+
77
+ ### Lancer l'interface web
78
+
79
+ Ouvrir `frontend/index.html` dans un navigateur (l'API doit Γͺtre lancΓ©e).
80
+
81
+ ---
82
+
83
+ ## API β€” Endpoints
84
+
85
+ ### `POST /predict` — Analyse complète
86
+ Supporte : images (JPEG, PNG, WebP, HEIC), vidΓ©os (MP4, MOV, AVI, WebM), audio (WAV, MP3, M4A)
87
+
88
+ ```bash
89
+ # Image
90
+ curl -X POST http://localhost:8000/predict \
91
+ -F "file=@photo.jpg" \
92
+ -F "sensitivity=50" \
93
+ -F "robust_mode=false"
94
+
95
+ # VidΓ©o
96
+ curl -X POST http://localhost:8000/predict \
97
+ -F "file=@video.mp4" \
98
+ -F "sensitivity=50"
99
+
100
+ # Avec CLIP (GPU recommandΓ©)
101
+ curl -X POST "http://localhost:8000/predict?use_clip=true" \
102
+ -F "file=@photo.jpg"
103
+ ```
104
+
105
+ ### `POST /predict/fast` β€” Mode rapide
106
+ Modèle principal seul, sans couches forensiques complètes (~5-15s CPU).
107
+
108
+ ```bash
109
+ curl -X POST http://localhost:8000/predict/fast \
110
+ -F "file=@photo.jpg" \
111
+ -F "sensitivity=50"
112
+ ```
113
+
114
+ ### `GET /health` β€” Statut de l'API
115
+
116
+ ```bash
117
+ curl http://localhost:8000/health
118
+ ```
119
+
120
+ **Exemple de rΓ©ponse (image) :**
121
+ ```json
122
+ {
123
+ "status": "success",
124
+ "verdict": "DEEPFAKE",
125
+ "fake_prob": 0.8731,
126
+ "real_prob": 0.1269,
127
+ "media_type": "IMAGE",
128
+ "ai_source": "Google Gemini",
129
+ "forensic_details": {
130
+ "fusion_profile": "EXIF_IA_DETECTE",
131
+ "layer_scores": {
132
+ "ensemble": 0.82,
133
+ "exif": 0.97,
134
+ "fft": 0.61,
135
+ "texture": 0.55,
136
+ "color": 0.70
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## CLI
145
+
146
+ ```bash
147
+ # Installation globale
148
+ pip install authenticvision-cli
149
+
150
+ # Analyse d'une image
151
+ deepfake photo.jpg
152
+
153
+ # Avec options
154
+ deepfake photo.jpg --sensitivity 70 --robust --audit
155
+
156
+ # Sortie JSON (pour scripts/pipelines)
157
+ deepfake photo.jpg --json
158
+
159
+ # Mode rapide (ViT seul)
160
+ deepfake photo.jpg --fast
161
+
162
+ # Audio
163
+ deepfake voix.mp3 --sensitivity 60
164
+ ```
165
+
166
+ **Exemple de sortie :**
167
+ ```
168
+ ==================================================
169
+ RÉSULTAT DE L'ANALYSE — AuthenticVision v2.0
170
+ ==================================================
171
+ Fichier : photo.jpg
172
+ Type : IMAGE
173
+ SensibilitΓ©: 50% | Robustesse: OFF
174
+ --------------------------------------------------
175
+ VERDICT : AUTHENTIQUE
176
+ Confiance : 96.0% rΓ©el
177
+ ==================================================
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Benchmark
183
+
184
+ Le script `verify_robustness.py` Γ©value les performances sur un dataset local.
185
+
186
+ ```bash
187
+ # Benchmark complet avec rapport
188
+ python verify_robustness.py \
189
+ --real_dir datasets/real \
190
+ --fake_dir datasets/fake \
191
+ --output rapport.json
192
+
193
+ # Mode rapide
194
+ python verify_robustness.py \
195
+ --real_dir datasets/real \
196
+ --fake_dir datasets/fake \
197
+ --mode fast
198
+
199
+ # Test unitaire sur une image
200
+ python verify_robustness.py --image photo.jpg
201
+ ```
202
+
203
+ **MΓ©triques calculΓ©es :** Accuracy, Precision, Recall, F1, AUC-ROC, TP/TN/FP/FN
204
+
205
+ ---
206
+
207
+ ## Datasets de test recommandΓ©s
208
+
209
+ ### Images
210
+
211
+ | Dataset | Contenu | Accès | Lien |
212
+ |---------|---------|-------|------|
213
+ | **Deepfake-Eval-2024** | 759 rΓ©elles + 1191 fakes "in-the-wild" (rΓ©seaux sociaux 2024) | Direct HuggingFace | [lien](https://huggingface.co/datasets/nuriachandra/Deepfake-Eval-2024) |
214
+ | **CIFAKE** | 60k rΓ©elles vs gΓ©nΓ©rΓ©es (Stable Diffusion) | Kaggle | [lien](https://www.kaggle.com/datasets/birdy654/cifake-real-and-ai-generated-synthetic-images) |
215
+ | **DeepfakeJudge** | Benchmark VLM avec labels rΓ©el/fake + reasoning | HuggingFace | [lien](https://huggingface.co/datasets/MBZUAI/DeepfakeJudge-Dataset) |
216
+
217
+ ```bash
218
+ # TΓ©lΓ©charger CIFAKE via Kaggle CLI
219
+ pip install kaggle
220
+ kaggle datasets download -d birdy654/cifake-real-and-ai-generated-synthetic-images
221
+ unzip cifake-real-and-ai-generated-synthetic-images.zip -d datasets/cifake
222
+
223
+ # Lancer le benchmark
224
+ python verify_robustness.py \
225
+ --real_dir datasets/cifake/test/REAL \
226
+ --fake_dir datasets/cifake/test/FAKE \
227
+ --output rapport_cifake.json
228
+ ```
229
+
230
+ ### VidΓ©os
231
+
232
+ | Dataset | Contenu | Accès | Lien |
233
+ |---------|---------|-------|------|
234
+ | **DFDC (Facebook)** | 128k clips 10s, acteurs consentants, très diversifié | Kaggle (gratuit) | [lien](https://www.kaggle.com/competitions/deepfake-detection-challenge/data) |
235
+ | **FaceForensics++** | 1000 vidΓ©os originales + 4 mΓ©thodes de manipulation (Deepfakes, Face2Face, FaceSwap, NeuralTextures) | Formulaire Google (gratuit) | [lien](https://github.com/ondyari/FaceForensics) |
236
+ | **Celeb-DF v2** | 5639 deepfakes haute qualitΓ© de cΓ©lΓ©britΓ©s | Formulaire (gratuit) | [lien](https://github.com/yuezunli/celeb-deepfakeforensics) |
237
+ | **Deepfake-Eval-2024** | 45h de vidΓ©os "in-the-wild" 2024 | HuggingFace | [lien](https://huggingface.co/datasets/nuriachandra/Deepfake-Eval-2024) |
238
+ | **UniDataPro deepfake-videos** | 10k+ fichiers, 7k+ personnes | HuggingFace | [lien](https://huggingface.co/datasets/UniDataPro/deepfake-videos-dataset) |
239
+
240
+ **VidΓ©os de test rapides (sans inscription) :**
241
+
242
+ Pour tester immΓ©diatement sans tΓ©lΓ©charger un dataset complet, tu peux utiliser ces sources :
243
+
244
+ 1. **VidΓ©os rΓ©elles** β€” tΓ©lΓ©charge quelques clips depuis [Pexels](https://www.pexels.com/videos/) (licence gratuite, visages rΓ©els)
245
+
246
+ 2. **VidΓ©os deepfake** β€” le repo [deepfakes-in-the-wild](https://github.com/jmpu/webconf21-deepfakes-in-the-wild) contient des liens vers des exemples publics
247
+
248
+ 3. **GΓ©nΓ©rer tes propres tests** avec [Deep-Live-Cam](https://github.com/hacksider/Deep-Live-Cam) (open source) sur une vidΓ©o Pexels
249
+
250
+ ```bash
251
+ # Tester une vidΓ©o directement via l'API
252
+ curl -X POST http://localhost:8000/predict \
253
+ -F "file=@ma_video.mp4" \
254
+ -F "sensitivity=50" | python -m json.tool
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Paramètres
260
+
261
+ | Paramètre | Valeurs | Description |
262
+ |-----------|---------|-------------|
263
+ | `sensitivity` | 1–99 (dΓ©faut: 50) | Rigueur de dΓ©tection. 80+ = strict, 20- = indulgent |
264
+ | `robust_mode` | true/false | Compense les filtres Snap/Insta, rΓ©duit les faux positifs |
265
+ | `use_clip` | true/false | Active l'analyse sΓ©mantique CLIP (GPU recommandΓ©) |
266
+
267
+ ---
268
+
269
+ ## Profils de fusion
270
+
271
+ L'API adapte automatiquement les poids selon le contexte dΓ©tectΓ© :
272
+
273
+ | Profil | DΓ©clencheur | Comportement |
274
+ |--------|-------------|--------------|
275
+ | `EXIF_IA_DETECTE` | Source IA trouvΓ©e dans les mΓ©tadonnΓ©es | EXIF = 60% du score |
276
+ | `FILTRE_SOCIAL` | Filtre Snap/Insta dΓ©tectΓ© | EXIF ignorΓ©, ensemble ViT prioritaire |
277
+ | `EXIF_FIABLE` | Appareil photo identifiΓ© dans EXIF | EXIF = 32% du score |
278
+ | `EXIF_ABSENT` | Pas de mΓ©tadonnΓ©es (strip rΓ©seau social) | FFT + texture renforcΓ©s |
279
+ | `STANDARD` | Cas gΓ©nΓ©ral | PondΓ©ration Γ©quilibrΓ©e |
280
+
281
+ ---
282
+
283
+ ## Structure du projet
284
+
285
+ ```
286
+ deepfake_detection/
287
+ β”œβ”€β”€ api.py β€” API FastAPI v3.0 (image + vidΓ©o + audio)
288
+ β”œβ”€β”€ video_analyzer.py β€” Analyse vidΓ©o multi-couches
289
+ β”œβ”€β”€ cli.py β€” CLI (commande globale `deepfake`)
290
+ β”œβ”€β”€ verify_robustness.py β€” Benchmark avec mΓ©triques complΓ¨tes
291
+ β”œβ”€β”€ setup.py β€” Configuration PyPI
292
+ β”œβ”€β”€ frontend/
293
+ β”‚ β”œβ”€β”€ index.html β€” Interface web (tabs Image / Audio / VidΓ©o / Texte)
294
+ β”‚ β”œβ”€β”€ script.js β€” Logique frontend
295
+ β”‚ └── style.css β€” Styles
296
+ └── README.md
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Corrections & AmΓ©liorations rΓ©centes
302
+
303
+ ### v2.1 β€” Garde-fous filtre social (correctif faux positifs)
304
+
305
+ Problème identifié : avec `--sensitivity 85`, le shift de +0.18 était appliqué **avant** les garde-fous, ce qui pouvait faire passer une photo filtrée (Snapchat/Instagram) en DEEPFAKE.
306
+
307
+ Corrections apportΓ©es dans `cli.py` et `api.py` :
308
+
309
+ 1. Les garde-fous sont maintenant appliquΓ©s sur le **score brut** (avant le shift sensitivity)
310
+ 2. Seuil filtre social Γ©largi : `vit_fake < 0.70` (au lieu de `< 0.55`) pour couvrir la zone grise
311
+ 3. Seuil de dΓ©clenchement filtre abaissΓ© : `fc > 0.45` (au lieu de `> 0.60`)
312
+
313
+ Comportement attendu après correction :
314
+ - Photo rΓ©elle avec filtre Snap + sensitivity=85 β†’ AUTHENTIQUE (score brut plafonnΓ© Γ  0.46, final ≀ 0.64)
315
+ - Image Gemini/DALL-E + sensitivity=85 β†’ DEEPFAKE maintenu (vit_fake > 0.70, garde-fou inactif)
316
+
317
+ ---
318
+
319
+ ## VidΓ©os de test recommandΓ©es
320
+
321
+ ### Sans inscription (accès immédiat)
322
+
323
+ | Source | Type | Lien |
324
+ |--------|------|------|
325
+ | **Pexels** | VidΓ©os rΓ©elles (visages, portraits) β€” licence gratuite | [pexels.com/videos](https://www.pexels.com/videos/) |
326
+ | **Pixabay** | VidΓ©os rΓ©elles libres de droits | [pixabay.com/videos](https://pixabay.com/videos/) |
327
+ | **FaceForensics++ samples** | Exemples deepfake publics (GitHub) | [github.com/ondyari/FaceForensics](https://github.com/ondyari/FaceForensics) |
328
+ | **Deepfakes-in-the-wild** | Liens vers deepfakes publics collectΓ©s | [github.com/jmpu/webconf21-deepfakes-in-the-wild](https://github.com/jmpu/webconf21-deepfakes-in-the-wild) |
329
+
330
+ ### Datasets complets (formulaire ou Kaggle)
331
+
332
+ | Dataset | Contenu | Accès | Lien |
333
+ |---------|---------|-------|------|
334
+ | **DFDC (Facebook)** | 128k clips 10s, très diversifié | Kaggle (gratuit) | [lien](https://www.kaggle.com/competitions/deepfake-detection-challenge/data) |
335
+ | **FaceForensics++** | 1000 vidΓ©os + 4 mΓ©thodes (Deepfakes, Face2Face, FaceSwap, NeuralTextures) | Formulaire Google | [lien](https://github.com/ondyari/FaceForensics) |
336
+ | **Celeb-DF v2** | 5639 deepfakes haute qualitΓ© de cΓ©lΓ©britΓ©s | Formulaire gratuit | [lien](https://github.com/yuezunli/celeb-deepfakeforensics) |
337
+ | **UniDataPro deepfake-videos** | 10k+ fichiers, 7k+ personnes | HuggingFace | [lien](https://huggingface.co/datasets/UniDataPro/deepfake-videos-dataset) |
338
+
339
+ ### Tester rapidement une vidΓ©o
340
+
341
+ ```bash
342
+ # Via l'API
343
+ curl -X POST http://localhost:8000/predict \
344
+ -F "file=@ma_video.mp4" \
345
+ -F "sensitivity=50" | python -m json.tool
346
+
347
+ # Mode rapide (moins de modèles, plus rapide)
348
+ curl -X POST http://localhost:8000/predict/fast \
349
+ -F "file=@ma_video.mp4" \
350
+ -F "sensitivity=50"
351
+ ```
352
+
353
+ **VidΓ©os de test suggΓ©rΓ©es (Pexels, tΓ©lΓ©chargement direct) :**
354
+ - Portrait femme en intΓ©rieur : [pexels.com/video/3209828](https://www.pexels.com/video/3209828/) (rΓ©elle)
355
+ - Portrait homme en extΓ©rieur : [pexels.com/video/3195394](https://www.pexels.com/video/3195394/) (rΓ©elle)
356
+ - Pour les deepfakes : utilise les samples du repo FaceForensics++ (lien ci-dessus)
357
+
358
+ ---
359
+
360
+ ## Roadmap
361
+
362
+ - [x] DΓ©tection image multi-couches (ViT + EXIF + FFT + Texture + Palette)
363
+ - [x] Ensemble 3 modèles ViT
364
+ - [x] DΓ©tection sources IA gΓ©nΓ©ratives (Gemini, DALL-E, Flux, Firefly...)
365
+ - [x] Analyse vidΓ©o (cohΓ©rence temporelle + ensemble frames)
366
+ - [x] DΓ©tection audio (voix clonΓ©e)
367
+ - [x] Interface web avec tab VidΓ©o
368
+ - [x] Script de benchmark (Accuracy / F1 / AUC-ROC)
369
+ - [x] Correctif garde-fous filtre social (v2.1)
370
+ - [ ] DΓ©tection texte LLM (DeepSeek, ChatGPT, Claude) β€” en cours
371
+ - [ ] Support streaming vidΓ©o temps rΓ©el
372
+ - [ ] Fine-tuning sur Deepfake-Eval-2024
app/__init__.py ADDED
File without changes
app/core/__init__.py ADDED
File without changes
app/core/config.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MAX_FILE_SIZE = 20 * 1024 * 1024 # 20 MB
2
+ MAX_VIDEO_SIZE = 200 * 1024 * 1024 # 200 MB
3
+
4
+ ALLOWED_IMAGE_MIMETYPES = [
5
+ "image/jpeg", "image/png", "image/webp",
6
+ "image/heic", "image/heif",
7
+ "image/jfif", "image/pjpeg", "image/bmp",
8
+ "image/gif", "image/tiff", "image/avif",
9
+ "image/x-jfif",
10
+ ]
11
+
12
+ ALLOWED_AUDIO_MIMETYPES = [
13
+ "audio/wav", "audio/mpeg", "audio/mp3",
14
+ "audio/ogg", "audio/flac", "audio/x-m4a", "audio/x-wav",
15
+ ]
16
+
17
+ ALLOWED_VIDEO_MIMETYPES = [
18
+ "video/mp4", "video/quicktime", "video/x-msvideo",
19
+ "video/webm", "video/mpeg", "video/x-matroska",
20
+ ]
21
+
22
+ # ── Image models ───────────────────────────────────────────────────────────────
23
+
24
+ IMAGE_ENSEMBLE = [
25
+ {
26
+ "key": "ai_vs_human",
27
+ "name": "Ateeqq/ai-vs-human-image-detector",
28
+ "weight": 0.45,
29
+ "desc": "AI vs Human 120k (ViT)",
30
+ },
31
+ {
32
+ "key": "ai_vs_deepfake_vs_real",
33
+ "name": "prithivMLmods/AI-vs-Deepfake-vs-Real",
34
+ "weight": 0.35,
35
+ "desc": "AI/Deepfake/Real 3-class (ViT)",
36
+ },
37
+ {
38
+ "key": "deepfake_detector",
39
+ "name": "prithivMLmods/Deep-Fake-Detector-Model",
40
+ "weight": 0.20,
41
+ "desc": "Deepfake faces (ViT)",
42
+ },
43
+ ]
44
+
45
+ IMAGE_FAST_ENSEMBLE = [
46
+ {
47
+ "key": "ai_vs_human",
48
+ "name": "Ateeqq/ai-vs-human-image-detector",
49
+ "weight": 0.45,
50
+ "desc": "AI vs Human 120k (ViT)",
51
+ },
52
+ {
53
+ "key": "deepfake_detector",
54
+ "name": "prithivMLmods/Deep-Fake-Detector-Model",
55
+ "weight": 0.55,
56
+ "desc": "Deepfake faces (ViT)",
57
+ },
58
+ ]
59
+
60
+ # ── Audio model ────────────────────────────────────────────────────────────────
61
+
62
+ AUDIO_MODEL = {
63
+ "key": "deepfake_audio_v2",
64
+ "name": "MelodyMachine/Deepfake-audio-detection-V2",
65
+ "desc": "Deepfake Audio Detection V2",
66
+ }
67
+
68
+ # ── Video ensemble (reuses ViT models) ────────────────────────────────────────
69
+
70
+ VIDEO_ENSEMBLE = [
71
+ {
72
+ "key": "deepfake_detector",
73
+ "name": "prithivMLmods/Deep-Fake-Detector-Model",
74
+ "weight": 0.40,
75
+ "desc": "Deepfake faces (ViT)",
76
+ },
77
+ {
78
+ "key": "ai_vs_deepfake_vs_real",
79
+ "name": "prithivMLmods/AI-vs-Deepfake-vs-Real",
80
+ "weight": 0.35,
81
+ "desc": "AI/Deepfake/Real 3-class",
82
+ },
83
+ {
84
+ "key": "ai_vs_human",
85
+ "name": "Ateeqq/ai-vs-human-image-detector",
86
+ "weight": 0.25,
87
+ "desc": "AI vs Human 120k",
88
+ },
89
+ ]
90
+
91
+ # ── Text models ────────────────────────────────────────────────────────────────
92
+
93
+ TEXT_MODELS = {
94
+ "ai1": {
95
+ "name": "fakespot-ai/roberta-base-ai-text-detection-v1",
96
+ "desc": "RoBERTa AI text detector (Fakespot)",
97
+ },
98
+ "ai2": {
99
+ "name": "Hello-SimpleAI/chatgpt-detector-roberta",
100
+ "desc": "RoBERTa ChatGPT detector",
101
+ },
102
+ "fn1": {
103
+ "name": "vikram71198/distilroberta-base-finetuned-fake-news-detection",
104
+ "desc": "DistilRoBERTa fake news detector",
105
+ },
106
+ "fn2": {
107
+ "name": "jy46604790/Fake-News-Bert-Detect",
108
+ "desc": "BERT fake news detector",
109
+ },
110
+ }
app/core/device.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ if torch.backends.mps.is_available():
4
+ DEVICE = torch.device("mps")
5
+ elif torch.cuda.is_available():
6
+ DEVICE = torch.device("cuda")
7
+ else:
8
+ DEVICE = torch.device("cpu")
app/main.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+
4
+ from app.routers import image as image_router
5
+ from app.routers import audio as audio_router
6
+ from app.routers import video as video_router
7
+ from app.routers import text as text_router
8
+
9
+ app = FastAPI(title="KAMY Vision AI", description="Plateforme de dΓ©tection de deepfakes")
10
+
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=[
14
+ "http://localhost:3000",
15
+ "http://127.0.0.1:3000",
16
+ "http://localhost:5173",
17
+ "http://127.0.0.1:5173",
18
+ "http://localhost:8000",
19
+ "http://127.0.0.1:8000",
20
+ "null",
21
+ ],
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ app.include_router(image_router.router)
28
+ app.include_router(audio_router.router)
29
+ app.include_router(video_router.router)
30
+ app.include_router(text_router.router)
31
+
32
+
33
+ @app.get("/health")
34
+ async def health():
35
+ return {"status": "ok"}
app/models/__init__.py ADDED
File without changes
app/models/loader.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import torch
3
+ from transformers import AutoImageProcessor, AutoModelForImageClassification
4
+
5
+ from app.core.device import DEVICE
6
+
7
+ _cache: dict = {}
8
+
9
+
10
+ def load_image_model(cfg: dict):
11
+ """Lazy-load a model by config key. Returns (processor, model) or None on failure."""
12
+ key = cfg["key"]
13
+ if key in _cache:
14
+ return _cache[key]
15
+
16
+ print(f"Loading {cfg['desc']} ({cfg['name']})...")
17
+ try:
18
+ proc = AutoImageProcessor.from_pretrained(cfg["name"])
19
+ model = AutoModelForImageClassification.from_pretrained(cfg["name"]).to(DEVICE)
20
+ model.eval()
21
+ _cache[key] = (proc, model)
22
+ print(f"{key} ready β€” labels: {model.config.id2label}")
23
+ except Exception as e:
24
+ print(f"Failed to load {key}: {e}")
25
+ _cache[key] = None
26
+
27
+ return _cache[key]
28
+
29
+
30
+ def unload_all():
31
+ global _cache
32
+ for entry in _cache.values():
33
+ if entry is not None:
34
+ proc, model = entry
35
+ del model
36
+ del proc
37
+ _cache = {}
38
+ gc.collect()
39
+ if torch.backends.mps.is_available():
40
+ torch.mps.empty_cache()
41
+ elif torch.cuda.is_available():
42
+ torch.cuda.empty_cache()
app/pipelines/__init__.py ADDED
File without changes
app/pipelines/audio.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+
4
+ import librosa
5
+ import numpy as np
6
+ import torch
7
+ from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
8
+
9
+ from app.core.config import AUDIO_MODEL
10
+ from app.core.device import DEVICE
11
+
12
+ _audio_model = None
13
+ _audio_proc = None
14
+
15
+
16
+ def _get_audio_model():
17
+ global _audio_model, _audio_proc
18
+ if _audio_model is None:
19
+ name = AUDIO_MODEL["name"]
20
+ print(f"Loading {AUDIO_MODEL['desc']} ({name})...")
21
+ _audio_proc = AutoFeatureExtractor.from_pretrained(name)
22
+ _audio_model = AutoModelForAudioClassification.from_pretrained(name).to(DEVICE)
23
+ _audio_model.eval()
24
+ print(f"{AUDIO_MODEL['key']} ready")
25
+ return _audio_proc, _audio_model
26
+
27
+
28
+ def run(audio_bytes: bytes, sensitivity: int = 50) -> dict:
29
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tf:
30
+ tf.write(audio_bytes)
31
+ tmp = tf.name
32
+ try:
33
+ speech, sr = librosa.load(tmp, sr=16000)
34
+ finally:
35
+ if os.path.exists(tmp):
36
+ os.remove(tmp)
37
+
38
+ proc, model = _get_audio_model()
39
+ inputs = proc(speech, sampling_rate=sr, return_tensors="pt").to(DEVICE)
40
+ with torch.no_grad():
41
+ probs_tensor = torch.nn.functional.softmax(model(**inputs).logits, dim=-1)[0].cpu().numpy()
42
+
43
+ # Resolve fake/real indices dynamically β€” label order varies by model
44
+ id2label = {int(k): v.lower() for k, v in model.config.id2label.items()}
45
+ fake_kw = ["fake", "spoof", "synthetic", "generated", "deepfake", "ai"]
46
+ real_kw = ["real", "human", "authentic", "genuine", "bonafide", "natural"]
47
+ fake_idx = [i for i, lbl in id2label.items() if any(w in lbl for w in fake_kw)]
48
+ real_idx = [i for i, lbl in id2label.items() if any(w in lbl for w in real_kw)]
49
+ print(f" audio id2label: {id2label} | fake_idx={fake_idx} real_idx={real_idx}") # noqa: T201
50
+
51
+ if fake_idx:
52
+ fake_prob = float(sum(probs_tensor[i] for i in fake_idx))
53
+ elif real_idx:
54
+ fake_prob = float(1.0 - sum(probs_tensor[i] for i in real_idx))
55
+ else:
56
+ # Fallback: assume index 0 = fake (common for binary audio models)
57
+ fake_prob = float(probs_tensor[0])
58
+
59
+ shift = (sensitivity - 50.0) / 50.0
60
+ adjusted = float(np.clip(fake_prob + shift * 0.18, 0.0, 1.0))
61
+
62
+ if adjusted > 0.65:
63
+ verdict, reason = "DEEPFAKE", "Signal vocal prΓ©sentant des caractΓ©ristiques synthΓ©tiques dΓ©tectΓ©es."
64
+ elif adjusted < 0.35:
65
+ verdict, reason = "AUTHENTIQUE", "Signal vocal naturel, aucun artefact de synthèse détecté."
66
+ else:
67
+ verdict, reason = "INDÉTERMINÉ", "Signal vocal ambigu, analyse non concluante."
68
+
69
+ confidence = "haute" if adjusted > 0.85 or adjusted < 0.15 else ("moyenne" if adjusted > 0.70 or adjusted < 0.30 else "faible")
70
+
71
+ return {
72
+ "verdict": verdict,
73
+ "confidence": confidence,
74
+ "reason": reason,
75
+ "fake_prob": round(adjusted, 4),
76
+ "real_prob": round(1.0 - adjusted, 4),
77
+ "sensitivity_used": sensitivity,
78
+ "models": {
79
+ AUDIO_MODEL["key"]: {
80
+ "score": round(fake_prob, 4),
81
+ "desc": AUDIO_MODEL["desc"],
82
+ }
83
+ },
84
+ }
app/pipelines/fakenews.py ADDED
File without changes
app/pipelines/image.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ from PIL import Image, ImageFilter
4
+
5
+ from app.core.config import IMAGE_ENSEMBLE, IMAGE_FAST_ENSEMBLE
6
+ from app.core.device import DEVICE
7
+ from app.models.loader import load_image_model
8
+
9
+
10
+ # ── Model inference ────────────────────────────────────────────────────────────
11
+
12
+ def _infer_fake_score(proc, model, img: Image.Image) -> float:
13
+ """
14
+ Stable inference: average over 3 passes to reduce variance.
15
+ Dynamically resolves fake/real indices from id2label β€” no hardcoded assumptions.
16
+ Returns a score 0β†’1 (1 = synthetic/fake).
17
+ """
18
+ inputs = proc(images=img, return_tensors="pt").to(DEVICE)
19
+ with torch.no_grad():
20
+ logits_list = [model(**inputs).logits for _ in range(3)]
21
+ logits_mean = torch.stack(logits_list).mean(dim=0)
22
+ probs = torch.nn.functional.softmax(logits_mean, dim=-1)[0].cpu().numpy()
23
+
24
+ id2label = {int(k): v.lower() for k, v in model.config.id2label.items()}
25
+ fake_kw = ["fake", "ai", "artificial", "synthetic", "generated", "deepfake"]
26
+ real_kw = ["real", "human", "authentic", "genuine"]
27
+
28
+ fake_indices = [i for i, lbl in id2label.items() if any(w in lbl for w in fake_kw)]
29
+ real_indices = [i for i, lbl in id2label.items() if any(w in lbl for w in real_kw)]
30
+
31
+ if not fake_indices and not real_indices:
32
+ return float(probs[1]) if len(probs) >= 2 else 0.5
33
+
34
+ fake_score = float(np.sum([probs[i] for i in fake_indices])) if fake_indices else 0.0
35
+ real_score = float(np.sum([probs[i] for i in real_indices])) if real_indices else 0.0
36
+ total = fake_score + real_score
37
+ return fake_score / total if total > 1e-9 else 0.5
38
+
39
+
40
+ def _run_ensemble(img: Image.Image, ensemble: list) -> dict:
41
+ """Run all models in the ensemble and return weighted score + per-model details."""
42
+ results = {}
43
+ weighted_sum = 0.0
44
+ total_weight = 0.0
45
+
46
+ for cfg in ensemble:
47
+ loaded = load_image_model(cfg)
48
+ if loaded is None:
49
+ print(f" {cfg['key']} skipped (load failed)")
50
+ continue
51
+ proc, model = loaded
52
+ try:
53
+ score = _infer_fake_score(proc, model, img)
54
+ results[cfg["key"]] = {"score": round(score, 4), "weight": cfg["weight"], "desc": cfg["desc"]}
55
+ weighted_sum += score * cfg["weight"]
56
+ total_weight += cfg["weight"]
57
+ print(f" [{cfg['key']}] fake={score:.4f} Γ— {cfg['weight']}")
58
+ except Exception as e:
59
+ print(f" [{cfg['key']}] error: {e}")
60
+
61
+ ensemble_score = weighted_sum / total_weight if total_weight > 0 else 0.5
62
+ return {"models": results, "ensemble_score": round(ensemble_score, 4)}
63
+
64
+
65
+ # ── Forensic layers ────────────────────────────────────────────────────────────
66
+
67
+ def _analyze_exif(image_bytes: bytes) -> dict:
68
+ result = {"score": 0.50, "exif_absent": False, "has_camera_info": False,
69
+ "suspicious_software": False, "ai_source": None, "details": []}
70
+ try:
71
+ import piexif
72
+ exif_data = piexif.load(image_bytes)
73
+ has_content = any(len(exif_data.get(b, {})) > 0 for b in ["0th", "Exif", "GPS", "1st"])
74
+ if not has_content:
75
+ result["exif_absent"] = True
76
+ result["details"].append("EXIF absent")
77
+ return result
78
+
79
+ zeroth = exif_data.get("0th", {})
80
+ exif_ifd = exif_data.get("Exif", {})
81
+ gps_ifd = exif_data.get("GPS", {})
82
+
83
+ sw = zeroth.get(piexif.ImageIFD.Software, b"").decode("utf-8", errors="ignore").lower()
84
+ desc = zeroth.get(piexif.ImageIFD.ImageDescription, b"").decode("utf-8", errors="ignore").lower()
85
+ artist = zeroth.get(piexif.ImageIFD.Artist, b"").decode("utf-8", errors="ignore").lower()
86
+ combined = sw + " " + desc + " " + artist
87
+
88
+ ai_sources = {
89
+ "stable diffusion": "Stable Diffusion", "midjourney": "Midjourney",
90
+ "dall-e": "DALL-E", "dallΒ·e": "DALL-E", "comfyui": "ComfyUI/SD",
91
+ "automatic1111": "Automatic1111/SD", "generative": "IA GΓ©nΓ©rative",
92
+ "diffusion": "Modèle Diffusion", "novelai": "NovelAI",
93
+ "firefly": "Adobe Firefly", "imagen": "Google Imagen",
94
+ "gemini": "Google Gemini", "flux": "Flux (BFL)",
95
+ "ideogram": "Ideogram", "leonardo": "Leonardo.ai",
96
+ "adobe ai": "Adobe AI", "ai generated": "IA GΓ©nΓ©rique",
97
+ "synthid": "Google SynthID",
98
+ }
99
+ for kw, source in ai_sources.items():
100
+ if kw in combined:
101
+ result["suspicious_software"] = True
102
+ result["ai_source"] = source
103
+ result["score"] = 0.97
104
+ result["details"].append(f"Source IA dΓ©tectΓ©e: {source}")
105
+ return result
106
+
107
+ make = zeroth.get(piexif.ImageIFD.Make, b"")
108
+ cam = zeroth.get(piexif.ImageIFD.Model, b"")
109
+ iso = exif_ifd.get(piexif.ExifIFD.ISOSpeedRatings)
110
+ shut = exif_ifd.get(piexif.ExifIFD.ExposureTime)
111
+ gps = bool(gps_ifd and len(gps_ifd) > 2)
112
+
113
+ if make or cam:
114
+ result["has_camera_info"] = True
115
+ result["details"].append(
116
+ f"Appareil: {make.decode('utf-8', errors='ignore')} {cam.decode('utf-8', errors='ignore')}".strip()
117
+ )
118
+ if gps:
119
+ result["details"].append("GPS prΓ©sent")
120
+
121
+ if result["has_camera_info"] and gps and iso and shut:
122
+ result["score"] = 0.05
123
+ elif result["has_camera_info"] and (iso or shut):
124
+ result["score"] = 0.12
125
+ elif result["has_camera_info"]:
126
+ result["score"] = 0.28
127
+ else:
128
+ result["score"] = 0.55
129
+
130
+ except Exception as e:
131
+ result["exif_absent"] = True
132
+ result["details"].append(f"Erreur EXIF: {str(e)[:60]}")
133
+ return result
134
+
135
+
136
+ def _analyze_fft(img: Image.Image, fc: float = 0.0) -> dict:
137
+ result = {"score": 0.50, "details": []}
138
+ try:
139
+ gray = np.array(img.convert("L")).astype(np.float32)
140
+ mag = np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(gray))))
141
+ h, w = mag.shape
142
+ cy, cx = h // 2, w // 2
143
+ Y, X = np.ogrid[:h, :w]
144
+ dist = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2)
145
+ rl, rm = min(h, w) // 8, min(h, w) // 4
146
+ le = np.mean(mag[dist <= rl])
147
+ he = np.mean(mag[(dist > rl) & (dist <= rm)])
148
+ fr = he / (le + 1e-9)
149
+ tl = 0.18 if fc > 0.45 else 0.25
150
+ th = 0.85 if fc > 0.45 else 0.72
151
+ ss = 0.70 if fr < tl else (0.55 if fr > th else 0.20)
152
+ result["details"].append(f"Ratio freq. {fr:.3f}" + (" β†’ sur-lissage IA" if fr < tl else " βœ“"))
153
+
154
+ pr = np.sum((mag * (dist > 5)) > (np.mean(mag) + 5 * np.std(mag))) / (h * w)
155
+ ps = 0.85 if pr > 0.003 else (0.50 if pr > 0.001 else 0.15)
156
+ result["details"].append(f"Pics GAN: {pr:.4f}" + (" ⚠️" if pr > 0.003 else " βœ“"))
157
+
158
+ result["score"] = float(0.55 * ss + 0.45 * ps)
159
+ except Exception as e:
160
+ result["details"].append(f"Erreur FFT: {str(e)[:60]}")
161
+ return result
162
+
163
+
164
+ def _analyze_texture(img: Image.Image, fc: float = 0.0) -> dict:
165
+ result = {"score": 0.50, "details": []}
166
+ try:
167
+ arr = np.array(img).astype(np.float32)
168
+ gray = np.array(img.convert("L")).astype(np.float32)
169
+ lap = np.array(img.convert("L").filter(ImageFilter.FIND_EDGES)).astype(np.float32)
170
+ nl = float(np.std(lap))
171
+
172
+ if arr.shape[2] >= 3:
173
+ r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2]
174
+ if float(np.mean(np.abs(r - g) < 1)) > 0.98 and float(np.mean(np.abs(g - b) < 1)) > 0.98:
175
+ result["score"] = 0.85
176
+ result["details"].append("Canaux RGB identiques β†’ image IA synthΓ©tique")
177
+ return result
178
+
179
+ ts, tm = (5.0, 14.0) if fc > 0.45 else (8.0, 20.0)
180
+ ns = 0.75 if nl > 20.0 else (0.72 if nl < ts else (0.42 if nl < tm else 0.15))
181
+ result["details"].append(f"Bruit: {nl:.1f}")
182
+
183
+ h, w, bl = gray.shape[0], gray.shape[1], 32
184
+ stds = [np.std(gray[y:y + bl, x:x + bl]) for y in range(0, h - bl, bl) for x in range(0, w - bl, bl)]
185
+ u = np.std(stds) / (np.mean(stds) + 1e-9) if stds else 0.5
186
+ ul, uh = (0.20, 0.50) if fc > 0.45 else (0.30, 0.60)
187
+ us = 0.72 if u < ul else (0.38 if u < uh else 0.15)
188
+ result["details"].append(f"UniformitΓ©: {u:.3f}")
189
+
190
+ bg_ratio = float(np.mean(gray > 200))
191
+ border_std = float(np.std(gray[:h // 8, :]))
192
+ if bg_ratio > 0.50 and border_std < 6.0:
193
+ studio_score = 0.88
194
+ elif bg_ratio > 0.50 and border_std < 15.0:
195
+ studio_score = 0.82
196
+ elif bg_ratio > 0.35 and border_std < 25.0:
197
+ studio_score = 0.55
198
+ else:
199
+ studio_score = 0.10
200
+ result["details"].append(f"Fond: {bg_ratio:.0%}")
201
+
202
+ result["score"] = float(0.35 * ns + 0.25 * us + 0.40 * studio_score)
203
+ except Exception as e:
204
+ result["details"].append(f"Erreur texture: {str(e)[:60]}")
205
+ return result
206
+
207
+
208
+ def _analyze_color(img: Image.Image) -> dict:
209
+ result = {"score": 0.50, "details": []}
210
+ try:
211
+ arr = np.array(img.convert("RGB")).astype(np.float32)
212
+ r, g, b = arr[:, :, 0].flatten(), arr[:, :, 1].flatten(), arr[:, :, 2].flatten()
213
+
214
+ def channel_entropy(ch):
215
+ hist, _ = np.histogram(ch, bins=64, range=(0, 255), density=True)
216
+ hist = hist[hist > 0]
217
+ return float(-np.sum(hist * np.log2(hist + 1e-9)))
218
+
219
+ er, eg, eb = channel_entropy(r), channel_entropy(g), channel_entropy(b)
220
+ mean_entropy = (er + eg + eb) / 3.0
221
+ entropy_std = float(np.std([er, eg, eb]))
222
+
223
+ if mean_entropy > 5.2 and entropy_std < 0.15:
224
+ ent_score = 0.72
225
+ elif mean_entropy > 4.8 and entropy_std < 0.25:
226
+ ent_score = 0.45
227
+ else:
228
+ ent_score = 0.20
229
+ result["details"].append(f"Entropie couleur: {mean_entropy:.2f}")
230
+
231
+ lum = 0.299 * r + 0.587 * g + 0.114 * b
232
+ extreme_ratio = float(np.mean((lum < 8) | (lum > 247)))
233
+ ext_score = 0.65 if extreme_ratio < 0.005 else (0.35 if extreme_ratio < 0.02 else 0.15)
234
+ result["details"].append(f"Pixels extrΓͺmes: {extreme_ratio:.4f}")
235
+
236
+ result["score"] = float(0.60 * ent_score + 0.40 * ext_score)
237
+ except Exception as e:
238
+ result["details"].append(f"Erreur palette: {str(e)[:60]}")
239
+ return result
240
+
241
+
242
+ # ── Fusion ─────────────────────────────────────────────────────────────────────
243
+
244
+ def _fuse(ensemble_score: float, exif_r: dict, fft_r: dict, tex_r: dict, color_r: dict) -> dict:
245
+ exif_absent = exif_r.get("exif_absent", False)
246
+
247
+ if exif_r.get("suspicious_software"):
248
+ profile = "EXIF_IA_DETECTE"
249
+ w = {"ensemble": 0.20, "exif": 0.60, "fft": 0.12, "texture": 0.05, "color": 0.03}
250
+ elif not exif_absent and exif_r["has_camera_info"] and exif_r["score"] < 0.20:
251
+ profile = "EXIF_FIABLE"
252
+ w = {"ensemble": 0.45, "exif": 0.32, "fft": 0.12, "texture": 0.07, "color": 0.04}
253
+ elif exif_absent:
254
+ profile = "EXIF_ABSENT"
255
+ w = {"ensemble": 0.52, "exif": 0.00, "fft": 0.24, "texture": 0.14, "color": 0.10}
256
+ else:
257
+ profile = "STANDARD"
258
+ w = {"ensemble": 0.48, "exif": 0.22, "fft": 0.16, "texture": 0.09, "color": 0.05}
259
+
260
+ scores = {
261
+ "ensemble": ensemble_score,
262
+ "exif": exif_r["score"],
263
+ "fft": fft_r["score"],
264
+ "texture": tex_r["score"],
265
+ "color": color_r["score"],
266
+ }
267
+
268
+ raw = sum(w[k] * scores[k] for k in w)
269
+
270
+ # Anti-false-positive guardrails
271
+ if ensemble_score < 0.35 and fft_r["score"] < 0.38:
272
+ raw = min(raw, 0.46)
273
+ if not exif_absent and exif_r["has_camera_info"] and exif_r["score"] < 0.15:
274
+ raw = min(raw, 0.82)
275
+ if exif_r.get("suspicious_software") and raw < 0.85:
276
+ raw = max(raw, 0.90)
277
+
278
+ # High-confidence ensemble override β€” modern diffusion models evade forensic layers;
279
+ # when all ML models agree strongly, trust them over FFT/texture/color heuristics.
280
+ if ensemble_score >= 0.80 and not exif_r.get("has_camera_info"):
281
+ raw = max(raw, ensemble_score * 0.90)
282
+ if ensemble_score <= 0.20:
283
+ raw = min(raw, ensemble_score * 1.10 + 0.05)
284
+
285
+ return {
286
+ "fake_prob": round(raw, 4),
287
+ "real_prob": round(1.0 - raw, 4),
288
+ "layer_scores": {k: round(v, 4) for k, v in scores.items()},
289
+ "weights_used": {k: round(v, 2) for k, v in w.items()},
290
+ "fusion_profile": profile,
291
+ "ai_source": exif_r.get("ai_source"),
292
+ }
293
+
294
+
295
+ # ── Verdict ────────────────────────────────────────────────────────────────────
296
+
297
+ def _verdict(fake_prob: float, details: dict) -> dict:
298
+ if fake_prob > 0.65:
299
+ verdict = "DEEPFAKE"
300
+ confidence = "haute" if fake_prob > 0.85 else "moyenne"
301
+ reason = "Artefacts de synthèse détectés."
302
+ elif fake_prob < 0.35:
303
+ verdict = "AUTHENTIQUE"
304
+ confidence = "haute" if fake_prob < 0.15 else "moyenne"
305
+ reason = "Aucun artefact de synthèse détecté."
306
+ else:
307
+ verdict = "INDÉTERMINÉ"
308
+ confidence = "faible"
309
+ reason = "Signal ambigu, analyse non concluante."
310
+
311
+ if details.get("ai_source"):
312
+ reason = f"Source IA identifiΓ©e dans les mΓ©tadonnΓ©es: {details['ai_source']}."
313
+
314
+ return {"verdict": verdict, "confidence": confidence, "reason": reason}
315
+
316
+
317
+ # ── Public API ─────────────────────────────────────────────────────────────────
318
+
319
+ def run(img: Image.Image, image_bytes: bytes) -> dict:
320
+ """Full analysis: 3-model ensemble + forensic layers."""
321
+ ensemble_result = _run_ensemble(img, IMAGE_ENSEMBLE)
322
+ exif_r = _analyze_exif(image_bytes)
323
+ fft_r = _analyze_fft(img)
324
+ tex_r = _analyze_texture(img)
325
+ color_r = _analyze_color(img)
326
+
327
+ fusion = _fuse(ensemble_result["ensemble_score"], exif_r, fft_r, tex_r, color_r)
328
+ verdict = _verdict(fusion["fake_prob"], fusion)
329
+
330
+ return {**verdict, **fusion, "models": ensemble_result["models"]}
331
+
332
+
333
+ def run_fast(img: Image.Image, image_bytes: bytes) -> dict:
334
+ """Fast analysis: 2-model ensemble + EXIF only."""
335
+ ensemble_result = _run_ensemble(img, IMAGE_FAST_ENSEMBLE)
336
+ exif_r = _analyze_exif(image_bytes)
337
+ fft_r = {"score": 0.50, "details": []}
338
+ tex_r = {"score": 0.50, "details": []}
339
+ color_r = {"score": 0.50, "details": []}
340
+
341
+ fusion = _fuse(ensemble_result["ensemble_score"], exif_r, fft_r, tex_r, color_r)
342
+ verdict = _verdict(fusion["fake_prob"], fusion)
343
+
344
+ return {**verdict, **fusion, "models": ensemble_result["models"]}
app/pipelines/text_ai.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
3
+
4
+ from app.core.config import TEXT_MODELS
5
+ from app.core.device import DEVICE
6
+
7
+ _cache: dict = {"tokenizers": {}, "models": {}}
8
+
9
+
10
+ def _get_text_model(model_key: str):
11
+ if model_key not in _cache["models"]:
12
+ cfg = TEXT_MODELS[model_key]
13
+ name = cfg["name"]
14
+ print(f"Loading {cfg['desc']} ({name})...")
15
+ tok = AutoTokenizer.from_pretrained(name)
16
+ mod = AutoModelForSequenceClassification.from_pretrained(name).to(DEVICE)
17
+ mod.eval()
18
+ _cache["tokenizers"][model_key] = tok
19
+ _cache["models"][model_key] = mod
20
+ print(f"{model_key} ready")
21
+ return _cache["tokenizers"][model_key], _cache["models"][model_key]
22
+
23
+
24
+ def _predict(model_key: str, text: str) -> float:
25
+ """Returns fake/AI probability 0β†’1, resolved dynamically from id2label."""
26
+ tok, model = _get_text_model(model_key)
27
+ inputs = tok(text, return_tensors="pt", truncation=True, max_length=512).to(model.device)
28
+ with torch.inference_mode():
29
+ probs = torch.nn.functional.softmax(model(**inputs).logits, dim=-1)[0].cpu().numpy()
30
+
31
+ id2label = {int(k): v.lower() for k, v in model.config.id2label.items()}
32
+ fake_kw = ["fake", "ai", "generated", "machine", "chatgpt", "artificial", "spam", "label_1"]
33
+ real_kw = ["real", "human", "authentic", "genuine", "original", "label_0"]
34
+ fake_idx = [i for i, lbl in id2label.items() if any(w in lbl for w in fake_kw)]
35
+ real_idx = [i for i, lbl in id2label.items() if any(w in lbl for w in real_kw)]
36
+ print(f" [{model_key}] id2label: {id2label} | fake_idx={fake_idx} real_idx={real_idx}")
37
+
38
+ if fake_idx:
39
+ return float(sum(probs[i] for i in fake_idx))
40
+ elif real_idx:
41
+ return float(1.0 - sum(probs[i] for i in real_idx))
42
+ else:
43
+ # Fallback: index 1 is conventionally the positive/fake class
44
+ return float(probs[1]) if len(probs) > 1 else float(probs[0])
45
+
46
+
47
+ def run_ai_detection(text: str) -> dict:
48
+ """Detect AI-generated text using 2-model ensemble."""
49
+ prob_ai1 = _predict("ai1", text)
50
+ prob_ai2 = _predict("ai2", text)
51
+ avg = (prob_ai1 + prob_ai2) / 2.0
52
+
53
+ verdict = "TEXTE IA" if avg > 0.65 else ("TEXTE HUMAIN" if avg < 0.35 else "INDÉTERMINÉ")
54
+ confidence = "haute" if avg > 0.85 or avg < 0.15 else ("moyenne" if avg > 0.70 or avg < 0.30 else "faible")
55
+
56
+ if verdict == "TEXTE IA":
57
+ reason = "Distribution lexicale et perplexitΓ© caractΓ©ristiques d'un LLM."
58
+ elif verdict == "TEXTE HUMAIN":
59
+ reason = "Variations stylistiques naturelles, aucun pattern IA dΓ©tectΓ©."
60
+ else:
61
+ reason = "Signal textuel ambigu, analyse non concluante."
62
+
63
+ return {
64
+ "verdict": verdict,
65
+ "confidence": confidence,
66
+ "reason": reason,
67
+ "ai_prob": round(avg, 4),
68
+ "human_prob": round(1.0 - avg, 4),
69
+ "models": {
70
+ "fakespot": round(prob_ai1, 4),
71
+ "chatgpt_detector": round(prob_ai2, 4),
72
+ },
73
+ }
74
+
75
+
76
+ def run_fakenews_detection(text: str) -> dict:
77
+ """Detect fake news using 2-model weighted ensemble."""
78
+ prob_fn1 = _predict("fn1", text)
79
+ prob_fn2 = _predict("fn2", text)
80
+ weighted = (prob_fn1 * 0.60) + (prob_fn2 * 0.40)
81
+
82
+ verdict = "FAKE NEWS" if weighted > 0.65 else ("INFO VRAIE" if weighted < 0.35 else "INDÉTERMINÉ")
83
+ confidence = "haute" if weighted > 0.85 or weighted < 0.15 else ("moyenne" if weighted > 0.70 or weighted < 0.30 else "faible")
84
+
85
+ if verdict == "FAKE NEWS":
86
+ reason = "Patterns linguistiques associΓ©s Γ  la dΓ©sinformation dΓ©tectΓ©s."
87
+ elif verdict == "INFO VRAIE":
88
+ reason = "Aucun pattern de dΓ©sinformation dΓ©tectΓ©."
89
+ else:
90
+ reason = "Signal ambigu, analyse non concluante."
91
+
92
+ return {
93
+ "verdict": verdict,
94
+ "confidence": confidence,
95
+ "reason": reason,
96
+ "fake_prob": round(weighted, 4),
97
+ "real_prob": round(1.0 - weighted, 4),
98
+ "models": {
99
+ "distilroberta_fake_news": round(prob_fn1, 4),
100
+ "bert_fake_news": round(prob_fn2, 4),
101
+ },
102
+ }
103
+
104
+
105
+ def run_full(text: str) -> dict:
106
+ """Combined AI detection + fake news detection."""
107
+ ai_result = run_ai_detection(text)
108
+ fn_result = run_fakenews_detection(text)
109
+
110
+ is_ai = ai_result["ai_prob"] > 0.50
111
+ is_fake = fn_result["fake_prob"] > 0.50
112
+
113
+ if is_ai and is_fake:
114
+ verdict = "DANGER MAX : Fake news gΓ©nΓ©rΓ©e par IA"
115
+ elif is_ai and not is_fake:
116
+ verdict = "Texte IA mais contenu vΓ©rifiΓ©"
117
+ elif not is_ai and is_fake:
118
+ verdict = "DΓ©sinformation humaine"
119
+ else:
120
+ verdict = "Texte humain, contenu vΓ©rifiΓ©"
121
+
122
+ return {
123
+ "verdict": verdict,
124
+ "ai_prob": ai_result["ai_prob"],
125
+ "fake_news_prob": fn_result["fake_prob"],
126
+ "is_ai_generated": is_ai,
127
+ "is_fake_news": is_fake,
128
+ "ai_detection": ai_result,
129
+ "fakenews_detection": fn_result,
130
+ }
app/pipelines/video.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+
4
+ import cv2
5
+ import numpy as np
6
+ from PIL import Image
7
+
8
+ from app.core.config import VIDEO_ENSEMBLE
9
+ from app.pipelines.image import _run_ensemble
10
+
11
+ # Number of frames sampled per video
12
+ MAX_FRAMES = 16
13
+
14
+
15
+ def _extract_frames(video_path: str, n: int = MAX_FRAMES) -> list[Image.Image]:
16
+ """Extract n evenly-spaced frames from a video file."""
17
+ cap = cv2.VideoCapture(video_path)
18
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
19
+ if total <= 0:
20
+ cap.release()
21
+ raise ValueError("Impossible de lire les frames de la vidΓ©o.")
22
+
23
+ indices = np.linspace(0, total - 1, min(n, total), dtype=int)
24
+ frames = []
25
+ for idx in indices:
26
+ cap.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
27
+ ret, frame = cap.read()
28
+ if ret:
29
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
30
+ frames.append(Image.fromarray(rgb))
31
+ cap.release()
32
+ return frames
33
+
34
+
35
+ def run(video_bytes: bytes) -> dict:
36
+ """
37
+ Analyze a video for deepfake content.
38
+ Samples MAX_FRAMES frames evenly across the video,
39
+ runs the image ensemble on each, then aggregates.
40
+ """
41
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tf:
42
+ tf.write(video_bytes)
43
+ tmp_path = tf.name
44
+
45
+ try:
46
+ frames = _extract_frames(tmp_path)
47
+ finally:
48
+ if os.path.exists(tmp_path):
49
+ os.remove(tmp_path)
50
+
51
+ if not frames:
52
+ raise ValueError("Aucune frame exploitable extraite de la vidΓ©o.")
53
+
54
+ # Run ensemble on each frame
55
+ frame_scores = []
56
+ per_model_scores: dict[str, list[float]] = {}
57
+
58
+ for i, frame in enumerate(frames):
59
+ result = _run_ensemble(frame, VIDEO_ENSEMBLE)
60
+ frame_scores.append(result["ensemble_score"])
61
+ for key, data in result["models"].items():
62
+ per_model_scores.setdefault(key, []).append(data["score"])
63
+ print(f" Frame {i + 1}/{len(frames)} β†’ score={result['ensemble_score']:.4f}")
64
+
65
+ scores_arr = np.array(frame_scores)
66
+ fake_prob = float(np.mean(scores_arr))
67
+ high_ratio = float(np.mean(scores_arr > 0.65))
68
+
69
+ # Boost when most frames agree on deepfake
70
+ if high_ratio > 0.60:
71
+ fake_prob = min(fake_prob * 1.10, 1.0)
72
+
73
+ fake_prob = round(fake_prob, 4)
74
+
75
+ model_summary = {
76
+ key: round(float(np.mean(v)), 4)
77
+ for key, v in per_model_scores.items()
78
+ }
79
+
80
+ if fake_prob > 0.65:
81
+ verdict = "DEEPFAKE"
82
+ confidence = "haute" if fake_prob > 0.85 else "moyenne"
83
+ reason = "Artefacts de synthèse détectés sur plusieurs frames."
84
+ elif fake_prob < 0.35:
85
+ verdict = "AUTHENTIQUE"
86
+ confidence = "haute" if fake_prob < 0.15 else "moyenne"
87
+ reason = "Aucun artefact de synthèse détecté."
88
+ else:
89
+ verdict = "INDÉTERMINÉ"
90
+ confidence = "faible"
91
+ reason = "Signal ambigu β€” les frames prΓ©sentent des rΓ©sultats mixtes."
92
+
93
+ return {
94
+ "verdict": verdict,
95
+ "confidence": confidence,
96
+ "reason": reason,
97
+ "fake_prob": fake_prob,
98
+ "real_prob": round(1.0 - fake_prob, 4),
99
+ "frames_analyzed": len(frames),
100
+ "suspicious_frames_ratio": round(high_ratio, 4),
101
+ "models": model_summary,
102
+ }
app/routers/__init__.py ADDED
File without changes
app/routers/audio.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, File, HTTPException, UploadFile
2
+
3
+ from app.core.config import ALLOWED_AUDIO_MIMETYPES, MAX_FILE_SIZE
4
+ from app.pipelines import audio as audio_pipeline
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.post("/analyze/audio")
10
+ async def analyze_audio(file: UploadFile = File(...)):
11
+ content_type = getattr(file, "content_type", "")
12
+ if content_type not in ALLOWED_AUDIO_MIMETYPES:
13
+ raise HTTPException(
14
+ status_code=400,
15
+ detail=f"Format non supportΓ©: {content_type}. Formats acceptΓ©s: WAV, MP3, OGG, FLAC, M4A.",
16
+ )
17
+ contents = await file.read()
18
+ if len(contents) > MAX_FILE_SIZE:
19
+ raise HTTPException(status_code=413, detail="Fichier trop volumineux (max 20 Mo).")
20
+ result = audio_pipeline.run(contents)
21
+ return {"status": "success", **result}
app/routers/image.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from fastapi import APIRouter, File, HTTPException, UploadFile
3
+ from PIL import Image
4
+
5
+ from app.core.config import ALLOWED_IMAGE_MIMETYPES, MAX_FILE_SIZE
6
+ from app.pipelines import image as image_pipeline
7
+
8
+ try:
9
+ from pillow_heif import register_heif_opener
10
+ register_heif_opener()
11
+ except ImportError:
12
+ pass
13
+
14
+ Image.MAX_IMAGE_PIXELS = 100_000_000
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ def _validate_and_read(file: UploadFile, contents: bytes) -> Image.Image:
20
+ content_type = getattr(file, "content_type", "")
21
+ if content_type not in ALLOWED_IMAGE_MIMETYPES:
22
+ raise HTTPException(
23
+ status_code=400,
24
+ detail=f"Format non supportΓ©: {content_type}. Formats acceptΓ©s: JPEG, PNG, WEBP, HEIC.",
25
+ )
26
+ if len(contents) > MAX_FILE_SIZE:
27
+ raise HTTPException(status_code=413, detail="Fichier trop volumineux (max 20 Mo).")
28
+ try:
29
+ return Image.open(io.BytesIO(contents)).convert("RGB")
30
+ except Exception:
31
+ raise HTTPException(status_code=400, detail="Fichier image corrompu ou illisible.")
32
+
33
+
34
+ @router.post("/analyze/image")
35
+ async def analyze_image(file: UploadFile = File(...)):
36
+ contents = await file.read()
37
+ img = _validate_and_read(file, contents)
38
+ result = image_pipeline.run(img, contents)
39
+ return {"status": "success", **result}
40
+
41
+
42
+ @router.post("/analyze/image/fast")
43
+ async def analyze_image_fast(file: UploadFile = File(...)):
44
+ contents = await file.read()
45
+ img = _validate_and_read(file, contents)
46
+ result = image_pipeline.run_fast(img, contents)
47
+ return {"status": "success", **result}
app/routers/text.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from app.pipelines import text_ai as text_pipeline
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class TextRequest(BaseModel):
10
+ text: str
11
+
12
+
13
+ @router.post("/analyze/text")
14
+ async def analyze_text(body: TextRequest):
15
+ if not body.text or not body.text.strip():
16
+ raise HTTPException(status_code=400, detail="Le texte ne peut pas Γͺtre vide.")
17
+ result = text_pipeline.run_full(body.text)
18
+ return {"status": "success", **result}
app/routers/video.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, File, HTTPException, UploadFile
2
+
3
+ from app.core.config import ALLOWED_VIDEO_MIMETYPES, MAX_VIDEO_SIZE
4
+ from app.pipelines import video as video_pipeline
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.post("/analyze/video")
10
+ async def analyze_video(file: UploadFile = File(...)):
11
+ content_type = getattr(file, "content_type", "")
12
+ if content_type not in ALLOWED_VIDEO_MIMETYPES:
13
+ raise HTTPException(
14
+ status_code=400,
15
+ detail=f"Format non supportΓ©: {content_type}. Formats acceptΓ©s: MP4, MOV, AVI, WEBM, MKV.",
16
+ )
17
+ contents = await file.read()
18
+ if len(contents) > MAX_VIDEO_SIZE:
19
+ raise HTTPException(status_code=413, detail="Fichier trop volumineux (max 200 Mo).")
20
+ try:
21
+ result = video_pipeline.run(contents)
22
+ except ValueError as e:
23
+ raise HTTPException(status_code=400, detail=str(e))
24
+ return {"status": "success", **result}
docker-compose.yml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.9'
2
+
3
+ services:
4
+
5
+ api:
6
+ build: .
7
+ container_name: kamyvision-api
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ./app:/app/app # hot reload du code
12
+ - model-cache:/root/.cache/huggingface # cache modèles HF
13
+ - ./models:/app/models # modèle ONNX Assietou
14
+ environment:
15
+ - PYTHONUNBUFFERED=1
16
+ - HF_HOME=/root/.cache/huggingface
17
+ restart: unless-stopped
18
+ healthcheck:
19
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
20
+ interval: 30s
21
+ timeout: 10s
22
+ retries: 3
23
+ start_period: 90s
24
+
25
+ frontend:
26
+ image: nginx:alpine
27
+ container_name: kamyvision-frontend
28
+ ports:
29
+ - "3000:80"
30
+ volumes:
31
+ - ./frontend:/usr/share/nginx/html:ro
32
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
33
+ depends_on:
34
+ api:
35
+ condition: service_healthy
36
+ restart: unless-stopped
37
+
38
+ volumes:
39
+ model-cache: # persiste les modèles HF entre les redémarrages
nginx.conf ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 80;
3
+ server_name localhost;
4
+ root /usr/share/nginx/html;
5
+ index index.html;
6
+
7
+ # Fichiers statiques
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ # Proxy vers l'API backend
13
+ location /predict {
14
+ proxy_pass http://api:8000/predict;
15
+ proxy_set_header Host $host;
16
+ proxy_set_header X-Real-IP $remote_addr;
17
+ proxy_read_timeout 120s;
18
+ }
19
+
20
+ location /analyze/ {
21
+ proxy_pass http://api:8000/analyze/;
22
+ proxy_set_header Host $host;
23
+ proxy_set_header X-Real-IP $remote_addr;
24
+ proxy_read_timeout 120s;
25
+ }
26
+
27
+ location /health {
28
+ proxy_pass http://api:8000/health;
29
+ }
30
+ }
requirements.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AuthenticVision β€” DΓ©pendances
2
+ # Installation : pip install -r requirements.txt
3
+ #
4
+ # Note Windows : opencv-python-headless 4.8.x est requis (numpy < 2 compatible)
5
+ # Si vous avez numpy >= 2 : pip install "opencv-python-headless==4.8.1.78" "numpy>=1.24,<2.0"
6
+
7
+ # ── Core IA ──────────────────────────────────────────────────────────────────
8
+ torch>=2.0.0
9
+ torchvision>=0.15.0
10
+ transformers>=4.40.0
11
+ Pillow>=10.0.0
12
+
13
+ # ── API ──────────────────────────────────────────────────────────────────────
14
+ fastapi>=0.104.0
15
+ uvicorn[standard]>=0.24.0
16
+ python-multipart>=0.0.6
17
+
18
+ # ── Audio ────────────────────────────────────────────────────────────────────
19
+ librosa>=0.10.0
20
+ soundfile>=0.12.0
21
+
22
+ # ── VidΓ©o ────────────────────────────────────────────────────────────────────
23
+ opencv-python-headless==4.8.1.78
24
+
25
+ # ── Forensique ───────────────────────────────────────────────────────────────
26
+ piexif>=1.1.3
27
+ numpy>=1.24,<2.0
28
+ scikit-learn>=1.3.0
29
+ scipy>=1.11.0
30
+
31
+ # ── Optionnel : HEIC (photos iPhone) ─────────────────────────────────────────
32
+ # pillow-heif>=0.13.0
scripts/preload_models.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preload all models into HuggingFace Hub cache at Docker build time.
3
+ This avoids cold-start downloads on the first request in production.
4
+ """
5
+ from transformers import (
6
+ AutoFeatureExtractor,
7
+ AutoModelForAudioClassification,
8
+ AutoModelForSequenceClassification,
9
+ AutoModelForImageClassification,
10
+ AutoTokenizer,
11
+ )
12
+
13
+ import sys
14
+
15
+ MODEL_GROUPS = {
16
+ "Audio": [
17
+ ("AutoFeatureExtractor", "MelodyMachine/Deepfake-audio-detection-V2"),
18
+ ("AutoModelForAudioClassification", "MelodyMachine/Deepfake-audio-detection-V2"),
19
+ ],
20
+ "Text": [
21
+ ("AutoTokenizer", "fakespot-ai/roberta-base-ai-text-detection-v1"),
22
+ ("AutoModelForSequenceClassification", "fakespot-ai/roberta-base-ai-text-detection-v1"),
23
+ ("AutoTokenizer", "Hello-SimpleAI/chatgpt-detector-roberta"),
24
+ ("AutoModelForSequenceClassification", "Hello-SimpleAI/chatgpt-detector-roberta"),
25
+ ("AutoTokenizer", "vikram71198/distilroberta-base-finetuned-fake-news-detection"),
26
+ ("AutoModelForSequenceClassification", "vikram71198/distilroberta-base-finetuned-fake-news-detection"),
27
+ ("AutoTokenizer", "jy46604790/Fake-News-Bert-Detect"),
28
+ ("AutoModelForSequenceClassification", "jy46604790/Fake-News-Bert-Detect"),
29
+ ],
30
+ "Image": [
31
+ ("AutoModelForImageClassification", "Ateeqq/ai-vs-human-image-detector"),
32
+ ("AutoModelForImageClassification", "prithivMLmods/AI-vs-Deepfake-vs-Real"),
33
+ ("AutoModelForImageClassification", "prithivMLmods/Deep-Fake-Detector-Model"),
34
+ ],
35
+ }
36
+
37
+ LOADERS = {
38
+ "AutoFeatureExtractor": AutoFeatureExtractor,
39
+ "AutoModelForAudioClassification": AutoModelForAudioClassification,
40
+ "AutoModelForSequenceClassification": AutoModelForSequenceClassification,
41
+ "AutoModelForImageClassification": AutoModelForImageClassification,
42
+ "AutoTokenizer": AutoTokenizer,
43
+ }
44
+
45
+ errors = []
46
+ for group, models in MODEL_GROUPS.items():
47
+ print(f"\n── {group} ──")
48
+ for loader_name, model_name in models:
49
+ try:
50
+ print(f" Downloading {model_name} ({loader_name})...", end=" ", flush=True)
51
+ LOADERS[loader_name].from_pretrained(model_name)
52
+ print("OK")
53
+ except Exception as e:
54
+ print(f"FAILED: {e}")
55
+ errors.append((model_name, str(e)))
56
+
57
+ if errors:
58
+ print(f"\n⚠️ {len(errors)} model(s) failed to preload (will download on first request):")
59
+ for name, err in errors:
60
+ print(f" - {name}: {err}")
61
+ else:
62
+ print("\nAll models preloaded successfully.")