Spaces:
Sleeping
Sleeping
| """ | |
| ============================================ | |
| Human Behavior Simulator | |
| - Random delays between actions | |
| - Mouse movement patterns | |
| - Random scrolling | |
| - Realistic typing speed | |
| - Makes the bot look like a real human reading | |
| ============================================ | |
| """ | |
| import asyncio | |
| import random | |
| import logging | |
| import math | |
| from typing import Optional | |
| from playwright.async_api import Page | |
| from app.config import settings | |
| logger = logging.getLogger(__name__) | |
| class HumanSimulator: | |
| """ | |
| Simulates human behavior on web pages. | |
| Why this matters: | |
| - Websites track mouse movement patterns | |
| - Constant timing = bot detection | |
| - No scrolling = suspicious | |
| - Instant navigation = not human | |
| """ | |
| def __init__(self, page: Page): | |
| self.page = page | |
| # ============================================ | |
| # Random Delays | |
| # ============================================ | |
| async def random_delay( | |
| self, | |
| min_seconds: Optional[float] = None, | |
| max_seconds: Optional[float] = None, | |
| reason: str = "general", | |
| ): | |
| """ | |
| Wait for a random amount of time. | |
| Like a human naturally pauses between actions. | |
| """ | |
| min_s = min_seconds or settings.MIN_DELAY_SECONDS | |
| max_s = max_seconds or settings.MAX_DELAY_SECONDS | |
| # Use a slightly weighted random (humans tend toward shorter waits) | |
| delay = random.uniform(min_s, max_s) | |
| # Occasionally add a longer "thinking" pause (10% chance) | |
| if random.random() < 0.10: | |
| delay += random.uniform(2.0, 5.0) | |
| logger.debug(f" 💭 Extended thinking pause ({reason})") | |
| logger.debug(f" ⏳ Waiting {delay:.1f}s ({reason})") | |
| await asyncio.sleep(delay) | |
| async def short_delay(self, reason: str = "short action"): | |
| """Quick delay: 0.5 to 2 seconds.""" | |
| await self.random_delay(0.5, 2.0, reason) | |
| async def medium_delay(self, reason: str = "medium action"): | |
| """Medium delay: 2 to 5 seconds.""" | |
| await self.random_delay(2.0, 5.0, reason) | |
| async def long_delay(self, reason: str = "long action"): | |
| """Long delay: 5 to 10 seconds.""" | |
| await self.random_delay(5.0, 10.0, reason) | |
| async def reading_delay(self, text_length: int): | |
| """ | |
| Simulate reading time based on text length. | |
| Average human reads ~250 words per minute. | |
| We'll go faster since we're "speed reading." | |
| """ | |
| word_count = text_length // 5 # Rough estimate: 5 chars per word | |
| # Speed reading: ~500-800 WPM | |
| reading_speed = random.uniform(500, 800) | |
| read_time = (word_count / reading_speed) * 60 # seconds | |
| # Cap the reading time (don't wait too long) | |
| read_time = min(read_time, 8.0) | |
| read_time = max(read_time, 1.0) | |
| logger.debug(f" 📖 Reading pause: {read_time:.1f}s for ~{word_count} words") | |
| await asyncio.sleep(read_time) | |
| # ============================================ | |
| # Mouse Movements | |
| # ============================================ | |
| async def move_mouse_naturally(self, target_x: int, target_y: int): | |
| """ | |
| Move mouse to target coordinates with natural curve. | |
| Humans don't move mouse in straight lines. | |
| Uses bezier-like curve with random intermediate points. | |
| """ | |
| try: | |
| # Get current viewport size | |
| viewport = self.page.viewport_size | |
| if not viewport: | |
| viewport = {"width": 1920, "height": 1080} | |
| # Start from a random position (simulating current mouse position) | |
| start_x = random.randint(100, viewport["width"] - 100) | |
| start_y = random.randint(100, viewport["height"] - 100) | |
| # Generate intermediate points for a natural curve | |
| steps = random.randint(10, 25) | |
| for i in range(steps): | |
| progress = (i + 1) / steps | |
| # Add slight curve/wobble | |
| wobble_x = random.randint(-3, 3) | |
| wobble_y = random.randint(-2, 2) | |
| # Ease-in-out interpolation | |
| eased = self._ease_in_out(progress) | |
| current_x = int(start_x + (target_x - start_x) * eased + wobble_x) | |
| current_y = int(start_y + (target_y - start_y) * eased + wobble_y) | |
| # Clamp to viewport | |
| current_x = max(0, min(current_x, viewport["width"])) | |
| current_y = max(0, min(current_y, viewport["height"])) | |
| await self.page.mouse.move(current_x, current_y) | |
| # Tiny delay between mouse moves | |
| await asyncio.sleep(random.uniform(0.01, 0.04)) | |
| logger.debug(f" 🖱️ Mouse moved to ({target_x}, {target_y})") | |
| except Exception as e: | |
| logger.debug(f"Mouse movement skipped: {e}") | |
| def _ease_in_out(t: float) -> float: | |
| """Ease-in-out function for natural movement curves.""" | |
| if t < 0.5: | |
| return 2 * t * t | |
| else: | |
| return 1 - (-2 * t + 2) ** 2 / 2 | |
| async def random_mouse_movement(self): | |
| """ | |
| Make a random mouse movement on the page. | |
| Like a human idly moving their mouse while reading. | |
| """ | |
| try: | |
| viewport = self.page.viewport_size | |
| if not viewport: | |
| return | |
| x = random.randint(100, viewport["width"] - 100) | |
| y = random.randint(100, viewport["height"] - 100) | |
| await self.move_mouse_naturally(x, y) | |
| except Exception: | |
| pass # Mouse movements are optional, don't crash | |
| # ============================================ | |
| # Scrolling | |
| # ============================================ | |
| async def scroll_down_naturally(self, scroll_amount: Optional[int] = None): | |
| """ | |
| Scroll down the page naturally. | |
| Humans scroll in chunks, not smoothly. | |
| """ | |
| try: | |
| if scroll_amount is None: | |
| scroll_amount = random.randint(200, 600) | |
| # Scroll in small chunks | |
| remaining = scroll_amount | |
| while remaining > 0: | |
| chunk = min(remaining, random.randint(50, 150)) | |
| await self.page.mouse.wheel(0, chunk) | |
| remaining -= chunk | |
| # Small pause between scroll chunks | |
| await asyncio.sleep(random.uniform(0.05, 0.15)) | |
| logger.debug(f" 📜 Scrolled down {scroll_amount}px") | |
| except Exception as e: | |
| logger.debug(f"Scroll failed: {e}") | |
| async def scroll_to_bottom_gradually(self): | |
| """ | |
| Scroll to bottom of page gradually, like a reader. | |
| """ | |
| try: | |
| # Get page height | |
| page_height = await self.page.evaluate("document.body.scrollHeight") | |
| viewport_height = await self.page.evaluate("window.innerHeight") | |
| current_scroll = await self.page.evaluate("window.scrollY") | |
| remaining = page_height - current_scroll - viewport_height | |
| while remaining > 50: | |
| scroll_chunk = random.randint(200, 500) | |
| await self.scroll_down_naturally(min(scroll_chunk, remaining)) | |
| # Pause between scrolls (reading) | |
| await asyncio.sleep(random.uniform(0.5, 2.0)) | |
| # Occasionally do a random mouse movement while scrolling | |
| if random.random() < 0.3: | |
| await self.random_mouse_movement() | |
| current_scroll = await self.page.evaluate("window.scrollY") | |
| remaining = page_height - current_scroll - viewport_height | |
| logger.debug(" 📜 Scrolled to bottom of page") | |
| except Exception as e: | |
| logger.debug(f"Scroll to bottom failed: {e}") | |
| async def scroll_up_slightly(self): | |
| """ | |
| Occasionally scroll up a little, like re-reading a paragraph. | |
| """ | |
| try: | |
| scroll_up = random.randint(-200, -50) | |
| await self.page.mouse.wheel(0, scroll_up) | |
| logger.debug(f" 📜 Scrolled up slightly ({scroll_up}px)") | |
| except Exception: | |
| pass | |
| # ============================================ | |
| # Typing | |
| # ============================================ | |
| async def type_like_human(self, selector: str, text: str): | |
| """ | |
| Type text character by character with human-like speed. | |
| Includes occasional typos and corrections (optional). | |
| """ | |
| try: | |
| # Click on the input field first | |
| await self.page.click(selector) | |
| await asyncio.sleep(random.uniform(0.3, 0.8)) | |
| for char in text: | |
| await self.page.keyboard.type(char) | |
| # Random delay between keystrokes | |
| # Humans type at 40-80 WPM | |
| delay = random.uniform(0.05, 0.20) | |
| # Occasionally pause longer (thinking) | |
| if random.random() < 0.05: | |
| delay += random.uniform(0.3, 0.8) | |
| await asyncio.sleep(delay) | |
| logger.debug(f" ⌨️ Typed {len(text)} characters into '{selector}'") | |
| except Exception as e: | |
| logger.warning(f"Human typing failed, using fast fill: {e}") | |
| await self.page.fill(selector, text) | |
| # ============================================ | |
| # Combined Human Actions | |
| # ============================================ | |
| async def simulate_page_arrival(self): | |
| """ | |
| Simulate what a human does when arriving at a new page: | |
| 1. Wait for page to visually load | |
| 2. Small mouse movement | |
| 3. Brief pause (looking at the page) | |
| """ | |
| await self.short_delay("page arrival - initial look") | |
| if random.random() < 0.5: | |
| await self.random_mouse_movement() | |
| await self.short_delay("page arrival - orientation") | |
| async def simulate_reading_chapter(self): | |
| """ | |
| Simulate reading a chapter: | |
| 1. Scroll down gradually | |
| 2. Occasional mouse movements | |
| 3. Occasional scroll up (re-reading) | |
| 4. Reading delay at the end | |
| """ | |
| # Scroll through the content | |
| await self.scroll_to_bottom_gradually() | |
| # Maybe scroll up to re-read something | |
| if random.random() < 0.2: | |
| await self.scroll_up_slightly() | |
| await self.short_delay("re-reading") | |
| # Final reading pause | |
| await self.medium_delay("finishing chapter") | |
| async def simulate_before_click(self, x: int, y: int): | |
| """ | |
| What a human does before clicking a button: | |
| 1. Look at the button (pause) | |
| 2. Move mouse toward it | |
| 3. Small hesitation | |
| 4. Click | |
| """ | |
| await self.short_delay("looking at button") | |
| await self.move_mouse_naturally(x, y) | |
| await asyncio.sleep(random.uniform(0.2, 0.5)) # Hesitation |