refactor: nettoyage agent + fix SomEngine review (singleton partagé, cache, thread-safe)

Nettoyage Windows agent :
- Suppression lea_ui inutilisés (chat_widget, overlay, styles, etc. — -1991 lignes)
- Suppression window_info*.py dupliqués (racine + core/ — -494 lignes)
- build/ + dist/ supprimés (48 MB PyInstaller abandonné, gitignorés)

Fix SomEngine (review quality guardian) :
- Singleton GPU partagé via get_shared_engine() (1 instance au lieu de 2)
- Thread-safe avec threading.Lock (double-checked locking)
- Cache SomResult par screenshot_id (max 50, évite YOLO+OCR redondants)
- Fuite fichier temp docTR corrigée (finally block)
- Chemin YOLO configurable via SOM_YOLO_WEIGHTS env var
- Guard som_image None avant VLM
- Match texte partiel : len(label) >= 3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-31 10:04:27 +02:00
parent 13390a71e7
commit a92d04621a
15 changed files with 84 additions and 2540 deletions

View File

@@ -431,21 +431,17 @@ def _needs_post_wait(action: dict) -> int:
# SomEngine — enrichissement Set-of-Mark des clics pendant le build_replay
# ---------------------------------------------------------------------------
_som_engine = None # Singleton, chargé à la demande
_som_cache: Dict[str, Any] = {} # screenshot_id -> SomResult (cache build_replay)
_SOM_CACHE_MAX = 50
def _get_som_engine():
"""Singleton SomEngine (lazy-loaded, GPU)."""
global _som_engine
if _som_engine is None:
try:
from core.detection.som_engine import SomEngine
_som_engine = SomEngine(device="cuda")
logger.info("SomEngine initialisé (lazy singleton)")
except Exception as e:
logger.warning("SomEngine non disponible : %s", e)
_som_engine = False # Marqueur "indisponible"
return _som_engine if _som_engine is not False else None
"""Singleton SomEngine partagé."""
try:
from core.detection.som_engine import get_shared_engine
return get_shared_engine()
except ImportError:
return None
def _som_identify_clicked_element(
@@ -486,19 +482,38 @@ def _som_identify_clicked_element(
if not full_path.is_file():
return None
try:
from PIL import Image
img = Image.open(full_path).convert("RGB")
except Exception as e:
logger.debug("SoM: impossible de charger %s : %s", full_path, e)
return None
# Vérifier le cache SomResult par (session_dir, screenshot_id)
cache_key = f"{session_dir}:{screenshot_id}"
if cache_key in _som_cache:
result = _som_cache[cache_key]
else:
try:
from PIL import Image
img = Image.open(full_path).convert("RGB")
except Exception as e:
logger.debug("SoM: impossible de charger %s : %s", full_path, e)
return None
# Lancer SomEngine
try:
result = engine.analyze(img)
except Exception as e:
logger.warning("SoM: erreur d'analyse : %s", e)
return None
# Lancer SomEngine
try:
result = engine.analyze(img)
except Exception as e:
logger.warning("SoM: erreur d'analyse : %s", e)
return None
# Stocker dans le cache (éléments seulement, pas l'image annotée)
from core.detection.som_engine import SomResult
cached = SomResult(
elements=result.elements,
width=result.width,
height=result.height,
analysis_time_ms=result.analysis_time_ms,
)
if len(_som_cache) >= _SOM_CACHE_MAX:
# Supprimer la plus ancienne entrée (FIFO)
oldest_key = next(iter(_som_cache))
del _som_cache[oldest_key]
_som_cache[cache_key] = cached
if not result.elements:
return None