""" 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).")