Suppression du .git embarqué dans agent_v0/ — le code est maintenant tracké normalement dans le repo principal. Inclut : agent_v1 (client), server_v1 (streaming), lea_ui (chat client) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
85 lines
3.5 KiB
Python
85 lines
3.5 KiB
Python
# 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
|
|
|
|
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
|
|
|
|
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))
|
|
|
|
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()
|