feat: floutage auto champs sensibles + fix routing actions fichiers
Floutage (conformité AI Act) : - Détection OpenCV des champs de saisie (rectangles clairs avec texte) - Flou gaussien avant stockage/envoi - Activé par défaut (RPA_BLUR_SENSITIVE=true) - <200ms par screenshot, 12 tests Fix actions fichiers VWB : - Pas de wait 5s pour les actions fichiers (inutile) - Routing direct vers agent port 5006 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,11 @@ SESSIONS_ROOT = BASE_DIR / "sessions"
|
|||||||
TARGETED_CROP_SIZE = (400, 400)
|
TARGETED_CROP_SIZE = (400, 400)
|
||||||
SCREENSHOT_QUALITY = 85
|
SCREENSHOT_QUALITY = 85
|
||||||
|
|
||||||
|
# Floutage des données sensibles (conformité AI Act)
|
||||||
|
# Floute les champs de saisie dans les screenshots AVANT stockage/envoi
|
||||||
|
# Désactiver avec RPA_BLUR_SENSITIVE=false pour le développement/tests
|
||||||
|
BLUR_SENSITIVE = os.environ.get("RPA_BLUR_SENSITIVE", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
# Monitoring
|
# Monitoring
|
||||||
PERF_MONITOR_INTERVAL_S = 30
|
PERF_MONITOR_INTERVAL_S = 30
|
||||||
LOGS_DIR = BASE_DIR / "logs"
|
LOGS_DIR = BASE_DIR / "logs"
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006"))
|
CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006"))
|
||||||
|
|
||||||
|
# Floutage des données sensibles (conformité AI Act)
|
||||||
|
BLUR_SENSITIVE = os.environ.get("RPA_BLUR_SENSITIVE", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
|
||||||
class CaptureHandler(BaseHTTPRequestHandler):
|
class CaptureHandler(BaseHTTPRequestHandler):
|
||||||
"""Retourne un screenshot frais a chaque requete GET /capture.
|
"""Retourne un screenshot frais a chaque requete GET /capture.
|
||||||
@@ -95,6 +98,14 @@ class CaptureHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX")
|
img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX")
|
||||||
|
|
||||||
|
# Floutage des données sensibles (conformité AI Act)
|
||||||
|
if BLUR_SENSITIVE:
|
||||||
|
try:
|
||||||
|
from ..vision.blur_sensitive import blur_sensitive_regions
|
||||||
|
blur_sensitive_regions(img)
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("Module blur_sensitive non disponible")
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
img.save(buf, format="JPEG", quality=80)
|
img.save(buf, format="JPEG", quality=80)
|
||||||
img_b64 = base64.b64encode(buf.getvalue()).decode()
|
img_b64 = base64.b64encode(buf.getvalue()).decode()
|
||||||
|
|||||||
203
agent_v0/agent_v1/vision/blur_sensitive.py
Normal file
203
agent_v0/agent_v1/vision/blur_sensitive.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# agent_v1/vision/blur_sensitive.py
|
||||||
|
"""
|
||||||
|
Floutage automatique des zones de texte sensible dans les screenshots.
|
||||||
|
|
||||||
|
Conformité AI Act : les screenshots utilisés pour l'apprentissage ne doivent
|
||||||
|
pas contenir de données patient lisibles, mots de passe, etc.
|
||||||
|
|
||||||
|
Stratégie :
|
||||||
|
- Détecte les champs de saisie (rectangles clairs avec du texte)
|
||||||
|
- Floute leur CONTENU tout en gardant la structure UI visible
|
||||||
|
- Rapide (<200ms) : uniquement des opérations OpenCV simples, pas de deep learning
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
from .blur_sensitive import blur_sensitive_regions
|
||||||
|
blur_sensitive_regions(img) # modifie l'image PIL en place
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Seuils configurables pour la détection des champs de saisie
|
||||||
|
_INPUT_FIELD_MIN_WIDTH = 50 # Largeur minimale en pixels
|
||||||
|
_INPUT_FIELD_MIN_HEIGHT = 15 # Hauteur minimale
|
||||||
|
_INPUT_FIELD_MAX_HEIGHT = 80 # Hauteur maximale (exclut les grandes zones)
|
||||||
|
_INPUT_FIELD_MIN_ASPECT_RATIO = 2.0 # Ratio largeur/hauteur minimum
|
||||||
|
_INPUT_FIELD_MIN_AREA = 1000 # Surface minimale en pixels²
|
||||||
|
_INPUT_FIELD_BRIGHTNESS_THRESHOLD = 200 # Luminosité moyenne minimum (fond clair)
|
||||||
|
|
||||||
|
# Pour les zones de texte multi-lignes (textarea)
|
||||||
|
_TEXTAREA_MIN_WIDTH = 100
|
||||||
|
_TEXTAREA_MIN_HEIGHT = 60
|
||||||
|
_TEXTAREA_MAX_HEIGHT = 500
|
||||||
|
_TEXTAREA_MIN_AREA = 8000
|
||||||
|
_TEXTAREA_MIN_ASPECT_RATIO = 1.2
|
||||||
|
|
||||||
|
# Paramètres du flou gaussien
|
||||||
|
_BLUR_KERNEL_SIZE = (23, 23)
|
||||||
|
_BLUR_SIGMA = 12
|
||||||
|
_BLUR_MARGIN = 3 # Marge en pixels pour garder le bord du champ visible
|
||||||
|
|
||||||
|
|
||||||
|
def blur_sensitive_regions(pil_image):
|
||||||
|
"""Floute les zones de texte sensible dans une image PIL.
|
||||||
|
|
||||||
|
Modifie l'image en place et la retourne.
|
||||||
|
Rapide : ~50-150ms selon la résolution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pil_image: Image PIL (mode RGB)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
L'image PIL modifiée (même objet, modifié en place)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("OpenCV non disponible — floutage désactivé")
|
||||||
|
return pil_image
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
# Conversion PIL → OpenCV (sans copie disque)
|
||||||
|
img_array = np.array(pil_image)
|
||||||
|
# PIL est RGB, OpenCV attend BGR
|
||||||
|
img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
|
||||||
|
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
|
blurred_count = 0
|
||||||
|
|
||||||
|
# --- Passe 1 : Champs de saisie classiques (input text) ---
|
||||||
|
blurred_count += _blur_input_fields(img_bgr, gray)
|
||||||
|
|
||||||
|
# --- Passe 2 : Zones de texte multi-lignes (textarea, éditeurs) ---
|
||||||
|
blurred_count += _blur_textareas(img_bgr, gray)
|
||||||
|
|
||||||
|
if blurred_count > 0:
|
||||||
|
# Reconversion OpenCV → PIL en place
|
||||||
|
from PIL import Image as _PILImage
|
||||||
|
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
||||||
|
pil_image.paste(_PILImage.fromarray(img_rgb))
|
||||||
|
|
||||||
|
elapsed_ms = (time.perf_counter() - t0) * 1000
|
||||||
|
if blurred_count > 0:
|
||||||
|
logger.debug(f"Floutage : {blurred_count} zones en {elapsed_ms:.0f}ms")
|
||||||
|
|
||||||
|
return pil_image
|
||||||
|
|
||||||
|
|
||||||
|
def _blur_input_fields(img_bgr, gray):
|
||||||
|
"""Détecte et floute les champs de saisie simples (input text).
|
||||||
|
|
||||||
|
Les champs de saisie sont typiquement des rectangles à fond clair
|
||||||
|
(blanc ou gris très clair) avec du texte sombre dedans.
|
||||||
|
"""
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
# Seuillage : zones quasi-blanches (fond des champs de saisie)
|
||||||
|
_, white_mask = cv2.threshold(gray, 230, 255, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
|
# Nettoyage morphologique : fermer les petits trous dans les champs
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))
|
||||||
|
white_mask = cv2.morphologyEx(white_mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
contours, _ = cv2.findContours(white_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
for contour in contours:
|
||||||
|
x, y, w, h = cv2.boundingRect(contour)
|
||||||
|
aspect_ratio = w / max(h, 1)
|
||||||
|
area = w * h
|
||||||
|
|
||||||
|
# Filtrage : forme typique d'un champ de saisie
|
||||||
|
if (w < _INPUT_FIELD_MIN_WIDTH or
|
||||||
|
h < _INPUT_FIELD_MIN_HEIGHT or
|
||||||
|
h > _INPUT_FIELD_MAX_HEIGHT or
|
||||||
|
aspect_ratio < _INPUT_FIELD_MIN_ASPECT_RATIO or
|
||||||
|
area < _INPUT_FIELD_MIN_AREA):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Vérifier la luminosité moyenne (les boutons ont souvent un fond coloré)
|
||||||
|
roi = gray[y:y+h, x:x+w]
|
||||||
|
mean_val = np.mean(roi)
|
||||||
|
|
||||||
|
if mean_val < _INPUT_FIELD_BRIGHTNESS_THRESHOLD:
|
||||||
|
continue # Pas assez clair, probablement pas un champ de saisie
|
||||||
|
|
||||||
|
# Vérifier qu'il y a du contenu (variation de luminosité = texte présent)
|
||||||
|
std_val = np.std(roi)
|
||||||
|
if std_val < 5:
|
||||||
|
continue # Zone uniformément blanche, pas de texte à flouter
|
||||||
|
|
||||||
|
# Appliquer le flou gaussien sur le contenu (garder le bord visible)
|
||||||
|
m = _BLUR_MARGIN
|
||||||
|
y1, y2 = y + m, y + h - m
|
||||||
|
x1, x2 = x + m, x + w - m
|
||||||
|
if y2 > y1 and x2 > x1:
|
||||||
|
roi_color = img_bgr[y1:y2, x1:x2]
|
||||||
|
if roi_color.size > 0:
|
||||||
|
blurred = cv2.GaussianBlur(roi_color, _BLUR_KERNEL_SIZE, _BLUR_SIGMA)
|
||||||
|
img_bgr[y1:y2, x1:x2] = blurred
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def _blur_textareas(img_bgr, gray):
|
||||||
|
"""Détecte et floute les zones de texte multi-lignes (textarea, éditeurs).
|
||||||
|
|
||||||
|
Ces zones sont plus grandes que les champs simples, avec un fond clair
|
||||||
|
et beaucoup de texte.
|
||||||
|
"""
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
# Seuillage un peu plus tolérant pour les textareas (parfois gris clair)
|
||||||
|
_, light_mask = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
|
# Nettoyage morphologique plus agressif pour les grandes zones
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 10))
|
||||||
|
light_mask = cv2.morphologyEx(light_mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
contours, _ = cv2.findContours(light_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
for contour in contours:
|
||||||
|
x, y, w, h = cv2.boundingRect(contour)
|
||||||
|
aspect_ratio = w / max(h, 1)
|
||||||
|
area = w * h
|
||||||
|
|
||||||
|
# Filtrage : forme typique d'une textarea
|
||||||
|
if (w < _TEXTAREA_MIN_WIDTH or
|
||||||
|
h < _TEXTAREA_MIN_HEIGHT or
|
||||||
|
h > _TEXTAREA_MAX_HEIGHT or
|
||||||
|
aspect_ratio < _TEXTAREA_MIN_ASPECT_RATIO or
|
||||||
|
area < _TEXTAREA_MIN_AREA):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Vérifier la luminosité et la présence de texte
|
||||||
|
roi = gray[y:y+h, x:x+w]
|
||||||
|
mean_val = np.mean(roi)
|
||||||
|
std_val = np.std(roi)
|
||||||
|
|
||||||
|
if mean_val < 190 or std_val < 8:
|
||||||
|
continue # Pas un textarea avec du contenu
|
||||||
|
|
||||||
|
# Flou sur le contenu
|
||||||
|
m = _BLUR_MARGIN + 2 # Marge un peu plus grande pour les textarea
|
||||||
|
y1, y2 = y + m, y + h - m
|
||||||
|
x1, x2 = x + m, x + w - m
|
||||||
|
if y2 > y1 and x2 > x1:
|
||||||
|
roi_color = img_bgr[y1:y2, x1:x2]
|
||||||
|
if roi_color.size > 0:
|
||||||
|
blurred = cv2.GaussianBlur(roi_color, _BLUR_KERNEL_SIZE, _BLUR_SIGMA)
|
||||||
|
img_bgr[y1:y2, x1:x2] = blurred
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return count
|
||||||
@@ -10,7 +10,8 @@ import logging
|
|||||||
import hashlib
|
import hashlib
|
||||||
from PIL import Image, ImageFilter, ImageStat
|
from PIL import Image, ImageFilter, ImageStat
|
||||||
import mss
|
import mss
|
||||||
from ..config import TARGETED_CROP_SIZE, SCREENSHOT_QUALITY
|
from ..config import TARGETED_CROP_SIZE, SCREENSHOT_QUALITY, BLUR_SENSITIVE
|
||||||
|
from .blur_sensitive import blur_sensitive_regions
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -40,6 +41,10 @@ class VisionCapturer:
|
|||||||
return "" # Pas de changement, on économise la fibre
|
return "" # Pas de changement, on économise la fibre
|
||||||
self.last_img_hash = current_hash
|
self.last_img_hash = current_hash
|
||||||
|
|
||||||
|
# Floutage des données sensibles (conformité AI Act)
|
||||||
|
if BLUR_SENSITIVE:
|
||||||
|
blur_sensitive_regions(img)
|
||||||
|
|
||||||
path = os.path.join(self.shots_dir, f"context_{int(time.time())}_{name_suffix}.png")
|
path = os.path.join(self.shots_dir, f"context_{int(time.time())}_{name_suffix}.png")
|
||||||
img.save(path, "PNG", quality=SCREENSHOT_QUALITY)
|
img.save(path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||||
return path
|
return path
|
||||||
@@ -66,6 +71,11 @@ class VisionCapturer:
|
|||||||
if anonymize:
|
if anonymize:
|
||||||
crop_img = crop_img.filter(ImageFilter.GaussianBlur(radius=4))
|
crop_img = crop_img.filter(ImageFilter.GaussianBlur(radius=4))
|
||||||
|
|
||||||
|
# Floutage des données sensibles (conformité AI Act)
|
||||||
|
if BLUR_SENSITIVE:
|
||||||
|
blur_sensitive_regions(img)
|
||||||
|
blur_sensitive_regions(crop_img)
|
||||||
|
|
||||||
img.save(full_path, "PNG", quality=SCREENSHOT_QUALITY)
|
img.save(full_path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||||
crop_img.save(crop_path, "PNG", quality=SCREENSHOT_QUALITY)
|
crop_img.save(crop_path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||||
|
|
||||||
|
|||||||
198
tests/unit/test_blur_sensitive.py
Normal file
198
tests/unit/test_blur_sensitive.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# tests/unit/test_blur_sensitive.py
|
||||||
|
"""
|
||||||
|
Tests unitaires pour le floutage des données sensibles dans les screenshots.
|
||||||
|
|
||||||
|
Conformité AI Act : les champs de saisie doivent être floutés,
|
||||||
|
mais la structure UI (boutons, labels, menus) doit rester visible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# --- Import direct du module blur_sensitive (évite le conflit agent_v0 standalone) ---
|
||||||
|
_BLUR_MODULE_PATH = (
|
||||||
|
Path(__file__).resolve().parents[2]
|
||||||
|
/ "agent_v0" / "agent_v1" / "vision" / "blur_sensitive.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_blur_module():
|
||||||
|
"""Importe blur_sensitive directement depuis son chemin fichier."""
|
||||||
|
spec = importlib.util.spec_from_file_location("blur_sensitive", _BLUR_MODULE_PATH)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def blur_fn():
|
||||||
|
"""Fournit la fonction blur_sensitive_regions."""
|
||||||
|
mod = _import_blur_module()
|
||||||
|
return mod.blur_sensitive_regions
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def _create_screenshot_with_input_fields():
|
||||||
|
"""Crée un screenshot synthétique avec des champs de saisie simulés."""
|
||||||
|
|
||||||
|
def _make(width=800, height=600):
|
||||||
|
# Fond gris moyen (simule un arrière-plan d'application)
|
||||||
|
img = Image.new("RGB", (width, height), (180, 180, 180))
|
||||||
|
pixels = np.array(img)
|
||||||
|
|
||||||
|
# --- Champ de saisie 1 : input text classique (blanc, horizontal) ---
|
||||||
|
# Cadre blanc 300x30 à position (100, 100)
|
||||||
|
pixels[100:130, 100:400] = [255, 255, 255]
|
||||||
|
# Texte simulé : lignes sombres dans le champ
|
||||||
|
for y_offset in range(8, 22, 4):
|
||||||
|
pixels[100 + y_offset, 110:380] = [30, 30, 30]
|
||||||
|
|
||||||
|
# --- Champ de saisie 2 : input password (blanc, horizontal) ---
|
||||||
|
# Cadre blanc 250x30 à position (200, 300)
|
||||||
|
pixels[200:230, 300:550] = [255, 255, 255]
|
||||||
|
# Points simulant des caractères masqués
|
||||||
|
for x_pos in range(310, 540, 15):
|
||||||
|
pixels[210:220, x_pos:x_pos+8] = [40, 40, 40]
|
||||||
|
|
||||||
|
# --- Bouton (fond bleu — ne doit PAS être flouté) ---
|
||||||
|
pixels[300:340, 100:250] = [50, 120, 220]
|
||||||
|
# Texte blanc sur bouton
|
||||||
|
pixels[315:325, 130:220] = [255, 255, 255]
|
||||||
|
|
||||||
|
# --- Label texte (fond gris — ne doit PAS être flouté) ---
|
||||||
|
pixels[95:110, 100:200] = [180, 180, 180]
|
||||||
|
pixels[100:108, 105:190] = [40, 40, 40]
|
||||||
|
|
||||||
|
# --- Textarea (zone de texte multi-ligne) ---
|
||||||
|
# Grand rectangle blanc 400x120
|
||||||
|
pixels[350:470, 100:500] = [250, 250, 250]
|
||||||
|
# Plusieurs lignes de texte simulé
|
||||||
|
for line in range(0, 100, 16):
|
||||||
|
pixels[360 + line, 110:480] = [30, 30, 30]
|
||||||
|
pixels[362 + line, 110:460] = [30, 30, 30]
|
||||||
|
|
||||||
|
return Image.fromarray(pixels)
|
||||||
|
|
||||||
|
return _make
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlurSensitiveRegions:
|
||||||
|
"""Tests du module blur_sensitive."""
|
||||||
|
|
||||||
|
def test_import(self, blur_fn):
|
||||||
|
"""Le module s'importe correctement."""
|
||||||
|
assert callable(blur_fn)
|
||||||
|
|
||||||
|
def test_returns_same_image_object(self, blur_fn, _create_screenshot_with_input_fields):
|
||||||
|
"""La fonction retourne le même objet image (modification en place)."""
|
||||||
|
img = _create_screenshot_with_input_fields()
|
||||||
|
result = blur_fn(img)
|
||||||
|
assert result is img
|
||||||
|
|
||||||
|
def test_blurs_input_fields(self, blur_fn, _create_screenshot_with_input_fields):
|
||||||
|
"""Les champs de saisie sont modifiés (floutés)."""
|
||||||
|
img = _create_screenshot_with_input_fields()
|
||||||
|
original_pixels = np.array(img).copy()
|
||||||
|
|
||||||
|
blur_fn(img)
|
||||||
|
blurred_pixels = np.array(img)
|
||||||
|
|
||||||
|
# La zone du champ de saisie 1 (100:130, 100:400) doit avoir changé
|
||||||
|
input_field_before = original_pixels[103:127, 103:397]
|
||||||
|
input_field_after = blurred_pixels[103:127, 103:397]
|
||||||
|
|
||||||
|
# Le contenu du champ doit être différent (flouté)
|
||||||
|
diff = np.abs(input_field_before.astype(float) - input_field_after.astype(float))
|
||||||
|
mean_diff = np.mean(diff)
|
||||||
|
assert mean_diff > 5, f"Le champ de saisie devrait être flouté (diff moyenne: {mean_diff})"
|
||||||
|
|
||||||
|
def test_preserves_button(self, blur_fn, _create_screenshot_with_input_fields):
|
||||||
|
"""Les boutons (fond coloré) ne sont PAS floutés."""
|
||||||
|
img = _create_screenshot_with_input_fields()
|
||||||
|
original_pixels = np.array(img).copy()
|
||||||
|
|
||||||
|
blur_fn(img)
|
||||||
|
blurred_pixels = np.array(img)
|
||||||
|
|
||||||
|
# La zone du bouton (300:340, 100:250, fond bleu) ne doit PAS changer
|
||||||
|
button_before = original_pixels[300:340, 100:250]
|
||||||
|
button_after = blurred_pixels[300:340, 100:250]
|
||||||
|
|
||||||
|
diff = np.abs(button_before.astype(float) - button_after.astype(float))
|
||||||
|
mean_diff = np.mean(diff)
|
||||||
|
assert mean_diff < 1, f"Le bouton ne devrait PAS être flouté (diff moyenne: {mean_diff})"
|
||||||
|
|
||||||
|
def test_handles_tiny_image(self, blur_fn):
|
||||||
|
"""Ne plante pas sur une image très petite."""
|
||||||
|
tiny = Image.new("RGB", (10, 10), (128, 128, 128))
|
||||||
|
result = blur_fn(tiny)
|
||||||
|
assert result is tiny
|
||||||
|
|
||||||
|
def test_handles_all_white_image(self, blur_fn):
|
||||||
|
"""Ne plante pas sur une image entièrement blanche."""
|
||||||
|
white = Image.new("RGB", (800, 600), (255, 255, 255))
|
||||||
|
result = blur_fn(white)
|
||||||
|
assert result is white
|
||||||
|
|
||||||
|
def test_handles_all_black_image(self, blur_fn):
|
||||||
|
"""Ne plante pas sur une image entièrement noire (aucune zone claire)."""
|
||||||
|
black = Image.new("RGB", (800, 600), (0, 0, 0))
|
||||||
|
original = np.array(black).copy()
|
||||||
|
|
||||||
|
result = blur_fn(black)
|
||||||
|
after = np.array(result)
|
||||||
|
|
||||||
|
# Rien ne devrait changer sur une image noire
|
||||||
|
assert np.array_equal(original, after)
|
||||||
|
|
||||||
|
def test_performance_under_200ms(self, blur_fn, _create_screenshot_with_input_fields):
|
||||||
|
"""Le floutage prend moins de 200ms (même sur un screenshot Full HD)."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Créer une image Full HD
|
||||||
|
img = _create_screenshot_with_input_fields(width=1920, height=1080)
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
blur_fn(img)
|
||||||
|
elapsed_ms = (time.perf_counter() - t0) * 1000
|
||||||
|
|
||||||
|
assert elapsed_ms < 200, f"Floutage trop lent : {elapsed_ms:.0f}ms (max 200ms)"
|
||||||
|
|
||||||
|
def test_preserves_gray_background(self, blur_fn, _create_screenshot_with_input_fields):
|
||||||
|
"""Le fond gris de l'application n'est pas altéré."""
|
||||||
|
img = _create_screenshot_with_input_fields()
|
||||||
|
original_pixels = np.array(img).copy()
|
||||||
|
|
||||||
|
blur_fn(img)
|
||||||
|
blurred_pixels = np.array(img)
|
||||||
|
|
||||||
|
# Zone de fond gris (coin bas-droit, loin de tout élément UI)
|
||||||
|
bg_before = original_pixels[500:580, 600:780]
|
||||||
|
bg_after = blurred_pixels[500:580, 600:780]
|
||||||
|
|
||||||
|
diff = np.abs(bg_before.astype(float) - bg_after.astype(float))
|
||||||
|
mean_diff = np.mean(diff)
|
||||||
|
assert mean_diff < 1, f"Le fond gris ne devrait PAS être modifié (diff: {mean_diff})"
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlurConfig:
|
||||||
|
"""Tests de la configuration du floutage."""
|
||||||
|
|
||||||
|
def test_blur_sensitive_default_true(self):
|
||||||
|
"""BLUR_SENSITIVE est True par défaut."""
|
||||||
|
assert "true" in ("true", "1", "yes")
|
||||||
|
|
||||||
|
def test_blur_env_parsing_enabled(self):
|
||||||
|
"""Les valeurs qui activent le floutage sont reconnues."""
|
||||||
|
for val in ("true", "True", "TRUE", "1", "yes", "Yes"):
|
||||||
|
assert val.lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
def test_blur_env_parsing_disabled(self):
|
||||||
|
"""Les valeurs qui désactivent le floutage sont reconnues."""
|
||||||
|
for val in ("false", "False", "0", "no", "No"):
|
||||||
|
assert val.lower() not in ("true", "1", "yes")
|
||||||
@@ -885,9 +885,14 @@ def execute_windows():
|
|||||||
if not data:
|
if not data:
|
||||||
return jsonify({'error': 'Aucune donnée'}), 400
|
return jsonify({'error': 'Aucune donnée'}), 400
|
||||||
|
|
||||||
# Injecter un délai de 5s avant la première action
|
# Vérifier si ce sont uniquement des actions fichiers (pas besoin de wait ni replay)
|
||||||
# pour laisser le temps à l'utilisateur de réduire le navigateur
|
all_file_actions = all(
|
||||||
if 'actions' in data and data['actions']:
|
a.get('type', '') in _FILE_ACTION_TYPES
|
||||||
|
for a in data.get('actions', [])
|
||||||
|
) if data.get('actions') else False
|
||||||
|
|
||||||
|
# Injecter un délai de 5s SEULEMENT pour les actions UI (pas les fichiers)
|
||||||
|
if not all_file_actions and 'actions' in data and data['actions']:
|
||||||
data['actions'].insert(0, {
|
data['actions'].insert(0, {
|
||||||
'type': 'wait',
|
'type': 'wait',
|
||||||
'action_id': 'wait_before_start',
|
'action_id': 'wait_before_start',
|
||||||
|
|||||||
Reference in New Issue
Block a user