Refonte majeure du système Agent Chat et ajout de nombreux modules : - Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat avec résolution en 3 niveaux (workflow → geste → "montre-moi") - GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique, substitution automatique dans les replays, et endpoint /api/gestures - Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket (approve/skip/abort) avant chaque action - Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent pour feedback visuel pendant le replay - Data Extraction (core/extraction/) : moteur d'extraction visuelle de données (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel - ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison de screenshots, avec logique de retry (max 3) - IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés - Dashboard : nouvelles pages gestures, streaming, extractions - Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants - Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410, suppression du code hardcodé _plan_to_replay_actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
645 lines
23 KiB
Python
645 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
RPA Vision V3 - Catalogue de Primitives Gestuelles
|
||
|
||
Bibliothèque de gestes universels Windows (raccourcis clavier) que le système
|
||
connaît nativement, sans apprentissage visuel.
|
||
|
||
Trois usages :
|
||
1. Chat : l'utilisateur demande "ferme la fenêtre" → match direct → exécution
|
||
2. Replay : une action enregistrée correspond à un geste connu → substitution
|
||
automatique par le raccourci clavier (plus fiable que le clic visuel)
|
||
3. Workflows : enrichissement automatique des workflows avec les primitives
|
||
|
||
Auteur: Dom — Mars 2026
|
||
"""
|
||
|
||
import logging
|
||
import re
|
||
import uuid
|
||
from dataclasses import dataclass, field
|
||
from difflib import SequenceMatcher
|
||
from typing import Dict, List, Optional, Tuple
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class Gesture:
|
||
"""Un geste primitif universel."""
|
||
id: str
|
||
name: str
|
||
description: str
|
||
keys: List[str] # Ex: ["alt", "f4"], ["ctrl", "t"]
|
||
aliases: List[str] = field(default_factory=list) # Termes alternatifs
|
||
tags: List[str] = field(default_factory=list)
|
||
context: str = "windows" # "windows", "chrome", "explorer", etc.
|
||
category: str = "window" # "window", "navigation", "editing", "system"
|
||
|
||
def to_replay_action(self) -> Dict:
|
||
"""Convertir en action de replay pour l'Agent V1."""
|
||
return {
|
||
"action_id": f"gesture_{self.id}_{uuid.uuid4().hex[:6]}",
|
||
"type": "key_combo",
|
||
"keys": self.keys,
|
||
"gesture_id": self.id,
|
||
"gesture_name": self.name,
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# Catalogue des primitives
|
||
# =============================================================================
|
||
|
||
GESTURES: List[Gesture] = [
|
||
# --- Gestion de fenêtres ---
|
||
Gesture(
|
||
id="win_close", name="Fermer la fenêtre",
|
||
description="Fermer la fenêtre active",
|
||
keys=["alt", "f4"],
|
||
aliases=["fermer", "close", "quitter la fenêtre", "fermer l'application",
|
||
"fermer le programme", "close window"],
|
||
tags=["fenêtre", "fermer", "close"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_maximize", name="Agrandir la fenêtre",
|
||
description="Agrandir la fenêtre au maximum",
|
||
keys=["super", "up"],
|
||
aliases=["agrandir", "maximize", "plein écran", "maximiser",
|
||
"fullscreen", "agrandir la fenêtre"],
|
||
tags=["fenêtre", "agrandir", "maximize"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_minimize", name="Réduire la fenêtre",
|
||
description="Réduire la fenêtre dans la barre des tâches",
|
||
keys=["super", "down"],
|
||
aliases=["réduire", "minimize", "minimiser", "réduire la fenêtre",
|
||
"mettre en bas"],
|
||
tags=["fenêtre", "réduire", "minimize"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_minimize_all", name="Afficher le bureau",
|
||
description="Réduire toutes les fenêtres (afficher le bureau)",
|
||
keys=["super", "d"],
|
||
aliases=["bureau", "desktop", "afficher le bureau", "tout réduire",
|
||
"montrer le bureau", "show desktop"],
|
||
tags=["bureau", "desktop", "minimize all"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_switch", name="Basculer entre fenêtres",
|
||
description="Basculer vers la fenêtre suivante",
|
||
keys=["alt", "tab"],
|
||
aliases=["basculer", "switch", "changer de fenêtre",
|
||
"fenêtre suivante", "alt tab"],
|
||
tags=["fenêtre", "basculer", "switch"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_snap_left", name="Fenêtre à gauche",
|
||
description="Ancrer la fenêtre à gauche de l'écran",
|
||
keys=["super", "left"],
|
||
aliases=["fenêtre à gauche", "snap left", "ancrer à gauche",
|
||
"moitié gauche"],
|
||
tags=["fenêtre", "snap", "gauche"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_snap_right", name="Fenêtre à droite",
|
||
description="Ancrer la fenêtre à droite de l'écran",
|
||
keys=["super", "right"],
|
||
aliases=["fenêtre à droite", "snap right", "ancrer à droite",
|
||
"moitié droite"],
|
||
tags=["fenêtre", "snap", "droite"],
|
||
category="window",
|
||
),
|
||
Gesture(
|
||
id="win_restore", name="Restaurer la fenêtre",
|
||
description="Restaurer la taille normale de la fenêtre",
|
||
keys=["super", "down"],
|
||
aliases=["restaurer", "restore", "taille normale",
|
||
"fenêtre normale"],
|
||
tags=["fenêtre", "restaurer", "restore"],
|
||
category="window",
|
||
),
|
||
|
||
# --- Navigation Chrome / navigateur ---
|
||
Gesture(
|
||
id="chrome_new_tab", name="Nouvel onglet",
|
||
description="Ouvrir un nouvel onglet dans le navigateur",
|
||
keys=["ctrl", "t"],
|
||
aliases=["nouvel onglet", "new tab", "ouvrir un onglet",
|
||
"ajouter un onglet", "nouveau tab"],
|
||
tags=["chrome", "onglet", "tab", "nouveau"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_close_tab", name="Fermer l'onglet",
|
||
description="Fermer l'onglet actif du navigateur",
|
||
keys=["ctrl", "w"],
|
||
aliases=["fermer l'onglet", "close tab", "fermer le tab",
|
||
"fermer cet onglet"],
|
||
tags=["chrome", "onglet", "fermer"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_next_tab", name="Onglet suivant",
|
||
description="Passer à l'onglet suivant",
|
||
keys=["ctrl", "tab"],
|
||
aliases=["onglet suivant", "next tab", "tab suivant",
|
||
"prochain onglet"],
|
||
tags=["chrome", "onglet", "suivant"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_prev_tab", name="Onglet précédent",
|
||
description="Passer à l'onglet précédent",
|
||
keys=["ctrl", "shift", "tab"],
|
||
aliases=["onglet précédent", "previous tab", "tab précédent",
|
||
"onglet d'avant"],
|
||
tags=["chrome", "onglet", "précédent"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_reopen_tab", name="Rouvrir le dernier onglet",
|
||
description="Rouvrir le dernier onglet fermé",
|
||
keys=["ctrl", "shift", "t"],
|
||
aliases=["rouvrir l'onglet", "reopen tab", "onglet fermé",
|
||
"restaurer l'onglet"],
|
||
tags=["chrome", "onglet", "rouvrir"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_address_bar", name="Barre d'adresse",
|
||
description="Sélectionner la barre d'adresse du navigateur",
|
||
keys=["ctrl", "l"],
|
||
aliases=["barre d'adresse", "address bar", "url bar",
|
||
"aller à l'adresse", "sélectionner l'url"],
|
||
tags=["chrome", "url", "adresse"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_refresh", name="Rafraîchir la page",
|
||
description="Recharger la page web actuelle",
|
||
keys=["f5"],
|
||
aliases=["rafraîchir", "refresh", "recharger", "actualiser",
|
||
"reload"],
|
||
tags=["chrome", "rafraîchir", "reload"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_back", name="Page précédente",
|
||
description="Retourner à la page précédente",
|
||
keys=["alt", "left"],
|
||
aliases=["retour", "back", "page précédente", "revenir en arrière",
|
||
"page d'avant"],
|
||
tags=["chrome", "retour", "back"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_forward", name="Page suivante",
|
||
description="Aller à la page suivante",
|
||
keys=["alt", "right"],
|
||
aliases=["avancer", "forward", "page suivante"],
|
||
tags=["chrome", "avancer", "forward"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_find", name="Rechercher dans la page",
|
||
description="Ouvrir la barre de recherche dans la page",
|
||
keys=["ctrl", "f"],
|
||
aliases=["rechercher", "find", "chercher dans la page", "ctrl f",
|
||
"trouver"],
|
||
tags=["chrome", "rechercher", "find"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
Gesture(
|
||
id="chrome_new_window", name="Nouvelle fenêtre",
|
||
description="Ouvrir une nouvelle fenêtre de navigateur",
|
||
keys=["ctrl", "n"],
|
||
aliases=["nouvelle fenêtre", "new window", "ouvrir une fenêtre"],
|
||
tags=["chrome", "fenêtre", "nouveau"],
|
||
context="chrome",
|
||
category="navigation",
|
||
),
|
||
|
||
# --- Édition / presse-papier ---
|
||
Gesture(
|
||
id="edit_copy", name="Copier",
|
||
description="Copier la sélection dans le presse-papier",
|
||
keys=["ctrl", "c"],
|
||
aliases=["copier", "copy", "ctrl c"],
|
||
tags=["édition", "copier", "presse-papier"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="edit_paste", name="Coller",
|
||
description="Coller le contenu du presse-papier",
|
||
keys=["ctrl", "v"],
|
||
aliases=["coller", "paste", "ctrl v"],
|
||
tags=["édition", "coller", "presse-papier"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="edit_cut", name="Couper",
|
||
description="Couper la sélection",
|
||
keys=["ctrl", "x"],
|
||
aliases=["couper", "cut", "ctrl x"],
|
||
tags=["édition", "couper"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="edit_undo", name="Annuler",
|
||
description="Annuler la dernière action",
|
||
keys=["ctrl", "z"],
|
||
aliases=["annuler", "undo", "défaire", "ctrl z"],
|
||
tags=["édition", "annuler", "undo"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="edit_redo", name="Rétablir",
|
||
description="Rétablir l'action annulée",
|
||
keys=["ctrl", "y"],
|
||
aliases=["rétablir", "redo", "refaire", "ctrl y"],
|
||
tags=["édition", "rétablir", "redo"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="edit_select_all", name="Tout sélectionner",
|
||
description="Sélectionner tout le contenu",
|
||
keys=["ctrl", "a"],
|
||
aliases=["tout sélectionner", "select all", "sélectionner tout",
|
||
"ctrl a"],
|
||
tags=["édition", "sélection", "tout"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="edit_save", name="Enregistrer",
|
||
description="Enregistrer le document/fichier actuel",
|
||
keys=["ctrl", "s"],
|
||
aliases=["enregistrer", "save", "sauvegarder", "ctrl s"],
|
||
tags=["édition", "enregistrer", "save"],
|
||
category="editing",
|
||
),
|
||
|
||
# --- Système ---
|
||
Gesture(
|
||
id="sys_start_menu", name="Menu Démarrer",
|
||
description="Ouvrir le menu Démarrer Windows",
|
||
keys=["super"],
|
||
aliases=["menu démarrer", "start menu", "démarrer", "windows",
|
||
"touche windows"],
|
||
tags=["système", "démarrer", "menu"],
|
||
category="system",
|
||
),
|
||
Gesture(
|
||
id="sys_task_manager", name="Gestionnaire des tâches",
|
||
description="Ouvrir le gestionnaire des tâches",
|
||
keys=["ctrl", "shift", "escape"],
|
||
aliases=["gestionnaire des tâches", "task manager",
|
||
"gestionnaire tâches", "processes"],
|
||
tags=["système", "tâches", "processus"],
|
||
category="system",
|
||
),
|
||
Gesture(
|
||
id="sys_lock", name="Verrouiller le PC",
|
||
description="Verrouiller la session Windows",
|
||
keys=["super", "l"],
|
||
aliases=["verrouiller", "lock", "verrouiller le pc",
|
||
"verrouiller la session"],
|
||
tags=["système", "verrouiller", "lock"],
|
||
category="system",
|
||
),
|
||
Gesture(
|
||
id="sys_screenshot", name="Capture d'écran",
|
||
description="Prendre une capture d'écran",
|
||
keys=["super", "shift", "s"],
|
||
aliases=["capture d'écran", "screenshot", "capture écran",
|
||
"impr écran"],
|
||
tags=["système", "capture", "screenshot"],
|
||
category="system",
|
||
),
|
||
Gesture(
|
||
id="sys_explorer", name="Ouvrir l'explorateur",
|
||
description="Ouvrir l'explorateur de fichiers Windows",
|
||
keys=["super", "e"],
|
||
aliases=["explorateur", "explorer", "ouvrir l'explorateur",
|
||
"mes fichiers", "file explorer", "explorateur de fichiers"],
|
||
tags=["système", "explorateur"],
|
||
category="system",
|
||
),
|
||
Gesture(
|
||
id="sys_run", name="Exécuter (Run)",
|
||
description="Ouvrir la boîte de dialogue Exécuter",
|
||
keys=["super", "r"],
|
||
aliases=["exécuter", "run", "boîte exécuter"],
|
||
tags=["système", "exécuter", "run"],
|
||
category="system",
|
||
),
|
||
Gesture(
|
||
id="sys_settings", name="Paramètres Windows",
|
||
description="Ouvrir les paramètres Windows",
|
||
keys=["super", "i"],
|
||
aliases=["paramètres", "settings", "réglages",
|
||
"paramètres windows"],
|
||
tags=["système", "paramètres", "settings"],
|
||
category="system",
|
||
),
|
||
|
||
# --- Navigation texte ---
|
||
Gesture(
|
||
id="nav_home", name="Début de ligne",
|
||
description="Aller au début de la ligne",
|
||
keys=["home"],
|
||
aliases=["début de ligne", "home", "début"],
|
||
tags=["navigation", "texte", "début"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="nav_end", name="Fin de ligne",
|
||
description="Aller à la fin de la ligne",
|
||
keys=["end"],
|
||
aliases=["fin de ligne", "end", "fin"],
|
||
tags=["navigation", "texte", "fin"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="nav_enter", name="Valider / Entrée",
|
||
description="Appuyer sur Entrée",
|
||
keys=["enter"],
|
||
aliases=["entrée", "enter", "valider", "confirmer", "ok"],
|
||
tags=["navigation", "entrée", "valider"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="nav_escape", name="Échap / Annuler",
|
||
description="Appuyer sur Échap (fermer popup, annuler)",
|
||
keys=["escape"],
|
||
aliases=["échap", "escape", "esc", "annuler", "fermer le popup",
|
||
"fermer la popup", "fermer le dialogue"],
|
||
tags=["navigation", "échap", "annuler", "popup"],
|
||
category="editing",
|
||
),
|
||
Gesture(
|
||
id="nav_tab", name="Champ suivant",
|
||
description="Passer au champ suivant (Tab)",
|
||
keys=["tab"],
|
||
aliases=["tab", "champ suivant", "suivant", "prochain champ",
|
||
"tabulation"],
|
||
tags=["navigation", "tab", "champ"],
|
||
category="editing",
|
||
),
|
||
]
|
||
|
||
|
||
class GestureCatalog:
|
||
"""
|
||
Catalogue de gestes primitifs avec matching sémantique.
|
||
|
||
Utilisé par :
|
||
- Le chat (match direct quand l'utilisateur demande un geste)
|
||
- Le replay (substitution automatique d'actions enregistrées)
|
||
"""
|
||
|
||
def __init__(self, gestures: List[Gesture] = None):
|
||
self.gestures = gestures or GESTURES
|
||
# Index pour recherche rapide
|
||
self._by_id: Dict[str, Gesture] = {g.id: g for g in self.gestures}
|
||
# Pré-calculer les termes de recherche normalisés
|
||
self._search_index: List[Tuple[Gesture, List[str]]] = []
|
||
for g in self.gestures:
|
||
terms = [g.name.lower(), g.description.lower()]
|
||
terms.extend(a.lower() for a in g.aliases)
|
||
terms.extend(t.lower() for t in g.tags)
|
||
self._search_index.append((g, terms))
|
||
|
||
logger.info(f"GestureCatalog: {len(self.gestures)} primitives chargées")
|
||
|
||
def match(self, query: str, min_score: float = 0.45) -> Optional[Tuple[Gesture, float]]:
|
||
"""
|
||
Trouver le geste le plus proche d'une requête textuelle.
|
||
|
||
Returns:
|
||
(Gesture, score) si match trouvé, None sinon.
|
||
"""
|
||
query_lower = query.lower().strip()
|
||
if not query_lower:
|
||
return None
|
||
|
||
best_gesture = None
|
||
best_score = 0.0
|
||
|
||
for gesture, terms in self._search_index:
|
||
score = self._compute_score(query_lower, terms, gesture)
|
||
if score > best_score:
|
||
best_score = score
|
||
best_gesture = gesture
|
||
|
||
if best_gesture and best_score >= min_score:
|
||
logger.debug(f"Gesture match: '{query}' → {best_gesture.id} (score={best_score:.2f})")
|
||
return (best_gesture, best_score)
|
||
|
||
return None
|
||
|
||
def match_action(self, action: Dict) -> Optional[Gesture]:
|
||
"""
|
||
Détecter si une action de workflow correspond à un geste primitif.
|
||
|
||
Utilisé pendant le replay pour auto-substituer les actions visuelles
|
||
par des raccourcis clavier plus fiables.
|
||
|
||
Patterns détectés :
|
||
- Clic sur boutons de contrôle fenêtre (X, □, ─)
|
||
- key_combo qui matche déjà un geste
|
||
- Actions avec target_text contenant des mots-clés de geste
|
||
"""
|
||
action_type = action.get("type", "")
|
||
|
||
# key_combo → vérifier si c'est déjà un geste connu
|
||
if action_type == "key_combo":
|
||
keys = action.get("keys", [])
|
||
return self._match_by_keys(keys)
|
||
|
||
# Clic sur un bouton de contrôle de fenêtre
|
||
if action_type == "click":
|
||
return self._match_click_as_gesture(action)
|
||
|
||
return None
|
||
|
||
def get_by_id(self, gesture_id: str) -> Optional[Gesture]:
|
||
return self._by_id.get(gesture_id)
|
||
|
||
def get_by_category(self, category: str) -> List[Gesture]:
|
||
return [g for g in self.gestures if g.category == category]
|
||
|
||
def get_by_context(self, context: str) -> List[Gesture]:
|
||
"""Gestes applicables à un contexte (inclut toujours 'windows')."""
|
||
return [
|
||
g for g in self.gestures
|
||
if g.context == context or g.context == "windows"
|
||
]
|
||
|
||
def list_all(self) -> List[Dict]:
|
||
"""Lister tous les gestes pour l'affichage."""
|
||
return [
|
||
{
|
||
"id": g.id,
|
||
"name": g.name,
|
||
"description": g.description,
|
||
"keys": "+".join(g.keys),
|
||
"category": g.category,
|
||
"context": g.context,
|
||
}
|
||
for g in self.gestures
|
||
]
|
||
|
||
# =========================================================================
|
||
# Scoring interne
|
||
# =========================================================================
|
||
|
||
def _compute_score(self, query: str, terms: List[str], gesture: Gesture) -> float:
|
||
"""Calculer le score de correspondance entre une requête et un geste."""
|
||
best = 0.0
|
||
query_words = set(query.split())
|
||
|
||
for term in terms:
|
||
# Match exact
|
||
if query == term:
|
||
return 1.0
|
||
|
||
# Contenu dans l'un ou l'autre sens
|
||
if query in term:
|
||
score = len(query) / len(term) * 0.95
|
||
best = max(best, score)
|
||
continue
|
||
if term in query:
|
||
# Si le terme est un alias exact (mot unique) présent dans la requête
|
||
# c'est un signal très fort : "copier le texte" contient "copier"
|
||
if term in query_words:
|
||
best = max(best, 0.85)
|
||
else:
|
||
score = len(term) / len(query) * 0.9
|
||
best = max(best, score)
|
||
continue
|
||
|
||
# Similarité de séquence
|
||
ratio = SequenceMatcher(None, query, term).ratio()
|
||
best = max(best, ratio)
|
||
|
||
# Bonus si tous les mots de la requête sont présents dans les termes
|
||
all_terms_text = " ".join(terms)
|
||
matched_words = sum(1 for w in query_words if w in all_terms_text)
|
||
if query_words:
|
||
word_ratio = matched_words / len(query_words)
|
||
if word_ratio >= 0.8:
|
||
best = max(best, 0.5 + word_ratio * 0.4)
|
||
|
||
return best
|
||
|
||
def _match_by_keys(self, keys: List[str]) -> Optional[Gesture]:
|
||
"""Trouver un geste par sa combinaison de touches exacte."""
|
||
keys_normalized = [k.lower() for k in keys]
|
||
for gesture in self.gestures:
|
||
if gesture.keys == keys_normalized:
|
||
return gesture
|
||
return None
|
||
|
||
def _match_click_as_gesture(self, action: Dict) -> Optional[Gesture]:
|
||
"""
|
||
Détecter si un clic correspond à un geste primitif.
|
||
|
||
Patterns :
|
||
- Clic en haut à droite de la fenêtre (x > 95%, y < 5%) → fermer
|
||
- target_text contenant ✕, ×, X, □, ─, etc.
|
||
"""
|
||
# Vérifier le target_text
|
||
target_text = (
|
||
action.get("target_text", "") or
|
||
action.get("target_spec", {}).get("by_text", "")
|
||
).strip()
|
||
|
||
if target_text:
|
||
target_lower = target_text.lower()
|
||
# Bouton fermer
|
||
if target_lower in ("✕", "×", "x", "close", "fermer"):
|
||
return self._by_id.get("win_close")
|
||
# Bouton maximiser
|
||
if target_lower in ("□", "☐", "maximize", "agrandir"):
|
||
return self._by_id.get("win_maximize")
|
||
# Bouton minimiser
|
||
if target_lower in ("─", "—", "_", "minimize", "réduire"):
|
||
return self._by_id.get("win_minimize")
|
||
|
||
# Vérifier la position relative (coin haut-droite = fermer)
|
||
x_pct = action.get("x_pct", 0)
|
||
y_pct = action.get("y_pct", 0)
|
||
|
||
if x_pct > 0.96 and y_pct < 0.04:
|
||
return self._by_id.get("win_close")
|
||
if 0.92 < x_pct < 0.96 and y_pct < 0.04:
|
||
return self._by_id.get("win_maximize")
|
||
if 0.88 < x_pct < 0.92 and y_pct < 0.04:
|
||
return self._by_id.get("win_minimize")
|
||
|
||
return None
|
||
|
||
def optimize_replay_actions(self, actions: List[Dict]) -> List[Dict]:
|
||
"""
|
||
Optimiser une liste d'actions de replay en substituant les gestes connus.
|
||
|
||
Pour chaque action, si elle correspond à un geste primitif,
|
||
on la remplace par le raccourci clavier équivalent.
|
||
|
||
Retourne la liste d'actions optimisée (les originales non-matchées
|
||
sont conservées telles quelles).
|
||
"""
|
||
optimized = []
|
||
substitutions = 0
|
||
|
||
for action in actions:
|
||
gesture = self.match_action(action)
|
||
if gesture and action.get("type") != "key_combo":
|
||
# Substituer par le raccourci clavier
|
||
new_action = gesture.to_replay_action()
|
||
# Conserver l'action_id original pour le tracking
|
||
new_action["action_id"] = action.get("action_id", new_action["action_id"])
|
||
new_action["original_type"] = action.get("type")
|
||
optimized.append(new_action)
|
||
substitutions += 1
|
||
logger.debug(
|
||
f"Geste substitué: {action.get('type')} → {gesture.id} ({gesture.name})"
|
||
)
|
||
else:
|
||
optimized.append(action)
|
||
|
||
if substitutions:
|
||
logger.info(
|
||
f"Replay optimisé: {substitutions} action(s) substituée(s) par des primitives"
|
||
)
|
||
|
||
return optimized
|
||
|
||
|
||
# Singleton
|
||
_catalog: Optional[GestureCatalog] = None
|
||
|
||
|
||
def get_gesture_catalog() -> GestureCatalog:
|
||
global _catalog
|
||
if _catalog is None:
|
||
_catalog = GestureCatalog()
|
||
return _catalog
|