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

@@ -25,6 +25,7 @@ from __future__ import annotations
import base64
import io
import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple
@@ -33,8 +34,10 @@ from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger(__name__)
# Chemin vers les poids YOLO d'OmniParser
_YOLO_WEIGHTS = Path("/home/dom/ai/OmniParser/weights/icon_detect/model.pt")
# Chemin vers les poids YOLO d'OmniParser (configurable via env)
_YOLO_WEIGHTS = Path(
os.environ.get("SOM_YOLO_WEIGHTS", "/home/dom/ai/OmniParser/weights/icon_detect/model.pt")
)
@dataclass
@@ -165,17 +168,17 @@ class SomEngine:
# ── 2. docTR : OCR pour lire le texte ──
if self._ocr is not None:
try:
import numpy as np
from doctr.io import DocumentFile
# Convertir PIL → fichier temporaire pour docTR
import tempfile
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
screenshot.save(tmp, format="JPEG", quality=90)
tmp_path = tmp.name
doc = DocumentFile.from_images([tmp_path])
import os
os.unlink(tmp_path)
result_ocr = self._ocr(doc)
try:
doc = DocumentFile.from_images([tmp_path])
result_ocr = self._ocr(doc)
finally:
os.unlink(tmp_path)
for page in result_ocr.pages:
for block in page.blocks:
@@ -288,3 +291,25 @@ class SomEngine:
buf = io.BytesIO()
image.save(buf, format="JPEG", quality=quality)
return base64.b64encode(buf.getvalue()).decode()
# ---------------------------------------------------------------------------
# Singleton partagé (lazy-loaded, thread-safe)
# ---------------------------------------------------------------------------
_shared_engine: Optional[SomEngine] = None
_shared_lock = __import__("threading").Lock()
def get_shared_engine(device: str = "cuda") -> Optional[SomEngine]:
"""Singleton SomEngine partagé entre tous les modules."""
global _shared_engine
if _shared_engine is None:
with _shared_lock:
if _shared_engine is None:
try:
_shared_engine = SomEngine(device=device)
logger.info("SomEngine singleton partagé initialisé")
except Exception as e:
logger.warning("SomEngine non disponible : %s", e)
return None
return _shared_engine