# agent_v1/vision/capturer.py """ Gestionnaire de vision avancé pour Agent V1. Optimisé pour le streaming fibre avec détection de changement. """ import os import time import logging import hashlib from PIL import Image, ImageFilter, ImageStat import mss from ..config import TARGETED_CROP_SIZE, SCREENSHOT_QUALITY, BLUR_SENSITIVE from .blur_sensitive import blur_sensitive_regions logger = logging.getLogger(__name__) class VisionCapturer: def __init__(self, session_dir: str): self.session_dir = session_dir self.shots_dir = os.path.join(session_dir, "shots") os.makedirs(self.shots_dir, exist_ok=True) # On ne crée plus self.sct ici car mss n'est pas thread-safe sous Windows self.last_img_hash = None def capture_full_context(self, name_suffix: str, force=False) -> str: """ Capture l'écran complet. Si force=False, vérifie d'abord si l'écran a changé. """ try: with mss.mss() as sct: monitor = sct.monitors[1] sct_img = sct.grab(monitor) img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") # Détection de changement (pour Heartbeat) if not force: current_hash = self._compute_quick_hash(img) if current_hash == self.last_img_hash: return "" # Pas de changement, on économise la fibre 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") img.save(path, "PNG", quality=SCREENSHOT_QUALITY) return path except Exception as e: logger.error(f"Erreur Context Capture: {e}") return "" def capture_dual(self, x: int, y: int, screenshot_id: str, anonymize=False) -> dict: """Capture duale (Full + Crop) systématique (forcée car liée à une action).""" try: with mss.mss() as sct: full_path = os.path.join(self.shots_dir, f"{screenshot_id}_full.png") monitor = sct.monitors[1] sct_img = sct.grab(monitor) img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") # Capture du Crop (Cœur de l'apprentissage qwen3-vl) crop_path = os.path.join(self.shots_dir, f"{screenshot_id}_crop.png") w, h = TARGETED_CROP_SIZE left = max(0, x - w // 2) top = max(0, y - h // 2) crop_img = img.crop((left, top, left + w, top + h)) if anonymize: 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) crop_img.save(crop_path, "PNG", quality=SCREENSHOT_QUALITY) # Mise à jour du hash pour le prochain heartbeat self.last_img_hash = self._compute_quick_hash(img) return {"full": full_path, "crop": crop_path} except Exception as e: logger.error(f"Erreur Dual Capture: {e}") return {} def _compute_quick_hash(self, img: Image) -> str: """Calcule un hash rapide basé sur une vignette réduite pour détecter les changements.""" # On réduit l'image à 64x64 pour comparer les masses de couleurs (très rapide) small_img = img.resize((64, 64), Image.NEAREST).convert("L") return hashlib.md5(small_img.tobytes()).hexdigest()