From b33490f291041f5378421ab71686fb6e2fd89574 Mon Sep 17 00:00:00 2001 From: Vinyzu <50874994+Vinyzu@users.noreply.github.com> Date: Mon, 12 Feb 2024 20:23:05 +0100 Subject: [PATCH] Update v1.3 - Fixed Timeout Retrying - Faster Detector Model Loading - Improved ReadMe - Updated Github Workflow to PreInstall Huggingface Models - Updated Requirements --- .github/workflows/ci.yml | 5 +++ README.md | 17 +++++++-- recognizer/__init__.py | 2 +- recognizer/agents/playwright/async_control.py | 29 +++++++++------ recognizer/agents/playwright/sync_control.py | 29 +++++++++------ recognizer/components/detector.py | 37 +++++++++++++------ requirements-test.txt | 2 +- requirements.txt | 6 +-- setup.cfg | 2 +- 9 files changed, 85 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c036251..df1416f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,5 +49,10 @@ jobs: uses: browser-actions/setup-chrome@v1 - name: Install Chromium Driver run: python -m playwright install chromium + - name: Install HuggingFace Models + run: | + pip install -U "huggingface_hub[cli]" + huggingface-cli download flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K config.json + huggingface-cli download CIDAS/clipseg-rd64-refined config.json - name: Test with PyTest run: pytest \ No newline at end of file diff --git a/README.md b/README.md index ad41905..922ed7b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -# reCognizer v1.2 -![Tests & Linting](https://github.com/Vinyzu/recognizer/actions/workflows/tests.yml/badge.svg) +# reCognizer v1.3 +![Tests & Linting](https://github.com/Vinyzu/recognizer/actions/workflows/ci.yml/badge.svg) + #### 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. @@ -13,6 +14,16 @@ pip install recognizer --- +## Examples + +### Possible Image Inputs +![Accepted Formats](https://i.ibb.co/nztTD9Z/formats.png) + +### Example Solve Video (Good IP & Botright) +https://github.com/Vinyzu/recognizer/assets/50874994/95a713e3-bb46-474b-994f-cb3dacae9279 + +--- + ## Basic Usage ```py @@ -107,7 +118,7 @@ asyncio.run(main()) --- -![Version](https://img.shields.io/badge/reCognizer-v1.2-blue) +![Version](https://img.shields.io/badge/reCognizer-v1.3-blue) ![License](https://img.shields.io/badge/License-GNU%20GPL-green) ![Python](https://img.shields.io/badge/Python-v3.x-lightgrey) diff --git a/recognizer/__init__.py b/recognizer/__init__.py index 3680bdb..0a2b1cd 100644 --- a/recognizer/__init__.py +++ b/recognizer/__init__.py @@ -2,6 +2,6 @@ from .components.detector import Detector -VERSION = 1.2 +VERSION = 1.3 __all__ = ["Detector", "VERSION"] diff --git a/recognizer/agents/playwright/async_control.py b/recognizer/agents/playwright/async_control.py index 13d67fe..17e70f6 100644 --- a/recognizer/agents/playwright/async_control.py +++ b/recognizer/agents/playwright/async_control.py @@ -31,11 +31,6 @@ def __init__(self, page: Page, click_timeout: Optional[int] = None, retry_times: self.dynamic: bool = False self.captcha_token: Optional[str] = None - def check_retry(self): - self.retried += 1 - if self.retried >= self.retry_times: - raise RecursionError(f"Exceeded maximum retry times of {self.retry_times}") - async def route_handler(self, route: Route, request: Request) -> None: response = await route.fetch() await route.fulfill(response=response) # Instant Fulfillment to save Time @@ -69,10 +64,11 @@ async def check_captcha_visible(self): label_obj = captcha_frame.locator("//strong") try: await label_obj.wait_for(state="visible", timeout=10000) - return True except TimeoutError: return False + return await label_obj.is_visible() + async def click_checkbox(self) -> bool: # Clicking Captcha Checkbox try: @@ -98,7 +94,9 @@ async def detect_tiles(self, prompt: str, area_captcha: bool) -> bool: async def load_captcha(self, captcha_frame: Optional[FrameLocator] = None, reset: Optional[bool] = False) -> Union[str, bool]: # Retrying - self.check_retry() + self.retried += 1 + if self.retried >= self.retry_times: + raise RecursionError(f"Exceeded maximum retry times of {self.retry_times}") if not await self.check_captcha_visible(): if captcha_token := await self.check_result(): @@ -111,8 +109,11 @@ async def load_captcha(self, captcha_frame: Optional[FrameLocator] = None, reset # Clicking Reload Button if reset: assert isinstance(captcha_frame, FrameLocator) - reload_button = captcha_frame.locator("#recaptcha-verify-button") - await reload_button.click() + try: + reload_button = captcha_frame.locator("#recaptcha-reload-button") + await reload_button.click() + except TimeoutError: + return await self.load_captcha() # Resetting Values self.dynamic = False @@ -147,7 +148,7 @@ async def handle_recaptcha(self) -> Union[str, bool]: area_captcha = len(recaptcha_tiles) == 16 result_clicked = await self.detect_tiles(prompt, area_captcha) - if self.dynamic and not len(recaptcha_tiles) == 16: + if self.dynamic and not area_captcha: while result_clicked: await self.page.wait_for_timeout(5000) result_clicked = await self.detect_tiles(prompt, area_captcha) @@ -170,9 +171,13 @@ async def handle_recaptcha(self) -> Union[str, bool]: await self.page.wait_for_timeout(1000) + # Check if error occurred whilst solving + incorrect = self.page.locator("[class='rc-imageselect-incorrect-response']") + errors = self.page.locator("[class *= 'rc-imageselect-error']") + if await incorrect.is_visible() or any([await error.is_visible() for error in await errors.all()]): + await self.load_captcha(captcha_frame, reset=True) + # Retrying - self.check_retry() - await self.load_captcha(captcha_frame, reset=True) return await self.handle_recaptcha() async def solve_recaptcha(self) -> Union[str, bool]: diff --git a/recognizer/agents/playwright/sync_control.py b/recognizer/agents/playwright/sync_control.py index bfd64c1..e8d1d82 100644 --- a/recognizer/agents/playwright/sync_control.py +++ b/recognizer/agents/playwright/sync_control.py @@ -31,11 +31,6 @@ def __init__(self, page: Page, click_timeout: Optional[int] = None, retry_times: self.dynamic: bool = False self.captcha_token: Optional[str] = None - def check_retry(self): - self.retried += 1 - if self.retried >= self.retry_times: - raise RecursionError(f"Exceeded maximum retry times of {self.retry_times}") - def route_handler(self, route: Route, request: Request) -> None: response = route.fetch() route.fulfill(response=response) # Instant Fulfillment to save Time @@ -69,10 +64,11 @@ def check_captcha_visible(self): label_obj = captcha_frame.locator("//strong") try: label_obj.wait_for(state="visible", timeout=10000) - return True except TimeoutError: return False + return label_obj.is_visible() + def click_checkbox(self) -> bool: # Clicking Captcha Checkbox try: @@ -98,7 +94,9 @@ def detect_tiles(self, prompt: str, area_captcha: bool) -> bool: def load_captcha(self, captcha_frame: Optional[FrameLocator] = None, reset: Optional[bool] = False) -> Union[str, bool]: # Retrying - self.check_retry() + self.retried += 1 + if self.retried >= self.retry_times: + raise RecursionError(f"Exceeded maximum retry times of {self.retry_times}") if not self.check_captcha_visible(): if captcha_token := self.check_result(): @@ -111,8 +109,11 @@ def load_captcha(self, captcha_frame: Optional[FrameLocator] = None, reset: Opti # Clicking Reload Button if reset: assert isinstance(captcha_frame, FrameLocator) - reload_button = captcha_frame.locator("#recaptcha-verify-button") - reload_button.click() + try: + reload_button = captcha_frame.locator("#recaptcha-reload-button") + reload_button.click() + except TimeoutError: + return self.load_captcha() # Resetting Values self.dynamic = False @@ -147,7 +148,7 @@ def handle_recaptcha(self) -> Union[str, bool]: area_captcha = len(recaptcha_tiles) == 16 result_clicked = self.detect_tiles(prompt, area_captcha) - if self.dynamic and not len(recaptcha_tiles) == 16: + if self.dynamic and not area_captcha: while result_clicked: self.page.wait_for_timeout(5000) result_clicked = self.detect_tiles(prompt, area_captcha) @@ -170,9 +171,13 @@ def handle_recaptcha(self) -> Union[str, bool]: self.page.wait_for_timeout(1000) + # Check if error occurred whilst solving + incorrect = self.page.locator("[class='rc-imageselect-incorrect-response']") + errors = self.page.locator("[class *= 'rc-imageselect-error']") + if incorrect.is_visible() or any([error.is_visible() for error in errors.all()]): + self.load_captcha(captcha_frame, reset=True) + # Retrying - self.check_retry() - self.load_captcha(captcha_frame, reset=True) return self.handle_recaptcha() def solve_recaptcha(self) -> Union[str, bool]: diff --git a/recognizer/components/detector.py b/recognizer/components/detector.py index b772617..d21e9b9 100644 --- a/recognizer/components/detector.py +++ b/recognizer/components/detector.py @@ -3,10 +3,10 @@ import math import random import warnings -from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import Future, ThreadPoolExecutor from os import PathLike from pathlib import Path -from typing import List, Optional, Sequence, Tuple, Union +from typing import Callable, List, Optional, Sequence, Tuple, Union import cv2 from numpy import generic, uint8 @@ -23,12 +23,16 @@ class DetectionModels: def __init__(self) -> None: # Preloading: Loading Models takes ~9 seconds - set_num_threads(2) - self.executor = ThreadPoolExecutor(max_workers=2) + set_num_threads(5) + self.executor = ThreadPoolExecutor(max_workers=5) + self.loading_futures: List[Future[Callable[..., None]]] = [] try: - self.yolo_loading_feature = self.executor.submit(self._load_yolo_detector) - self.clip_loading_feature = self.executor.submit(self._load_clip_detector) + self.loading_futures.append(self.executor.submit(self._load_yolo_detector)) + self.loading_futures.append(self.executor.submit(self._load_vit_model)) + self.loading_futures.append(self.executor.submit(self._load_vit_processor)) + self.loading_futures.append(self.executor.submit(self._load_seg_model)) + self.loading_futures.append(self.executor.submit(self._load_seg_processor)) except Exception as e: self.executor.shutdown(wait=True, cancel_futures=True) raise e @@ -38,20 +42,31 @@ def _load_yolo_detector(self): self.yolo_model = YOLO("yolov8m-seg.pt") - def _load_clip_detector(self): - from transformers import CLIPModel, CLIPProcessor, CLIPSegForImageSegmentation, CLIPSegProcessor + def _load_vit_model(self): + from transformers import CLIPModel self.vit_model = CLIPModel.from_pretrained("flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K") + + def _load_vit_processor(self): + from transformers import CLIPProcessor + self.vit_processor = CLIPProcessor.from_pretrained("flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K") + def _load_seg_model(self): + from transformers import CLIPSegForImageSegmentation + self.seg_model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined") + + def _load_seg_processor(self): + from transformers import CLIPSegProcessor + self.seg_processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined") def check_loaded(self): try: - if not self.yolo_loading_feature.done() or not self.clip_loading_feature.done(): - self.yolo_loading_feature.result() - self.clip_loading_feature.result() + if not all([future.done() for future in self.loading_futures]): + for future in self.loading_futures: + future.result() assert self.yolo_model assert self.seg_model diff --git a/requirements-test.txt b/requirements-test.txt index a35876f..084b456 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ flake8==7.0.0 tox==4.12.1 pytest==7.4.4 -pytest_asyncio==0.23.4 +pytest_asyncio==0.23.5 mypy==1.8.0 types-setuptools==69.0.0.20240125 botright==0.5 diff --git a/requirements.txt b/requirements.txt index 865b5e9..a686a8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -setuptools~=69.0.3 +setuptools~=69.1.0 opencv-python~=4.9.0.80 imageio~=2.33.1 ultralytics~=8.1.9 transformers~=4.37.2 -numpy~=1.26.3 -playwright~=1.41.0 \ No newline at end of file +numpy~=1.26.4 +playwright~=1.41.2 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 32240db..64a7cc4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = recognizer version = attr: recognizer.VERSION -description = 🦉 Gracefully face reCAPTCHA challenge with ultralytics YOLOv8-seg and CLIPs VIT-B/32. Implemented in playwright or an easy-to-use API. +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. long_description = file: README.md long_description_content_type = text/markdown author = Vinyzu