Spaces:
Sleeping
Sleeping
Commit Β·
5f01c8d
0
Parent(s):
deploy: 2026-03-29T18:02:04Z
Browse files- Dockerfile +32 -0
- README.md +372 -0
- app/__init__.py +0 -0
- app/core/__init__.py +0 -0
- app/core/config.py +110 -0
- app/core/device.py +8 -0
- app/main.py +35 -0
- app/models/__init__.py +0 -0
- app/models/loader.py +42 -0
- app/pipelines/__init__.py +0 -0
- app/pipelines/audio.py +84 -0
- app/pipelines/fakenews.py +0 -0
- app/pipelines/image.py +344 -0
- app/pipelines/text_ai.py +130 -0
- app/pipelines/video.py +102 -0
- app/routers/__init__.py +0 -0
- app/routers/audio.py +21 -0
- app/routers/image.py +47 -0
- app/routers/text.py +18 -0
- app/routers/video.py +24 -0
- docker-compose.yml +39 -0
- nginx.conf +30 -0
- requirements.txt +32 -0
- scripts/preload_models.py +62 -0
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.")
|