Skip to content

yellow-dog-dog/humanwright

Repository files navigation

humanwright

A toolkit for human-like behaviour in Playwright — making automation look like a real person rather than a script. Works with both the async and sync Playwright APIs.

The first behaviour is scrolling: instead of a robotic element.scroll_into_view_if_needed() jump, it spins the real mouse wheel with an accelerate → cruise → decelerate motion profile, breaks long scrolls into human-sized segments, pauses to "read", and occasionally scrolls back up to re-read — the way a person actually browses. More behaviours (mouse movement, cursor visualisation, …) will land alongside it under the same API.

Extracted and cleaned up from a production AdSense automation project.

Install

uv add humanwright            # or: pip install humanwright
playwright install chromium   # once, if you haven't already

Quick start

Async

from playwright.async_api import async_playwright
from humanwright.async_api import scroll_into_view, scroll_to_bottom

async with async_playwright() as p:
    page = await (await p.chromium.launch()).new_page()
    await page.goto("https://example.com")

    await scroll_to_bottom(page, speed="medium")          # wheel down the page
    await scroll_into_view(page, page.locator("#cta"))    # bring an element up

Sync

from playwright.sync_api import sync_playwright
from humanwright.sync_api import scroll_into_view, scroll_to_bottom

with sync_playwright() as p:
    page = p.chromium.launch().new_page()
    page.goto("https://example.com")

    scroll_to_bottom(page, speed="medium")
    scroll_into_view(page, page.locator("#cta"))

API

Both humanwright.async_api and humanwright.sync_api expose the same names (the async versions are coroutines):

Function Purpose
scroll_by(page, distance, *, speed, config) One continuous eased scroll of distance px (negative = up).
scroll_by_steps(page, distance, *, speed, config, back_read, avg_segment_height) Segmented scroll with reading pauses + optional re-read. Returns True if the bottom was hit.
scroll_into_view(page, element, *, speed, config) Wheel an element (Locator or ElementHandle) into the viewport.
scroll_to_bottom(page, *, speed, config, max_rounds) Step down until the page bottom is reached.
scroll_height(page) scrollHeight - innerHeight.
is_at_bottom(page, config) Within bottom_threshold px of the bottom?
viewport_size(page) {"width", "height"} of the viewport.

Speed

speed is one of "faster", "fast", "medium", "slow", or "random" (default — picks one of fast/medium/slow per call).

Tuning with ScrollConfig

Every behaviour is configurable. The defaults reproduce the original project's (deliberately slow, "really reading") behaviour — you'll usually want shorter pauses:

from humanwright import ScrollConfig

cfg = ScrollConfig(
    pause=(0.4, 1.2),            # reading pause between segments (default 5-20s!)
    back_read_probability=0.05,  # how often to scroll back up and re-read
    segment_height_range=(300, 600),
)
await scroll_into_view(page, locator, config=cfg)

For reproducible motion (e.g. in tests) inject a seeded RNG:

import random
cfg = ScrollConfig(rng=random.Random(42))

How it works

  1. generate_swipe_path() produces a stream of (wheel_delta, delay) ticks whose cumulative motion lands on the target, accelerating then easing off.
  2. segment_distances() chops a long scroll into slightly-varied chunks that sum exactly to the target.
  3. The async/sync executors replay those ticks through page.mouse.wheel, adding reading pauses and the occasional back-scroll.

The algorithm is pure Python in humanwright._core (no browser), so it's unit tested without launching Chromium.

Notes vs. the original

This extraction fixes two bugs in the source helper:

  • the segment-execution loop was nested inside the sum-reconciliation loop, causing duplicated scrolling;
  • a fractional distance could make the reconciliation while loop spin forever (integer segments can never sum to a float). Targets are now truncated to int.

Development

This project is managed with uv.

uv sync --extra dev   # create .venv and install everything
uv run pytest         # core algorithm tests (no browser needed)
uv run ruff check .
uv run mypy src
uv build              # build wheel + sdist

License

MIT

About

Human-like behaviour for Playwright -- eased mouse-wheel scrolling, segmentation, reading pauses (more behaviours to come).

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages