""" core/grounding/dialog_handler.py — Gestion intelligente des dialogues Quand un dialogue inattendu apparaît (pHash change après une action) : 1. Lire le titre de la fenêtre (EasyOCR crop 45px, ~130ms) 2. Si titre connu (Enregistrer sous, Confirmer, etc.) → action connue 3. Demander à InfiGUI de cliquer sur le bon bouton (~3s) 4. Vérifier que le dialogue a disparu (pHash) Pas de patterns prédéfinis pour les boutons. InfiGUI comprend visuellement le dialogue et clique au bon endroit. Utilisation : from core.grounding.dialog_handler import DialogHandler handler = DialogHandler() result = handler.handle_if_dialog(screenshot_pil) if result['handled']: print(f"Dialogue '{result['title']}' géré → {result['action']}") """ from __future__ import annotations import time from typing import Any, Dict, Optional # Titres connus → quelle action demander à InfiGUI 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"}, "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à"}, "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"}, } class DialogHandler: """Gestion intelligente des dialogues via titre + InfiGUI.""" GROUNDING_URL = "http://localhost:8200" def __init__(self): self._easyocr_reader = None def handle_if_dialog( self, screenshot_pil, previous_title: str = "", ) -> Dict[str, Any]: """Vérifie si l'écran montre un dialogue et le gère. Args: screenshot_pil: Screenshot PIL actuel. previous_title: Titre de la fenêtre avant l'action (pour comparaison). Returns: Dict avec 'handled' (bool), 'title', 'action', 'position'. """ t0 = time.time() # 1. Lire le titre de la fenêtre title = self._read_title(screenshot_pil) if not title or len(title) < 3: return {'handled': False, 'title': '', 'reason': 'Titre illisible'} print(f"🔍 [Dialog] Titre lu: '{title}'") # 2. Chercher si c'est un dialogue connu matched_dialog = None for key, action_info in KNOWN_DIALOGS.items(): if key in title.lower(): matched_dialog = (key, action_info) break if not matched_dialog: # Pas un dialogue connu — le workflow continue normalement return {'handled': False, 'title': title, 'reason': 'Pas un dialogue connu'} dialog_key, action_info = matched_dialog target = action_info['target'] description = action_info['description'] print(f"🧠 [Dialog] Dialogue détecté: '{dialog_key}' → clic '{target}'") # 3. Demander à InfiGUI de cliquer sur le bouton click_result = self._click_via_infigui( target, description, screenshot_pil ) dt = (time.time() - t0) * 1000 if click_result: print(f"✅ [Dialog] Clic '{target}' à ({click_result['x']}, {click_result['y']}) ({dt:.0f}ms)") return { 'handled': True, 'title': title, 'dialog_type': dialog_key, 'action': f"click '{target}'", 'position': (click_result['x'], click_result['y']), 'time_ms': dt, } else: # InfiGUI n'a pas trouvé le bouton — essayer le clic direct via OCR print(f"⚠️ [Dialog] InfiGUI n'a pas trouvé '{target}', essai OCR direct") ocr_result = self._click_via_ocr(target, screenshot_pil) dt = (time.time() - t0) * 1000 if ocr_result: print(f"✅ [Dialog] OCR clic '{target}' à ({ocr_result[0]}, {ocr_result[1]}) ({dt:.0f}ms)") return { 'handled': True, 'title': title, 'dialog_type': dialog_key, 'action': f"click '{target}' (OCR)", 'position': ocr_result, 'time_ms': dt, } print(f"❌ [Dialog] Impossible de cliquer '{target}' ({dt:.0f}ms)") return { 'handled': False, 'title': title, 'dialog_type': dialog_key, 'reason': f"Bouton '{target}' introuvable", 'time_ms': dt, } # ------------------------------------------------------------------ # Lecture titre # ------------------------------------------------------------------ def _read_title(self, screenshot_pil) -> str: """Lit TOUT le texte visible via EasyOCR full-screen (~500ms). En VM QEMU, la barre de titre Windows est à l'intérieur du framebuffer, pas en haut absolu de l'écran. On fait l'OCR full-screen et on cherche les mots-clés des dialogues connus dans le texte complet. """ try: import numpy as np reader = self._get_easyocr() if reader is None: return "" results = reader.readtext(np.array(screenshot_pil)) full_text = ' '.join(r[1] for r in results if r[1].strip()) return full_text except Exception as e: print(f"⚠️ [Dialog] Erreur lecture écran: {e}") return "" # ------------------------------------------------------------------ # Clic via InfiGUI (serveur grounding) # ------------------------------------------------------------------ def _click_via_infigui( self, target: str, description: str, screenshot_pil ) -> Optional[Dict]: """Demande à InfiGUI de localiser et cliquer sur le bouton.""" try: import requests import base64 import io buf = io.BytesIO() screenshot_pil.save(buf, format='JPEG', quality=85) b64 = base64.b64encode(buf.getvalue()).decode() 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 return None except Exception as e: print(f"⚠️ [Dialog/InfiGUI] Erreur: {e}") return None # ------------------------------------------------------------------ # Clic via OCR (fallback rapide) # ------------------------------------------------------------------ def _click_via_ocr(self, target: str, screenshot_pil) -> Optional[tuple]: """Cherche le bouton par OCR et clique dessus.""" try: import numpy as np reader = self._get_easyocr() if reader is None: return None results = reader.readtext(np.array(screenshot_pil)) target_lower = target.lower() matches = [] for (bbox_pts, text, conf) in results: if target_lower in text.lower() or text.lower() in target_lower: x = int(sum(p[0] for p in bbox_pts) / 4) y = int(sum(p[1] for p in bbox_pts) / 4) matches.append((x, y, text)) if matches: # Prendre le match le plus bas (boutons = bas du dialogue) best = max(matches, key=lambda m: m[1]) import pyautogui pyautogui.click(best[0], best[1]) return (best[0], best[1]) return None except Exception as e: print(f"⚠️ [Dialog/OCR] Erreur: {e}") return None # ------------------------------------------------------------------ # EasyOCR singleton # ------------------------------------------------------------------ def _get_easyocr(self): if self._easyocr_reader is not None: return self._easyocr_reader try: import easyocr self._easyocr_reader = easyocr.Reader( ['fr', 'en'], gpu=True, verbose=False ) return self._easyocr_reader except ImportError: return None