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:
@@ -52,8 +52,9 @@ API_TOKEN = os.environ.get("RPA_API_TOKEN", "")
|
|||||||
MAX_SESSION_DURATION_S = 60 * 60 # 1 heure
|
MAX_SESSION_DURATION_S = 60 * 60 # 1 heure
|
||||||
SESSIONS_ROOT = BASE_DIR / "sessions"
|
SESSIONS_ROOT = BASE_DIR / "sessions"
|
||||||
|
|
||||||
# Paramètres Vision (Crops pour qwen3-vl)
|
# Paramètres Vision (Crops pour la résolution visuelle)
|
||||||
TARGETED_CROP_SIZE = (150, 150)
|
# 80x80 : assez petit pour être discriminant (icônes), assez grand pour le contexte
|
||||||
|
TARGETED_CROP_SIZE = (80, 80)
|
||||||
SCREENSHOT_QUALITY = 85
|
SCREENSHOT_QUALITY = 85
|
||||||
|
|
||||||
# Floutage des données sensibles (conformité AI Act)
|
# Floutage des données sensibles (conformité AI Act)
|
||||||
|
|||||||
@@ -750,8 +750,8 @@ def _load_crop_for_event(
|
|||||||
pos = event_data.get("pos", [])
|
pos = event_data.get("pos", [])
|
||||||
if pos and len(pos) == 2:
|
if pos and len(pos) == 2:
|
||||||
cx, cy = int(pos[0]), int(pos[1])
|
cx, cy = int(pos[0]), int(pos[1])
|
||||||
# Crop 150x150 centré sur le clic (plus discriminant, moins de bruit)
|
# Crop 80x80 centré sur le clic (discriminant pour icônes)
|
||||||
crop_size = 75
|
crop_size = 40
|
||||||
x1 = max(0, cx - crop_size)
|
x1 = max(0, cx - crop_size)
|
||||||
y1 = max(0, cy - crop_size)
|
y1 = max(0, cy - crop_size)
|
||||||
x2 = min(img.width, cx + 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"):
|
if evt_type in ("key_combo", "key_press"):
|
||||||
keys = _sanitize_keys(evt.get("keys", []))
|
keys = _sanitize_keys(evt.get("keys", []))
|
||||||
printable = _key_combo_printable_char(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:
|
if printable:
|
||||||
# Transformer en text_input pour fusion
|
# Transformer en text_input pour fusion
|
||||||
evt = dict(evt, type="text_input", text=printable)
|
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):
|
elif reconstructed and len(reconstructed) < len(original):
|
||||||
# Longueur différente → des chars viennent de key_combos convertis
|
# 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(
|
logger.debug(
|
||||||
"Texte non reconstruit (longueur diff) : '%s' (%d) vs '%s' (%d)",
|
"Texte corrigé (key_combo fusionné) : '%s' → raw_keys supprimé, "
|
||||||
original[:50], len(original), reconstructed[:50], len(reconstructed),
|
"replay par copier-coller",
|
||||||
|
original[:50],
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 4. Convertir en actions replay normalisées ──
|
# ── 4. Convertir en actions replay normalisées ──
|
||||||
@@ -1168,8 +1181,18 @@ def build_replay_from_raw_events(
|
|||||||
action["type"] = "type"
|
action["type"] = "type"
|
||||||
action["text"] = text
|
action["text"] = text
|
||||||
# Propager les raw_keys pour le replay exact (solution AZERTY)
|
# 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"):
|
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"):
|
elif evt_type in ("key_press", "key_combo"):
|
||||||
keys = evt.get("keys", [])
|
keys = evt.get("keys", [])
|
||||||
@@ -1235,6 +1258,19 @@ def build_replay_from_raw_events(
|
|||||||
if session_dir_path:
|
if session_dir_path:
|
||||||
_attach_expected_screenshots(result, events, 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
|
# Stats visual replay
|
||||||
visual_clicks = sum(
|
visual_clicks = sum(
|
||||||
1 for a in result
|
1 for a in result
|
||||||
|
|||||||
Reference in New Issue
Block a user