#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: "2025-10-10 23:56:58 (ywatanabe)"
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/browser/stealth/HumanBehavior.py
# ----------------------------------------
from __future__ import annotations
import os
__FILE__ = "./src/scitex/browser/stealth/HumanBehavior.py"
__DIR__ = os.path.dirname(__FILE__)
# ----------------------------------------
__FILE__ = __file__
import asyncio
import random
from typing import Optional
from playwright.async_api import Page
from ..debugging import browser_logger
[docs]
class HumanBehavior:
"""Simulates human-like behavior patterns for browser automation."""
[docs]
def __init__(
self,
):
self.name = self.__class__.__name__
[docs]
async def random_delay_async(
self, min_ms: int = 1000, max_ms: int = 3000, page: Page = None
) -> None:
"""Add random delay to simulate human timing."""
delay_ms = random.randint(min_ms, max_ms)
if page:
await browser_logger.debug(
page, f"{self.name}: Adding delay ({delay_ms}ms)..."
)
await asyncio.sleep(delay_ms / 1000)
[docs]
async def reading_delay_async(
self, content_length: int = 1000, page: Page = None
) -> None:
"""Simulate time taken to read content based on length."""
# Avg human reading speed: 200-250 words/minute
# Assume ~5 chars per word
words = content_length / 5
reading_time_ms = (words / 250) * 60 * 1000 # Convert to milliseconds
reading_time_ms = min(
max(reading_time_ms, 2000), 10000
) # Clamp between 2-10 seconds
# Add randomness
actual_delay = reading_time_ms * random.uniform(0.8, 1.2)
if page:
await browser_logger.debug(
page,
f"{self.name}: Reading delay: {actual_delay:.0f}ms for {content_length} chars...",
)
await asyncio.sleep(actual_delay / 1000)
[docs]
async def mouse_move_async(
self, page: Page, x: Optional[int] = None, y: Optional[int] = None
) -> None:
"""Move mouse to position with human-like movement."""
await browser_logger.debug(page, f"{self.name}: Moving Cursor...")
if x is None:
x = random.randint(100, 1200)
if y is None:
y = random.randint(100, 800)
# Move in steps for more natural movement
current_x, current_y = 0, 0
steps = random.randint(3, 7)
for i in range(steps):
progress = (i + 1) / steps
# Ease-in-out curve
t = progress * progress * (3.0 - 2.0 * progress)
next_x = int(current_x + (x - current_x) * t)
next_y = int(current_y + (y - current_y) * t)
await page.mouse.move(next_x, next_y)
await asyncio.sleep(random.uniform(0.01, 0.03))
current_x, current_y = next_x, next_y
[docs]
async def hover_and_click_async(
self, page: Page, selector: str = None, element=None
) -> None:
"""Hover over element before clicking with human-like timing."""
await browser_logger.debug(page, f"{self.name}: Hovering and clicking...")
if selector:
element = page.locator(selector)
if not element:
raise ValueError("Either selector or element must be provided")
# Move to element area first
box = await element.bounding_box()
if box:
# Add small offset to avoid clicking exact center every time
offset_x = random.randint(-10, 10)
offset_y = random.randint(-10, 10)
target_x = box["x"] + box["width"] / 2 + offset_x
target_y = box["y"] + box["height"] / 2 + offset_y
await HumanBehavior.mouse_move_async(page, int(target_x), int(target_y))
# Hover
await element.hover()
await HumanBehavior.random_delay_async(200, 800)
# Click
await element.click()
[docs]
@staticmethod
async def type_text_async(
page: Page,
selector: str = None,
element=None,
text: str = "",
clear_first: bool = False,
) -> None:
"""Type text with human-like timing and occasional mistakes."""
if selector:
element = page.locator(selector)
if not element:
raise ValueError("Either selector or element must be provided")
# Click to focus
await element.click()
await HumanBehavior.random_delay_async(100, 300)
# Clear if requested
if clear_first:
await element.clear()
await HumanBehavior.random_delay_async(100, 200)
# Type character by character
for i, char in enumerate(text):
# Occasionally make a typo and correct it (1% chance)
if random.random() < 0.01 and i > 0:
wrong_char = random.choice("abcdefghijklmnopqrstuvwxyz")
await element.type(wrong_char)
await HumanBehavior.random_delay_async(100, 300)
await page.keyboard.press("Backspace")
await HumanBehavior.random_delay_async(50, 150)
await element.type(char)
# Variable typing speed
if char == " ":
await HumanBehavior.random_delay_async(50, 150)
elif char in ".,!?;:":
await HumanBehavior.random_delay_async(150, 300)
else:
await HumanBehavior.random_delay_async(30, 120)
[docs]
@staticmethod
async def random_mouse_movement_async(page: Page) -> None:
"""Perform random mouse movements to appear active."""
movements = random.randint(2, 4)
for _ in range(movements):
await HumanBehavior.mouse_move_async(page)
await HumanBehavior.random_delay_async(500, 1500)
[docs]
@staticmethod
async def pdf_viewing_behavior_async(page: Page) -> None:
"""Simulate human behavior when viewing a PDF."""
await browser_logger.debug(page, "Simulating PDF viewing behavior")
# Initial load and orientation
human = HumanBehavior()
await human.random_delay_async(2000, 4000, page)
# Scroll down a bit to "read" the first page
await human.scroll_async(page, "down", random.randint(200, 400))
await human.random_delay_async(3000, 5000, page)
# Random mouse movement while "reading"
await HumanBehavior.random_mouse_movement_async(page)
# Scroll back up as if checking something
await human.scroll_async(page, "up", random.randint(100, 200))
await human.random_delay_async(1000, 2000, page)
# Move mouse to download area (typically top-right)
viewport = page.viewport_size
if viewport:
await human.mouse_move_async(
page,
viewport["width"] - random.randint(50, 150),
random.randint(50, 150),
)
await human.random_delay_async(500, 1000, page)
[docs]
@staticmethod
async def wait_for_download_async(page: Page) -> None:
"""Wait for download with human-like patience."""
# Humans don't immediately close after clicking download
human = HumanBehavior()
await human.random_delay_async(1000, 2000, page)
# Might move mouse around while waiting
if random.random() < 0.3:
await HumanBehavior.random_mouse_movement_async(page)
def main(args):
"""Demonstrate HumanBehavior functionality."""
import asyncio
from playwright.async_api import async_playwright
async def demo():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
# Navigate with human behavior
await page.goto("https://www.google.com")
print("✓ Navigated to page")
# Simulate human reading the page
await HumanBehavior.reading_delay_async(500)
print("✓ Reading delay complete")
# Random mouse movements
await HumanBehavior.random_mouse_movement_async(page)
print("✓ Random mouse movements complete")
# Type in search box
search_box = page.locator('input[name="q"]')
count = await search_box.count()
if count > 0:
await HumanBehavior.type_text_async(
page, selector='input[name="q"]', text="test search"
)
print("✓ Human-like typing complete")
print("✓ Demo complete")
await browser.close()
asyncio.run(demo())
return 0
def parse_args():
"""Parse command line arguments."""
import argparse
parser = argparse.ArgumentParser(description="HumanBehavior demo")
return parser.parse_args()
def run_main() -> None:
"""Initialize scitex framework, run main function, and cleanup."""
global CONFIG, CC, sys, plt, rng
import sys
import matplotlib.pyplot as plt
import scitex as stx
args = parse_args()
CONFIG, sys.stdout, sys.stderr, plt, CC, rng = stx.session.start(
sys,
plt,
args=args,
file=__FILE__,
sdir_suffix=None,
verbose=False,
agg=True,
)
exit_status = main(args)
stx.session.close(
CONFIG,
verbose=False,
notify=False,
message="",
exit_status=exit_status,
)
if __name__ == "__main__":
run_main()
# python -m scitex_browser.stealth.HumanBehavior
# EOF