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

489 lines
16 KiB
Python

"""
Capture des événements utilisateur (clavier et souris) pour l'apprentissage.
Utilise pynput pour capturer les actions en temps réel.
"""
import time
from datetime import datetime
from typing import Dict, List, Optional, Callable, Any
from threading import Thread, Lock
from collections import deque
try:
from pynput import mouse, keyboard
PYNPUT_AVAILABLE = True
except ImportError:
PYNPUT_AVAILABLE = False
print("⚠️ pynput n'est pas installé. Capture d'événements désactivée.")
from .logger import Logger
from .utils.image_utils import get_active_window
from .session_manager import SessionManager
from .workflow_detector import WorkflowDetector
class EventCapture:
"""
Capture les événements clavier et souris pour détecter les patterns répétitifs.
"""
def __init__(
self,
logger: Optional[Logger] = None,
max_history: int = 1000,
pattern_threshold: int = 3,
config: Optional[Dict[str, Any]] = None
):
"""
Initialise le capteur d'événements.
Args:
logger: Logger pour journalisation
max_history: Nombre max d'événements à garder en mémoire
pattern_threshold: Nombre de répétitions pour détecter un pattern
config: Configuration pour les workflows
"""
self.logger = logger
self.max_history = max_history
self.pattern_threshold = pattern_threshold
self.config = config or {}
# Historique des événements
self.events: deque = deque(maxlen=max_history)
self.events_lock = Lock()
# État de capture
self.capturing = False
self.mouse_listener = None
self.keyboard_listener = None
# Callbacks pour patterns détectés
self.pattern_callbacks: List[Callable] = []
# Dernière fenêtre active
self.last_window = None
# Composants pour la détection de workflows
self.session_manager = SessionManager(logger, self.config)
self.workflow_detector = WorkflowDetector(logger, self.config)
# Connecter les callbacks
self.session_manager.on_session_completed = self._on_session_completed
self.workflow_detector.on_workflow_detected = self._on_workflow_detected
if not PYNPUT_AVAILABLE:
if logger:
logger.log_action({
"action": "event_capture_unavailable",
"reason": "pynput not installed"
})
def start(self):
"""Démarre la capture d'événements."""
if not PYNPUT_AVAILABLE:
print("⚠️ Impossible de démarrer la capture : pynput non disponible")
return False
if self.capturing:
return True
self.capturing = True
# Démarrer les listeners
self.mouse_listener = mouse.Listener(
on_click=self._on_mouse_click,
on_move=self._on_mouse_move,
on_scroll=self._on_mouse_scroll
)
self.keyboard_listener = keyboard.Listener(
on_press=self._on_key_press
)
self.mouse_listener.start()
self.keyboard_listener.start()
if self.logger:
self.logger.log_action({
"action": "event_capture_started"
})
print("✅ Capture d'événements démarrée")
return True
def stop(self):
"""Arrête la capture d'événements de manière synchrone."""
if not self.capturing:
return
self.capturing = False
# Arrêter les listeners et attendre qu'ils se terminent
if self.mouse_listener:
self.mouse_listener.stop()
try:
# Attendre max 2 secondes que le listener se termine
self.mouse_listener.join(timeout=2.0)
except Exception as e:
if self.logger:
self.logger.log_action({
"action": "mouse_listener_stop_error",
"error": str(e)
})
if self.keyboard_listener:
self.keyboard_listener.stop()
try:
# Attendre max 2 secondes que le listener se termine
self.keyboard_listener.join(timeout=2.0)
except Exception as e:
if self.logger:
self.logger.log_action({
"action": "keyboard_listener_stop_error",
"error": str(e)
})
if self.logger:
self.logger.log_action({
"action": "event_capture_stopped",
"total_events": len(self.events)
})
print("⏹️ Capture d'événements arrêtée")
def _on_mouse_click(self, x: int, y: int, button, pressed: bool):
"""Callback pour les clics souris."""
if not pressed: # On enregistre seulement les clics (pas les relâchements)
return
from .utils.image_utils import capture_screen
window = get_active_window()
# Capturer l'écran immédiatement
screenshot = capture_screen()
event = {
"type": "mouse_click",
"x": x,
"y": y,
"button": str(button),
"window": window,
"timestamp": datetime.now(),
"screenshot": screenshot # Ajout du screenshot
}
self._add_event(event)
def _on_mouse_move(self, x: int, y: int):
"""Callback pour les mouvements souris (optionnel, peut être bruyant)."""
# On n'enregistre pas tous les mouvements pour éviter le bruit
pass
def _on_mouse_scroll(self, x: int, y: int, dx: int, dy: int):
"""Callback pour le scroll."""
from .utils.image_utils import capture_screen
window = get_active_window()
screenshot = capture_screen()
event = {
"type": "scroll",
"x": x,
"y": y,
"dx": dx,
"dy": dy,
"window": window,
"timestamp": datetime.now(),
"screenshot": screenshot
}
self._add_event(event)
def _on_key_press(self, key):
"""Callback pour les frappes clavier."""
from .utils.image_utils import capture_screen
window = get_active_window()
try:
key_char = key.char
except AttributeError:
key_char = str(key)
# Détecter les combinaisons (Ctrl+C, Ctrl+V, etc.)
is_ctrl = hasattr(key, 'name') and 'ctrl' in str(key).lower()
is_combo = is_ctrl or (hasattr(key, 'name') and key.name in ['ctrl_l', 'ctrl_r', 'alt_l', 'alt_r'])
# Capturer screenshot seulement pour les combos importants
screenshot = None
if is_combo or key_char in ['c', 'v', 'a', 'x', 'z']:
screenshot = capture_screen()
event = {
"type": "key_press",
"key": key_char,
"window": window,
"timestamp": datetime.now(),
"screenshot": screenshot,
"is_combo": is_combo
}
self._add_event(event)
def _add_event(self, event: Dict[str, Any]):
"""Ajoute un événement à l'historique."""
with self.events_lock:
self.events.append(event)
# Limiter la mémoire : garder seulement les 100 derniers
if len(self.events) > 100:
# Supprimer le plus ancien
old_event = self.events.popleft()
# Libérer la mémoire du screenshot
if 'screenshot' in old_event:
del old_event['screenshot']
# Passer l'événement au SessionManager pour segmentation
try:
self.session_manager.add_action(event)
except Exception as e:
if self.logger:
self.logger.log_action({
"action": "session_add_action_failed",
"error": str(e)
})
# Vérifier si on détecte un pattern
self._check_for_patterns()
def _check_for_patterns(self):
"""Vérifie si les derniers événements forment un pattern répétitif."""
# Détecter le pattern avec le lock
with self.events_lock:
if len(self.events) < self.pattern_threshold * 2:
return
# Analyser les N derniers événements
recent_events = list(self.events)[-20:] # 20 derniers événements
# Détecter des séquences répétitives
pattern = self._detect_repetitive_sequence(recent_events)
# Appeler les callbacks EN DEHORS du lock pour éviter les deadlocks
if pattern:
print(f"🎯 Pattern détecté dans event_capture !")
print(f" Répétitions: {pattern['repetitions']}")
print(f" Longueur: {pattern['length']}")
# Notifier les callbacks
for callback in self.pattern_callbacks:
try:
callback(pattern)
except Exception as e:
print(f"❌ Erreur dans callback: {e}")
import traceback
traceback.print_exc()
def _detect_repetitive_sequence(
self,
events: List[Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
"""
Détecte une séquence répétitive dans les événements.
Returns:
Dictionnaire décrivant le pattern ou None
"""
if len(events) < self.pattern_threshold:
return None
# Simplifier les événements pour la comparaison
simplified = []
for e in events:
if e["type"] == "mouse_click":
# Regrouper les clics proches (tolérance de 100px - plus permissif)
simplified.append({
"type": "click",
"x_zone": e["x"] // 100, # Zones de 100px
"y_zone": e["y"] // 100,
"window": e["window"]
})
elif e["type"] == "key_press":
simplified.append({
"type": "key",
"key": e["key"],
"window": e["window"]
})
elif e["type"] == "scroll":
simplified.append({
"type": "scroll",
"window": e["window"]
})
# Chercher des répétitions
for seq_len in range(1, len(simplified) // self.pattern_threshold + 1):
sequence = simplified[-seq_len:]
# Vérifier si cette séquence se répète
repetitions = 0
for i in range(len(simplified) - seq_len, -1, -seq_len):
if simplified[i:i+seq_len] == sequence:
repetitions += 1
else:
break
if repetitions >= self.pattern_threshold:
return {
"sequence": sequence,
"repetitions": repetitions,
"length": seq_len,
"window": sequence[0]["window"] if sequence else None
}
return None
def register_pattern_callback(self, callback: Callable):
"""Enregistre un callback à appeler quand un pattern est détecté."""
self.pattern_callbacks.append(callback)
def get_recent_events(self, count: int = 10) -> List[Dict[str, Any]]:
"""Retourne les N derniers événements."""
with self.events_lock:
return list(self.events)[-count:]
def get_events_for_window(self, window_title: str) -> List[Dict[str, Any]]:
"""Retourne tous les événements pour une fenêtre donnée."""
with self.events_lock:
return [e for e in self.events if e.get("window") == window_title]
def clear_history(self):
"""Efface l'historique des événements."""
with self.events_lock:
self.events.clear()
if self.logger:
self.logger.log_action({
"action": "event_history_cleared"
})
def get_last_screenshots(self, count: int = 3) -> List[Dict[str, Any]]:
"""
Retourne les N derniers événements avec screenshots.
Returns:
Liste d'événements avec screenshots
"""
with self.events_lock:
events_with_screenshots = [
e for e in self.events
if e.get('screenshot') is not None
]
return events_with_screenshots[-count:] if events_with_screenshots else []
def _on_session_completed(self, session):
"""
Callback appelé quand une session est terminée.
Args:
session: Session terminée
"""
if self.logger:
self.logger.log_action({
"action": "session_completed_callback",
"session_id": session.session_id,
"action_count": session.action_count
})
# Analyser les sessions récentes pour détecter des workflows
recent_sessions = self.session_manager.get_recent_sessions(10)
self.workflow_detector.analyze_sessions(recent_sessions)
def _on_workflow_detected(self, workflow):
"""
Callback appelé quand un workflow est détecté.
Args:
workflow: Workflow détecté (dictionnaire)
"""
if self.logger:
self.logger.log_action({
"action": "workflow_detected_callback",
"workflow_id": workflow.get("workflow_id"),
"name": workflow.get("name"),
"steps": len(workflow.get("steps", [])),
"repetitions": workflow.get("repetitions")
})
# Notifier les callbacks de pattern (pour compatibilité)
pattern_data = {
"type": "workflow",
"workflow_id": workflow.get("workflow_id"),
"name": workflow.get("name"),
"steps": len(workflow.get("steps", [])),
"repetitions": workflow.get("repetitions"),
"confidence": workflow.get("confidence")
}
for callback in self.pattern_callbacks:
try:
callback(pattern_data)
except Exception as e:
if self.logger:
self.logger.log_action({
"action": "workflow_callback_error",
"error": str(e)
})
def get_workflows(self):
"""
Retourne les workflows détectés.
Returns:
Liste des workflows
"""
return self.workflow_detector.get_workflows()
def get_sessions(self, count: int = 10):
"""
Retourne les sessions récentes.
Args:
count: Nombre de sessions à retourner
Returns:
Liste des sessions
"""
return self.session_manager.get_recent_sessions(count)
def get_workflow_stats(self):
"""
Retourne les statistiques des workflows.
Returns:
Dictionnaire de statistiques
"""
return {
"sessions": self.session_manager.get_stats(),
"workflows": self.workflow_detector.get_stats()
}
def force_finalize_session(self):
"""
Force la finalisation de la session courante.
"""
self.session_manager.force_finalize_session()
def capture_event(self, action: Dict[str, Any]):
"""
Capture un événement manuellement (pour les tests).
Args:
action: Action à capturer
"""
# Ajouter à la session
self.session_manager.add_action(action)
# Ajouter à l'historique
self._add_event(action)