Initial commit
This commit is contained in:
488
geniusia2/core/event_capture.py
Normal file
488
geniusia2/core/event_capture.py
Normal file
@@ -0,0 +1,488 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user