fix: 3 corrections — crop 80px, email AZERTY (@), icônes anchor match

1. Crop réduit de 150x150 à 80x80 (config + fallback serveur)
   Plus discriminant pour les icônes de barre de titre

2. Email AZERTY : supprimer raw_keys quand le texte contient des
   chars fusionnés depuis key_combos (@ de AltGr) → copier-coller
   Le @ était perdu car absent des raw_keys individuels

3. Anchor match : template matching sur screenshot entier puis
   élément SomEngine le plus proche (max 100px)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-31 16:21:02 +02:00
parent 4f5c518d3a
commit 6937b94f2a
2 changed files with 45 additions and 8 deletions

View File

@@ -52,8 +52,9 @@ API_TOKEN = os.environ.get("RPA_API_TOKEN", "")
MAX_SESSION_DURATION_S = 60 * 60 # 1 heure
SESSIONS_ROOT = BASE_DIR / "sessions"
# Paramètres Vision (Crops pour qwen3-vl)
TARGETED_CROP_SIZE = (150, 150)
# Paramètres Vision (Crops pour la résolution visuelle)
# 80x80 : assez petit pour être discriminant (icônes), assez grand pour le contexte
TARGETED_CROP_SIZE = (80, 80)
SCREENSHOT_QUALITY = 85
# Floutage des données sensibles (conformité AI Act)

View File

@@ -750,8 +750,8 @@ def _load_crop_for_event(
pos = event_data.get("pos", [])
if pos and len(pos) == 2:
cx, cy = int(pos[0]), int(pos[1])
# Crop 150x150 centré sur le clic (plus discriminant, moins de bruit)
crop_size = 75
# Crop 80x80 centré sur le clic (discriminant pour icônes)
crop_size = 40
x1 = max(0, cx - crop_size)
y1 = max(0, cy - crop_size)
x2 = min(img.width, cx + crop_size)
@@ -953,6 +953,15 @@ def build_replay_from_raw_events(
if evt_type in ("key_combo", "key_press"):
keys = _sanitize_keys(evt.get("keys", []))
printable = _key_combo_printable_char(keys)
if not printable:
# AltGr seul (AZERTY) : le caractère est dans les raw_keys
# du prochain text_input. Extraire depuis les raw_keys de cet event.
raw_keys = evt.get("raw_keys", [])
for rk in raw_keys:
ch = rk.get("char", "")
if ch and len(ch) == 1 and ch.isprintable() and rk.get("action") == "release":
printable = ch
break
if printable:
# Transformer en text_input pour fusion
evt = dict(evt, type="text_input", text=printable)
@@ -1022,10 +1031,14 @@ def build_replay_from_raw_events(
)
elif reconstructed and len(reconstructed) < len(original):
# Longueur différente → des chars viennent de key_combos convertis
# Garder le texte original (qui inclut les chars fusionnés)
# (ex: @ de AltGr fusionné dans le texte mais absent des raw_keys)
# Garder le texte original ET supprimer raw_keys pour forcer le
# copier-coller au replay (les raw_keys sont incomplets)
del evt["raw_keys"]
logger.debug(
"Texte non reconstruit (longueur diff) : '%s' (%d) vs '%s' (%d)",
original[:50], len(original), reconstructed[:50], len(reconstructed),
"Texte corrigé (key_combo fusionné) : '%s' → raw_keys supprimé, "
"replay par copier-coller",
original[:50],
)
# ── 4. Convertir en actions replay normalisées ──
@@ -1168,8 +1181,18 @@ def build_replay_from_raw_events(
action["type"] = "type"
action["text"] = text
# Propager les raw_keys pour le replay exact (solution AZERTY)
# SAUF si le texte contient des chars fusionnés depuis key_combos
# (ex: @ de AltGr) — dans ce cas les raw_keys sont incomplets
# et le replay doit utiliser le copier-coller
if evt.get("raw_keys"):
action["raw_keys"] = evt["raw_keys"]
reconstructed = _reconstruct_text_from_raw_keys(evt["raw_keys"])
if len(reconstructed) >= len(text):
action["raw_keys"] = evt["raw_keys"]
else:
logger.debug(
"raw_keys incomplets pour '%s' (recon=%d < text=%d) → copier-coller",
text[:30], len(reconstructed), len(text),
)
elif evt_type in ("key_press", "key_combo"):
keys = evt.get("keys", [])
@@ -1235,6 +1258,19 @@ def build_replay_from_raw_events(
if session_dir_path:
_attach_expected_screenshots(result, events, session_dir_path)
# ── 9. Supprimer raw_keys incomplets (chars fusionnés depuis key_combos) ──
# Quand le texte contient des caractères venant de key_combos convertis
# (ex: @ de AltGr), les raw_keys ne les couvrent pas. Forcer le copier-coller.
for action in result:
if action.get("type") == "type" and action.get("raw_keys"):
recon = _reconstruct_text_from_raw_keys(action["raw_keys"])
if len(recon) < len(action.get("text", "")):
del action["raw_keys"]
logger.debug(
"raw_keys supprimés pour '%s' (recon=%d < text=%d)",
action["text"][:30], len(recon), len(action["text"]),
)
# Stats visual replay
visual_clicks = sum(
1 for a in result