File size: 10,453 Bytes
2648814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492b161
 
 
 
9a200aa
2648814
9a200aa
2648814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c6406f
 
 
 
2648814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492b161
 
 
9a200aa
 
492b161
2c6406f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2648814
 
2c6406f
492b161
2648814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
"""
model_bootstrap.py — Provisionnement automatique des modèles au démarrage.

Si les modèles ONNX ne sont pas présents localement, ils sont récupérés depuis
HuggingFace (via `huggingface_hub.hf_hub_download`, authentifié par `HF_TOKEN`)
puis préparés, **en arrière-plan** (thread démon) pour ne pas bloquer le serveur.

1. Détecteur IA global -> models/ai_detector.onnx (+ sidecar ai_detector.json)
   Source : SDXL_MODEL_REPO (défaut `umm-maybe/AI-image-detector`).
   - Si SDXL_MODEL_FILE est défini : téléchargement direct de l'ONNX publié.
   - Sinon : téléchargement de pytorch_model.bin + config + preprocessor, puis
     conversion PyTorch -> ONNX (transformers/torch) et écriture du sidecar.

2. Modèle visage -> models/deepfake_efficientnet.onnx
   Source : ONNX_MODEL_REPO + ONNX_MODEL_FILE (repo HF + fichier .onnx).

Le modèle VIDÉO (ResNext50+LSTM, Naman712) n'est PLUS provisionné ici : il est
téléchargé ET chargé PARESSEUSEMENT par video_model.VideoDeepfakeAnalyzer, au
premier appel à predict() (première analyse vidéo). Voir video_model.py pour
VIDEO_MODEL_REPO / VIDEO_MODEL_FILE / VIDEO_MODEL_PATH / VIDEO_FAKE_INDEX.

Variables d'environnement :
  HF_TOKEN            jeton HuggingFace (repos privés / gated / quotas)
  SDXL_MODEL_PATH     chemin .onnx local explicite (court-circuite le DL)
  SDXL_MODEL_REPO     repo HF du détecteur IA (défaut umm-maybe/AI-image-detector)
  SDXL_MODEL_FILE     nom d'un .onnx déjà publié dans le repo (téléchargement direct)
  ONNX_MODEL_PATH     chemin .onnx visage local explicite
  ONNX_MODEL_REPO     repo HF du modèle visage
  ONNX_MODEL_FILE     fichier .onnx visage dans le repo
"""

import os
import json
import shutil
import threading

_BASE = os.path.dirname(os.path.abspath(__file__))
_MODELS = os.path.join(_BASE, "models")
AI_ONNX = os.path.join(_MODELS, "ai_detector.onnx")
AI_SIDECAR = os.path.join(_MODELS, "ai_detector.json")
FACE_ONNX = os.path.join(_MODELS, "deepfake_efficientnet.onnx")
YUNET_ONNX = os.path.join(_MODELS, "face_detector_yunet.onnx")
YUNET_URL = os.getenv(
    "FACE_DETECTOR_URL",
    "https://huggingface.co/opencv/face_detection_yunet/resolve/main/face_detection_yunet_2023mar.onnx")

_DEF_MEAN = [0.485, 0.456, 0.406]
_DEF_STD = [0.229, 0.224, 0.225]


def _log(msg):
    print(f"[bootstrap] {msg}", flush=True)


def _hf_token():
    return os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") or None


# ── 1. Détecteur IA global ──────────────────────────────────────────────────
def ensure_sdxl_model():
    if os.path.exists(AI_ONNX):
        _log("ai_detector.onnx déjà présent — rien à télécharger.")
        return
    explicit = os.getenv("SDXL_MODEL_PATH")
    if explicit and os.path.exists(explicit):
        _log(f"SDXL_MODEL_PATH fourni ({explicit}) — pas de téléchargement.")
        return

    from huggingface_hub import hf_hub_download

    repo = os.getenv("SDXL_MODEL_REPO", "umm-maybe/AI-image-detector")
    token = _hf_token()
    os.makedirs(_MODELS, exist_ok=True)

    # (a) ONNX déjà publié dans le repo -> téléchargement direct
    onnx_file = os.getenv("SDXL_MODEL_FILE")
    if onnx_file:
        try:
            f = hf_hub_download(repo_id=repo, filename=onnx_file, token=token)
            shutil.copy(f, AI_ONNX)
            try:
                s = hf_hub_download(repo_id=repo, filename="ai_detector.json", token=token)
                shutil.copy(s, AI_SIDECAR)
            except Exception:
                pass
            _log(f"ai_detector.onnx téléchargé directement depuis {repo}/{onnx_file}")
            return
        except Exception as e:
            _log(f"ONNX direct indisponible ({e}) -> conversion PyTorch")

    # (b) Téléchargement des fichiers source puis conversion PyTorch -> ONNX
    src = os.path.join(_MODELS, "_ai_src")
    os.makedirs(src, exist_ok=True)
    hf_hub_download(repo_id=repo, filename="config.json", token=token, local_dir=src)
    try:
        hf_hub_download(repo_id=repo, filename="preprocessor_config.json", token=token, local_dir=src)
    except Exception:
        _log("preprocessor_config.json absent — valeurs ImageNet par défaut.")
    try:
        hf_hub_download(repo_id=repo, filename="pytorch_model.bin", token=token, local_dir=src)
    except Exception:
        hf_hub_download(repo_id=repo, filename="model.safetensors", token=token, local_dir=src)

    _log(f"poids récupérés depuis {repo} -> conversion ONNX en cours…")
    _convert_to_onnx(src)


def _convert_to_onnx(src_dir):
    import numpy as np
    import torch
    import torch.nn as nn
    from transformers import AutoModelForImageClassification

    model, info = AutoModelForImageClassification.from_pretrained(
        src_dir, output_loading_info=True, attn_implementation="eager")
    miss = [k for k in info.get("missing_keys", [])
            if "relative_position_index" not in k and "attn_mask" not in k]
    if miss:
        raise RuntimeError(f"poids manquants après chargement: {miss[:8]}")
    model.eval()

    class Wrap(nn.Module):
        def __init__(self, m):
            super().__init__()
            self.m = m

        def forward(self, x):
            return self.m(pixel_values=x).logits

    size = int(getattr(model.config, "image_size", 224) or 224)
    dummy = torch.randn(1, 3, size, size)
    torch.onnx.export(
        Wrap(model).eval(), dummy, AI_ONNX,
        input_names=["pixel_values"], output_names=["logits"],
        opset_version=17, do_constant_folding=True, dynamo=False)

    # Sidecar : labels, taille, normalisation, index « fake »
    id2label = {int(k): v for k, v in (model.config.id2label or {0: "fake", 1: "real"}).items()}

    def is_fake(name):
        n = str(name).lower()
        return any(t in n for t in ("artificial", "ai", "fake", "synth", "generated", "diffusion", "midjourney"))

    env_idx = os.getenv("SDXL_FAKE_INDEX")
    if env_idx not in (None, ""):
        fake_index = int(env_idx)
    else:
        cands = [i for i, n in id2label.items() if is_fake(n)]
        fake_index = cands[0] if cands else 0

    mean, std, psize = list(_DEF_MEAN), list(_DEF_STD), size
    pp = os.path.join(src_dir, "preprocessor_config.json")
    if os.path.exists(pp):
        try:
            d = json.load(open(pp, encoding="utf-8"))
            mean = [float(x) for x in d.get("image_mean", mean)]
            std = [float(x) for x in d.get("image_std", std)]
            s = d.get("size", size)
            if isinstance(s, dict):
                psize = int(s.get("height") or s.get("shortest_edge") or size)
            else:
                psize = int(s)
        except Exception:
            pass

    sidecar = {"size": int(psize), "mean": mean, "std": std,
               "labels": {str(k): v for k, v in id2label.items()},
               "fake_index": int(fake_index)}
    with open(AI_SIDECAR, "w", encoding="utf-8") as f:
        json.dump(sidecar, f, ensure_ascii=False, indent=2)
    _log(f"ai_detector.onnx prêt (labels={id2label}, fake_index={fake_index}, size={psize}).")


# ── 2. Modèle visage (EfficientNet / FaceForensics++) ───────────────────────
def ensure_face_model():
    if os.path.exists(FACE_ONNX):
        _log("deepfake_efficientnet.onnx déjà présent — rien à télécharger.")
        return
    explicit = os.getenv("ONNX_MODEL_PATH")
    if explicit and os.path.exists(explicit):
        _log(f"ONNX_MODEL_PATH fourni ({explicit}) — pas de téléchargement.")
        return

    repo = os.getenv("ONNX_MODEL_REPO")
    fname = os.getenv("ONNX_MODEL_FILE")
    if not repo or not fname:
        _log("modèle visage non téléchargé : définir ONNX_MODEL_REPO + ONNX_MODEL_FILE "
             "(repo HF + fichier .onnx). L'app fonctionne sans (analyse visage désactivée).")
        return

    from huggingface_hub import hf_hub_download
    os.makedirs(_MODELS, exist_ok=True)
    f = hf_hub_download(repo_id=repo, filename=fname, token=_hf_token())
    shutil.copy(f, FACE_ONNX)
    _log(f"deepfake_efficientnet.onnx téléchargé depuis {repo}/{fname}")


# ── Modèle vidéo ResNext50+LSTM ─────────────────────────────────────────────
# Téléchargé ET chargé PARESSEUSEMENT par video_model.VideoDeepfakeAnalyzer, au
# tout premier appel à predict() (première analyse vidéo) — plus au démarrage.


# ── 3. Détecteur de visages YuNet (famille RetinaFace, ~230 Ko) ─────────────
def ensure_yunet():
    if os.path.exists(YUNET_ONNX):
        _log("face_detector_yunet.onnx déjà présent — rien à télécharger.")
        return
    import urllib.request
    os.makedirs(_MODELS, exist_ok=True)
    tmp = YUNET_ONNX + ".part"
    urllib.request.urlretrieve(YUNET_URL, tmp)
    if os.path.getsize(tmp) < 100_000:      # un pointeur LFS/HTML fait < 100 Ko
        os.unlink(tmp)
        raise RuntimeError("téléchargement YuNet invalide (fichier trop petit)")
    os.replace(tmp, YUNET_ONNX)
    _log("face_detector_yunet.onnx téléchargé (YuNet).")


# ── Orchestration arrière-plan ──────────────────────────────────────────────
def _run(detector):
    for label, fn in (("détecteur de visages", ensure_yunet),
                      ("IA global", ensure_sdxl_model), ("visage", ensure_face_model)):
        try:
            fn()
        except Exception as e:
            _log(f"échec modèle {label} : {e}")
    # Les analyseurs chargent leur ONNX paresseusement : comme le fichier était
    # absent au démarrage, `_tried` est resté None et le prochain `predict()`
    # rechargera automatiquement le modèle fraîchement téléchargé. Rien à forcer.
    _log("provisionnement terminé.")


def start_background_bootstrap(detector=None):
    """Lance le provisionnement des modèles dans un thread démon (non bloquant)."""
    if os.getenv("DISABLE_MODEL_BOOTSTRAP", "0").lower() in ("1", "true", "yes"):
        _log("désactivé (DISABLE_MODEL_BOOTSTRAP).")
        return
    threading.Thread(target=_run, args=(detector,), name="model-bootstrap", daemon=True).start()
    _log("démarré (arrière-plan).")