Skip to content

Commit 84d7da7

Browse files
committed
v2.1.0
1 parent f752aff commit 84d7da7

File tree

6 files changed

+258
-165
lines changed

6 files changed

+258
-165
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2024 Vladislav Zenkevich
1+
Copyright (c) 2024-2025 Vladislav Zenkevich
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy
44
of this software and associated documentation files (the "Software"), to deal

README.MD

Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
11
# 🤖 Emunium
22

3-
A Python module for automating interactions to mimic human behavior in standalone apps or browsers when using Selenium, Pyppeteer, or Playwright. Provides utilities to programmatically move the mouse cursor, click on page elements, type text, and scroll as if performed by a human user.
4-
3+
Emunium is a Python module that helps you automate interactions in a human-like way. It works with standalone applications or browsers when using Selenium, Pyppeteer, or Playwright. Emunium makes the mouse movements, clicks, typing, and scrolling appear more natural, which can help your tests avoid detection.
54

65
![Emunium preview](https://raw.githubusercontent.com/DedInc/emunium/main/preview.gif)
76

7+
---
88

99
## 🚀 Quickstart (Standalone)
1010

11+
Below is a basic example that shows how to search for an image on your screen, type some text, and click a button. This example uses standalone mode.
12+
1113
```python
12-
from emunium import Emunium
14+
from emunium import Emunium, ClickType
1315

16+
# Create an instance of Emunium
1417
emunium = Emunium()
1518

19+
# Find a text field on the screen using an image of the field
1620
elements = emunium.find_elements('field.png', min_confidence=0.8)
1721

22+
# Type into the first found element
1823
emunium.type_at(elements[0], 'Automating searches')
1924

25+
# Find the search icon using an image and click it
2026
elements = emunium.find_elements('search_icon.png', min_confidence=0.8)
2127
emunium.click_at(elements[0])
2228
```
2329

30+
---
31+
32+
## 🔍 OCR Text Search (only in Standalone)
33+
34+
Emunium can also search for text on the screen using Optical Character Recognition (OCR). To use this feature, create your Emunium instance with OCR enabled. This uses [EasyOCR](https://github.com/JaidedAI/EasyOCR) under the hood.
35+
36+
### How It Works
37+
38+
The new `find_text_elements()` method scans the screen for text that matches your query. You can adjust the minimum confidence and limit the number of results.
39+
40+
### Example
41+
42+
```python
43+
from emunium import Emunium
44+
45+
# Create an Emunium instance with OCR enabled.
46+
emunium = Emunium(ocr=True, use_gpu=True, langs=['en']) # use_gpu is default True, langs is default ['en'], ocr is default False
47+
48+
# Search for text that contains the word "Submit"
49+
text_elements = emunium.find_text_elements('Submit', min_confidence=0.8) # min_confidence is default 0.8
50+
51+
# If the text is found, click on the first occurrence.
52+
if text_elements:
53+
emunium.click_at(text_elements[0])
54+
```
55+
56+
*Note:* Make sure you have EasyOCR installed by running `pip install easyocr` before using the OCR feature.
57+
58+
---
59+
60+
Quickstarts for one of more cases. The code below opens DuckDuckGo, types a query, and clicks the search button.
61+
2462
## 🚀 Quickstart (with Selenium)
2563

2664
```python
@@ -36,16 +74,19 @@ emunium = EmuniumSelenium(driver)
3674

3775
driver.get('https://duckduckgo.com/')
3876

77+
# Wait for the search field to be clickable and type your query
3978
element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-state="suggesting"]')))
40-
4179
emunium.type_at(element, 'Automating searches')
4280

81+
# Find and click the search button
4382
submit = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '[aria-label="Search"]')))
4483
emunium.click_at(submit)
4584

4685
driver.quit()
4786
```
4887

88+
---
89+
4990
## 🚀 Quickstart (with Pyppeteer)
5091

5192
```python
@@ -68,11 +109,14 @@ async def main():
68109

69110
await browser.close()
70111

71-
asyncio.get_event_loop().run_until_complete(main())
112+
asyncio.run(main())
72113
```
73114

115+
---
116+
74117
## 🚀 Quickstart (with Playwright)
75118

119+
76120
```python
77121
import asyncio
78122
from playwright.async_api import async_playwright
@@ -97,63 +141,60 @@ async def main():
97141
asyncio.run(main())
98142
```
99143

100-
## 🖱️ Moving the Mouse
144+
---
101145

102-
The `move_to()` method moves the mouse cursor smoothly to the provided element with small randomizations in speed and path to seem human.
146+
## 🖱️ Mouse Movements and Clicks
103147

104-
Options:
105-
- `offset_x` and `offset_y` - offset mouse position from element center
148+
Emunium simulates natural mouse movements and clicks:
106149

107-
## 🖱️ Clicking Elements
150+
- **Moving the Mouse:**
151+
The `move_to()` method moves the cursor smoothly to the target position. You can add small random offsets for a more human-like behavior.
108152

109-
The `click_at()` method moves via `move_to()` and clicks at the center of the provided element.
153+
- **Clicking Elements:**
154+
Use `click_at()` to click on an element after moving to it. You can specify the type of click (left, right, middle, or double):
110155

111-
Emunium supports multiple mouse click types:
156+
```python
157+
from emunium import ClickType
112158

113-
```python
114-
from emunium import ClickType
115-
116-
emunium.click_at(element) # left click
117-
emunium.click_at(element, ClickType.RIGHT) # right click
118-
emunium.click_at(element, ClickType.MIDDLE) # middle click
119-
emunium.click_at(element, ClickType.DOUBLE) # double click
120-
```
159+
emunium.click_at(element) # left click
160+
emunium.click_at(element, ClickType.RIGHT) # right click
161+
emunium.click_at(element, ClickType.MIDDLE) # middle click
162+
emunium.click_at(element, ClickType.DOUBLE) # double click
163+
```
121164

122-
## 🔎 Finding Elements
165+
---
123166

124-
In standalone mode, Emunium can locate elements on the screen using image matching with the `find_elements` method:
167+
## 🔎 Finding Elements on the Screen (only in Standalone)
125168

126-
```python
127-
elements = emunium.find_elements('search_icon.png', min_confidence=0.8)
128-
```
169+
Emunium uses image matching to find elements:
129170

130-
The `find_elements` method takes the following parameters:
171+
- **find_elements():**
172+
Locate elements on the screen using an image file.
131173

132-
- `image_path` (required): The path to the image file to search for on the screen.
133-
- `min_confidence` (optional, default 0.8): The minimum confidence level (between 0 and 1) for image matching. Higher values result in more precise matching but may miss some elements.
134-
- `target_height` (optional): The expected height of the elements to find. If provided along with `target_width`, elements that don't match the specified size (within a tolerance based on `min_confidence`) will be filtered out.
135-
- `target_width` (optional): The expected width of the elements to find. Must be provided together with `target_height`.
136-
- `max_elements` (optional, default 0): The maximum number of elements to return. If set to 0 or not provided, all matching elements will be returned.
174+
```python
175+
elements = emunium.find_elements('search_icon.png', min_confidence=0.8)
176+
```
137177

138-
The `find_elements` method returns a list of dictionaries, each containing the 'x' and 'y' coordinates of the center point of a matched element.
178+
You can also set target sizes and limit the number of elements found.
139179

180+
---
140181

141182
## ⌨️ Typing Text
142183

143-
The `type_at()` method moves to the provided element via `move_to()`, clicks it via `click_to()`, and types the provided text in a "silent" way, spreading out key presses over time with small randomizations to mimic human typing.
184+
The `type_at()` method moves to an element, clicks on it, and types text in a "silent" way. This method mimics human typing by spreading out key presses with small, random delays.
144185

145-
Options:
146-
- `characters_per_minute` - typing speed in characters per minute (default 280)
147-
- `offset` - randomization (threshold) in milliseconds between key presses (default 20ms)
186+
Options include:
187+
- `characters_per_minute`: Typing speed (default is 280 CPM).
188+
- `offset`: Random delay (default is 20ms).
189+
190+
---
148191

149192
## 📜 Scrolling Pages
150193

151-
The `scroll_to()` method scrolls the page to bring the provided element into view using smooth scrolling.
194+
The `scroll_to()` method scrolls smoothly to bring an element into view. It uses timeouts and checks to ensure smooth scrolling even when there are minor hiccups.
152195

153-
Includes timeouts and checks to handle issues with scrolling getting stuck.
196+
---
154197

155198
## 🏁 Conclusion
156199

157-
Emunium provides a set of utilities to help automate browser interactions in a more human-like way when using Selenium, Pyppeteer, or Playwright. By moving the mouse, clicking, typing, and scrolling in a less robotic fashion, tests can avoid detection and run more reliably.
158-
159-
While basic automation scripts can still get the job done, Emunium aims to make tests appear even more life-like. Using the randomizations and smooth behaviors it offers can be beneficial for automation projects that require avoiding detections.
200+
Emunium provides a set of easy-to-use tools for automating user interactions. Whether you need to automate clicks, type text, or even search for text on your screen using OCR, Emunium offers flexible solutions for both browser and standalone applications. Its human-like behavior helps make your tests more robust and less likely to be detected as automation.

emunium/base.py

Lines changed: 41 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,31 @@
1212
keyboard = None
1313

1414
import pyautogui
15-
import pyclick
16-
15+
from humancursor import SystemCursor
1716
from enum import Enum
1817

19-
20-
class ClickType(Enum):
21-
LEFT = 0
22-
RIGHT = 1
23-
MIDDLE = 2
24-
DOUBLE = 3
25-
26-
2718
def get_image_size(file_path):
2819
with open(file_path, "rb") as file:
2920
file.seek(16)
3021
width_bytes = file.read(4)
3122
height_bytes = file.read(4)
3223
width = struct.unpack(">I", width_bytes)[0]
3324
height = struct.unpack(">I", height_bytes)[0]
34-
return (
35-
width,
36-
height,
37-
)
25+
return (width, height)
3826

27+
class ClickType(Enum):
28+
LEFT = 0
29+
RIGHT = 1
30+
MIDDLE = 2
31+
DOUBLE = 3
3932

4033
class EmuniumBase:
4134
def __init__(self):
42-
self.clicker = pyclick.HumanClicker()
43-
self._extend_clicker()
35+
36+
self.cursor = SystemCursor()
4437
self.browser_offsets = ()
4538
self.browser_inner_window = ()
4639

47-
def _extend_clicker(self):
48-
def right_click(self):
49-
pyautogui.click(button="right")
50-
51-
def middle_click(self):
52-
pyautogui.click(button="middle")
53-
54-
def double_click(self):
55-
pyautogui.doubleClick()
56-
57-
self.clicker.right_click = right_click.__get__(self.clicker)
58-
self.clicker.middle_click = middle_click.__get__(self.clicker)
59-
self.clicker.double_click = double_click.__get__(self.clicker)
60-
6140
async def _get_browser_properties_if_not_found(self, screenshot_func):
6241
if not self.browser_offsets or not self.browser_inner_window:
6342
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
@@ -68,83 +47,64 @@ async def _get_browser_properties_if_not_found(self, screenshot_func):
6847
screenshot_func(temp_screen_path)
6948

7049
location = pyautogui.locateOnScreen(temp_screen_path, confidence=0.6)
71-
self.browser_offsets = (
72-
location.left,
73-
location.top,
74-
)
50+
if location is not None:
51+
self.browser_offsets = (location.left, location.top)
52+
else:
53+
self.browser_offsets = (0, 0)
7554
self.browser_inner_window = get_image_size(temp_screen_path)
7655
os.remove(temp_screen_path)
7756

7857
def _get_center(self, element_location, element_size):
79-
offset_to_screen_x, offset_to_screen_y = self.browser_offsets
58+
offset_to_screen_x, offset_to_screen_y = self.browser_offsets if self.browser_offsets else (0, 0)
8059
element_x = element_location["x"] + offset_to_screen_x
8160
element_y = element_location["y"] + offset_to_screen_y
82-
8361
centered_x = element_x + (element_size["width"] // 2)
8462
centered_y = element_y + (element_size["height"] // 2)
85-
8663
return {"x": centered_x, "y": centered_y}
8764

88-
def _move(
89-
self,
90-
center,
91-
offset_x=random.uniform(0.0, 1.5),
92-
offset_y=random.uniform(0.0, 1.5),
93-
):
94-
target_x, target_y = round(center["x"] + offset_x), round(
95-
center["y"] + offset_y
96-
)
65+
def _move(self, center, offset_x=None, offset_y=None):
66+
if offset_x is None:
67+
offset_x = random.uniform(0.0, 1.5)
68+
if offset_y is None:
69+
offset_y = random.uniform(0.0, 1.5)
70+
target_x = round(center["x"] + offset_x)
71+
target_y = round(center["y"] + offset_y)
72+
self.cursor.move_to([target_x, target_y])
9773

98-
current_x, current_y = pyautogui.position()
99-
distance = math.sqrt((target_x - current_x) ** 2 + (target_y - current_y) ** 2)
100-
101-
speed = max(
102-
random.uniform(0.3, 0.6),
103-
min(random.uniform(2.0, 2.5), distance / random.randint(500, 700)),
104-
)
105-
106-
self.clicker.move((target_x, target_y), speed)
107-
108-
def _click(self, click_type=ClickType.LEFT):
74+
def _click(self, coordinate, click_type=ClickType.LEFT, click_duration=0):
10975
if click_type == ClickType.LEFT:
110-
self.clicker.click()
76+
self.cursor.click_on(coordinate, click_duration=click_duration)
11177
elif click_type == ClickType.RIGHT:
112-
self.clicker.right_click()
78+
pyautogui.click(x=coordinate[0], y=coordinate[1], button="right")
11379
elif click_type == ClickType.MIDDLE:
114-
self.clicker.middle_click()
80+
pyautogui.click(x=coordinate[0], y=coordinate[1], button="middle")
11581
elif click_type == ClickType.DOUBLE:
116-
self.clicker.double_click()
11782

118-
def silent_type(self, text, characters_per_minute=280, offset=20):
119-
total_chars = len(text)
120-
time_per_char = 60 / characters_per_minute
83+
self.cursor.click_on(coordinate)
84+
time.sleep(0.1)
85+
self.cursor.click_on(coordinate)
12186

122-
for i, char in enumerate(text):
87+
def _silent_type(self, text, characters_per_minute=280, offset=20):
88+
time_per_char = 60 / characters_per_minute
89+
for char in text:
12390
randomized_offset = random.uniform(-offset, offset) / 1000
12491
delay = time_per_char + randomized_offset
125-
126-
# Update by Pranav (https://github.com/ps428)
127-
# keyboard.write used in silent_type needs sudo mode on Linux machines
128-
# This uses pyautogui.press instead of keyboard.write
12992
if keyboard is None:
13093
pyautogui.press(char)
13194
else:
13295
keyboard.write(char)
133-
13496
time.sleep(delay)
13597

136-
13798
def _scroll_smoothly_to_element(self, element_rect):
138-
window_width = self.browser_inner_window[0]
139-
window_height = self.browser_inner_window[1]
99+
if self.browser_inner_window:
100+
window_width, window_height = self.browser_inner_window
101+
else:
102+
screen_size = pyautogui.size()
103+
window_width, window_height = screen_size.width, screen_size.height
140104

141105
scroll_amount = element_rect["y"] - window_height // 2
142106
scroll_steps = abs(scroll_amount) // 100
143-
144-
if scroll_amount > 0:
145-
scroll_direction = -1
146-
else:
147-
scroll_direction = 1
107+
scroll_direction = -1 if scroll_amount > 0 else 1
148108

149109
for _ in range(scroll_steps):
150110
pyautogui.scroll(scroll_direction * 100)
@@ -154,3 +114,6 @@ def _scroll_smoothly_to_element(self, element_rect):
154114
if remaining_scroll != 0:
155115
pyautogui.scroll(scroll_direction * remaining_scroll)
156116
time.sleep(random.uniform(0.05, 0.1))
117+
118+
def drag_and_drop(self, start_coords, end_coords):
119+
self.cursor.drag_and_drop(start_coords, end_coords)

0 commit comments

Comments
 (0)