iso-bot/engine/input/humanize.py

112 lines
3.9 KiB
Python

"""Human-like behavior patterns for input simulation.
Provides randomization utilities to make bot inputs appear natural,
including variable delays, mouse jitter, and activity scheduling.
"""
import random
import time
import logging
from typing import Tuple, Optional
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class HumanProfile:
"""Defines a human behavior profile for input randomization."""
# Reaction time range in seconds
reaction_min: float = 0.15
reaction_max: float = 0.45
# Mouse movement speed range (pixels per second)
mouse_speed_min: float = 400.0
mouse_speed_max: float = 1200.0
# Click position jitter in pixels
click_jitter: int = 3
# Chance of double-reading (hesitation before action)
hesitation_chance: float = 0.1
hesitation_duration: Tuple[float, float] = (0.3, 1.2)
# Break scheduling
micro_break_interval: Tuple[int, int] = (120, 300) # seconds
micro_break_duration: Tuple[int, int] = (2, 8) # seconds
long_break_interval: Tuple[int, int] = (1800, 3600) # seconds
long_break_duration: Tuple[int, int] = (60, 300) # seconds
class Humanizer:
"""Applies human-like randomization to bot actions."""
def __init__(self, profile: Optional[HumanProfile] = None):
self.profile = profile or HumanProfile()
self._last_micro_break = time.time()
self._last_long_break = time.time()
self._next_micro_break = self._schedule_break(self.profile.micro_break_interval)
self._next_long_break = self._schedule_break(self.profile.long_break_interval)
self._action_count = 0
def reaction_delay(self) -> float:
"""Generate a human-like reaction delay."""
base = random.uniform(self.profile.reaction_min, self.profile.reaction_max)
# Occasionally add hesitation
if random.random() < self.profile.hesitation_chance:
base += random.uniform(*self.profile.hesitation_duration)
# Slight fatigue factor based on actions performed
fatigue = min(self._action_count / 1000, 0.3)
base *= (1 + fatigue * random.random())
return base
def jitter_position(self, x: int, y: int) -> Tuple[int, int]:
"""Add small random offset to click position."""
jitter = self.profile.click_jitter
return (
x + random.randint(-jitter, jitter),
y + random.randint(-jitter, jitter),
)
def mouse_speed(self) -> float:
"""Get randomized mouse movement speed."""
return random.uniform(
self.profile.mouse_speed_min,
self.profile.mouse_speed_max,
)
def should_take_break(self) -> Optional[float]:
"""Check if it's time for a break. Returns break duration or None."""
now = time.time()
if now >= self._next_long_break:
duration = random.uniform(*self.profile.long_break_duration)
self._next_long_break = now + duration + self._schedule_break(
self.profile.long_break_interval
)
logger.info(f"Long break: {duration:.0f}s")
return duration
if now >= self._next_micro_break:
duration = random.uniform(*self.profile.micro_break_duration)
self._next_micro_break = now + duration + self._schedule_break(
self.profile.micro_break_interval
)
logger.debug(f"Micro break: {duration:.1f}s")
return duration
return None
def wait(self) -> None:
"""Wait for a human-like reaction delay."""
delay = self.reaction_delay()
time.sleep(delay)
self._action_count += 1
def _schedule_break(self, interval: Tuple[int, int]) -> float:
"""Schedule next break with randomized interval."""
return time.time() + random.uniform(*interval)