feat(anonymisation): blur PII côté serveur via EDS-NLP + VLM local-first
Blur PII server-side (core/anonymisation/pii_blur.py) : - Pipeline OCR (docTR) → NER (EDS-NLP + fallback regex) - Détection ciblée noms/prénoms/adresses/NIR/téléphone/email - Protection explicite CIM-10, CCAM, montants €, dates, IDs techniques - Dual-storage : shot_XXXX_full.png (brut) + _blurred.png (affichage) - 18 tests Client : - RPA_BLUR_SENSITIVE=false par défaut (blur serveur uniquement) - Zéro overhead côté poste utilisateur VLM config : - vlm_config.py : gemma4:latest, fallbacks qwen3-vl:8b + UI-TARS - think=false auto pour gemma4 (bug Ollama 0.20.x) - VLM provider VWB : local-first (Ollama), cloud opt-in via VLM_ALLOW_CLOUD Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
194
core/detection/vlm_config.py
Normal file
194
core/detection/vlm_config.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Configuration centralisée du modèle VLM (Vision-Language Model).
|
||||
|
||||
Point unique de configuration pour le modèle VLM utilisé dans tout le pipeline.
|
||||
Gère la variable d'environnement RPA_VLM_MODEL avec fallback automatique
|
||||
si le modèle configuré n'est pas disponible dans Ollama.
|
||||
|
||||
Ordre de résolution du modèle :
|
||||
1. Variable d'env RPA_VLM_MODEL (prioritaire)
|
||||
2. Variable d'env VLM_MODEL (compatibilité)
|
||||
3. Modèle par défaut : gemma4:latest
|
||||
|
||||
Fallback automatique :
|
||||
Si le modèle choisi n'est pas trouvé dans Ollama, on essaie les
|
||||
modèles de fallback dans l'ordre (FALLBACK_VLM_MODELS).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Modèle VLM par défaut — Gemma 4 latest (8B dense, Q4_K_M)
|
||||
# Nécessite think=false dans le payload (sinon tokens vides sur Ollama >=0.20)
|
||||
DEFAULT_VLM_MODEL = "gemma4:latest"
|
||||
|
||||
# Modèles de fallback, testés dans l'ordre si le modèle principal n'est pas dispo
|
||||
FALLBACK_VLM_MODELS = ["qwen3-vl:8b", "0000/ui-tars-1.5-7b-q8_0:7b"]
|
||||
|
||||
# Endpoint Ollama par défaut
|
||||
DEFAULT_OLLAMA_ENDPOINT = "http://localhost:11434"
|
||||
|
||||
# Cache du modèle résolu (évite de requêter Ollama à chaque appel)
|
||||
_resolved_model: Optional[str] = None
|
||||
_resolved_model_checked = False
|
||||
|
||||
|
||||
def get_vlm_model(
|
||||
endpoint: str = DEFAULT_OLLAMA_ENDPOINT,
|
||||
force_check: bool = False,
|
||||
) -> str:
|
||||
"""Retourne le nom du modèle VLM à utiliser, avec fallback automatique.
|
||||
|
||||
Vérifie la disponibilité du modèle dans Ollama au premier appel,
|
||||
puis cache le résultat pour les appels suivants.
|
||||
|
||||
Args:
|
||||
endpoint: URL de l'API Ollama
|
||||
force_check: Forcer une nouvelle vérification (ignorer le cache)
|
||||
|
||||
Returns:
|
||||
Nom du modèle VLM disponible (ex: "gemma4:latest")
|
||||
"""
|
||||
global _resolved_model, _resolved_model_checked
|
||||
|
||||
if _resolved_model_checked and not force_check:
|
||||
return _resolved_model
|
||||
|
||||
# Lire le modèle configuré depuis l'environnement
|
||||
configured = (
|
||||
os.environ.get("RPA_VLM_MODEL")
|
||||
or os.environ.get("VLM_MODEL")
|
||||
or DEFAULT_VLM_MODEL
|
||||
)
|
||||
|
||||
# Vérifier la disponibilité dans Ollama
|
||||
available = _list_ollama_models(endpoint)
|
||||
|
||||
if available is None:
|
||||
# Ollama non joignable — utiliser le modèle configuré sans vérification
|
||||
logger.warning(
|
||||
"Ollama non joignable (%s) — utilisation de '%s' sans vérification",
|
||||
endpoint, configured,
|
||||
)
|
||||
_resolved_model = configured
|
||||
_resolved_model_checked = True
|
||||
return _resolved_model
|
||||
|
||||
# Vérifier si le modèle configuré est disponible
|
||||
if _model_available(configured, available):
|
||||
logger.info("VLM model: %s (configuré, disponible)", configured)
|
||||
_resolved_model = configured
|
||||
_resolved_model_checked = True
|
||||
return _resolved_model
|
||||
|
||||
# Fallback : essayer les modèles alternatifs
|
||||
logger.warning(
|
||||
"Modèle VLM '%s' non trouvé dans Ollama. Recherche d'un fallback...",
|
||||
configured,
|
||||
)
|
||||
|
||||
# Construire la liste de fallback complète
|
||||
fallback_candidates = [DEFAULT_VLM_MODEL] + FALLBACK_VLM_MODELS
|
||||
for candidate in fallback_candidates:
|
||||
if candidate == configured:
|
||||
continue # Déjà testé
|
||||
if _model_available(candidate, available):
|
||||
logger.info(
|
||||
"VLM model: %s (fallback, '%s' non disponible)",
|
||||
candidate, configured,
|
||||
)
|
||||
_resolved_model = candidate
|
||||
_resolved_model_checked = True
|
||||
return _resolved_model
|
||||
|
||||
# Aucun fallback trouvé — utiliser le modèle configuré quand même
|
||||
# (Ollama le téléchargera peut-être au premier appel)
|
||||
logger.warning(
|
||||
"Aucun modèle VLM trouvé dans Ollama. "
|
||||
"Modèles disponibles : %s. Utilisation de '%s' par défaut.",
|
||||
[m for m in available if "vl" in m.lower() or "gemma" in m.lower()],
|
||||
configured,
|
||||
)
|
||||
_resolved_model = configured
|
||||
_resolved_model_checked = True
|
||||
return _resolved_model
|
||||
|
||||
|
||||
def reset_vlm_model_cache():
|
||||
"""Réinitialiser le cache du modèle résolu.
|
||||
|
||||
Utile après un changement de configuration ou un pull de modèle.
|
||||
"""
|
||||
global _resolved_model, _resolved_model_checked
|
||||
_resolved_model = None
|
||||
_resolved_model_checked = False
|
||||
|
||||
|
||||
def is_thinking_model(model_name: str) -> bool:
|
||||
"""Détermine si un modèle est un modèle 'thinking' (qwen3).
|
||||
|
||||
Les modèles thinking nécessitent un assistant prefill pour éviter
|
||||
le mode réflexion interne qui peut durer >180s avec des images.
|
||||
|
||||
Args:
|
||||
model_name: Nom du modèle (ex: "qwen3-vl:8b", "gemma4:e4b")
|
||||
|
||||
Returns:
|
||||
True si le modèle est de type thinking (nécessite prefill workaround)
|
||||
"""
|
||||
return "qwen3" in model_name.lower()
|
||||
|
||||
|
||||
def needs_think_false(model_name: str) -> bool:
|
||||
"""Détermine si un modèle nécessite think=false dans le payload.
|
||||
|
||||
Sur Ollama >=0.20, gemma4 produit des tokens vides si think n'est pas
|
||||
explicitement désactivé. Ce flag doit être envoyé dans le payload chat.
|
||||
|
||||
Args:
|
||||
model_name: Nom du modèle (ex: "gemma4:latest", "gemma4:e4b")
|
||||
|
||||
Returns:
|
||||
True si le modèle nécessite think=false
|
||||
"""
|
||||
return "gemma4" in model_name.lower()
|
||||
|
||||
|
||||
def _list_ollama_models(endpoint: str) -> Optional[List[str]]:
|
||||
"""Lister les modèles disponibles dans Ollama.
|
||||
|
||||
Returns:
|
||||
Liste des noms de modèles, ou None si Ollama n'est pas joignable.
|
||||
"""
|
||||
try:
|
||||
resp = requests.get(f"{endpoint}/api/tags", timeout=5)
|
||||
if resp.status_code == 200:
|
||||
models = resp.json().get("models", [])
|
||||
return [m["name"] for m in models]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _model_available(model_name: str, available_models: List[str]) -> bool:
|
||||
"""Vérifie si un modèle est disponible dans la liste Ollama.
|
||||
|
||||
Supporte la correspondance exacte et le match sans tag de version
|
||||
(ex: "gemma4:e4b" match "gemma4:e4b" ou "gemma4:e4b-q4_0").
|
||||
"""
|
||||
# Match exact
|
||||
if model_name in available_models:
|
||||
return True
|
||||
|
||||
# Match par préfixe (sans tag) — "gemma4:e4b" match "gemma4:e4b"
|
||||
base_name = model_name.split(":")[0] if ":" in model_name else model_name
|
||||
for m in available_models:
|
||||
if m.startswith(base_name + ":"):
|
||||
return True
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user