Files
Geniusia_v2/geniusia2/core/utils/input_utils.py
2026-03-05 00:20:25 +01:00

609 lines
18 KiB
Python

"""
Utilitaires d'entrée pour exécuter des actions UI (souris, clavier, etc.).
Support du clavier AZERTY et gestion du rollback d'actions.
"""
import time
import pyautogui
from typing import Dict, Any, Optional, Tuple
from enum import Enum
from ..logger import Logger
class ActionType(Enum):
"""Types d'actions UI supportées."""
CLICK = "click"
TYPE = "type"
SCROLL = "scroll"
WAIT = "wait"
MOVE = "move"
DRAG = "drag"
class InputUtils:
"""
Gestionnaire d'entrées utilisateur pour exécuter des actions UI.
Support du clavier AZERTY et rollback d'actions.
"""
def __init__(self, logger: Logger, config: Dict[str, Any]):
"""
Initialise les utilitaires d'entrée.
Args:
logger: Logger pour journalisation
config: Configuration globale
"""
self.logger = logger
self.config = config
# Configuration PyAutoGUI
pyautogui.FAILSAFE = True # Déplacer souris dans coin = arrêt
pyautogui.PAUSE = config.get("input", {}).get("pause_between_actions", 0.1)
# Historique des actions pour rollback
self.action_history = []
# Mapping AZERTY pour caractères spéciaux
self.azerty_mapping = {
'0': 'à',
'1': '&',
'2': 'é',
'3': '"',
'4': "'",
'5': '(',
'6': '-',
'7': 'è',
'8': '_',
'9': 'ç',
'.': ':',
'/': '!',
',': ';',
';': ',',
':': '.',
'!': '/',
'?': 'M', # Shift + ,
}
self.logger.log_action({
"action": "input_utils_initialized",
"failsafe": True,
"pause": pyautogui.PAUSE
})
def click(
self,
x: int,
y: int,
button: str = "left",
clicks: int = 1,
interval: float = 0.0
) -> bool:
"""
Effectue un clic souris à la position spécifiée.
Args:
x: Coordonnée X
y: Coordonnée Y
button: Bouton souris ("left", "right", "middle")
clicks: Nombre de clics
interval: Intervalle entre clics multiples
Returns:
True si succès, False sinon
"""
try:
# Enregistrer position actuelle pour rollback
current_pos = pyautogui.position()
# Effectuer le clic
pyautogui.click(x, y, clicks=clicks, interval=interval, button=button)
# Enregistrer dans l'historique
action_record = {
"type": ActionType.CLICK.value,
"x": x,
"y": y,
"button": button,
"clicks": clicks,
"previous_position": current_pos,
"timestamp": time.time()
}
self.action_history.append(action_record)
self.logger.log_action({
"action": "click_executed",
**action_record
})
return True
except Exception as e:
self.logger.log_action({
"action": "click_failed",
"x": x,
"y": y,
"error": str(e)
})
return False
def type_text(
self,
text: str,
interval: float = 0.0,
use_azerty: bool = True
) -> bool:
"""
Saisit du texte au clavier.
Args:
text: Texte à saisir
interval: Intervalle entre chaque caractère
use_azerty: Utiliser le mapping AZERTY
Returns:
True si succès, False sinon
"""
try:
# Convertir pour AZERTY si nécessaire
if use_azerty:
converted_text = self._convert_to_azerty(text)
else:
converted_text = text
# Saisir le texte
pyautogui.write(converted_text, interval=interval)
# Enregistrer dans l'historique
action_record = {
"type": ActionType.TYPE.value,
"text": text,
"converted_text": converted_text,
"length": len(text),
"timestamp": time.time()
}
self.action_history.append(action_record)
self.logger.log_action({
"action": "text_typed",
**action_record
})
return True
except Exception as e:
self.logger.log_action({
"action": "type_text_failed",
"text": text[:50], # Limiter pour logs
"error": str(e)
})
return False
def scroll(
self,
direction: str,
amount: int = 3,
x: Optional[int] = None,
y: Optional[int] = None
) -> bool:
"""
Effectue un défilement.
Args:
direction: Direction ("up", "down", "left", "right")
amount: Quantité de défilement (nombre de "clics" de molette)
x: Position X optionnelle
y: Position Y optionnelle
Returns:
True si succès, False sinon
"""
try:
# Calculer le montant de défilement
if direction in ["up", "right"]:
scroll_amount = amount
elif direction in ["down", "left"]:
scroll_amount = -amount
else:
raise ValueError(f"Direction invalide: {direction}")
# Déplacer la souris si position spécifiée
if x is not None and y is not None:
pyautogui.moveTo(x, y)
# Effectuer le défilement
if direction in ["up", "down"]:
pyautogui.scroll(scroll_amount)
else:
pyautogui.hscroll(scroll_amount)
# Enregistrer dans l'historique
action_record = {
"type": ActionType.SCROLL.value,
"direction": direction,
"amount": amount,
"x": x,
"y": y,
"timestamp": time.time()
}
self.action_history.append(action_record)
self.logger.log_action({
"action": "scroll_executed",
**action_record
})
return True
except Exception as e:
self.logger.log_action({
"action": "scroll_failed",
"direction": direction,
"amount": amount,
"error": str(e)
})
return False
def wait(self, duration: float) -> bool:
"""
Attend pendant une durée spécifiée.
Args:
duration: Durée en secondes
Returns:
True
"""
try:
time.sleep(duration)
action_record = {
"type": ActionType.WAIT.value,
"duration": duration,
"timestamp": time.time()
}
self.action_history.append(action_record)
self.logger.log_action({
"action": "wait_executed",
**action_record
})
return True
except Exception as e:
self.logger.log_action({
"action": "wait_failed",
"duration": duration,
"error": str(e)
})
return False
def move(self, x: int, y: int, duration: float = 0.2) -> bool:
"""
Déplace la souris vers une position.
Args:
x: Coordonnée X
y: Coordonnée Y
duration: Durée du mouvement en secondes
Returns:
True si succès, False sinon
"""
try:
current_pos = pyautogui.position()
pyautogui.moveTo(x, y, duration=duration)
action_record = {
"type": ActionType.MOVE.value,
"x": x,
"y": y,
"previous_position": current_pos,
"duration": duration,
"timestamp": time.time()
}
self.action_history.append(action_record)
self.logger.log_action({
"action": "move_executed",
**action_record
})
return True
except Exception as e:
self.logger.log_action({
"action": "move_failed",
"x": x,
"y": y,
"error": str(e)
})
return False
def drag(
self,
start_x: int,
start_y: int,
end_x: int,
end_y: int,
duration: float = 0.5,
button: str = "left"
) -> bool:
"""
Effectue un glisser-déposer.
Args:
start_x: X de départ
start_y: Y de départ
end_x: X d'arrivée
end_y: Y d'arrivée
duration: Durée du glissement
button: Bouton souris
Returns:
True si succès, False sinon
"""
try:
current_pos = pyautogui.position()
pyautogui.moveTo(start_x, start_y)
pyautogui.drag(end_x - start_x, end_y - start_y, duration=duration, button=button)
action_record = {
"type": ActionType.DRAG.value,
"start_x": start_x,
"start_y": start_y,
"end_x": end_x,
"end_y": end_y,
"previous_position": current_pos,
"duration": duration,
"button": button,
"timestamp": time.time()
}
self.action_history.append(action_record)
self.logger.log_action({
"action": "drag_executed",
**action_record
})
return True
except Exception as e:
self.logger.log_action({
"action": "drag_failed",
"start": (start_x, start_y),
"end": (end_x, end_y),
"error": str(e)
})
return False
def execute_inverse_action(self, action: Dict[str, Any]) -> bool:
"""
Exécute l'action inverse pour rollback.
Args:
action: Action à inverser
Returns:
True si succès, False sinon
"""
inverse = self.get_inverse_action(action)
if not inverse:
return False
action_type = inverse.get("type")
if action_type == ActionType.MOVE.value:
return self.move(inverse["x"], inverse["y"], inverse.get("duration", 0.2))
elif action_type == ActionType.SCROLL.value:
return self.scroll(
inverse["direction"],
inverse["amount"],
inverse.get("x"),
inverse.get("y")
)
elif action_type == ActionType.DRAG.value:
return self.drag(
inverse["start_x"],
inverse["start_y"],
inverse["end_x"],
inverse["end_y"],
inverse.get("duration", 0.5),
inverse.get("button", "left")
)
elif action_type == "press_key":
# Exécuter les suppressions
for _ in range(inverse.get("presses", 0)):
pyautogui.press("backspace")
return True
return False
def get_inverse_action(self, action: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Génère l'action inverse pour rollback.
Args:
action: Action à inverser
Returns:
Action inverse ou None si non inversible
"""
action_type = action.get("type")
if action_type == ActionType.CLICK.value:
# Un clic n'est pas vraiment inversible
# On peut retourner à la position précédente
prev_pos = action.get("previous_position")
if prev_pos:
return {
"type": ActionType.MOVE.value,
"x": prev_pos[0],
"y": prev_pos[1],
"duration": 0.2
}
elif action_type == ActionType.TYPE.value:
# Inverser la saisie = supprimer le texte
text_length = action.get("length", 0)
return {
"type": "press_key",
"key": "backspace",
"presses": text_length
}
elif action_type == ActionType.SCROLL.value:
# Inverser le défilement
direction = action.get("direction")
amount = action.get("amount")
inverse_direction = {
"up": "down",
"down": "up",
"left": "right",
"right": "left"
}.get(direction)
return {
"type": ActionType.SCROLL.value,
"direction": inverse_direction,
"amount": amount,
"x": action.get("x"),
"y": action.get("y")
}
elif action_type == ActionType.MOVE.value:
# Retourner à la position précédente
prev_pos = action.get("previous_position")
if prev_pos:
return {
"type": ActionType.MOVE.value,
"x": prev_pos[0],
"y": prev_pos[1],
"duration": 0.2
}
elif action_type == ActionType.DRAG.value:
# Inverser le glissement
return {
"type": ActionType.DRAG.value,
"start_x": action.get("end_x"),
"start_y": action.get("end_y"),
"end_x": action.get("start_x"),
"end_y": action.get("start_y"),
"duration": action.get("duration", 0.5),
"button": action.get("button", "left")
}
elif action_type == ActionType.WAIT.value:
# L'attente n'a pas d'inverse
return None
return None
def _convert_to_azerty(self, text: str) -> str:
"""
Convertit du texte pour clavier AZERTY.
Args:
text: Texte à convertir
Returns:
Texte converti
"""
# Pour l'instant, retourner tel quel
# PyAutoGUI gère déjà le layout clavier du système
# Cette méthode peut être étendue si nécessaire
return text
def get_action_history(self, limit: int = 50) -> list:
"""
Retourne l'historique des actions.
Args:
limit: Nombre maximum d'actions à retourner
Returns:
Liste des dernières actions
"""
return self.action_history[-limit:]
def clear_history(self):
"""Efface l'historique des actions."""
self.action_history = []
self.logger.log_action({
"action": "action_history_cleared"
})
def execute_action(self, action_data: Dict[str, Any]) -> bool:
"""
Exécute une action depuis un dictionnaire de données.
Args:
action_data: Données de l'action à exécuter
{
"action_type": str,
"bbox": (x, y, w, h),
"parameters": dict
}
Returns:
True si succès, False sinon
"""
action_type = action_data.get("action_type", "").lower()
bbox = action_data.get("bbox", (0, 0, 0, 0))
params = action_data.get("parameters", {})
# Calculer le centre de la bbox pour les actions de clic
x, y, w, h = bbox
center_x = x + w // 2
center_y = y + h // 2
if action_type == "click":
button = params.get("button", "left")
clicks = params.get("clicks", 1)
return self.click(center_x, center_y, button=button, clicks=clicks)
elif action_type == "double_click":
return self.click(center_x, center_y, clicks=2)
elif action_type == "right_click":
return self.click(center_x, center_y, button="right")
elif action_type == "type":
text = params.get("text", "")
interval = params.get("interval", 0.0)
return self.type_text(text, interval=interval)
elif action_type == "scroll":
direction = params.get("direction", "down")
amount = params.get("amount", 3)
return self.scroll(direction, amount, center_x, center_y)
elif action_type == "wait":
duration = params.get("duration", 1.0)
return self.wait(duration)
elif action_type == "move":
duration = params.get("duration", 0.2)
return self.move(center_x, center_y, duration=duration)
elif action_type == "drag":
end_bbox = params.get("end_bbox", bbox)
end_x, end_y, end_w, end_h = end_bbox
end_center_x = end_x + end_w // 2
end_center_y = end_y + end_h // 2
duration = params.get("duration", 0.5)
button = params.get("button", "left")
return self.drag(center_x, center_y, end_center_x, end_center_y, duration, button)
else:
self.logger.log_action({
"action": "unknown_action_type",
"action_type": action_type
})
return False