"""Human-like keyboard input simulation with realistic timing. Provides keyboard input with natural typing patterns, including varied keystroke timing and realistic human typing characteristics. """ from typing import List, Dict, Optional, Union from dataclasses import dataclass import time import random import logging import pyautogui from pynput import keyboard logger = logging.getLogger(__name__) @dataclass class KeySequence: """Represents a sequence of keys with timing information.""" keys: List[str] delays: List[float] total_duration: float class TypingProfile: """Defines typing characteristics for human-like input.""" def __init__(self, wpm: int = 60, accuracy: float = 0.95): """Initialize typing profile. Args: wpm: Words per minute typing speed accuracy: Typing accuracy (0.0 to 1.0) """ self.wpm = wpm self.accuracy = accuracy # Calculate base timing from WPM (assuming 5 characters per word) chars_per_minute = wpm * 5 self.base_char_delay = 60.0 / chars_per_minute # Timing variations self.min_delay = 0.02 # Minimum delay between keys self.max_delay = 0.5 # Maximum delay between keys self.word_pause = 0.1 # Additional pause after spaces # Common typing patterns self.difficult_sequences = { 'th', 'ch', 'sh', 'qu', 'tion', 'ing', 'er', 'ed' } # Keys that typically take longer to find self.slow_keys = { 'q', 'z', 'x', 'j', 'k', 'shift', 'ctrl', 'alt' } class KeyboardController: """Controller for human-like keyboard input.""" def __init__(self, typing_profile: Optional[TypingProfile] = None): """Initialize keyboard controller. Args: typing_profile: Typing characteristics, or None for default """ self.profile = typing_profile or TypingProfile() self.shift_held = False self.ctrl_held = False self.alt_held = False # Key mapping for special keys self.key_mapping = { 'enter': '\n', 'return': '\n', 'tab': '\t', 'space': ' ', 'backspace': 'backspace', 'delete': 'delete', 'escape': 'esc', 'esc': 'esc', } def calculate_key_delay(self, key: str, previous_key: Optional[str] = None) -> float: """Calculate realistic delay for typing a key. Args: key: Key to type previous_key: Previously typed key for sequence analysis Returns: Delay in seconds before typing this key """ # Base delay from typing speed delay = self.profile.base_char_delay # Adjust for key difficulty if key.lower() in self.profile.slow_keys: delay *= random.uniform(1.2, 1.8) # Adjust for difficult sequences if previous_key: sequence = previous_key.lower() + key.lower() if any(seq in sequence for seq in self.profile.difficult_sequences): delay *= random.uniform(1.1, 1.5) # Add natural variation delay *= random.uniform(0.7, 1.4) # Extra pause after spaces (word boundaries) if previous_key and previous_key == ' ': delay += self.profile.word_pause * random.uniform(0.5, 1.5) # Clamp to reasonable bounds return max(self.profile.min_delay, min(self.profile.max_delay, delay)) def type_text(self, text: str, include_errors: bool = False) -> None: """Type text with human-like timing and optional errors. Args: text: Text to type include_errors: Whether to include typing errors and corrections """ if not text: return logger.debug(f"Typing text: '{text[:50]}{'...' if len(text) > 50 else ''}'") previous_key = None for i, char in enumerate(text): # Calculate delay before this character delay = self.calculate_key_delay(char, previous_key) # Sleep before typing time.sleep(delay) # Occasionally make typing errors if enabled if include_errors and self.should_make_error(): self.make_typing_error(char) else: self.type_key(char) previous_key = char def should_make_error(self) -> bool: """Determine if a typing error should be made. Returns: True if an error should be made """ return random.random() > self.profile.accuracy def make_typing_error(self, intended_key: str) -> None: """Make a typing error and correct it. Args: intended_key: The key that was supposed to be typed """ # Type wrong key (usually adjacent on keyboard) wrong_key = self.get_adjacent_key(intended_key) self.type_key(wrong_key) # Pause as human realizes mistake time.sleep(random.uniform(0.1, 0.4)) # Backspace to correct self.type_key('backspace') time.sleep(random.uniform(0.05, 0.15)) # Type correct key self.type_key(intended_key) def get_adjacent_key(self, key: str) -> str: """Get an adjacent key for typing errors. Args: key: Original key Returns: Adjacent key that could be mistyped """ # Simplified adjacent key mapping adjacent_map = { 'a': 'sq', 'b': 'vgn', 'c': 'xvd', 'd': 'sfe', 'e': 'wrd', 'f': 'dgr', 'g': 'fht', 'h': 'gyu', 'i': 'uko', 'j': 'hnu', 'k': 'jmo', 'l': 'kpo', 'm': 'njk', 'n': 'bhm', 'o': 'ilp', 'p': 'olo', 'q': 'wa', 'r': 'etf', 's': 'adw', 't': 'rgy', 'u': 'yhi', 'v': 'cfg', 'w': 'qse', 'x': 'zdc', 'y': 'tgu', 'z': 'xas' } adjacent_keys = adjacent_map.get(key.lower(), 'abcd') return random.choice(adjacent_keys) def type_key(self, key: str) -> None: """Type a single key. Args: key: Key to type """ # Handle special keys if key.lower() in self.key_mapping: mapped_key = self.key_mapping[key.lower()] if mapped_key in ['backspace', 'delete', 'esc']: pyautogui.press(mapped_key) else: pyautogui.write(mapped_key) else: pyautogui.write(key) def press_key_combination(self, *keys: str) -> None: """Press a combination of keys (e.g., Ctrl+C). Args: keys: Keys to press together """ logger.debug(f"Pressing key combination: {'+'.join(keys)}") # Press all keys down for key in keys: pyautogui.keyDown(key) time.sleep(random.uniform(0.01, 0.03)) # Hold briefly time.sleep(random.uniform(0.05, 0.1)) # Release all keys (in reverse order) for key in reversed(keys): pyautogui.keyUp(key) time.sleep(random.uniform(0.01, 0.03)) def press_key(self, key: str, duration: Optional[float] = None) -> None: """Press and release a key. Args: key: Key to press duration: How long to hold key, or None for quick press """ if duration is None: pyautogui.press(key) else: pyautogui.keyDown(key) time.sleep(duration) pyautogui.keyUp(key) def hold_key(self, key: str) -> None: """Start holding a key down. Args: key: Key to hold """ pyautogui.keyDown(key) # Track modifier keys if key.lower() == 'shift': self.shift_held = True elif key.lower() in ['ctrl', 'control']: self.ctrl_held = True elif key.lower() == 'alt': self.alt_held = True def release_key(self, key: str) -> None: """Stop holding a key. Args: key: Key to release """ pyautogui.keyUp(key) # Track modifier keys if key.lower() == 'shift': self.shift_held = False elif key.lower() in ['ctrl', 'control']: self.ctrl_held = False elif key.lower() == 'alt': self.alt_held = False def release_all_keys(self) -> None: """Release all held modifier keys.""" if self.shift_held: self.release_key('shift') if self.ctrl_held: self.release_key('ctrl') if self.alt_held: self.release_key('alt') def type_number_sequence(self, numbers: Union[str, int], use_numpad: bool = False) -> None: """Type a sequence of numbers. Args: numbers: Numbers to type use_numpad: Whether to use numpad keys """ number_str = str(numbers) for digit in number_str: if digit.isdigit(): if use_numpad: key = f'num{digit}' else: key = digit self.type_key(key) time.sleep(self.calculate_key_delay(digit)) def simulate_pause(self, pause_type: str = 'thinking') -> None: """Simulate natural pauses in typing. Args: pause_type: Type of pause ('thinking', 'reading', 'short') """ if pause_type == 'thinking': duration = random.uniform(0.5, 2.0) elif pause_type == 'reading': duration = random.uniform(0.2, 0.8) else: # short duration = random.uniform(0.1, 0.3) logger.debug(f"Simulating {pause_type} pause for {duration:.2f}s") time.sleep(duration) def generate_key_sequence(self, text: str) -> KeySequence: """Generate a key sequence with timing for given text. Args: text: Text to generate sequence for Returns: KeySequence with keys and delays """ keys = list(text) delays = [] total_duration = 0.0 previous_key = None for key in keys: delay = self.calculate_key_delay(key, previous_key) delays.append(delay) total_duration += delay previous_key = key return KeySequence(keys, delays, total_duration) def set_typing_speed(self, wpm: int) -> None: """Set typing speed. Args: wpm: Words per minute """ self.profile.wpm = max(10, min(200, wpm)) chars_per_minute = self.profile.wpm * 5 self.profile.base_char_delay = 60.0 / chars_per_minute logger.info(f"Typing speed set to {self.profile.wpm} WPM") def set_accuracy(self, accuracy: float) -> None: """Set typing accuracy. Args: accuracy: Accuracy from 0.0 to 1.0 """ self.profile.accuracy = max(0.0, min(1.0, accuracy)) logger.info(f"Typing accuracy set to {self.profile.accuracy * 100:.1f}%")