human-input-kit 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ """Idle micro-movements."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING
7
+
8
+ from human_input_kit.bezier import bezier_mouse_path, move_mouse_along
9
+ from human_input_kit.rng import get_rng
10
+
11
+ if TYPE_CHECKING:
12
+ from playwright.async_api import Page
13
+
14
+
15
+ async def idle_jitter(
16
+ page: Page,
17
+ *,
18
+ origin: tuple[float, float] | None = None,
19
+ moves: int = 4,
20
+ radius: float = 18.0,
21
+ ) -> None:
22
+ """Small bezier moves around a point to mimic idle hand movement."""
23
+ rng = get_rng()
24
+ viewport = page.viewport_size
25
+ if origin is None:
26
+ width = viewport["width"] if viewport else 1280
27
+ height = viewport["height"] if viewport else 720
28
+ current = (width / 2, height / 2)
29
+ else:
30
+ current = origin
31
+
32
+ await page.mouse.move(current[0], current[1])
33
+ for _ in range(moves):
34
+ target = (
35
+ current[0] + rng.uniform(-radius, radius),
36
+ current[1] + rng.uniform(-radius, radius),
37
+ )
38
+ path = bezier_mouse_path(current, target, steps=8, spread=0.2)
39
+ await move_mouse_along(page, path, delay_ms=rng.uniform(6, 14))
40
+ current = target
41
+ await asyncio.sleep(rng.uniform(0.1, 0.35))
@@ -0,0 +1,31 @@
1
+ """Human-like typing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING
7
+
8
+ from human_input_kit.rng import get_rng
9
+
10
+ if TYPE_CHECKING:
11
+ from playwright.async_api import Page
12
+
13
+
14
+ async def human_type(
15
+ page: Page,
16
+ selector: str,
17
+ text: str,
18
+ *,
19
+ min_delay_ms: int = 45,
20
+ max_delay_ms: int = 180,
21
+ pause_chance: float = 0.08,
22
+ ) -> None:
23
+ """Type text with variable per-character delays and occasional pauses."""
24
+ rng = get_rng()
25
+ await page.click(selector)
26
+ for char in text:
27
+ await page.keyboard.type(char, delay=0)
28
+ delay = rng.randint(min_delay_ms, max_delay_ms) / 1000.0
29
+ await asyncio.sleep(delay)
30
+ if rng.random() < pause_chance:
31
+ await asyncio.sleep(rng.uniform(0.15, 0.45))
@@ -0,0 +1,90 @@
1
+ """Record gestures from a Playwright page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import TYPE_CHECKING
8
+
9
+ from human_input_kit.gestures import GestureEvent, GestureRecording
10
+ from human_input_kit.idle import idle_jitter
11
+ from human_input_kit.rng import get_rng
12
+ from human_input_kit.scroll import random_scroll
13
+
14
+ if TYPE_CHECKING:
15
+ from playwright.async_api import Page
16
+
17
+ RECORD_INIT_SCRIPT = """
18
+ () => {
19
+ window.__humanInputEvents = [];
20
+ const push = (event) => window.__humanInputEvents.push(event);
21
+ const t0 = performance.now();
22
+ const now = () => (performance.now() - t0) / 1000;
23
+ window.addEventListener("mousemove", (e) => {
24
+ push({ type: "move", t: now(), x: e.clientX, y: e.clientY });
25
+ }, { passive: true });
26
+ window.addEventListener("wheel", (e) => {
27
+ push({ type: "scroll", t: now(), dx: e.deltaX, dy: e.deltaY });
28
+ }, { passive: true });
29
+ window.addEventListener("keydown", (e) => {
30
+ if (e.key.length === 1) {
31
+ push({ type: "type", t: now(), text: e.key });
32
+ }
33
+ }, { passive: true });
34
+ }
35
+ """
36
+
37
+
38
+ async def record_gestures(
39
+ page: Page,
40
+ *,
41
+ duration: float = 30.0,
42
+ warmup_actions: bool = True,
43
+ ) -> GestureRecording:
44
+ """Record browser input events for a duration (seconds)."""
45
+ await page.evaluate(RECORD_INIT_SCRIPT)
46
+ start = time.monotonic()
47
+
48
+ if warmup_actions:
49
+ rng = get_rng()
50
+ await random_scroll(page, bursts=rng.randint(2, 4))
51
+ await idle_jitter(page, moves=rng.randint(2, 4))
52
+
53
+ while time.monotonic() - start < duration:
54
+ await asyncio.sleep(0.1)
55
+
56
+ raw = await page.evaluate("() => window.__humanInputEvents || []")
57
+ events: list[GestureEvent] = []
58
+ if isinstance(raw, list):
59
+ for item in raw:
60
+ if not isinstance(item, dict) or "type" not in item:
61
+ continue
62
+ event_type = item["type"]
63
+ if event_type not in {"move", "scroll", "type"}:
64
+ continue
65
+ events.append(
66
+ GestureEvent(
67
+ type=event_type,
68
+ t=float(item.get("t", 0)),
69
+ x=item.get("x"),
70
+ y=item.get("y"),
71
+ dx=item.get("dx"),
72
+ dy=item.get("dy"),
73
+ text=item.get("text"),
74
+ )
75
+ )
76
+
77
+ return GestureRecording(url=page.url, events=_thin_moves(events))
78
+
79
+
80
+ def _thin_moves(events: list[GestureEvent], *, min_gap_s: float = 0.05) -> list[GestureEvent]:
81
+ """Reduce noisy mousemove streams for compact replay files."""
82
+ thinned: list[GestureEvent] = []
83
+ last_move_t = -1.0
84
+ for event in events:
85
+ if event.type == "move":
86
+ if event.t - last_move_t < min_gap_s:
87
+ continue
88
+ last_move_t = event.t
89
+ thinned.append(event)
90
+ return thinned
@@ -0,0 +1,57 @@
1
+ """Replay recorded gestures on a Playwright page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from human_input_kit.bezier import bezier_mouse_path, move_mouse_along
10
+ from human_input_kit.gestures import GestureRecording, load_recording
11
+
12
+ if TYPE_CHECKING:
13
+ from playwright.async_api import Page
14
+
15
+
16
+ async def replay_gestures(
17
+ page: Page,
18
+ recording: GestureRecording,
19
+ *,
20
+ speed: float = 1.0,
21
+ ) -> None:
22
+ """Replay a gesture recording with original timing."""
23
+ if recording.url and recording.url != "about:blank":
24
+ await page.goto(recording.url, wait_until="domcontentloaded")
25
+
26
+ last_t = 0.0
27
+ last_pos: tuple[float, float] | None = None
28
+ typed_buffer = ""
29
+
30
+ for event in recording.events:
31
+ wait_s = max(0.0, (event.t - last_t) / speed)
32
+ if wait_s:
33
+ await asyncio.sleep(wait_s)
34
+ last_t = event.t
35
+
36
+ if event.type == "move" and event.x is not None and event.y is not None:
37
+ target = (event.x, event.y)
38
+ if last_pos is None:
39
+ await page.mouse.move(target[0], target[1])
40
+ else:
41
+ path = bezier_mouse_path(last_pos, target, steps=12)
42
+ await move_mouse_along(page, path, delay_ms=6)
43
+ last_pos = target
44
+ elif event.type == "scroll":
45
+ await page.mouse.wheel(event.dx or 0, event.dy or 0)
46
+ elif event.type == "type" and event.text:
47
+ typed_buffer += event.text
48
+ elif event.type == "wait" and event.duration_ms:
49
+ await asyncio.sleep(event.duration_ms / 1000.0 / speed)
50
+
51
+ if typed_buffer:
52
+ await page.keyboard.type(typed_buffer, delay=35)
53
+
54
+
55
+ async def replay_file(page: Page, path: Path | str, *, speed: float = 1.0) -> None:
56
+ """Load and replay a validated gestures JSON file."""
57
+ await replay_gestures(page, load_recording(path, validate=True), speed=speed)
human_input_kit/rng.py ADDED
@@ -0,0 +1,16 @@
1
+ """Seeded random number generator for reproducible human-like variance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+
7
+ _rng = random.Random(42)
8
+
9
+
10
+ def seed_rng(seed: int) -> None:
11
+ """Set the global RNG seed for deterministic behavior in tests."""
12
+ _rng.seed(seed)
13
+
14
+
15
+ def get_rng() -> random.Random:
16
+ return _rng
@@ -0,0 +1,28 @@
1
+ """Human-like scrolling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING
7
+
8
+ from human_input_kit.rng import get_rng
9
+
10
+ if TYPE_CHECKING:
11
+ from playwright.async_api import Page
12
+
13
+
14
+ async def random_scroll(
15
+ page: Page,
16
+ *,
17
+ bursts: int = 3,
18
+ min_delta: int = 120,
19
+ max_delta: int = 520,
20
+ ) -> None:
21
+ """Perform several scroll wheel bursts with random deltas and pauses."""
22
+ rng = get_rng()
23
+ for _ in range(bursts):
24
+ delta_y = rng.randint(min_delta, max_delta)
25
+ if rng.random() < 0.2:
26
+ delta_y *= -1
27
+ await page.mouse.wheel(0, delta_y)
28
+ await asyncio.sleep(rng.uniform(0.25, 0.9))
@@ -0,0 +1,143 @@
1
+ """Playwright sync API — human-like input helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from human_input_kit.bezier import Point, bezier_mouse_path
11
+ from human_input_kit.gestures import GestureRecording, load_recording
12
+ from human_input_kit.rng import get_rng
13
+
14
+ if TYPE_CHECKING:
15
+ from playwright.sync_api import Page
16
+
17
+ __all__ = [
18
+ "bezier_mouse_path",
19
+ "human_type",
20
+ "idle_jitter",
21
+ "move_mouse_along",
22
+ "random_scroll",
23
+ "replay_file",
24
+ "replay_gestures",
25
+ ]
26
+
27
+
28
+ def move_mouse_along(page: Page, path: Sequence[Point], *, delay_ms: float = 8.0) -> None:
29
+ """Move Playwright sync mouse along a bezier path."""
30
+ for x, y in path:
31
+ page.mouse.move(x, y)
32
+ if delay_ms > 0:
33
+ time.sleep(delay_ms / 1000.0)
34
+
35
+
36
+ def human_type(
37
+ page: Page,
38
+ selector: str,
39
+ text: str,
40
+ *,
41
+ min_delay_ms: int = 45,
42
+ max_delay_ms: int = 180,
43
+ pause_chance: float = 0.08,
44
+ ) -> None:
45
+ """Type text with variable per-character delays and occasional pauses."""
46
+ rng = get_rng()
47
+ page.click(selector)
48
+ for char in text:
49
+ page.keyboard.type(char, delay=0)
50
+ delay = rng.randint(min_delay_ms, max_delay_ms) / 1000.0
51
+ time.sleep(delay)
52
+ if rng.random() < pause_chance:
53
+ time.sleep(rng.uniform(0.15, 0.45))
54
+
55
+
56
+ def random_scroll(
57
+ page: Page,
58
+ *,
59
+ bursts: int = 3,
60
+ min_delta: int = 120,
61
+ max_delta: int = 520,
62
+ ) -> None:
63
+ """Perform several scroll wheel bursts with random deltas and pauses."""
64
+ rng = get_rng()
65
+ for _ in range(bursts):
66
+ delta_y = rng.randint(min_delta, max_delta)
67
+ if rng.random() < 0.2:
68
+ delta_y *= -1
69
+ page.mouse.wheel(0, delta_y)
70
+ time.sleep(rng.uniform(0.25, 0.9))
71
+
72
+
73
+ def idle_jitter(
74
+ page: Page,
75
+ *,
76
+ origin: tuple[float, float] | None = None,
77
+ moves: int = 4,
78
+ radius: float = 18.0,
79
+ ) -> None:
80
+ """Small bezier moves around a point to mimic idle hand movement."""
81
+ rng = get_rng()
82
+ viewport = page.viewport_size
83
+ if origin is None:
84
+ width = viewport["width"] if viewport else 1280
85
+ height = viewport["height"] if viewport else 720
86
+ current = (width / 2, height / 2)
87
+ else:
88
+ current = origin
89
+
90
+ page.mouse.move(current[0], current[1])
91
+ for _ in range(moves):
92
+ target = (
93
+ current[0] + rng.uniform(-radius, radius),
94
+ current[1] + rng.uniform(-radius, radius),
95
+ )
96
+ path = bezier_mouse_path(current, target, steps=8, spread=0.2)
97
+ move_mouse_along(page, path, delay_ms=rng.uniform(6, 14))
98
+ current = target
99
+ time.sleep(rng.uniform(0.1, 0.35))
100
+
101
+
102
+ def replay_gestures(
103
+ page: Page,
104
+ recording: GestureRecording,
105
+ *,
106
+ speed: float = 1.0,
107
+ ) -> None:
108
+ """Replay a gesture recording with original timing."""
109
+ if recording.url and recording.url != "about:blank":
110
+ page.goto(recording.url, wait_until="domcontentloaded")
111
+
112
+ last_t = 0.0
113
+ last_pos: tuple[float, float] | None = None
114
+ typed_buffer = ""
115
+
116
+ for event in recording.events:
117
+ wait_s = max(0.0, (event.t - last_t) / speed)
118
+ if wait_s:
119
+ time.sleep(wait_s)
120
+ last_t = event.t
121
+
122
+ if event.type == "move" and event.x is not None and event.y is not None:
123
+ target = (event.x, event.y)
124
+ if last_pos is None:
125
+ page.mouse.move(target[0], target[1])
126
+ else:
127
+ path = bezier_mouse_path(last_pos, target, steps=12)
128
+ move_mouse_along(page, path, delay_ms=6)
129
+ last_pos = target
130
+ elif event.type == "scroll":
131
+ page.mouse.wheel(event.dx or 0, event.dy or 0)
132
+ elif event.type == "type" and event.text:
133
+ typed_buffer += event.text
134
+ elif event.type == "wait" and event.duration_ms:
135
+ time.sleep(event.duration_ms / 1000.0 / speed)
136
+
137
+ if typed_buffer:
138
+ page.keyboard.type(typed_buffer, delay=35)
139
+
140
+
141
+ def replay_file(page: Page, path: Path | str, *, speed: float = 1.0) -> None:
142
+ """Load and replay a validated gestures JSON file."""
143
+ replay_gestures(page, load_recording(path, validate=True), speed=speed)
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.4
2
+ Name: human-input-kit
3
+ Version: 0.2.1
4
+ Summary: Human-like Playwright mouse, scroll, and typing — Bezier warmup with deterministic seeds. CLI: human-input.
5
+ Project-URL: Homepage, https://github.com/human-input-kit/human-input-kit
6
+ Project-URL: Documentation, https://github.com/human-input-kit/human-input-kit#readme
7
+ Project-URL: Repository, https://github.com/human-input-kit/human-input-kit
8
+ Project-URL: Issues, https://github.com/human-input-kit/human-input-kit/issues
9
+ Author: human-input-kit contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: behavior-replay,bezier-curve,bot-cadence,bot-detection,gesture-recording,human-behavior,human-like-mouse,idle-jitter,interaction-polish,mouse-movement,multilogin-warmup,playwright-input,playwright-scroll,playwright-warmup,scroll-simulation,typing-simulation,warmup-script
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: click>=8.1
27
+ Requires-Dist: playwright>=1.40
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.8; extra == 'dev'
32
+ Provides-Extra: mlx
33
+ Description-Content-Type: text/markdown
34
+
35
+ # human-input-kit
36
+
37
+ **Human-like Playwright mouse & scroll** — Bezier paths, typing delays, and warmup recipes with deterministic seeds.
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/human-input-kit.svg)](https://pypi.org/project/human-input-kit/)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/human-input-kit.svg)](https://pypi.org/project/human-input-kit/)
41
+ [![License: MIT](https://img.shields.io/pypi/l/human-input-kit.svg)](https://pypi.org/project/human-input-kit/)
42
+
43
+ ```bash
44
+ pip install human-input-kit
45
+ human-input --help
46
+ ```
47
+
48
+ CLI: **`human-input`** · Python **3.10+** · optional **`[mlx]`** for Launcher helpers
49
+
50
+ Human-like **mouse**, **scroll**, and **typing** helpers for Playwright — Bezier paths, variable delays, idle jitter, and gesture record/replay.
51
+
52
+ ## Problem
53
+
54
+ Raw `page.click()` and `page.keyboard.type()` bursts look mechanical. Warmup workflows need curved mouse paths, uneven scroll cadence, and typing pauses. Teams reinvent these snippets in every scraper.
55
+
56
+ `human-input-kit` packages the primitives and a small CLI for demo, record, and replay flows.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install human-input-kit
62
+ playwright install chromium
63
+ ```
64
+
65
+ Development:
66
+
67
+ ```bash
68
+ pip install human-input-kit[dev]
69
+ ```
70
+
71
+ ![Warmup demo](docs/assets/demo.gif)
72
+
73
+ > Placeholder: add a screen recording at `docs/assets/demo.gif` (not shipped in the wheel).
74
+
75
+ ## Quick start
76
+
77
+ ```bash
78
+ # Reproducible motion (default seed 42)
79
+ human-input --seed 42 demo-scroll --url https://news.ycombinator.com
80
+ human-input record --duration 30 -o gestures.json
81
+ human-input replay gestures.json # validates gesture JSON schema first
82
+
83
+ # Runnable warmup example
84
+ python examples/demo_warmup.py --seed 42
85
+
86
+ # Documented recipes (deterministic with --seed)
87
+ python recipes/scroll_news.py --seed 42
88
+ python recipes/youtube_idle.py --seed 42 --seconds 30 --headed
89
+ ```
90
+
91
+ **Sync Playwright API** (default package exports):
92
+
93
+ ```python
94
+ from human_input_kit import (
95
+ bezier_mouse_path,
96
+ human_type,
97
+ idle_jitter,
98
+ move_mouse_along,
99
+ random_scroll,
100
+ seed_rng,
101
+ )
102
+
103
+ seed_rng(42)
104
+ path = bezier_mouse_path((100, 100), (500, 300))
105
+ move_mouse_along(page, path) # sync Page
106
+ random_scroll(page, bursts=4)
107
+ idle_jitter(page)
108
+ human_type(page, "input[name='q']", "playwright warmup")
109
+ ```
110
+
111
+ **Async Playwright API** (`human_input_kit.async_`):
112
+
113
+ ```python
114
+ from human_input_kit import seed_rng
115
+ from human_input_kit import async_ as human_async
116
+
117
+ seed_rng(42)
118
+ path = human_async.bezier_mouse_path((100, 100), (500, 300))
119
+ await human_async.move_mouse_along(page, path)
120
+ await human_async.random_scroll(page, bursts=4)
121
+ ```
122
+
123
+ See `examples/demo_warmup.py` for a full async script.
124
+
125
+ ### Recipes
126
+
127
+ Copy-paste scripts in [`recipes/`](recipes/) for common warmup patterns. Each accepts `--seed` so demo motion is reproducible in CI and screen recordings.
128
+
129
+ | Recipe | Command | Behavior |
130
+ |--------|---------|----------|
131
+ | News scroll | `python recipes/scroll_news.py --seed 42` | Bezier drift → scroll bursts → idle jitter on a news feed |
132
+ | YouTube idle | `python recipes/youtube_idle.py --seed 42 --headed` | Idle micro-moves + rare scroll on `youtube.com` |
133
+
134
+ Options: `--url`, `--headed` (both); `--bursts` (scroll_news); `--seconds` (youtube_idle).
135
+
136
+ ## MLX warmup (single profile)
137
+
138
+ `human-input-kit` does **not** bundle MLX or Launcher clients. For one-off MLX profile warmup, follow the **cdp-connect-kit** peer pattern:
139
+
140
+ ```bash
141
+ pip install human-input-kit
142
+ pip install cdp-connect-kit[mlx] # peer dependency — not required by human-input-kit
143
+ playwright install chromium
144
+
145
+ export MLX_TOKEN=your_bearer_token
146
+ export MLX_FOLDER_ID=your-folder-uuid
147
+
148
+ # Pseudocode demo with TODO markers for Launcher start/stop:
149
+ python examples/mlx_warmup_demo.py --profile-id PROFILE_UUID
150
+
151
+ # Or warmup an already-started profile (CDP_URL from Launcher):
152
+ CDP_URL=http://127.0.0.1:55513 python examples/mlx_warmup_demo.py --profile-id PROFILE_UUID
153
+ ```
154
+
155
+ `examples/mlx_warmup_demo.py` documents: Launcher start → `connect_over_cdp` → Bezier scroll/jitter warmup → Launcher stop. Set `HUMAN_INPUT_KIT_MLX_DEMO=1` after installing `cdp-connect-kit[mlx]` to enable the live Launcher calls.
156
+
157
+ Optional marker extra (installs nothing): `pip install human-input-kit[mlx]`
158
+
159
+ ## CLI
160
+
161
+ | Command | Description |
162
+ |---------|-------------|
163
+ | `human-input demo-scroll --url URL` | Scroll + idle jitter demo |
164
+ | `human-input record --duration SEC -o FILE` | Record gestures JSON |
165
+ | `human-input replay FILE` | Replay saved gestures (schema-validated) |
166
+ | `human-input --seed N` | Global RNG seed on all subcommands (default `42`) |
167
+
168
+ ## API
169
+
170
+ | Module | Use with |
171
+ |--------|----------|
172
+ | `human_input_kit` | Playwright **sync** `Page` |
173
+ | `human_input_kit.async_` | Playwright **async** `Page` |
174
+ | `human_input_kit.bezier` | Path math only (`bezier_mouse_path`) |
175
+
176
+ | Function | Description |
177
+ |----------|-------------|
178
+ | `bezier_mouse_path(start, end, steps=25)` | Cubic Bezier point list (sync, fast) |
179
+ | `move_mouse_along(page, path)` | Animate mouse along path |
180
+ | `human_type(page, selector, text)` | Variable per-char typing delays |
181
+ | `random_scroll(page, bursts=3)` | Random wheel bursts with pauses |
182
+ | `idle_jitter(page, moves=4)` | Small idle hand movements |
183
+ | `seed_rng(n)` | Deterministic variance for tests/demos |
184
+ | `load_recording(path, validate=True)` | Load gestures JSON with schema check |
185
+
186
+ ### Gesture format
187
+
188
+ ```json
189
+ {
190
+ "version": 1,
191
+ "url": "https://news.ycombinator.com",
192
+ "events": [
193
+ {"type": "move", "t": 0.0, "x": 120, "y": 80},
194
+ {"type": "scroll", "t": 1.2, "dy": 320}
195
+ ]
196
+ }
197
+ ```
198
+
199
+ ## Behavioral mimicry vs profile isolation
200
+
201
+ Human-like mouse paths and scroll timing can reduce **obvious automation cadence**, but behavioral mimicry alone is **necessary and insufficient** for production multi-account workflows.
202
+
203
+ Detection stacks also correlate:
204
+
205
+ - Browser fingerprint consistency (UA, WebGL, Client Hints)
206
+ - IP / proxy reputation
207
+ - Cookie and storage isolation
208
+ - TLS and network signals
209
+
210
+ Use `human-input-kit` for **warmup and interaction polish** inside an already-isolated profile (dedicated browser profile, proxy, and storage boundary). Pair with [fingerprint-coherence](https://pypi.org/project/fingerprint-coherence/) and [playwright-cdp-probe](https://pypi.org/project/playwright-cdp-probe/) for profile-level QA.
211
+
212
+ ## When mechanical clicks still get flagged (playbook)
213
+
214
+ Behavior polish helps **cadence** — not fingerprint or IP. Typical order of operations:
215
+
216
+ | Signal still failing | Tool | Action |
217
+ |----------------------|------|--------|
218
+ | Teleport mouse / constant intervals | `human-input-kit` | Bezier paths, `--seed` for reproducible demos |
219
+ | UA / screen / timezone mismatch | [fingerprint-coherence](https://pypi.org/project/fingerprint-coherence/) | Audit profile before warmup |
220
+ | `navigator.webdriver` / headless leaks | [playwright-cdp-probe](https://pypi.org/project/playwright-cdp-probe/) | Compare vanilla vs Launcher CDP |
221
+ | One profile ok, N profiles not | `automation-farm-runner` | `farm-runner mlx-pool` + warmup script reading `CDP_URL` |
222
+
223
+ **Warmup pipeline (single profile):**
224
+
225
+ ```bash
226
+ export MLX_TOKEN=... MLX_FOLDER_ID=...
227
+ # cdp-connect-kit starts profile → CDP_URL
228
+ CDP_URL=http://127.0.0.1:PORT python examples/mlx_warmup_demo.py --profile-id UUID
229
+ human-input --seed 42 demo-scroll --url https://news.ycombinator.com
230
+ cdp-probe mlx --profile-id UUID --url https://example.com
231
+ ```
232
+
233
+ **Warmup at scale:** farm job imports `human_input_kit`, reads `CDP_URL` / `PROFILE_ID` from env — gestures stay in this package; queueing stays in `farm-runner`.
234
+
235
+ ## Limitations
236
+
237
+ - **Playwright only** — APIs expect a Playwright `Page`.
238
+ - **Record mode** captures DOM events + scripted warmup; not a full OS-level input recorder.
239
+ - **Not undetectable** — reduces mechanical patterns; does not spoof fingerprints or network identity.
240
+ - **Headless motion** — some sites behave differently in headless Chromium.
241
+
242
+ ## Production
243
+
244
+ For **many profiles** (folder pool, cron, concurrency), use **`farm-runner`** (`automation-farm-runner`) with a warmup script that imports `human_input_kit` and reads `CDP_URL` / `PROFILE_ID`. This package is the gesture library; the farm runner owns MLX pool lifecycle.
245
+
246
+ **Partner note (affiliate):** Human-like input pays off **inside** isolated Multilogin X profiles (proxy + fingerprint already matched) — not on leaky vanilla Chromium. If folder-scale warmup is your bottleneck, eligible new browser purchases may accept code **`SAAS50`** at [multilogin.com](https://multilogin.com/?ref=SAAS50) — verify terms before checkout. Demos and recipes work without MLX. Full disclosure: [docs/AFFILIATE.md](docs/AFFILIATE.md). Coupon details only via `human-input --show-deal` (never printed by default).
247
+
248
+ **FAQ:** [docs/FAQ.md](docs/FAQ.md) — human-like mouse movement, Playwright scroll warmup, bot detection cadence.
249
+
250
+
251
+
252
+
253
+
254
+ ## Related tools (on PyPI)
255
+
256
+ | Package | CLI | Role |
257
+ |---------|-----|------|
258
+ | [playwright-cdp-probe](https://pypi.org/project/playwright-cdp-probe/) | `cdp-probe` | CDP / WebDriver exposure score |
259
+ | [cookie-jar-bridge](https://pypi.org/project/cookie-jar-bridge/) | `cookie-bridge` | Netscape ↔ Playwright cookies |
260
+ | [proxy-lane-checker](https://pypi.org/project/proxy-lane-checker/) | `proxy-lane` | Proxy TCP / HTTP / geo / DNSBL |
261
+ | [fingerprint-coherence](https://pypi.org/project/fingerprint-coherence/) | `fp-coherence` | UA / screen / timezone lint |
262
+
263
+ **Toolkit pipeline:** `proxy-lane check` → `fp-coherence audit` → automate → `cdp-probe run` → `cookie-bridge validate`
264
+
265
+ ## License
266
+
267
+ MIT
268
+
269
+ ---
270
+
271
+ **Production antidetect:** [Multilogin X](https://multilogin.com/?ref=SAAS50) · Code `SAAS50` (-50% browser) · `MIN50` (-50% cloud phone)
272
+ More scripts: [@Multilogin_Scripts_Bot](https://t.me/Multilogin_Scripts_Bot) · [Coupons](https://anti-detect.github.io/)