fix(agent): capture JPEG+downscale (allege CPU/disque, frequence intacte) + robustesse chemin _background/shots

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-07-01 12:36:47 +02:00
parent 2a1b1ed80e
commit 144a5c288a
3 changed files with 465 additions and 12 deletions

View File

@@ -18,8 +18,9 @@ import platform
from typing import Any, Dict, List, Optional, Tuple
from PIL import Image, ImageFilter, ImageStat
import mss
from ..config import TARGETED_CROP_SIZE, SCREENSHOT_QUALITY, BLUR_SENSITIVE
from ..config import TARGETED_CROP_SIZE, BLUR_SENSITIVE
from .blur_sensitive import blur_sensitive_regions
from .capture_io import save_capture
logger = logging.getLogger(__name__)
@@ -425,6 +426,18 @@ class VisionCapturer:
# On ne crée plus self.sct ici car mss n'est pas thread-safe sous Windows
self.last_img_hash = None
def _ensure_shots_dir(self) -> None:
"""Garantit l'existence de `shots/` avant toute écriture.
Le dossier est créé dans `__init__`, mais l'auto-cleanup de
`SessionStorage` (`shutil.rmtree` par âge/taille) peut supprimer tout
le dossier de session — y compris la session permanente `_background`.
Sans ce garde, la capture suivante lève `[Errno 2] No such file or
directory` (bug observé poste Émilie). On recrée donc le répertoire
cible juste avant chaque sauvegarde.
"""
os.makedirs(self.shots_dir, exist_ok=True)
def capture_full_context(self, name_suffix: str, force=False) -> str:
"""
Capture l'écran complet.
@@ -460,9 +473,15 @@ class VisionCapturer:
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
# Politique d'écriture : les heartbeats sont de la liveness pure
# (le serveur vérifie juste qu'un écran a changé) → JPEG downscalé.
# Les autres contextes (focus_change, result_of_*) → JPEG q85.
kind = "heartbeat" if "heartbeat" in name_suffix else "context"
self._ensure_shots_dir()
path_base = os.path.join(
self.shots_dir, f"context_{int(time.time())}_{name_suffix}"
)
return save_capture(img, path_base, kind)
except Exception as e:
logger.error(f"Erreur Context Capture: {e}")
return ""
@@ -506,10 +525,10 @@ class VisionCapturer:
return result
return {}
full_path = os.path.join(self.shots_dir, f"{screenshot_id}_full.png")
full_base = os.path.join(self.shots_dir, f"{screenshot_id}_full")
# Capture du Crop (Cœur de l'apprentissage qwen3-vl)
crop_path = os.path.join(self.shots_dir, f"{screenshot_id}_crop.png")
crop_base = os.path.join(self.shots_dir, f"{screenshot_id}_crop")
w, h = TARGETED_CROP_SIZE
left = max(0, x - w // 2)
top = max(0, y - h // 2)
@@ -523,8 +542,11 @@ class VisionCapturer:
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)
# Politique d'écriture : full = vue contextuelle → JPEG q85 ;
# crop = cible de grounding qwen3-vl → PNG lossless (contrat serveur).
self._ensure_shots_dir()
full_path = save_capture(img, full_base, "full")
crop_path = save_capture(crop_img, crop_base, "crop")
# Mise à jour du hash pour le prochain heartbeat
self.last_img_hash = self._compute_quick_hash(img)
@@ -648,11 +670,12 @@ class VisionCapturer:
if BLUR_SENSITIVE:
blur_sensitive_regions(window_img)
# Sauvegarde
window_path = os.path.join(
self.shots_dir, f"{screenshot_id}_window.png"
# Sauvegarde — fenêtre = vue contextuelle → JPEG q85 (politique).
self._ensure_shots_dir()
window_base = os.path.join(
self.shots_dir, f"{screenshot_id}_window"
)
window_img.save(window_path, "PNG", quality=SCREENSHOT_QUALITY)
window_path = save_capture(window_img, window_base, "window")
result = {
"window_image": window_path,