.
674 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 🎭 reCognizer
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | #### reCognizer is a free-to-use AI based [reCaptcha](https://developers.google.com/recaptcha) Solver.
Usable with an easy-to-use API, also available for Async and Sync Playwright.
You can pass almost any format into the Challenger, from full-page screenshots, only-captcha images and no-border images to single images in a list.
22 |
23 | #### Note: You Should use an undetected browser engine like [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python) or [Botright](https://github.com/Vinyzu/Botright) to solve the Captchas consistently.
reCaptcha detects normal Playwright easily and you probably wont get any successful solves despite correct recognitions.
24 |
25 | ---
26 |
27 | ## Install it from PyPI
28 |
29 | ```bash
30 | pip install recognizer
31 | ```
32 |
33 | ---
34 |
35 | ## Examples
36 |
37 | ### Possible Image Inputs
38 | 
39 |
40 | ### Example Solve Video (Good IP & Botright)
41 | https://github.com/Vinyzu/recognizer/assets/50874994/95a713e3-bb46-474b-994f-cb3dacae9279
42 |
43 | ---
44 |
45 | ## Basic Usage
46 |
47 | ```py
48 | # Only for Type-Hints
49 | from typing import TypeVar, Sequence, Union
50 | from pathlib import Path
51 | from os import PathLike
52 |
53 | accepted_image_types = TypeVar("accepted_image_types", Path, Union[PathLike[str], str], bytes, Sequence[Path], Sequence[Union[PathLike[str], str]], Sequence[bytes])
54 |
55 | # Real Code
56 | from recognizer import Detector
57 |
58 | detector = Detector(optimize_click_order=True)
59 |
60 | task_type: str = "bicycle"
61 | images: accepted_image_types = "recaptcha_image.png"
62 | area_captcha: bool = False
63 |
64 | response, coordinates = detector.detect(task_type, images, area_captcha=area_captcha)
65 | ```
66 |
67 | ---
68 |
69 | ## Playwright Usage
70 | ### Sync Playwright
71 |
72 | ```py
73 | from playwright.sync_api import sync_playwright, Playwright
74 | from recognizer.agents.playwright import SyncChallenger
75 |
76 |
77 | def run(playwright: Playwright):
78 | browser = playwright.chromium.launch()
79 | page = browser.new_page()
80 |
81 | challenger = SyncChallenger(page, click_timeout=1000)
82 | page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
83 |
84 | challenger.solve_recaptcha()
85 |
86 | browser.close()
87 |
88 |
89 | with sync_playwright() as playwright:
90 | run(playwright)
91 | ```
92 |
93 |
94 | ### Async Playwright
95 |
96 | ```py
97 | import asyncio
98 |
99 | from playwright.async_api import async_playwright, Playwright
100 | from recognizer.agents.playwright import AsyncChallenger
101 |
102 |
103 | async def run(playwright: Playwright):
104 | browser = await playwright.chromium.launch()
105 | page = await browser.new_page()
106 |
107 | challenger = AsyncChallenger(page, click_timeout=1000)
108 | await page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
109 |
110 | await challenger.solve_recaptcha()
111 |
112 | await browser.close()
113 |
114 |
115 | async def main():
116 | async with async_playwright() as playwright:
117 | await run(playwright)
118 |
119 |
120 | asyncio.run(main())
121 | ```
122 | ---
123 |
124 | ## Copyright and License
125 | © [Vinyzu](https://github.com/Vinyzu/)
126 |
127 | [GNU GPL](https://choosealicense.com/licenses/gpl-3.0/)
128 |
129 | (Commercial Usage is allowed, but source, license and copyright has to made available. reCaptcha Challenger does not provide and Liability or Warranty)
130 |
131 | ---
132 |
133 | ## Projects/AIs Used
134 | [YOLO11m-seg](https://github.com/ultralytics/ultralytics)
135 |
136 | [flavour/CLIP ViT-L/14](https://huggingface.co/flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K)
137 |
138 | [CIDAS/clipseg](https://huggingface.co/CIDAS/clipseg-rd64-refined)
139 | []()
140 |
141 | ## Thanks to
142 |
143 | [QIN2DIM](https://github.com/QIN2DIM) (For basic project structure)
144 |
145 | ---
146 |
147 | ## Disclaimer
148 |
149 | This repository is provided for **educational purposes only**. \
150 | No warranties are provided regarding accuracy, completeness, or suitability for any purpose. **Use at your own risk**—the authors and maintainers assume **no liability** for **any damages**, **legal issues**, or **warranty breaches** resulting from use, modification, or distribution of this code.\
151 | **Any misuse or legal violations are the sole responsibility of the user**.
152 |
153 | ---
154 |
155 | 
156 | 
157 | 
158 |
159 | [](https://discordapp.com/users/935224495126487150)
160 | [](https://ko-fi.com/vinyzu)
161 |
--------------------------------------------------------------------------------
/examples/demo_botright.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 |
5 | import botright
6 |
7 | from recognizer.agents.playwright import AsyncChallenger
8 |
9 |
10 | async def bytedance():
11 | # playwright install chromium
12 | # playwright install-deps chromium
13 | botright_client = await botright.Botright(headless=False)
14 | browser = await botright_client.new_browser()
15 | page = await browser.new_page()
16 | challenger = AsyncChallenger(page)
17 |
18 | await page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
19 | await challenger.solve_recaptcha()
20 | await botright_client.close()
21 |
22 |
23 | if __name__ == "__main__":
24 | asyncio.run(bytedance())
25 |
--------------------------------------------------------------------------------
/examples/demo_detect.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import cv2
4 | import matplotlib.pyplot as plt
5 | from imageio.v2 import imread
6 |
7 | from recognizer import Detector
8 |
9 | image_dir = Path(__file__).parent.joinpath("images")
10 | show_results = True
11 |
12 |
13 | def draw_coordinates(img_bytes, coordinates):
14 | if show_results:
15 | image: cv2.typing.MatLike = imread(img_bytes)
16 | for x, y in coordinates:
17 | image = cv2.circle(image, (x, y), radius=5, color=(0, 0, 255), thickness=-1)
18 |
19 | plt.imshow(image)
20 | plt.show()
21 |
22 |
23 | if __name__ == "__main__":
24 | detector = Detector()
25 |
26 | # Classification
27 | print("-- CLASSIFICATION --")
28 | img_bytes = image_dir.joinpath("full_page.png").read_bytes()
29 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
30 | print(f"Path: [full_page.png], Task: [Bicycle], Result: {response}; Coordinates: {coordinates}")
31 | draw_coordinates(img_bytes, coordinates)
32 |
33 | img_bytes = image_dir.joinpath("classify_image.png").read_bytes()
34 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
35 | print(f"Path: [classify_image.png], Task: [Bicycle], Result: {response}; Coordinates: {coordinates}")
36 | draw_coordinates(img_bytes, coordinates)
37 |
38 | img_bytes = image_dir.joinpath("classify_no_yolo.png").read_bytes()
39 | response, coordinates = detector.detect("stairs", img_bytes, area_captcha=False)
40 | print(f"Path: [classify_no_yolo.png], Task: [Stairs], Result: {response}; Coordinates: {coordinates}")
41 | draw_coordinates(img_bytes, coordinates)
42 |
43 | # Area Detection
44 | print("-- AREA DETECTION --")
45 | img_bytes = image_dir.joinpath("only_captcha.png").read_bytes()
46 | response, coordinates = detector.detect("motorcycle", img_bytes, area_captcha=True)
47 | print(f"Path: [only_captcha.png], Task: [Motorcycle], Result: {response}; Coordinates: {coordinates}")
48 | draw_coordinates(img_bytes, coordinates)
49 |
50 | img_bytes = image_dir.joinpath("area_image.png").read_bytes()
51 | response, coordinates = detector.detect("fire hydrant", img_bytes, area_captcha=True)
52 | print(f"Path: [area_image.png], Task: [Fire Hydrant], Result: {response}; Coordinates: {coordinates}")
53 | draw_coordinates(img_bytes, coordinates)
54 |
55 | img_bytes = image_dir.joinpath("area_no_yolo.png").read_bytes()
56 | response, coordinates = detector.detect("chimney", img_bytes, area_captcha=True)
57 | print(f"Path: [area_no_yolo.png], Task: [Chimney], Result: {response}; Coordinates: {coordinates}")
58 | draw_coordinates(img_bytes, coordinates)
59 |
60 | img_bytes = image_dir.joinpath("area_no_yolo1.png").read_bytes()
61 | response, coordinates = detector.detect("crosswalks", img_bytes, area_captcha=True)
62 | print(f"Path: [area_no_yolo.png], Task: [Crosswalks], Result: {response}; Coordinates: {coordinates}")
63 | draw_coordinates(img_bytes, coordinates)
64 |
--------------------------------------------------------------------------------
/examples/demo_normal_playwright.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 |
5 | from playwright.async_api import async_playwright
6 |
7 | from recognizer.agents.playwright import AsyncChallenger
8 |
9 |
10 | async def bytedance():
11 | # playwright install chromium
12 | # playwright install-deps chromium
13 | async with async_playwright() as p:
14 | browser = await p.chromium.launch(headless=False)
15 | context = await browser.new_context(locale="en-US")
16 | page = await context.new_page()
17 | challenger = AsyncChallenger(page)
18 |
19 | await page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
20 | await challenger.solve_recaptcha()
21 |
22 |
23 | if __name__ == "__main__":
24 | asyncio.run(bytedance())
25 |
--------------------------------------------------------------------------------
/examples/demo_patchright.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 |
5 | from patchright.async_api import async_playwright
6 |
7 | from recognizer.agents.playwright import AsyncChallenger
8 |
9 |
10 | async def bytedance():
11 | # patchright install chromium
12 | # patchright install-deps chromium
13 | async with async_playwright() as p:
14 | context = await p.chromium.launch_persistent_context(
15 | user_data_dir="./user_data",
16 | channel="chrome",
17 | headless=False,
18 | no_viewport=True,
19 | locale="en-US",
20 | )
21 | page = await context.new_page()
22 | challenger = AsyncChallenger(page)
23 |
24 | await page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
25 | await challenger.solve_recaptcha()
26 |
27 |
28 | if __name__ == "__main__":
29 | asyncio.run(bytedance())
30 |
--------------------------------------------------------------------------------
/examples/images/area_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/area_image.png
--------------------------------------------------------------------------------
/examples/images/area_no_yolo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/area_no_yolo.png
--------------------------------------------------------------------------------
/examples/images/area_no_yolo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/area_no_yolo1.png
--------------------------------------------------------------------------------
/examples/images/classify_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/classify_image.png
--------------------------------------------------------------------------------
/examples/images/classify_no_yolo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/classify_no_yolo.png
--------------------------------------------------------------------------------
/examples/images/full_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/full_page.png
--------------------------------------------------------------------------------
/examples/images/only_captcha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/examples/images/only_captcha.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=68.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.pytest.ini_options]
6 | testpaths = [
7 | "tests",
8 | ]
9 | filterwarnings = [
10 | "ignore::DeprecationWarning",
11 | ]
12 |
13 | [tool.mypy]
14 | mypy_path = "recognizer"
15 | check_untyped_defs = true
16 | disallow_any_generics = true
17 | ignore_missing_imports = true
18 | no_implicit_optional = true
19 | show_error_codes = true
20 | strict_equality = true
21 | warn_redundant_casts = true
22 | warn_return_any = true
23 | warn_unreachable = true
24 | warn_unused_configs = true
25 | no_implicit_reexport = true
26 |
27 | [tool.black]
28 | line-length = 200
29 |
30 | [tool.isort]
31 | py_version = 310
32 | line_length = 200
33 | multi_line_output = 7
34 |
35 | [tool.ruff]
36 | # Allow lines to be as long as 120.
37 | line-length = 200
--------------------------------------------------------------------------------
/recognizer/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from .components.detector import Detector
4 |
5 | VERSION = "1.4.0"
6 |
7 | __all__ = ["Detector", "VERSION"]
8 |
--------------------------------------------------------------------------------
/recognizer/agents/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/recognizer/agents/__init__.py
--------------------------------------------------------------------------------
/recognizer/agents/playwright/__init__.py:
--------------------------------------------------------------------------------
1 | from .async_control import AsyncChallenger
2 | from .sync_control import SyncChallenger
3 |
4 | __all__ = ["AsyncChallenger", "SyncChallenger"]
5 |
--------------------------------------------------------------------------------
/recognizer/agents/playwright/async_control.py:
--------------------------------------------------------------------------------
1 | # ruff: noqa: F821
2 | from __future__ import annotations
3 |
4 | import base64
5 | import re
6 | from contextlib import suppress
7 | from typing import Optional, Type, Union
8 |
9 | import cv2
10 | from imageio.v3 import imread
11 |
12 | try:
13 | from playwright.async_api import Error as PlaywrightError
14 | from playwright.async_api import FrameLocator as PlaywrightFrameLocator
15 | from playwright.async_api import Page as PlaywrightPage
16 | from playwright.async_api import Request as PlaywrightRequest
17 | from playwright.async_api import Route as PlaywrightRoute
18 | from playwright.async_api import TimeoutError as PlaywrightTimeoutError
19 | except ImportError:
20 | PlaywrightError: Type["Error"] = "Error" # type: ignore
21 | PlaywrightFrameLocator: Type["FrameLocator"] = "FrameLocator" # type: ignore
22 | PlaywrightPage: Type["Page"] = "Page" # type: ignore
23 | PlaywrightRequest: Type["Request"] = "Request" # type: ignore
24 | PlaywrightRoute: Type["Route"] = "Route" # type: ignore
25 | PlaywrightTimeoutError: Type["TimeoutError"] = "TimeoutError" # type: ignore
26 |
27 | try:
28 | from patchright.async_api import Error as PatchrightError
29 | from patchright.async_api import FrameLocator as PatchrightFrameLocator
30 | from patchright.async_api import Page as PatchrightPage
31 | from patchright.async_api import Request as PatchrightRequest
32 | from patchright.async_api import Route as PatchrightRoute
33 | from patchright.async_api import TimeoutError as PatchrightTimeoutError
34 | except ImportError:
35 | PatchrightError: Type["Error"] = "Error" # type: ignore
36 | PatchrightFrameLocator: Type["FrameLocator"] = "FrameLocator" # type: ignore
37 | PatchrightPage: Type["Page"] = "Page" # type: ignore
38 | PatchrightRequest: Type["Request"] = "Request" # type: ignore
39 | PatchrightRoute: Type["Route"] = "Route" # type: ignore
40 | PatchrightTimeoutError: Type["TimeoutError"] = "TimeoutError" # type: ignore
41 |
42 | from recognizer import Detector
43 |
44 |
45 | class AsyncChallenger:
46 | def __init__(
47 | self,
48 | page: Union[PlaywrightPage, PatchrightPage],
49 | click_timeout: Optional[int] = None,
50 | retry_times: int = 15,
51 | optimize_click_order: Optional[bool] = True,
52 | ) -> None:
53 | """
54 | Initialize a reCognizer AsyncChallenger instance with specified configurations.
55 |
56 | Args:
57 | page (Page): The Playwright Page to initialize on.
58 | click_timeout (int, optional): Click Timeouts between captcha-clicks.
59 | retry_times (int, optional): Maximum amount of retries before raising an Exception. Defaults to 15.
60 | optimize_click_order (bool, optional): Whether to optimize the click order with the Travelling Salesman Problem. Defaults to True.
61 | """
62 | self.page = page
63 | self.routed_page = False
64 | self.detector = Detector(optimize_click_order=optimize_click_order)
65 |
66 | self.click_timeout = click_timeout
67 | self.retry_times = retry_times
68 | self.retried = 0
69 |
70 | self.dynamic: bool = False
71 | self.captcha_token: Optional[str] = None
72 |
73 | async def route_handler(
74 | self,
75 | route: Union[PlaywrightRoute, PatchrightRoute],
76 | request: Union[PlaywrightRequest, PatchrightRequest],
77 | ) -> None:
78 | # Instant Fulfillment to save Time
79 | response = await route.fetch()
80 | await route.fulfill(response=response) # type: ignore[arg-type]
81 | response_text = await response.text()
82 | assert response_text
83 |
84 | self.dynamic = "dynamic" in response_text
85 |
86 | # Checking if captcha succeeded
87 | if "userverify" in request.url and "rresp" not in response_text and "bgdata" not in response_text:
88 | match = re.search(r'"uvresp"\s*,\s*"([^"]+)"', response_text)
89 | assert match
90 | self.captcha_token = match.group(1)
91 |
92 | async def check_result(self) -> Union[str, None]:
93 | if self.captcha_token:
94 | return self.captcha_token
95 |
96 | with suppress(PlaywrightError, PatchrightError):
97 | captcha_token: str = await self.page.evaluate("grecaptcha.getResponse()")
98 | return captcha_token
99 |
100 | with suppress(PlaywrightError, PatchrightError):
101 | enterprise_captcha_token: str = await self.page.evaluate("grecaptcha.enterprise.getResponse()")
102 | return enterprise_captcha_token
103 |
104 | return None
105 |
106 | async def check_captcha_visible(self):
107 | captcha_frame = self.page.frame_locator("//iframe[contains(@src,'bframe')]")
108 | label_obj = captcha_frame.locator("//strong")
109 | try:
110 | await label_obj.wait_for(state="visible", timeout=10000)
111 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
112 | return False
113 |
114 | return await label_obj.is_visible()
115 |
116 | async def click_checkbox(self) -> bool:
117 | # Clicking Captcha Checkbox
118 | try:
119 | checkbox = self.page.frame_locator("iframe[title='reCAPTCHA']").first
120 | await checkbox.locator(".recaptcha-checkbox-border").click()
121 | return True
122 | except (PlaywrightError, PatchrightError):
123 | return False
124 |
125 | async def adjust_coordinates(self, coordinates, img_bytes):
126 | image: cv2.typing.MatLike = imread(img_bytes)
127 | width, height = image.shape[1], image.shape[0]
128 | try:
129 | assert self.page.viewport_size
130 | page_width, page_height = (
131 | self.page.viewport_size["width"],
132 | self.page.viewport_size["height"],
133 | )
134 | except AssertionError:
135 | page_width = await self.page.evaluate("window.innerWidth")
136 | page_height = await self.page.evaluate("window.innerHeight")
137 |
138 | x_ratio = page_width / width
139 | y_ratio = page_height / height
140 |
141 | return [(int(x * x_ratio), int(y * y_ratio)) for x, y in coordinates]
142 |
143 | async def detect_tiles(self, prompt: str, area_captcha: bool) -> bool:
144 | client = await self.page.context.new_cdp_session(self.page) # type: ignore[arg-type]
145 | image = await client.send("Page.captureScreenshot")
146 |
147 | image_base64 = base64.b64decode(image["data"].encode())
148 | response, coordinates = self.detector.detect(prompt, image_base64, area_captcha=area_captcha)
149 | coordinates = await self.adjust_coordinates(coordinates, image_base64)
150 |
151 | if not any(response):
152 | return False
153 |
154 | for coord_x, coord_y in coordinates:
155 | await self.page.mouse.click(coord_x, coord_y)
156 | if self.click_timeout:
157 | await self.page.wait_for_timeout(self.click_timeout)
158 |
159 | return True
160 |
161 | async def load_captcha(
162 | self,
163 | captcha_frame: Optional[Union[PlaywrightFrameLocator, PatchrightFrameLocator]] = None,
164 | reset: Optional[bool] = False,
165 | ) -> Union[str, bool]:
166 | # Retrying
167 | self.retried += 1
168 | if self.retried >= self.retry_times:
169 | raise RecursionError(f"Exceeded maximum retry times of {self.retry_times}")
170 |
171 | TypedTimeoutError = PatchrightTimeoutError if isinstance(self.page, PatchrightPage) else PlaywrightTimeoutError
172 | if not await self.check_captcha_visible():
173 | if captcha_token := await self.check_result():
174 | return captcha_token
175 | elif not await self.click_checkbox():
176 | raise TypedTimeoutError("Invisible reCaptcha Timed Out.")
177 |
178 | assert await self.check_captcha_visible(), TypedTimeoutError("[ERROR] reCaptcha Challenge is not visible.")
179 |
180 | # Clicking Reload Button
181 | if reset:
182 | assert isinstance(captcha_frame, (PlaywrightFrameLocator, PatchrightFrameLocator))
183 | try:
184 | reload_button = captcha_frame.locator("#recaptcha-reload-button")
185 | await reload_button.click()
186 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
187 | return await self.load_captcha()
188 |
189 | # Resetting Values
190 | self.dynamic = False
191 | self.captcha_token = ""
192 |
193 | return True
194 |
195 | async def handle_recaptcha(self) -> Union[str, bool]:
196 | if isinstance(loaded_captcha := await self.load_captcha(), str):
197 | return loaded_captcha
198 |
199 | # Getting the Captcha Frame
200 | captcha_frame = self.page.frame_locator("//iframe[contains(@src,'bframe')]")
201 | label_obj = captcha_frame.locator("//strong")
202 | if not (prompt := await label_obj.text_content()):
203 | raise ValueError("reCaptcha Task Text did not load.")
204 |
205 | # Checking if Captcha Loaded Properly
206 | for _ in range(30):
207 | # Getting Recaptcha Tiles
208 | recaptcha_tiles = await captcha_frame.locator("[class='rc-imageselect-tile']").all()
209 | tiles_visibility = [await tile.is_visible() for tile in recaptcha_tiles]
210 | if len(recaptcha_tiles) in (9, 16) and len(tiles_visibility) in (9, 16):
211 | break
212 |
213 | await self.page.wait_for_timeout(1000)
214 | else:
215 | await self.load_captcha(captcha_frame, reset=True)
216 | await self.page.wait_for_timeout(2000)
217 | return await self.handle_recaptcha()
218 |
219 | # Detecting Images and Clicking right Coordinates
220 | area_captcha = len(recaptcha_tiles) == 16
221 | result_clicked = await self.detect_tiles(prompt, area_captcha)
222 |
223 | if self.dynamic and not area_captcha:
224 | while result_clicked:
225 | await self.page.wait_for_timeout(5000)
226 | result_clicked = await self.detect_tiles(prompt, area_captcha)
227 | elif not result_clicked:
228 | await self.load_captcha(captcha_frame, reset=True)
229 | await self.page.wait_for_timeout(2000)
230 | return await self.handle_recaptcha()
231 |
232 | # Submit challenge
233 | try:
234 | submit_button = captcha_frame.locator("#recaptcha-verify-button")
235 | await submit_button.click()
236 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
237 | await self.load_captcha(captcha_frame, reset=True)
238 | await self.page.wait_for_timeout(2000)
239 | return await self.handle_recaptcha()
240 |
241 | # Waiting for captcha_token for 5 seconds
242 | for _ in range(5):
243 | if captcha_token := await self.check_result():
244 | return captcha_token
245 |
246 | await self.page.wait_for_timeout(1000)
247 |
248 | # Check if error occurred whilst solving
249 | incorrect = captcha_frame.locator("[class='rc-imageselect-incorrect-response']")
250 | errors = captcha_frame.locator("[class *= 'rc-imageselect-error']")
251 | if await incorrect.is_visible() or any([await error.is_visible() for error in await errors.all()]):
252 | await self.load_captcha(captcha_frame, reset=True)
253 |
254 | # Retrying
255 | await self.page.wait_for_timeout(2000)
256 | return await self.handle_recaptcha()
257 |
258 | async def solve_recaptcha(self) -> Union[str, bool]:
259 | """
260 | Solve a hcaptcha-challenge on the specified Playwright Page
261 |
262 | Returns:
263 | str/bool: The result of the challenge
264 | Raises:
265 | RecursionError: If the challenger doesn´t succeed in the given retry times
266 | """
267 | # Resetting Values
268 | self.dynamic = False
269 | self.captcha_token = ""
270 | self.retried = 0
271 |
272 | # Checking if Page needs to be routed
273 | if not self.routed_page:
274 | route_captcha_regex = re.compile(r"(\b(?:google\.com.*(?:reload|userverify)|recaptcha\.net.*(?:reload|userverify))\b)")
275 | await self.page.route(route_captcha_regex, self.route_handler)
276 | self.routed_page = True
277 |
278 | await self.click_checkbox()
279 | await self.page.wait_for_timeout(2000)
280 | return await self.handle_recaptcha()
281 |
--------------------------------------------------------------------------------
/recognizer/agents/playwright/sync_control.py:
--------------------------------------------------------------------------------
1 | # ruff: noqa: F821
2 | from __future__ import annotations
3 |
4 | import base64
5 | import re
6 | from contextlib import suppress
7 | from typing import Optional, Type, Union
8 |
9 | import cv2
10 | from imageio.v3 import imread
11 |
12 | try:
13 | from playwright.sync_api import Error as PlaywrightError
14 | from playwright.sync_api import FrameLocator as PlaywrightFrameLocator
15 | from playwright.sync_api import Page as PlaywrightPage
16 | from playwright.sync_api import Request as PlaywrightRequest
17 | from playwright.sync_api import Route as PlaywrightRoute
18 | from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
19 | except ImportError:
20 | PlaywrightError: Type["Error"] = "Error" # type: ignore
21 | PlaywrightFrameLocator: Type["FrameLocator"] = "FrameLocator" # type: ignore
22 | PlaywrightPage: Type["Page"] = "Page" # type: ignore
23 | PlaywrightRequest: Type["Request"] = "Request" # type: ignore
24 | PlaywrightRoute: Type["Route"] = "Route" # type: ignore
25 | PlaywrightTimeoutError: Type["TimeoutError"] = "TimeoutError" # type: ignore
26 |
27 | try:
28 | from patchright.sync_api import Error as PatchrightError
29 | from patchright.sync_api import FrameLocator as PatchrightFrameLocator
30 | from patchright.sync_api import Page as PatchrightPage
31 | from patchright.sync_api import Request as PatchrightRequest
32 | from patchright.sync_api import Route as PatchrightRoute
33 | from patchright.sync_api import TimeoutError as PatchrightTimeoutError
34 | except ImportError:
35 | PatchrightError: Type["Error"] = "Error" # type: ignore
36 | PatchrightFrameLocator: Type["FrameLocator"] = "FrameLocator" # type: ignore
37 | PatchrightPage: Type["Page"] = "Page" # type: ignore
38 | PatchrightRequest: Type["Request"] = "Request" # type: ignore
39 | PatchrightRoute: Type["Route"] = "Route" # type: ignore
40 | PatchrightTimeoutError: Type["TimeoutError"] = "TimeoutError" # type: ignore # noqa
41 |
42 | from recognizer import Detector
43 |
44 |
45 | class SyncChallenger:
46 | def __init__(
47 | self,
48 | page: Union[PlaywrightPage, PatchrightPage],
49 | click_timeout: Optional[int] = None,
50 | retry_times: int = 15,
51 | optimize_click_order: Optional[bool] = True,
52 | ) -> None:
53 | """
54 | Initialize a reCognizer AsyncChallenger instance with specified configurations.
55 |
56 | Args:
57 | page (Page): The Playwright Page to initialize on.
58 | click_timeout (int, optional): Click Timeouts between captcha-clicks.
59 | retry_times (int, optional): Maximum amount of retries before raising an Exception. Defaults to 15.
60 | optimize_click_order (bool, optional): Whether to optimize the click order with the Travelling Salesman Problem. Defaults to True.
61 | """
62 | self.page: Union[PlaywrightPage, PatchrightPage] = page
63 | self.routed_page = False
64 | self.detector = Detector(optimize_click_order=optimize_click_order)
65 |
66 | self.click_timeout = click_timeout
67 | self.retry_times = retry_times
68 | self.retried = 0
69 |
70 | self.dynamic: bool = False
71 | self.captcha_token: Optional[str] = None
72 |
73 | def route_handler(
74 | self,
75 | route: Union[PlaywrightRoute, PatchrightRoute],
76 | request: Union[PlaywrightRequest, PatchrightRequest],
77 | ) -> None:
78 | # Instant Fulfillment to save Time
79 | response = route.fetch()
80 | route.fulfill(response=response) # type: ignore[arg-type]
81 | response_text = response.text()
82 | assert response_text
83 |
84 | self.dynamic = "dynamic" in response_text
85 |
86 | # Checking if captcha succeeded
87 | if "userverify" in request.url and "rresp" not in response_text and "bgdata" not in response_text:
88 | match = re.search(r'"uvresp"\s*,\s*"([^"]+)"', response_text)
89 | assert match
90 | self.captcha_token = match.group(1)
91 |
92 | def check_result(self) -> Union[str, None]:
93 | if self.captcha_token:
94 | return self.captcha_token
95 |
96 | with suppress(PlaywrightError, PatchrightError):
97 | captcha_token: str = self.page.evaluate("grecaptcha.getResponse()")
98 | return captcha_token
99 |
100 | with suppress(PlaywrightError, PatchrightError):
101 | enterprise_captcha_token: str = self.page.evaluate("grecaptcha.enterprise.getResponse()")
102 | return enterprise_captcha_token
103 |
104 | return None
105 |
106 | def check_captcha_visible(self):
107 | captcha_frame = self.page.frame_locator("//iframe[contains(@src,'bframe')]")
108 | label_obj = captcha_frame.locator("//strong")
109 | try:
110 | label_obj.wait_for(state="visible", timeout=10000)
111 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
112 | return False
113 |
114 | return label_obj.is_visible()
115 |
116 | def click_checkbox(self) -> bool:
117 | # Clicking Captcha Checkbox
118 | try:
119 | checkbox = self.page.frame_locator("iframe[title='reCAPTCHA']").first
120 | checkbox.locator(".recaptcha-checkbox-border").click()
121 | return True
122 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
123 | return False
124 |
125 | def adjust_coordinates(self, coordinates, img_bytes):
126 | image: cv2.typing.MatLike = imread(img_bytes)
127 | width, height = image.shape[1], image.shape[0]
128 | try:
129 | assert self.page.viewport_size
130 | page_width, page_height = (
131 | self.page.viewport_size["width"],
132 | self.page.viewport_size["height"],
133 | )
134 | except AssertionError:
135 | page_width = self.page.evaluate("window.innerWidth")
136 | page_height = self.page.evaluate("window.innerHeight")
137 |
138 | x_ratio = page_width / width
139 | y_ratio = page_height / height
140 |
141 | return [(int(x * x_ratio), int(y * y_ratio)) for x, y in coordinates]
142 |
143 | def detect_tiles(self, prompt: str, area_captcha: bool) -> bool:
144 | client = self.page.context.new_cdp_session(self.page) # type: ignore[arg-type]
145 | image = client.send("Page.captureScreenshot")
146 |
147 | image_base64 = base64.b64decode(image["data"].encode())
148 | response, coordinates = self.detector.detect(prompt, image_base64, area_captcha=area_captcha)
149 | coordinates = self.adjust_coordinates(coordinates, image_base64)
150 |
151 | if not any(response):
152 | return False
153 |
154 | for coord_x, coord_y in coordinates:
155 | self.page.mouse.click(coord_x, coord_y)
156 | if self.click_timeout:
157 | self.page.wait_for_timeout(self.click_timeout)
158 |
159 | return True
160 |
161 | def load_captcha(
162 | self,
163 | captcha_frame: Optional[Union[PlaywrightFrameLocator, PatchrightFrameLocator]] = None,
164 | reset: Optional[bool] = False,
165 | ) -> Union[str, bool]:
166 | # Retrying
167 | self.retried += 1
168 | if self.retried >= self.retry_times:
169 | raise RecursionError(f"Exceeded maximum retry times of {self.retry_times}")
170 |
171 | TypedTimeoutError = PatchrightTimeoutError if isinstance(self.page, PatchrightPage) else PlaywrightTimeoutError
172 | if not self.check_captcha_visible():
173 | if captcha_token := self.check_result():
174 | return captcha_token
175 | elif not self.click_checkbox():
176 | raise TypedTimeoutError("Invisible reCaptcha Timed Out.")
177 |
178 | assert self.check_captcha_visible(), TypedTimeoutError("[ERROR] reCaptcha Challenge is not visible.")
179 |
180 | # Clicking Reload Button
181 | if reset:
182 | assert isinstance(captcha_frame, (PlaywrightFrameLocator, PatchrightFrameLocator))
183 | try:
184 | reload_button = captcha_frame.locator("#recaptcha-reload-button")
185 | reload_button.click()
186 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
187 | return self.load_captcha()
188 |
189 | # Resetting Values
190 | self.dynamic = False
191 | self.captcha_token = ""
192 |
193 | return True
194 |
195 | def handle_recaptcha(self) -> Union[str, bool]:
196 | if isinstance(loaded_captcha := self.load_captcha(), str):
197 | return loaded_captcha
198 |
199 | # Getting the Captcha Frame
200 | captcha_frame = self.page.frame_locator("//iframe[contains(@src,'bframe')]")
201 | label_obj = captcha_frame.locator("//strong")
202 | if not (prompt := label_obj.text_content()):
203 | raise ValueError("reCaptcha Task Text did not load.")
204 |
205 | # Checking if Captcha Loaded Properly
206 | for _ in range(30):
207 | # Getting Recaptcha Tiles
208 | recaptcha_tiles = captcha_frame.locator("[class='rc-imageselect-tile']").all()
209 | tiles_visibility = [tile.is_visible() for tile in recaptcha_tiles]
210 | if len(recaptcha_tiles) in (9, 16) and len(tiles_visibility) in (9, 16):
211 | break
212 |
213 | self.page.wait_for_timeout(1000)
214 | else:
215 | self.load_captcha(captcha_frame, reset=True)
216 | self.page.wait_for_timeout(2000)
217 | return self.handle_recaptcha()
218 |
219 | # Detecting Images and Clicking right Coordinates
220 | area_captcha = len(recaptcha_tiles) == 16
221 | result_clicked = self.detect_tiles(prompt, area_captcha)
222 |
223 | if self.dynamic and not area_captcha:
224 | while result_clicked:
225 | self.page.wait_for_timeout(5000)
226 | result_clicked = self.detect_tiles(prompt, area_captcha)
227 | elif not result_clicked:
228 | self.load_captcha(captcha_frame, reset=True)
229 | self.page.wait_for_timeout(2000)
230 | return self.handle_recaptcha()
231 |
232 | # Submit challenge
233 | try:
234 | submit_button = captcha_frame.locator("#recaptcha-verify-button")
235 | submit_button.click()
236 | except (PlaywrightTimeoutError, PatchrightTimeoutError):
237 | self.load_captcha(captcha_frame, reset=True)
238 | self.page.wait_for_timeout(2000)
239 | return self.handle_recaptcha()
240 |
241 | # Waiting for captcha_token for 5 seconds
242 | for _ in range(5):
243 | if captcha_token := self.check_result():
244 | return captcha_token
245 |
246 | self.page.wait_for_timeout(1000)
247 |
248 | # Check if error occurred whilst solving
249 | incorrect = captcha_frame.locator("[class='rc-imageselect-incorrect-response']")
250 | errors = captcha_frame.locator("[class *= 'rc-imageselect-error']")
251 | if incorrect.is_visible() or any([error.is_visible() for error in errors.all()]):
252 | self.load_captcha(captcha_frame, reset=True)
253 |
254 | # Retrying
255 | return self.handle_recaptcha()
256 |
257 | def solve_recaptcha(self) -> Union[str, bool]:
258 | """
259 | Solve a hcaptcha-challenge on the specified Playwright Page
260 |
261 | Returns:
262 | str/bool: The result of the challenge
263 | Raises:
264 | RecursionError: If the challenger doesn´t succeed in the given retry times
265 | """
266 | # Resetting Values
267 | self.dynamic = False
268 | self.captcha_token = ""
269 | self.retried = 0
270 |
271 | # Checking if Page needs to be routed
272 | if not self.routed_page:
273 | route_captcha_regex = re.compile(r"(\b(?:google\.com.*(?:reload|userverify)|recaptcha\.net.*(?:reload|userverify))\b)")
274 | self.page.route(route_captcha_regex, self.route_handler)
275 | self.routed_page = True
276 |
277 | self.click_checkbox()
278 | self.page.wait_for_timeout(2000)
279 | return self.handle_recaptcha()
280 |
--------------------------------------------------------------------------------
/recognizer/components/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/recognizer/components/__init__.py
--------------------------------------------------------------------------------
/recognizer/components/detection_processor.py:
--------------------------------------------------------------------------------
1 | import math
2 | from itertools import permutations
3 | from typing import List, Tuple, Union
4 |
5 | from numpy import generic
6 | from numpy.typing import NDArray
7 | from scipy.spatial.distance import cdist
8 |
9 |
10 | def calculate_segmentation_response(
11 | mask: Union[NDArray[generic], List[Tuple[int, int]]],
12 | response: List[bool],
13 | tile_width: int,
14 | tile_height: int,
15 | tiles_per_row: int,
16 | threshold_image=None,
17 | ) -> List[bool]:
18 | for coord in mask:
19 | mask_point_x, mask_point_y = tuple(coord)
20 |
21 | # Calculate the column and row of the tile based on the coordinate
22 | col = int(mask_point_x // tile_width)
23 | row = int(mask_point_y // tile_height)
24 |
25 | # Ensure the column and row are within the valid range
26 | col = min(col, tiles_per_row - 1)
27 | row = min(row, tiles_per_row - 1)
28 |
29 | tile_index = row * tiles_per_row + col
30 | if response[tile_index]:
31 | continue
32 |
33 | # Calculate the boundary for the 8% area within the tile
34 | boundary_x = tile_width * 0.08
35 | boundary_y = tile_height * 0.08
36 |
37 | # Check if the point is within the 8% boundary of the tile area
38 | # fmt: off
39 | within_boundary = (
40 | boundary_x <= coord[1] % tile_width <= tile_width - boundary_x
41 | and boundary_y <= coord[0] % tile_height <= tile_height - boundary_y
42 | )
43 | # fmt: on
44 |
45 | if within_boundary:
46 | response[tile_index] = True
47 |
48 | if threshold_image is None:
49 | return response
50 |
51 | # Check for tiles that are completely enclosed by the mask
52 | for row in range(tiles_per_row):
53 | for col in range(tiles_per_row):
54 | tile_index = row * tiles_per_row + col
55 | if response[tile_index]:
56 | continue
57 |
58 | # Calculate the corners of the current tile
59 | tile_x_min = col * tile_width
60 | tile_x_max = tile_x_min + tile_width
61 | tile_y_min = row * tile_height
62 | tile_y_max = tile_y_min + tile_height
63 |
64 | # Check if all corners of the tile are inside the mask
65 | corners = [
66 | (tile_x_min, tile_y_min),
67 | (tile_x_max, tile_y_min),
68 | (tile_x_min, tile_y_max),
69 | (tile_x_max, tile_y_max),
70 | ]
71 |
72 | # Function to check if all corners are inside the mask
73 | def is_tile_fully_enclosed(tile_corners, threshold_image):
74 | for corner in tile_corners:
75 | x, y = corner
76 | # Check if x, y are within the bounds of the threshold_image
77 | if not (0 <= x < threshold_image.shape[1] and 0 <= y < threshold_image.shape[0]):
78 | return False
79 |
80 | # Check if the corner is inside the mask (non-zero pixel)
81 | if threshold_image[y, x] == 0: # 0 means the pixel is outside the contour
82 | return False
83 | return True
84 |
85 | response[tile_index] = is_tile_fully_enclosed(corners, threshold_image)
86 |
87 | return response
88 |
89 |
90 | def get_tiles_in_bounding_box(
91 | img: NDArray[generic],
92 | tile_amount: int,
93 | point_start: Tuple[int, int],
94 | point_end: Tuple[int, int],
95 | ) -> List[bool]:
96 | tiles_in_bbox = []
97 | # Define the size of the original image
98 | height, width, _ = img.shape
99 | tiles_per_row = int(math.sqrt(tile_amount))
100 |
101 | # Calculate the width and height of each tile
102 | tile_width = width // tiles_per_row
103 | tile_height = height // tiles_per_row
104 |
105 | for i in range(tiles_per_row):
106 | for j in range(tiles_per_row):
107 | # Calculate the coordinates of the current tile
108 | tile_x1 = j * tile_height
109 | tile_y1 = i * tile_width
110 | tile_x2 = (j + 1) * tile_height
111 | tile_y2 = (i + 1) * tile_width
112 |
113 | # Calculate Tile Area
114 | tile_area = (tile_x2 - tile_x1) * (tile_y2 - tile_y1)
115 |
116 | # Calculate the intersection area
117 | intersection_x1 = max(tile_x1, point_start[0])
118 | intersection_x2 = min(tile_x2, point_end[0])
119 | intersection_y1 = max(tile_y1, point_start[1])
120 | intersection_y2 = min(tile_y2, point_end[1])
121 |
122 | # Check if the current tile intersects with the bounding box
123 | if intersection_x1 < intersection_x2 and intersection_y1 < intersection_y2:
124 | # Getting intersection area coordinates and calculating Tile Coverage
125 | intersection_area = (intersection_x2 - intersection_x1) * (intersection_y2 - intersection_y1)
126 | if (intersection_area / tile_area) == 1:
127 | tiles_in_bbox.append(True)
128 | else:
129 | tiles_in_bbox.append(False)
130 | else:
131 | tiles_in_bbox.append(False)
132 |
133 | return tiles_in_bbox
134 |
135 |
136 | def calculate_approximated_coords(grid_width: int, grid_height: int, tile_amount: int) -> List[Tuple[int, int]]:
137 | # Calculate the middle points of the images within the grid
138 | middle_points = []
139 |
140 | for y in range(tile_amount):
141 | for x in range(tile_amount):
142 | # Calculate the coordinates of the middle point of each image
143 | middle_x = (x * grid_width) + (grid_width // 2)
144 | middle_y = (y * grid_height) + (grid_height // 2)
145 |
146 | # Append the middle point coordinates to the list
147 | middle_points.append((middle_x, middle_y))
148 |
149 | return middle_points
150 |
151 |
152 | def find_lowest_distance(start, coordinates):
153 | # Optimizing Click Order with Travelling Salesman Problem
154 | points = [start] + coordinates
155 | distances = cdist(points, points, metric="euclidean")
156 |
157 | num_points = len(points)
158 | min_distance = float("inf")
159 | best_path = None
160 |
161 | for perm in permutations(range(1, num_points)): # Permute only the other points
162 | path = [0] + list(perm) # Always start at the starting point
163 | distance = sum(distances[path[i], path[i + 1]] for i in range(len(path) - 1))
164 |
165 | if distance < min_distance:
166 | min_distance = distance
167 | best_path = path
168 |
169 | assert best_path
170 | # Convert path indices back to coordinates
171 | best_coordinates = [points[i] for i in best_path]
172 | return best_coordinates
173 |
--------------------------------------------------------------------------------
/recognizer/components/detector.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import math
4 | import random
5 | import sys
6 | import warnings
7 | from concurrent.futures import Future, ThreadPoolExecutor
8 | from os import PathLike
9 | from pathlib import Path
10 | from typing import Callable, List, Optional, Sequence, Tuple, Union
11 |
12 | import cv2
13 | import numpy as np
14 | import torch
15 | import torch.nn.functional as F
16 | from numpy import uint8
17 | from scipy.ndimage import label
18 | from torch import no_grad, set_num_threads
19 | from transformers import (
20 | CLIPModel,
21 | CLIPProcessor,
22 | CLIPSegForImageSegmentation,
23 | CLIPSegProcessor,
24 | )
25 |
26 | from .detection_processor import (
27 | calculate_approximated_coords,
28 | calculate_segmentation_response,
29 | find_lowest_distance,
30 | get_tiles_in_bounding_box,
31 | )
32 | from .image_processor import (
33 | create_image_grid,
34 | handle_multiple_images,
35 | handle_single_image,
36 | )
37 | from .prompt_handler import split_prompt_message
38 |
39 | warnings.filterwarnings("ignore", category=UserWarning, message="TypedStorage is deprecated")
40 |
41 |
42 | class DetectionModels:
43 | def __init__(self) -> None:
44 | # Preloading: Loading Models takes ~9 seconds
45 | set_num_threads(5)
46 | self.executor = ThreadPoolExecutor(max_workers=5)
47 | self.loading_futures: List[Future[Callable[..., None]]] = []
48 |
49 | try:
50 | self.loading_futures.append(self.executor.submit(self._load_yolo_detector))
51 | self.loading_futures.append(self.executor.submit(self._load_vit_model))
52 | self.loading_futures.append(self.executor.submit(self._load_vit_processor))
53 | self.loading_futures.append(self.executor.submit(self._load_seg_model))
54 | self.loading_futures.append(self.executor.submit(self._load_seg_processor))
55 | except Exception as e:
56 | if sys.version_info.minor >= 9:
57 | self.executor.shutdown(wait=True, cancel_futures=True)
58 | else:
59 | self.executor.shutdown(wait=True)
60 | raise e
61 |
62 | def _load_yolo_detector(self):
63 | from ultralytics import YOLO
64 |
65 | self.yolo_model = YOLO("yolo11m-seg.pt")
66 |
67 | def _load_vit_model(self):
68 | self.vit_model = CLIPModel.from_pretrained("flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K")
69 |
70 | def _load_vit_processor(self):
71 | self.vit_processor = CLIPProcessor.from_pretrained("flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K")
72 |
73 | def _load_seg_model(self):
74 | self.seg_model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined")
75 |
76 | def _load_seg_processor(self):
77 | self.seg_processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined")
78 |
79 | def check_loaded(self):
80 | try:
81 | if not all([future.done() for future in self.loading_futures]):
82 | for future in self.loading_futures:
83 | future.result()
84 |
85 | assert self.yolo_model
86 | assert self.seg_model
87 | assert self.vit_model
88 | except Exception as e:
89 | if sys.version_info.minor >= 9:
90 | self.executor.shutdown(wait=True, cancel_futures=True)
91 | else:
92 | self.executor.shutdown(wait=True)
93 | raise e
94 |
95 |
96 | detection_models = DetectionModels()
97 |
98 |
99 | class YoloDetector:
100 | # fmt: off
101 | yolo_classes = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog',
102 | 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite',
103 | 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich',
104 | 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote',
105 | 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']
106 | # fmt: on
107 |
108 | yolo_alias = {
109 | "bicycle": ["bicycle"],
110 | "car": ["car", "truck"],
111 | "bus": ["bus", "truck"],
112 | "motorcycle": ["motorcycle"],
113 | "boat": ["boat"],
114 | "fire hydrant": ["fire hydrant", "parking meter"],
115 | "parking meter": ["fire hydrant", "parking meter"],
116 | "traffic light": ["traffic light"],
117 | }
118 |
119 | def __init__(self) -> None:
120 | pass
121 |
122 | def detect_image(self, image: cv2.typing.MatLike, tile_amount: int, task_type: str) -> List[bool]:
123 | response = [False for _ in range(tile_amount)]
124 | height, width, _ = image.shape
125 | tiles_per_row = int(math.sqrt(tile_amount))
126 | tile_width, tile_height = width // tiles_per_row, height // tiles_per_row
127 |
128 | outputs = detection_models.yolo_model.predict(image, verbose=False, conf=0.2, iou=0.3) # , save=True
129 | results = outputs[0]
130 |
131 | for result in results:
132 | assert result
133 | # Check if correct task type
134 | class_index = int(result.boxes.cls[0])
135 | if self.yolo_classes[class_index] not in self.yolo_alias[task_type]:
136 | continue
137 |
138 | masks = result.masks
139 | mask = masks.xy[0]
140 | response = calculate_segmentation_response(mask, response, tile_width, tile_height, tiles_per_row)
141 |
142 | # In AreaCaptcha Mode, Calculate Tiles inside of boundary, which arent covered by mask point
143 | if tile_amount == 16:
144 | coords = result.boxes.xyxy.flatten().tolist()
145 | points_start, point_end = coords[:2], coords[2:]
146 | tiles_in_bbox = get_tiles_in_bounding_box(image, tile_amount, tuple(points_start), tuple(point_end))
147 | # Appending True Tiles to Response but not making True ones False again
148 | response = [x or y for x, y in zip(response, tiles_in_bbox)]
149 |
150 | return response
151 |
152 |
153 | class ClipDetector:
154 | # fmt: off
155 | plain_labels = ["bicycle", "boat", "bus", "car", "fire hydrant", "motorcycle", "traffic light", # YOLO TASKS
156 | "bridge", "chimney", "crosswalk", "mountain", "palm tree", "stair", "tractor", "taxi"]
157 |
158 | all_labels = ["a bicycle", "a boat", "a bus", "a car", "a fire hydrant", "a motorcycle", "a traffic light", # YOLO TASKS
159 | "the front or bottom or side of a concrete or steel bridge supported by concrete pillars over a street or highway",
160 | "A close-up of a chimney on a house, with rooftops and ceiling below",
161 | "striped pedestrian crossing with white/yellow of a crosswalk stretching over a gray ground of a street",
162 | "An californian green or grey landscape with trees or a bridge or street or road connecting two mountain slopes",
163 | "A feather-like warm palm growing behind to a tiled rooftop, with a californian road or street",
164 | "a stairway for pedestrians in front of a house or building leading to a walkway",
165 | "a tractor or agricultural vehicle driving on a street or field",
166 | "a taxi or a yellow car",
167 | "a house wall",
168 | "an empty street"]
169 | # fmt: on
170 |
171 | thresholds = {
172 | "bridge": 0.7285372716747225,
173 | "chimney": 0.7918647485226393,
174 | "crosswalk": 0.8879293048381806,
175 | "mountain": 0.5551278884819476,
176 | "palm tree": 0.8093279512040317,
177 | "stair": 0.9112694561691023,
178 | "tractor": 0.9385110986077537,
179 | "taxi": 0.7967491503432393,
180 | }
181 |
182 | area_captcha_labels = {
183 | "bridge": "A detailed perspective of a concrete bridge with cylindrical and rectangular supports spanning over a wide highway.",
184 | "chimney": "A close-up of a chimney on a house, with rooftops and ceiling below",
185 | "crosswalk": "striped pedestrian crossing with white/yellow of a crosswalk stretching over a gray ground of a street",
186 | "mountain": "An californian green or grey landscape with trees or a bridge or street or road connecting two mountain slopes",
187 | "palm tree": "A feather-like warm palm growing behind to a tiled rooftop, with a californian road or street",
188 | "stair": "a stairway for pedestrians in front of a house or building leading to a walkway",
189 | "tractor": "a tractor or agricultural vehicle",
190 | "taxi": "a yellow car or taxi",
191 | }
192 |
193 | def __init__(self) -> None:
194 | pass
195 |
196 | def clip_detect_vit(self, images: List[cv2.typing.MatLike], task_type: str) -> List[bool]:
197 | response = []
198 | inputs = detection_models.vit_processor(text=self.all_labels, images=images, return_tensors="pt", padding=True)
199 | with no_grad():
200 | outputs = detection_models.vit_model(**inputs)
201 | logits_per_image = outputs.logits_per_image # this is the image-text similarity score
202 | probs = logits_per_image.softmax(dim=1)
203 | results = probs.tolist()
204 |
205 | for result in results:
206 | task_index = self.plain_labels.index(task_type)
207 | prediction = result[task_index]
208 | choice = prediction >= (self.thresholds[task_type] - 0.2)
209 |
210 | response.append(choice)
211 |
212 | return response
213 |
214 | def clipseg_detect_rd64(self, image: cv2.typing.MatLike, task_type: str, tiles_amount: int) -> List[bool]:
215 | response = [False for _ in range(tiles_amount)]
216 | segment_label = self.area_captcha_labels[task_type]
217 |
218 | inputs = detection_models.seg_processor(text=segment_label, images=[image], return_tensors="pt")
219 | with no_grad():
220 | outputs = detection_models.seg_model(**inputs)
221 |
222 | heatmap = outputs.logits[0]
223 |
224 | # Step 1: Normalize the heatmap
225 | heatmap = torch.sigmoid(heatmap) # Apply sigmoid if logits are raw
226 | normalized_heatmap = (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min())
227 |
228 | # Step 2: Threshold the heatmap to find hotspots
229 | threshold = self.thresholds[task_type] - 0.2 # Adjust this value based on your needs
230 | hotspot_mask = (normalized_heatmap > threshold).float()
231 |
232 | # Step 3: Remove small specs from the heatmap
233 | hotspot_mask_np = hotspot_mask.cpu().numpy() # Convert to NumPy for processing
234 | labeled_array, num_features = label(hotspot_mask_np) # Label connected regions
235 | min_area = 0.005 * hotspot_mask_np.size # 0.5% of the image area
236 |
237 | # Remove regions smaller than the minimum area
238 | cleaned_mask = np.zeros_like(hotspot_mask_np)
239 | for region_label in range(1, num_features + 1):
240 | region_area = np.sum(labeled_array == region_label)
241 | if region_area >= min_area:
242 | cleaned_mask[labeled_array == region_label] = 1
243 |
244 | # Convert back to PyTorch tensor
245 | hotspot_mask_cleaned = torch.from_numpy(cleaned_mask).float()
246 |
247 | # Step 4: Resize the cleaned mask to match the original image dimensions
248 | mask_resized = (
249 | F.interpolate(
250 | hotspot_mask_cleaned.unsqueeze(0).unsqueeze(0), # Add batch and channel dimensions
251 | size=(
252 | image.shape[0],
253 | image.shape[1],
254 | ), # Match height and width of the input image
255 | mode="bilinear",
256 | align_corners=False,
257 | )
258 | .squeeze(0)
259 | .squeeze(0)
260 | ) # Remove batch and channel dimensions
261 |
262 | # Getting Tile Size from threshold mask
263 | tiles_per_row = int(math.sqrt(tiles_amount))
264 | mask_width, mask_height = mask_resized.shape
265 | tile_width, tile_height = (
266 | mask_width // tiles_per_row,
267 | mask_height // tiles_per_row,
268 | )
269 |
270 | # Creating Contours of Threshold Mask
271 | threshold_image = mask_resized.numpy().astype(uint8)
272 | contours, _ = cv2.findContours(threshold_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
273 |
274 | new_contours = []
275 | for contour in contours:
276 | # Checking Image area to only get large captcha areas (no image details)
277 | area = cv2.contourArea(contour)
278 | if area > 100:
279 | new_contours.append(contour)
280 | mask = contour.squeeze()
281 | response = calculate_segmentation_response(
282 | mask,
283 | response,
284 | tile_width,
285 | tile_height,
286 | tiles_per_row,
287 | threshold_image,
288 | )
289 |
290 | return response
291 |
292 | def detect_image(self, images: List[cv2.typing.MatLike], task_type: str) -> List[bool]:
293 | if len(images) == 9:
294 | return self.clip_detect_vit(images, task_type)
295 |
296 | tile_height, tile_width, _ = images[0].shape
297 | combined_image = create_image_grid(images)
298 | return self.clipseg_detect_rd64(combined_image, task_type, len(images))
299 |
300 |
301 | class Detector:
302 | # fmt: off
303 | challenge_alias = {
304 | "car": "car", "cars": "car", "vehicles": "car",
305 | "taxis": "taxi", "taxi": "taxi",
306 | "bus": "bus", "buses": "bus",
307 | "motorcycle": "motorcycle", "motorcycles": "motorcycle",
308 | "bicycle": "bicycle", "bicycles": "bicycle",
309 | "boats": "boat", "boat": "boat",
310 | "tractors": "tractor", "tractor": "tractor",
311 | "stairs": "stair", "stair": "stair",
312 | "palm trees": "palm tree", "palm tree": "palm tree",
313 | "fire hydrants": "fire hydrant", "a fire hydrant": "fire hydrant", "fire hydrant": "fire hydrant",
314 | "parking meters": "parking meter", "parking meter": "parking meter",
315 | "crosswalks": "crosswalk", "crosswalk": "crosswalk",
316 | "traffic lights": "traffic light", "traffic light": "traffic light",
317 | "bridges": "bridge", "bridge": "bridge",
318 | "mountains or hills": "mountain", "mountain or hill": "mountain", "mountain": "mountain", "mountains": "mountain", "hills": "mountain", "hill": "mountain",
319 | "chimney": "chimney", "chimneys": "chimney"
320 | }
321 |
322 | # fmt: on
323 |
324 | def __init__(self, optimize_click_order: Optional[bool] = True) -> None:
325 | """
326 | Spawn a new reCognizer Detector Instance
327 |
328 | Args:
329 | optimize_click_order (bool, optional): Whether to optimize the click order with the Travelling Salesman Problem. Defaults to True.
330 | """
331 | self.optimize_click_order = optimize_click_order
332 |
333 | self.detection_models: DetectionModels = detection_models
334 | self.yolo_detector = YoloDetector()
335 | self.clip_detector = ClipDetector()
336 |
337 | def detect(
338 | self,
339 | prompt: str,
340 | images: Union[
341 | Path,
342 | Union[PathLike[str], str],
343 | bytes,
344 | Sequence[Path],
345 | Sequence[Union[PathLike[str], str]],
346 | Sequence[bytes],
347 | ],
348 | area_captcha: Optional[bool] = None,
349 | ) -> Tuple[List[bool], List[Tuple[int, int]]]:
350 | """
351 | Solves a reCaptcha Task with the given prompt and images.
352 |
353 | Args:
354 | prompt (str): The prompt name/sentence of the captcha (e.g. "Select all images with crosswalks" / "crosswalk").
355 | images (Path | PathLike | bytes | Sequence[Path] | Sequence[PathLike] | Sequence[bytes]): The Image(s) to reCognize.
356 | area_captcha (bool, optional): Whether the Captcha Task is an area-captcha.
357 |
358 | Returns:
359 | List[bool], List[Tuple[int, int]]: The reCognizer Response and calculated click-coordinates for the response
360 | """
361 | detection_models.check_loaded()
362 |
363 | response = []
364 | coordinates: List[Tuple[int, int]] = []
365 | # Making best guess if its area_captcha if user did not specify
366 | area_captcha = "square" in prompt if area_captcha is None else area_captcha
367 | label = split_prompt_message(prompt)
368 |
369 | if label not in self.challenge_alias:
370 | print(f"[ERROR] Types of challenges of label {label} not yet scheduled (Prompt: {prompt}).")
371 | return [], []
372 | label = self.challenge_alias[label]
373 |
374 | # Image Splitting if Image-Bytes is provided, not list of Images
375 | if isinstance(images, (PathLike, str)):
376 | images = Path(images)
377 |
378 | if isinstance(images, bytes) or isinstance(images, Path):
379 | images, coordinates = handle_single_image(images, area_captcha) # type: ignore
380 |
381 | if isinstance(images, list):
382 | if len(images) == 1:
383 | if isinstance(images[0], (PathLike, str)):
384 | pathed_image = Path(images[0])
385 | byte_images, coordinates = handle_single_image(pathed_image, area_captcha)
386 | else:
387 | byte_images, coordinates = handle_single_image(images[0], area_captcha)
388 | else:
389 | byte_images = []
390 | for image in images:
391 | if isinstance(image, Path):
392 | byte_images.append(image.read_bytes())
393 | elif isinstance(image, (PathLike, str)):
394 | pathed_image = Path(image)
395 | byte_images.append(pathed_image.read_bytes())
396 | else:
397 | byte_images.append(image)
398 |
399 | if len(byte_images) not in (9, 16):
400 | print(f"[ERROR] Images amount must equal 9 or 16. Is: {len(byte_images)}")
401 | return [], []
402 |
403 | cv2_images = handle_multiple_images(byte_images)
404 |
405 | if not any(coordinates):
406 | height, width, _ = cv2_images[0].shape
407 | tiles_amount = 4 if area_captcha else 3
408 | coordinates = calculate_approximated_coords(height, width, tiles_amount)
409 |
410 | if label in self.yolo_detector.yolo_classes:
411 | # Creating Image Grid from List of Images
412 | cv2_image = create_image_grid(cv2_images)
413 | response = self.yolo_detector.detect_image(cv2_image, len(byte_images), label)
414 | else:
415 | response = self.clip_detector.detect_image(cv2_images, label)
416 |
417 | good_coordinates: List[Tuple[int, int]] = []
418 | for i, result in enumerate(response):
419 | if result:
420 | x, y = coordinates[i]
421 | good_coordinates.append((x + random.randint(-25, 25), y + random.randint(-25, 25)))
422 |
423 | # Optimizing Click Order with Travelling Salesman Problem
424 | if self.optimize_click_order and len(good_coordinates) > 2:
425 | good_coordinates = find_lowest_distance(good_coordinates[0], good_coordinates[1:])
426 |
427 | return response, good_coordinates
428 |
--------------------------------------------------------------------------------
/recognizer/components/image_processor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import base64
4 | import binascii
5 | import math
6 | from contextlib import suppress
7 | from pathlib import Path
8 | from statistics import median
9 | from typing import List, Tuple, Union
10 |
11 | import cv2
12 | from imageio.v2 import imread
13 | from numpy import concatenate
14 |
15 | from .detection_processor import calculate_approximated_coords
16 |
17 |
18 | def get_captcha_fields(
19 | img: cv2.typing.MatLike,
20 | ) -> Tuple[List[bytes], List[Tuple[int, int]]]:
21 | captcha_fields_with_sizes: List[Tuple[bytes, int, int, int]] = []
22 | captcha_fields: List[Tuple[bytes, int, int]] = []
23 |
24 | # Turn image to grayscale and Apply a white threshold to it
25 | gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
26 | ret, thresh = cv2.threshold(gray, 254, 255, cv2.CHAIN_APPROX_NONE)
27 | # Find Countours of the white threshold
28 | try:
29 | image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) # type: ignore
30 | except ValueError:
31 | contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) # type: ignore
32 |
33 | for contour in contours:
34 | # Checking Image area to only get large captcha areas (no image details)
35 | area = cv2.contourArea(contour)
36 | if area > 1000:
37 | # IDK what this does i copy pasted it :)
38 | approx = cv2.approxPolyDP(contour, 0.01 * cv2.arcLength(contour, True), True)
39 |
40 | # Calculating x, y, width, height, aspectRatio
41 | x, y, w, h = cv2.boundingRect(approx)
42 | aspectRatio = float(w) / h
43 |
44 | if 0.95 <= aspectRatio <= 1.05:
45 | # Cropping Image area to Captcha Field
46 | crop_img = img[y:y+h, x:x+w] # fmt: skip
47 | # Cv2 to Image Bytes
48 | image_bytes = cv2.imencode(".jpg", crop_img)[1].tobytes()
49 | image_size: int = w * h
50 | # Getting Center of Captcha Field
51 | center_x, center_y = x + (w // 2), y + (h // 2)
52 | captcha_fields_with_sizes.append((image_bytes, center_x, center_y, image_size))
53 |
54 | if len(captcha_fields_with_sizes) >= 9:
55 | # Dont use captcha fields that are too big
56 | size_median = median([sizes[3] for sizes in captcha_fields_with_sizes])
57 | for i, (image_bytes, center_x, center_y, image_size) in enumerate(captcha_fields_with_sizes):
58 | if int(image_size) == int(size_median):
59 | captcha_fields.append((image_bytes, center_x, center_y))
60 | else:
61 | for image_bytes, center_x, center_y, image_size in captcha_fields_with_sizes:
62 | captcha_fields.append((image_bytes, center_x, center_y))
63 |
64 | sorted_captcha_fields: List[Tuple[bytes, int, int]] = sorted(captcha_fields, key=lambda element: [element[2], element[1]])
65 | # return sorted_captcha_fields
66 | return (
67 | [field[0] for field in sorted_captcha_fields],
68 | [(field[1], field[2]) for field in sorted_captcha_fields],
69 | )
70 |
71 |
72 | def split_image_into_tiles(img: cv2.typing.MatLike, tile_count: int) -> List[bytes]:
73 | tiles = []
74 |
75 | # Get the dimensions of the image
76 | height, width, _ = img.shape
77 |
78 | # Calculate the size of each tile
79 | tile_width = width // tile_count
80 | tile_height = height // tile_count
81 |
82 | # Iterate through the image and crop it into tiles
83 | for i in range(tile_count):
84 | for j in range(tile_count):
85 | x_start = j * tile_width
86 | x_end = (j + 1) * tile_width
87 | y_start = i * tile_height
88 | y_end = (i + 1) * tile_height
89 |
90 | # Crop the image to create a tile
91 | tile = img[y_start:y_end, x_start:x_end]
92 | image_bytes = cv2.imencode(".jpg", tile)[1].tobytes()
93 | tiles.append(image_bytes)
94 |
95 | return tiles
96 |
97 |
98 | def create_image_grid(images: List[cv2.typing.MatLike]) -> cv2.typing.MatLike:
99 | cv2_images = images
100 | tile_count_per_row = int(math.sqrt(len(cv2_images)))
101 |
102 | # Combining horizontal layers together
103 | layers = []
104 | for i in range(tile_count_per_row):
105 | layer_images = [cv2_images[i * tile_count_per_row + j] for j in range(tile_count_per_row)]
106 | layer = concatenate(layer_images, axis=1)
107 | layers.append(layer)
108 |
109 | # Combining layers verticly to one image
110 | combined_img = concatenate(layers, axis=0)
111 |
112 | return combined_img
113 |
114 |
115 | def handle_single_image(single_image: Union[Path, bytes], area_captcha: bool) -> Tuple[List[bytes], List[Tuple[int, int]]]:
116 | if isinstance(single_image, bytes):
117 | with suppress(binascii.Error):
118 | single_image = base64.b64decode(single_image, validate=True)
119 |
120 | # Image Bytes to Cv2
121 | rgba_img = imread(single_image)
122 | img = cv2.cvtColor(rgba_img, cv2.COLOR_BGR2RGB)
123 |
124 | # Image Splitting Presuming has white barriers
125 | images, coords = get_captcha_fields(img)
126 |
127 | if len(images) == 1:
128 | # Turning bytes from get_captcha_fields back to Cv2
129 | rgba_img = imread(images[0])
130 | img = cv2.cvtColor(rgba_img, cv2.COLOR_BGR2RGB)
131 |
132 | # Either it is just a single image or no white barriers
133 | height, width, _ = img.shape
134 |
135 | if height > 200 and width > 200:
136 | tiles_amount = 4 if area_captcha else 3
137 | images = split_image_into_tiles(img, tiles_amount)
138 | coords = calculate_approximated_coords(height // tiles_amount, width // tiles_amount, tiles_amount)
139 |
140 | return images, coords
141 |
142 |
143 | def handle_multiple_images(images: List[bytes]) -> List[cv2.typing.MatLike]:
144 | cv2_images = []
145 | for image in images:
146 | try:
147 | byte_image = base64.b64decode(image, validate=True)
148 | except binascii.Error:
149 | byte_image = image
150 |
151 | # Image Bytes to Cv2
152 | rgba_img = imread(byte_image)
153 | cv2_img = cv2.cvtColor(rgba_img, cv2.COLOR_BGR2RGB)
154 | cv2_images.append(cv2_img)
155 |
156 | return cv2_images
157 |
--------------------------------------------------------------------------------
/recognizer/components/prompt_handler.py:
--------------------------------------------------------------------------------
1 | BAD_CODE = {
2 | "а": "a",
3 | "е": "e",
4 | "e": "e",
5 | "i": "i",
6 | "і": "i",
7 | "ο": "o",
8 | "с": "c",
9 | "ԁ": "d",
10 | "ѕ": "s",
11 | "һ": "h",
12 | "у": "y",
13 | "р": "p",
14 | "ϳ": "j",
15 | "х": "x",
16 | }
17 |
18 |
19 | def label_cleaning(raw_label: str) -> str:
20 | """cleaning errors-unicode"""
21 | clean_label = raw_label
22 | for c in BAD_CODE:
23 | clean_label = clean_label.replace(c, BAD_CODE[c])
24 | return clean_label
25 |
26 |
27 | def split_prompt_message(label: str) -> str:
28 | """Detach label from challenge prompt"""
29 | if "with" not in label:
30 | unclean_label = label.strip()
31 | elif " a " in label:
32 | unclean_label = label.split("with a ")[1].split()[0]
33 | else:
34 | unclean_label = label.split("with")[1].split()[0]
35 |
36 | return label_cleaning(unclean_label)
37 |
--------------------------------------------------------------------------------
/requirements-test.txt:
--------------------------------------------------------------------------------
1 | # This requirements are for development and testing only, not for production.
2 | pytest~=8.3.4
3 | pytest_asyncio~=0.25.2
4 | pytest-rerunfailures~=15.0
5 | mypy~=1.14.1
6 | ruff~=0.9.1
7 | types-setuptools~=75.8.0.20250110
8 | # botright~=0.5.1
9 | playwright~=1.49.1
10 | patchright~=1.49.1
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | setuptools~=75.8.0
2 | opencv-python~=4.10.0.84
3 | imageio~=2.36.1
4 | ultralytics~=8.3.59
5 | transformers~=4.48.0
6 | playwright~=1.49.1
7 | patchright~=1.49.1
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = recognizer
3 | version = attr: recognizer.VERSION
4 | description = 🦉Gracefully face reCAPTCHA challenge with ultralytics YOLOv8-seg, CLIPs VIT-B/16 and CLIP-Seg/RD64. Implemented in playwright or an easy-to-use API.
5 | long_description = file: README.md
6 | long_description_content_type = text/markdown
7 | author = Vinyzu
8 | url = https://github.com/Vinyzu/recognizer
9 | license = GNU General Public License v3.0
10 | license_file = LICENSE
11 | keywords = botright, playwright, browser, automation, fingerprints, fingerprinting, dataset, data, recaptcha, captcha
12 | project_urls =
13 | Source = https://github.com/Vinyzu/reCognizer
14 | Tracker = https://github.com/Vinyzu/reCognizer/issues
15 | classifiers =
16 | Topic :: Scientific/Engineering
17 | Topic :: Scientific/Engineering :: Artificial Intelligence
18 | Topic :: Software Development
19 | Topic :: Software Development :: Libraries
20 | Topic :: Software Development :: Libraries :: Python Modules
21 | Topic :: Internet :: WWW/HTTP :: Browsers
22 | License :: OSI Approved :: Apache Software License
23 | Programming Language :: Python :: 3
24 |
25 | [options]
26 | zip_safe = no
27 | python_requires = >=3.8
28 | packages = find:
29 | install_requires =
30 | opencv-python
31 | imageio
32 | ultralytics
33 | transformers
34 | numpy
35 | playwright
36 |
37 | [options.package_data]
38 | * = requirements.txt
39 |
40 | [options.packages.find]
41 | include = recognizer, recognizer.*, LICENSE
42 | exclude = tests, .github
43 |
44 | [options.extras_require]
45 | testing =
46 | pytest
47 | mypy
48 | flake8
49 | black
50 | isort
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pytest_asyncio
3 | from patchright.async_api import async_playwright
4 | from patchright.sync_api import sync_playwright
5 |
6 | from recognizer import Detector
7 |
8 |
9 | @pytest.fixture
10 | def detector() -> Detector:
11 | detector = Detector()
12 | return detector
13 |
14 |
15 | @pytest.fixture
16 | def sync_playwright_object():
17 | with sync_playwright() as playwright_object:
18 | yield playwright_object
19 |
20 |
21 | @pytest.fixture
22 | def sync_browser(sync_playwright_object):
23 | browser = sync_playwright_object.chromium.launch_persistent_context(user_data_dir="./user_data", channel="chrome", headless=True, no_viewport=True, locale="en-US")
24 |
25 | yield browser
26 | browser.close()
27 |
28 |
29 | @pytest.fixture
30 | def sync_page(sync_browser):
31 | page = sync_browser.new_page()
32 |
33 | yield page
34 | page.close()
35 |
36 |
37 | @pytest_asyncio.fixture
38 | async def async_playwright_object():
39 | async with async_playwright() as playwright_object:
40 | yield playwright_object
41 |
42 |
43 | @pytest_asyncio.fixture
44 | async def async_browser(async_playwright_object):
45 | browser = await async_playwright_object.chromium.launch_persistent_context(user_data_dir="./user_data", channel="chrome", headless=True, no_viewport=True, locale="en-US")
46 |
47 | yield browser
48 | await browser.close()
49 |
50 |
51 | @pytest_asyncio.fixture
52 | async def async_page(async_browser):
53 | page = await async_browser.new_page()
54 |
55 | yield page
56 | await page.close()
57 |
--------------------------------------------------------------------------------
/tests/images/area_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/area_image.png
--------------------------------------------------------------------------------
/tests/images/area_no_yolo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/area_no_yolo.png
--------------------------------------------------------------------------------
/tests/images/classify_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/classify_image.png
--------------------------------------------------------------------------------
/tests/images/classify_no_yolo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/classify_no_yolo.png
--------------------------------------------------------------------------------
/tests/images/full_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/full_page.png
--------------------------------------------------------------------------------
/tests/images/no_captcha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/no_captcha.png
--------------------------------------------------------------------------------
/tests/images/only_captcha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/only_captcha.png
--------------------------------------------------------------------------------
/tests/images/splitted/img0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img0.png
--------------------------------------------------------------------------------
/tests/images/splitted/img1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img1.png
--------------------------------------------------------------------------------
/tests/images/splitted/img2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img2.png
--------------------------------------------------------------------------------
/tests/images/splitted/img3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img3.png
--------------------------------------------------------------------------------
/tests/images/splitted/img4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img4.png
--------------------------------------------------------------------------------
/tests/images/splitted/img5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img5.png
--------------------------------------------------------------------------------
/tests/images/splitted/img6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img6.png
--------------------------------------------------------------------------------
/tests/images/splitted/img7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img7.png
--------------------------------------------------------------------------------
/tests/images/splitted/img8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vinyzu/recognizer/3868c59d6bbd1f13e121c5443116e28e61f33eee/tests/images/splitted/img8.png
--------------------------------------------------------------------------------
/tests/test_detector.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from recognizer import Detector
4 |
5 | image_dir = Path(__file__).parent.joinpath("images")
6 |
7 |
8 | def test_full_page_screenshot(detector: Detector):
9 | img_bytes = image_dir.joinpath("full_page.png").read_bytes()
10 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
11 |
12 | # General Checks
13 | assert response, coordinates
14 | assert len(response) == 9
15 |
16 | # Response Correctness
17 | assert sum(response) == len(coordinates)
18 | assert response == [True, True, False, False, True, False, False, False, False]
19 |
20 |
21 | def test_only_captcha(detector: Detector):
22 | img_bytes = image_dir.joinpath("only_captcha.png").read_bytes()
23 | response, coordinates = detector.detect("motorcycle", img_bytes, area_captcha=True)
24 |
25 | # General Checks
26 | assert response, coordinates
27 | assert len(response) == 16
28 |
29 | # Response Correctness
30 | assert sum(response) == len(coordinates)
31 | assert response == [
32 | True,
33 | True,
34 | True,
35 | False,
36 | True,
37 | True,
38 | True,
39 | False,
40 | False,
41 | True,
42 | True,
43 | False,
44 | False,
45 | False,
46 | False,
47 | False,
48 | ]
49 |
50 |
51 | def test_area_yolo_captcha(detector: Detector):
52 | img_bytes = image_dir.joinpath("area_image.png").read_bytes()
53 | response, coordinates = detector.detect("fire hydrant", img_bytes, area_captcha=True)
54 |
55 | # General Checks
56 | assert response, coordinates
57 | assert len(response) == 16
58 |
59 | # Response Correctness
60 | assert sum(response) == len(coordinates)
61 | assert response == [
62 | False,
63 | True,
64 | True,
65 | False,
66 | False,
67 | True,
68 | True,
69 | False,
70 | False,
71 | True,
72 | True,
73 | False,
74 | False,
75 | True,
76 | True,
77 | False,
78 | ]
79 |
80 |
81 | def test_area_clip_captcha(detector: Detector):
82 | img_bytes = image_dir.joinpath("area_no_yolo.png").read_bytes()
83 | response, coordinates = detector.detect("chimney", img_bytes, area_captcha=True)
84 |
85 | # General Checks
86 | assert response, coordinates
87 | assert len(response) == 16
88 |
89 | # Response Correctness
90 | assert sum(response) == len(coordinates)
91 | assert response == [
92 | True,
93 | True,
94 | True,
95 | True,
96 | True,
97 | True,
98 | True,
99 | False,
100 | False,
101 | False,
102 | False,
103 | False,
104 | False,
105 | False,
106 | False,
107 | False,
108 | ]
109 |
110 |
111 | def test_classify_yolo_captcha(detector: Detector):
112 | img_bytes = image_dir.joinpath("classify_image.png").read_bytes()
113 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
114 |
115 | # General Checks
116 | assert response, coordinates
117 | assert len(response) == 9
118 |
119 | # Response Correctness
120 | assert sum(response) == len(coordinates)
121 | assert response == [False, False, False, True, False, False, True, True, False]
122 |
123 |
124 | def test_classify_clip_captcha(detector: Detector):
125 | img_bytes = image_dir.joinpath("classify_no_yolo.png").read_bytes()
126 | response, coordinates = detector.detect("stairs", img_bytes, area_captcha=False)
127 |
128 | # General Checks
129 | assert response, coordinates
130 | assert len(response) == 9
131 |
132 | # Response Correctness
133 | assert sum(response) == len(coordinates)
134 | assert response == [True, True, False, True, False, False, False, False, True]
135 |
--------------------------------------------------------------------------------
/tests/test_inputs.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from recognizer import Detector
4 |
5 | image_dir = Path(__file__).parent.joinpath("images")
6 | splitted_image_dir = image_dir.joinpath("splitted")
7 | splitted_images = list(splitted_image_dir.iterdir())
8 |
9 |
10 | def test_single_pathlib_input(detector: Detector):
11 | img_bytes = image_dir.joinpath("full_page.png")
12 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
13 | print(f"Path: [full_page.png], Task: [Bicycle], Result: {response}; Coordinates: {coordinates}")
14 |
15 | # General Checks
16 | assert response, coordinates
17 | assert len(response) == 9
18 |
19 | # Response Correctness
20 | assert sum(response) == len(coordinates)
21 |
22 |
23 | def test_one_pathlib_input(detector: Detector):
24 | img_bytes = image_dir.joinpath("full_page.png")
25 | response, coordinates = detector.detect("bicycle", [img_bytes], area_captcha=False)
26 | print(f"Path: [full_page.png], Task: [Bicycle], Result: {response}; Coordinates: {coordinates}")
27 |
28 | # General Checks
29 | assert response, coordinates
30 | assert len(response) == 9
31 |
32 | # Response Correctness
33 | assert sum(response) == len(coordinates)
34 |
35 |
36 | def test_pathlibs_input(detector: Detector):
37 | response, coordinates = detector.detect("bicycle", splitted_images, area_captcha=False)
38 |
39 | # General Checks
40 | assert response, coordinates
41 | assert len(response) == 9
42 |
43 | # Response Correctness
44 | assert sum(response) == len(coordinates)
45 |
46 |
47 | def test_single_path_input(detector: Detector):
48 | img_bytes = image_dir.joinpath("full_page.png")
49 | response, coordinates = detector.detect("bicycle", str(img_bytes), area_captcha=False)
50 | print(f"Path: [full_page.png], Task: [Bicycle], Result: {response}; Coordinates: {coordinates}")
51 |
52 | # General Checks
53 | assert response, coordinates
54 | assert len(response) == 9
55 |
56 | # Response Correctness
57 | assert sum(response) == len(coordinates)
58 |
59 |
60 | def test_one_path_input(detector: Detector):
61 | img_bytes = image_dir.joinpath("full_page.png")
62 | response, coordinates = detector.detect("bicycle", [str(img_bytes)], area_captcha=False)
63 | print(f"Path: [full_page.png], Task: [Bicycle], Result: {response}; Coordinates: {coordinates}")
64 |
65 | # General Checks
66 | assert response, coordinates
67 | assert len(response) == 9
68 |
69 | # Response Correctness
70 | assert sum(response) == len(coordinates)
71 |
72 |
73 | def test_paths_input(detector: Detector):
74 | splitted_paths = [str(splitted_path) for splitted_path in splitted_images]
75 | response, coordinates = detector.detect("bicycle", splitted_paths, area_captcha=False)
76 |
77 | # General Checks
78 | assert response, coordinates
79 | assert len(response) == 9
80 |
81 | # Response Correctness
82 | assert sum(response) == len(coordinates)
83 |
84 |
85 | def test_single_bytes_input(detector: Detector):
86 | img_bytes = image_dir.joinpath("full_page.png").read_bytes()
87 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
88 |
89 | # General Checks
90 | assert response, coordinates
91 | assert len(response) == 9
92 |
93 | # Response Correctness
94 | assert sum(response) == len(coordinates)
95 |
96 |
97 | def test_one_bytes_input(detector: Detector):
98 | img_bytes = image_dir.joinpath("full_page.png").read_bytes()
99 | response, coordinates = detector.detect("bicycle", [img_bytes], area_captcha=False)
100 |
101 | # General Checks
102 | assert response, coordinates
103 | assert len(response) == 9
104 |
105 | # Response Correctness
106 | assert sum(response) == len(coordinates)
107 |
108 |
109 | def test_bytes_input(detector: Detector):
110 | img_bytes = [img.read_bytes() for img in splitted_images]
111 | response, coordinates = detector.detect("bicycle", img_bytes, area_captcha=False)
112 |
113 | # General Checks
114 | assert response, coordinates
115 | assert len(response) == 9
116 |
117 | # Response Correctness
118 | assert sum(response) == len(coordinates)
119 |
--------------------------------------------------------------------------------
/tests/test_playwright_async.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from contextlib import suppress
4 |
5 | import pytest
6 | from playwright.async_api import Page
7 |
8 | from recognizer.agents.playwright import AsyncChallenger
9 |
10 |
11 | @pytest.mark.asyncio
12 | async def test_async_challenger(async_page: Page):
13 | challenger = AsyncChallenger(async_page, click_timeout=1000)
14 | # For slow Pytest Loading
15 | challenger.detector.detection_models.check_loaded()
16 |
17 | await async_page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
18 |
19 | with suppress(RecursionError):
20 | res = await challenger.solve_recaptcha()
21 | assert res
22 |
23 | @pytest.mark.asyncio
24 | async def test_async_challenger1(async_page: Page):
25 | challenger = AsyncChallenger(async_page, click_timeout=1000)
26 | # For slow Pytest Loading
27 | challenger.detector.detection_models.check_loaded()
28 |
29 | await async_page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
30 |
31 | with suppress(RecursionError):
32 | res = await challenger.solve_recaptcha()
33 | assert res
--------------------------------------------------------------------------------
/tests/test_playwright_sync.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from contextlib import suppress
4 |
5 | from playwright.sync_api import Page
6 |
7 | from recognizer.agents.playwright import SyncChallenger
8 |
9 |
10 | def test_sync_challenger(sync_page: Page):
11 | challenger = SyncChallenger(sync_page, click_timeout=1000)
12 | # For slow Pytest Loading
13 | challenger.detector.detection_models.check_loaded()
14 |
15 | sync_page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
16 |
17 | with suppress(RecursionError):
18 | res = challenger.solve_recaptcha()
19 | assert res
20 |
--------------------------------------------------------------------------------
/tests/test_recaptcha_sites.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from contextlib import suppress
4 |
5 | import pytest
6 | from patchright.async_api import Page
7 |
8 | from recognizer.agents.playwright import AsyncChallenger
9 |
10 | # All URLs:
11 | # https://berstend.github.io/static/recaptcha/enterprise-checkbox-auto-recaptchadotnet.html
12 | # https://berstend.github.io/static/recaptcha/enterprise-checkbox-auto.html
13 | # https://berstend.github.io/static/recaptcha/enterprise-checkbox-explicit.html
14 | # https://berstend.github.io/static/recaptcha/v2-checkbox-auto-nowww.html
15 | # https://berstend.github.io/static/recaptcha/v2-checkbox-auto-recaptchadotnet-nowww.html
16 | # https://berstend.github.io/static/recaptcha/v2-checkbox-explicit.html
17 | # https://berstend.github.io/static/recaptcha/v2-invisible-auto.html
18 | # https://berstend.github.io/static/recaptcha/v2-invisible-explicit.html
19 | # https://berstend.github.io/static/recaptcha/v2-invisible-explicit-isolated.html
20 | # https://www.recaptcha.net/recaptcha/api2/demo
21 | # https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php
22 | # https://nopecha.com/demo/recaptcha#easy
23 | # https://nopecha.com/demo/recaptcha#moderate
24 | # https://nopecha.com/demo/recaptcha#hard
25 | # https://2captcha.com/demo/recaptcha-v2
26 | # https://2captcha.com/demo/recaptcha-v2-invisible
27 | # https://2captcha.com/demo/recaptcha-v2-callback
28 | # https://2captcha.com/demo/recaptcha-v2-enterprise
29 | # https://2captcha.com/demo/recaptcha-v3-enterprise
30 | # https://2captcha.com/demo/recaptcha-v3
31 | # https://patrickhlauke.github.io/recaptcha/
32 | # https://testrecaptcha.github.io/
33 | # http://www.recaptcha2.lyates.com/
34 | # https://ask.usda.gov/resource/1589940255000/recaptcha2
35 | # https://jfo.moj.go.th/page/complain3.php
36 | # https://huyliem.z23.web.core.windows.net/
37 | # https://www.opju.ac.in/nitincap
38 | # https://www.flight-simulators.co.uk/acatalog/mailtest1.php
39 | # https://evans-email.glitch.me/
40 |
41 |
42 | @pytest.mark.asyncio
43 | async def test_bernsted_enterprise_auto_recaptchadotnet(async_page: Page):
44 | challenger = AsyncChallenger(async_page, click_timeout=1000)
45 | # For slow Pytest Loading
46 | challenger.detector.detection_models.check_loaded()
47 |
48 | await async_page.goto("https://berstend.github.io/static/recaptcha/enterprise-checkbox-auto-recaptchadotnet.html")
49 |
50 | with suppress(RecursionError):
51 | res = await challenger.solve_recaptcha()
52 | assert res
53 |
54 |
55 | @pytest.mark.asyncio
56 | async def test_bernsted_enterprise_auto(async_page: Page):
57 | challenger = AsyncChallenger(async_page, click_timeout=1000)
58 | # For slow Pytest Loading
59 | challenger.detector.detection_models.check_loaded()
60 |
61 | await async_page.goto("https://berstend.github.io/static/recaptcha/enterprise-checkbox-auto.html")
62 |
63 | with suppress(RecursionError):
64 | res = await challenger.solve_recaptcha()
65 | assert res
66 |
67 |
68 | @pytest.mark.asyncio
69 | async def test_bernsted_enterprise_explicit(async_page: Page):
70 | challenger = AsyncChallenger(async_page, click_timeout=1000)
71 | # For slow Pytest Loading
72 | challenger.detector.detection_models.check_loaded()
73 |
74 | await async_page.goto("https://berstend.github.io/static/recaptcha/enterprise-checkbox-explicit.html")
75 |
76 | with suppress(RecursionError):
77 | res = await challenger.solve_recaptcha()
78 | assert res
79 |
80 |
81 | @pytest.mark.asyncio
82 | async def test_bernsted_v2_auto(async_page: Page):
83 | challenger = AsyncChallenger(async_page, click_timeout=1000)
84 | # For slow Pytest Loading
85 | challenger.detector.detection_models.check_loaded()
86 |
87 | await async_page.goto("https://berstend.github.io/static/recaptcha/v2-checkbox-auto-nowww.html")
88 |
89 | with suppress(RecursionError):
90 | res = await challenger.solve_recaptcha()
91 | assert res
92 |
93 |
94 | @pytest.mark.asyncio
95 | async def test_bernsted_v2_auto_recaptchadotnet(async_page: Page):
96 | challenger = AsyncChallenger(async_page, click_timeout=1000)
97 | # For slow Pytest Loading
98 | challenger.detector.detection_models.check_loaded()
99 |
100 | await async_page.goto("https://berstend.github.io/static/recaptcha/v2-checkbox-auto-recaptchadotnet-nowww.html")
101 |
102 | with suppress(RecursionError):
103 | res = await challenger.solve_recaptcha()
104 | assert res
105 |
106 |
107 | @pytest.mark.asyncio
108 | async def test_bernsted_v2_explicit(async_page: Page):
109 | challenger = AsyncChallenger(async_page, click_timeout=1000)
110 | # For slow Pytest Loading
111 | challenger.detector.detection_models.check_loaded()
112 |
113 | await async_page.goto("https://berstend.github.io/static/recaptcha/v2-checkbox-explicit.html")
114 |
115 | with suppress(RecursionError):
116 | res = await challenger.solve_recaptcha()
117 | assert res
118 |
119 |
120 | @pytest.mark.asyncio
121 | async def test_bernsted_v2_invisible_auto(async_page: Page):
122 | challenger = AsyncChallenger(async_page, click_timeout=1000)
123 | # For slow Pytest Loading
124 | challenger.detector.detection_models.check_loaded()
125 |
126 | await async_page.goto("https://berstend.github.io/static/recaptcha/v2-invisible-auto.html")
127 | await async_page.click("[data-callback='onSubmit']")
128 |
129 | with suppress(RecursionError):
130 | res = await challenger.solve_recaptcha()
131 | assert res
132 |
133 |
134 | @pytest.mark.asyncio
135 | async def test_bernsted_v2_invisible_explicit(async_page: Page):
136 | challenger = AsyncChallenger(async_page, click_timeout=1000)
137 | # For slow Pytest Loading
138 | challenger.detector.detection_models.check_loaded()
139 |
140 | await async_page.goto("https://berstend.github.io/static/recaptcha/v2-invisible-explicit.html")
141 | await async_page.click("[id='submit']")
142 |
143 | with suppress(RecursionError):
144 | res = await challenger.solve_recaptcha()
145 | assert res
146 |
147 |
148 | @pytest.mark.asyncio
149 | async def test_bernsted_v2_invisible_explicit_isolated(async_page: Page):
150 | challenger = AsyncChallenger(async_page, click_timeout=1000)
151 | # For slow Pytest Loading
152 | challenger.detector.detection_models.check_loaded()
153 |
154 | await async_page.goto("https://berstend.github.io/static/recaptcha/v2-invisible-explicit-isolated.html")
155 | await async_page.click("[id='submit']")
156 |
157 | with suppress(RecursionError):
158 | res = await challenger.solve_recaptcha()
159 | assert res
160 |
161 |
162 | @pytest.mark.asyncio
163 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
164 | async def test_recaptcha_net(async_page: Page):
165 | challenger = AsyncChallenger(async_page, click_timeout=1000)
166 | # For slow Pytest Loading
167 | challenger.detector.detection_models.check_loaded()
168 |
169 | await async_page.goto("https://www.recaptcha.net/recaptcha/api2/demo")
170 |
171 | with suppress(RecursionError):
172 | res = await challenger.solve_recaptcha()
173 | assert res
174 |
175 |
176 | @pytest.mark.asyncio
177 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
178 | async def test_recaptcha_demo_appspot(async_page: Page):
179 | challenger = AsyncChallenger(async_page, click_timeout=1000)
180 | # For slow Pytest Loading
181 | challenger.detector.detection_models.check_loaded()
182 |
183 | await async_page.goto("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php")
184 |
185 | with suppress(RecursionError):
186 | res = await challenger.solve_recaptcha()
187 | assert res
188 |
189 |
190 | @pytest.mark.asyncio
191 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
192 | async def test_nopecha_easy(async_page: Page):
193 | challenger = AsyncChallenger(async_page, click_timeout=1000)
194 | # For slow Pytest Loading
195 | challenger.detector.detection_models.check_loaded()
196 |
197 | await async_page.goto("https://nopecha.com/demo/recaptcha#easy")
198 |
199 | with suppress(RecursionError):
200 | res = await challenger.solve_recaptcha()
201 | assert res
202 |
203 |
204 | @pytest.mark.asyncio
205 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
206 | async def test_nopecha_moderate(async_page: Page):
207 | challenger = AsyncChallenger(async_page, click_timeout=1000)
208 | # For slow Pytest Loading
209 | challenger.detector.detection_models.check_loaded()
210 |
211 | await async_page.goto("https://nopecha.com/demo/recaptcha#moderate")
212 |
213 | with suppress(RecursionError):
214 | res = await challenger.solve_recaptcha()
215 | assert res
216 |
217 |
218 | @pytest.mark.asyncio
219 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
220 | async def test_nopecha_hard(async_page: Page):
221 | challenger = AsyncChallenger(async_page, click_timeout=1000)
222 | # For slow Pytest Loading
223 | challenger.detector.detection_models.check_loaded()
224 |
225 | await async_page.goto("https://nopecha.com/demo/recaptcha#hard")
226 |
227 | with suppress(RecursionError):
228 | res = await challenger.solve_recaptcha()
229 | assert res
230 |
231 |
232 | @pytest.mark.asyncio
233 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
234 | async def test_2captcha_v2(async_page: Page):
235 | challenger = AsyncChallenger(async_page, click_timeout=1000)
236 | # For slow Pytest Loading
237 | challenger.detector.detection_models.check_loaded()
238 |
239 | await async_page.goto("https://2captcha.com/demo/recaptcha-v2")
240 |
241 | with suppress(RecursionError):
242 | res = await challenger.solve_recaptcha()
243 | assert res
244 |
245 |
246 | @pytest.mark.asyncio
247 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
248 | async def test_2captcha_v2_invisible(async_page: Page):
249 | challenger = AsyncChallenger(async_page, click_timeout=1000)
250 | # For slow Pytest Loading
251 | challenger.detector.detection_models.check_loaded()
252 |
253 | await async_page.goto("https://2captcha.com/demo/recaptcha-v2-invisible")
254 |
255 | with suppress(RecursionError):
256 | res = await challenger.solve_recaptcha()
257 | assert res
258 |
259 |
260 | @pytest.mark.asyncio
261 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
262 | async def test_2captcha_v2_callback(async_page: Page):
263 | challenger = AsyncChallenger(async_page, click_timeout=1000)
264 | # For slow Pytest Loading
265 | challenger.detector.detection_models.check_loaded()
266 |
267 | await async_page.goto("https://2captcha.com/demo/recaptcha-v2-callback")
268 |
269 | with suppress(RecursionError):
270 | res = await challenger.solve_recaptcha()
271 | assert res
272 |
273 |
274 | @pytest.mark.asyncio
275 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
276 | async def test_2captcha_v2_enterprise(async_page: Page):
277 | challenger = AsyncChallenger(async_page, click_timeout=1000)
278 | # For slow Pytest Loading
279 | challenger.detector.detection_models.check_loaded()
280 |
281 | await async_page.goto("https://2captcha.com/demo/recaptcha-v2-enterprise")
282 |
283 | with suppress(RecursionError):
284 | res = await challenger.solve_recaptcha()
285 | assert res
286 |
287 |
288 | @pytest.mark.asyncio
289 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
290 | async def test_2captcha_v3_enterprise(async_page: Page):
291 | challenger = AsyncChallenger(async_page, click_timeout=1000)
292 | # For slow Pytest Loading
293 | challenger.detector.detection_models.check_loaded()
294 |
295 | await async_page.goto("https://2captcha.com/demo/recaptcha-v3-enterprise")
296 |
297 | with suppress(RecursionError):
298 | res = await challenger.solve_recaptcha()
299 | assert res
300 |
301 |
302 | @pytest.mark.asyncio
303 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
304 | async def test_2captcha_v3(async_page: Page):
305 | challenger = AsyncChallenger(async_page, click_timeout=1000)
306 | # For slow Pytest Loading
307 | challenger.detector.detection_models.check_loaded()
308 |
309 | await async_page.goto("https://2captcha.com/demo/recaptcha-v3")
310 |
311 | with suppress(RecursionError):
312 | res = await challenger.solve_recaptcha()
313 | assert res
314 |
315 |
316 | @pytest.mark.asyncio
317 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
318 | async def test_patrickhlauke(async_page: Page):
319 | challenger = AsyncChallenger(async_page, click_timeout=1000)
320 | # For slow Pytest Loading
321 | challenger.detector.detection_models.check_loaded()
322 |
323 | await async_page.goto("https://patrickhlauke.github.io/recaptcha/")
324 |
325 | with suppress(RecursionError):
326 | res = await challenger.solve_recaptcha()
327 | assert res
328 |
329 |
330 | @pytest.mark.asyncio
331 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
332 | async def test_testrecaptcha_github(async_page: Page):
333 | challenger = AsyncChallenger(async_page, click_timeout=1000)
334 | # For slow Pytest Loading
335 | challenger.detector.detection_models.check_loaded()
336 |
337 | await async_page.goto("https://testrecaptcha.github.io/")
338 |
339 | with suppress(RecursionError):
340 | res = await challenger.solve_recaptcha()
341 | assert res
342 |
343 |
344 | @pytest.mark.asyncio
345 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
346 | async def test_lyates_v2(async_page: Page):
347 | challenger = AsyncChallenger(async_page, click_timeout=1000)
348 | # For slow Pytest Loading
349 | challenger.detector.detection_models.check_loaded()
350 |
351 | await async_page.goto("http://www.recaptcha2.lyates.com/")
352 |
353 | with suppress(RecursionError):
354 | res = await challenger.solve_recaptcha()
355 | assert res
356 |
357 |
358 | @pytest.mark.asyncio
359 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
360 | async def test_usda_v2(async_page: Page):
361 | challenger = AsyncChallenger(async_page, click_timeout=1000)
362 | # For slow Pytest Loading
363 | challenger.detector.detection_models.check_loaded()
364 |
365 | await async_page.goto("https://ask.usda.gov/resource/1589940255000/recaptcha2")
366 |
367 | with suppress(RecursionError):
368 | res = await challenger.solve_recaptcha()
369 | assert res
370 |
371 |
372 | @pytest.mark.asyncio
373 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
374 | async def test_jfo_moj_go_th_v3(async_page: Page):
375 | challenger = AsyncChallenger(async_page, click_timeout=1000)
376 | # For slow Pytest Loading
377 | challenger.detector.detection_models.check_loaded()
378 |
379 | await async_page.goto("https://jfo.moj.go.th/page/complain3.php")
380 |
381 | with suppress(RecursionError):
382 | res = await challenger.solve_recaptcha()
383 | assert res
384 |
385 |
386 | @pytest.mark.asyncio
387 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
388 | async def test_huyliem_windows(async_page: Page):
389 | challenger = AsyncChallenger(async_page, click_timeout=1000)
390 | # For slow Pytest Loading
391 | challenger.detector.detection_models.check_loaded()
392 |
393 | await async_page.goto("https://huyliem.z23.web.core.windows.net/")
394 |
395 | with suppress(RecursionError):
396 | res = await challenger.solve_recaptcha()
397 | assert res
398 |
399 |
400 | @pytest.mark.asyncio
401 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
402 | async def test_opju_ac_in(async_page: Page):
403 | challenger = AsyncChallenger(async_page, click_timeout=1000)
404 | # For slow Pytest Loading
405 | challenger.detector.detection_models.check_loaded()
406 |
407 | await async_page.goto("https://www.opju.ac.in/nitincap")
408 |
409 | with suppress(RecursionError):
410 | res = await challenger.solve_recaptcha()
411 | assert res
412 |
413 |
414 | @pytest.mark.asyncio
415 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
416 | async def test_flight_simulators_mailtest(async_page: Page):
417 | challenger = AsyncChallenger(async_page, click_timeout=1000)
418 | # For slow Pytest Loading
419 | challenger.detector.detection_models.check_loaded()
420 |
421 | await async_page.goto("https://www.flight-simulators.co.uk/acatalog/mailtest1.php")
422 |
423 | with suppress(RecursionError):
424 | res = await challenger.solve_recaptcha()
425 | assert res
426 |
427 |
428 | @pytest.mark.asyncio
429 | @pytest.mark.skip(reason="No different challenge type, Skipping due to time complexity")
430 | async def test_evans_email_glitch(async_page: Page):
431 | challenger = AsyncChallenger(async_page, click_timeout=1000)
432 | # For slow Pytest Loading
433 | challenger.detector.detection_models.check_loaded()
434 |
435 | await async_page.goto("https://evans-email.glitch.me/")
436 |
437 | with suppress(RecursionError):
438 | res = await challenger.solve_recaptcha()
439 | assert res
440 |
--------------------------------------------------------------------------------