feat(execution): cascade post-raccourci pilotée par DialogHandler/OCR

Le pHash global 8x8 sur écran 1920x1080 ne détecte pas l'ouverture d'un
dialog modal dans une VM QEMU (un dialog 800x500 couvre ~3 pixels pHash,
distance Hamming typique = 1-2, sous le seuil de 3). Découvert sur Win11/
Notepad : Ctrl+Shift+S ouvrait bien le dialog mais Léa abortait à tort.

_handle_post_shortcut() poll désormais DialogHandler.handle_if_dialog()
toutes les 500ms (EasyOCR + KNOWN_DIALOGS). 8s pour le premier dialog,
3s de stabilité entre dialogs successifs, 60s budget total.

KNOWN_DIALOGS réordonné : popups modaux (confirmer/remplacer/écraser)
prioritaires sur fenêtres parents (enregistrer sous/save as) car l'OCR
full-screen capte les deux simultanément.

DialogHandler bascule sur UITarsGrounder subprocess one-shot (au lieu
du serveur HTTP localhost:8200 qui n'existait plus). InfiGUI worker,
think_arbiter et ui_tars_grounder alignés sur le même contrat.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-26 20:19:39 +02:00
parent 3d6868f029
commit 487bcb8618
6 changed files with 474 additions and 243 deletions

View File

@@ -25,31 +25,42 @@ import time
from typing import Any, Dict, Optional
# Titres connus → quelle action demander à InfiGUI
# Titres connus → quelle action demander à InfiGUI.
#
# IMPORTANT — ordre du dict = priorité de matching.
# L'OCR est full-screen et capte souvent le texte du dialog parent ET du popup
# modal qui apparaît par-dessus (ex: "Enregistrer sous" reste visible derrière
# "Confirmer l'enregistrement"). Les popups modaux DOIVENT matcher avant les
# fenêtres principales, sinon Léa clique sur le bouton du parent qui n'a pas
# le focus.
KNOWN_DIALOGS = {
"enregistrer sous": {"target": "Enregistrer", "description": "Clique sur le bouton Enregistrer dans le dialogue Enregistrer sous"},
"save as": {"target": "Save", "description": "Click the Save button in the Save As dialog"},
"confirmer": {"target": "Oui", "description": "Clique sur le bouton Oui dans le dialogue de confirmation"},
# ── Popups modaux de confirmation (priorité HAUTE) ──────────────────
"voulez-vous le remplacer": {"target": "Oui", "description": "Clique sur Oui pour confirmer le remplacement du fichier"},
"do you want to replace": {"target": "Yes", "description": "Click Yes to confirm file replacement"},
"existe déjà": {"target": "Oui", "description": "Clique sur Oui, le fichier existe déjà et doit être remplacé"},
"already exists": {"target": "Yes", "description": "Click Yes, the file already exists"},
"remplacer": {"target": "Oui", "description": "Clique sur le bouton Oui pour confirmer le remplacement du fichier"},
"replace": {"target": "Yes", "description": "Click Yes to confirm file replacement"},
"voulez-vous enregistrer": {"target": "Enregistrer", "description": "Clique sur Enregistrer pour sauvegarder les modifications"},
"do you want to save": {"target": "Save", "description": "Click Save to save changes"},
"overwrite": {"target": "Yes", "description": "Click Yes to overwrite"},
"écraser": {"target": "Oui", "description": "Clique sur Oui pour écraser le fichier"},
"already exists": {"target": "Yes", "description": "Click Yes, the file already exists"},
"existe déjà": {"target": "Oui", "description": "Clique sur Oui, le fichier existe déjà"},
"overwrite": {"target": "Yes", "description": "Click Yes to overwrite"},
"confirmer l'enregistrement": {"target": "Oui", "description": "Clique sur Oui dans le popup de confirmation d'enregistrement"},
"confirmer": {"target": "Oui", "description": "Clique sur le bouton Oui dans le dialogue de confirmation"},
# ── Avertissements/erreurs (priorité haute, 1 seul bouton OK) ───────
"erreur": {"target": "OK", "description": "Clique sur OK pour fermer le message d'erreur"},
"error": {"target": "OK", "description": "Click OK to close the error message"},
"avertissement": {"target": "OK", "description": "Clique sur OK pour fermer l'avertissement"},
"warning": {"target": "OK", "description": "Click OK to close the warning"},
# ── Dialogs principaux de sauvegarde (priorité BASSE — fenêtres parents) ─
"voulez-vous enregistrer": {"target": "Enregistrer", "description": "Clique sur Enregistrer pour sauvegarder les modifications"},
"do you want to save": {"target": "Save", "description": "Click Save to save changes"},
"enregistrer sous": {"target": "Enregistrer", "description": "Clique sur le bouton Enregistrer dans le dialogue Enregistrer sous"},
"save as": {"target": "Save", "description": "Click the Save button in the Save As dialog"},
}
class DialogHandler:
"""Gestion intelligente des dialogues via titre + InfiGUI."""
GROUNDING_URL = "http://localhost:8200"
def __init__(self):
self._easyocr_reader = None
@@ -169,29 +180,21 @@ class DialogHandler:
def _click_via_infigui(
self, target: str, description: str, screenshot_pil
) -> Optional[Dict]:
"""Demande à InfiGUI de localiser et cliquer sur le bouton."""
"""Demande à InfiGUI (subprocess one-shot) de localiser et cliquer sur le bouton."""
try:
import requests
import base64
import io
from core.grounding.ui_tars_grounder import UITarsGrounder
buf = io.BytesIO()
screenshot_pil.save(buf, format='JPEG', quality=85)
b64 = base64.b64encode(buf.getvalue()).decode()
grounder = UITarsGrounder.get_instance()
result = grounder.ground(
target_text=target,
target_description=description,
screen_pil=screenshot_pil,
)
resp = requests.post(f"{self.GROUNDING_URL}/ground", json={
'target_text': target,
'target_description': description,
'image_b64': b64,
}, timeout=15)
if resp.status_code == 200:
data = resp.json()
if data.get('x') is not None:
# Cliquer
import pyautogui
pyautogui.click(data['x'], data['y'])
return data
if result and result.x is not None:
import pyautogui
pyautogui.click(result.x, result.y)
return {'x': result.x, 'y': result.y}
return None