├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release_workflow.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── demo_botright.py ├── demo_detect.py ├── demo_normal_playwright.py ├── demo_patchright.py └── images │ ├── area_image.png │ ├── area_no_yolo.png │ ├── area_no_yolo1.png │ ├── classify_image.png │ ├── classify_no_yolo.png │ ├── full_page.png │ └── only_captcha.png ├── pyproject.toml ├── recognizer ├── __init__.py ├── agents │ ├── __init__.py │ └── playwright │ │ ├── __init__.py │ │ ├── async_control.py │ │ └── sync_control.py └── components │ ├── __init__.py │ ├── detection_processor.py │ ├── detector.py │ ├── image_processor.py │ └── prompt_handler.py ├── requirements-test.txt ├── requirements.txt ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── images ├── area_image.png ├── area_no_yolo.png ├── classify_image.png ├── classify_no_yolo.png ├── full_page.png ├── no_captcha.png ├── only_captcha.png └── splitted │ ├── img0.png │ ├── img1.png │ ├── img2.png │ ├── img3.png │ ├── img4.png │ ├── img5.png │ ├── img6.png │ ├── img7.png │ └── img8.png ├── test_detector.py ├── test_inputs.py ├── test_playwright_async.py ├── test_playwright_sync.py └── test_recaptcha_sites.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 200 3 | extend-ignore = E203, E704 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Code Sample** 14 | If applicable, add a code sample to replicate the bug. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about Botright 4 | title: '[Question] ' 5 | labels: question, help wanted, documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe your quesiton** 11 | A clear and concise question. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Desktop (please complete the following information):** 20 | - OS: [e.g. iOS] 21 | - Version [e.g. 22] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: reCognizer CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | Linting: 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 3.11 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.11' 16 | 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r requirements-test.txt 21 | 22 | - name: (Linting) Ruff 23 | run: ruff check . 24 | 25 | - name: (Formatting) Ruff 26 | run: ruff format . 27 | 28 | - name: (Type-Checking) MyPy 29 | run: mypy . 30 | 31 | Build: 32 | strategy: 33 | matrix: 34 | os: [windows-latest, ubuntu-latest] 35 | python-version: ['3.9', '3.10', '3.11', '3.12'] 36 | 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | 45 | - name: Install dependencies 46 | run: | 47 | python -m pip install --upgrade pip 48 | pip install -r requirements-test.txt 49 | pip install -e . 50 | python -c "import os; os.environ['TOKENIZERS_PARALLELISM'] = 'false'" 51 | 52 | - name: Install Chrome Browser 53 | uses: browser-actions/setup-chrome@v1 54 | 55 | - name: Install Playwright Chromium Driver 56 | run: python -m playwright install chromium 57 | 58 | - name: Install Patchright Chromium Driver 59 | run: python -m patchright install chromium 60 | 61 | - name: Install HuggingFace Models 62 | run: | 63 | pip install -U "huggingface_hub[cli]" 64 | huggingface-cli download flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K 65 | huggingface-cli download CIDAS/clipseg-rd64-refined 66 | 67 | - name: Test with PyTest 68 | run: pytest --reruns 5 --only-rerun TimeoutError -v --ignore tests/test_recaptcha_sites.py 69 | 70 | Test: 71 | runs-on: windows-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - name: Set up Python 3.11 75 | uses: actions/setup-python@v4 76 | with: 77 | python-version: '3.11' 78 | 79 | - name: Install dependencies 80 | run: | 81 | python -m pip install --upgrade pip 82 | pip install -r requirements-test.txt 83 | pip install -e . 84 | python -c "import os; os.environ['TOKENIZERS_PARALLELISM'] = 'false'" 85 | 86 | - name: Install Chrome Browser 87 | uses: browser-actions/setup-chrome@v1 88 | 89 | - name: Install Playwright Chromium Driver 90 | run: python -m playwright install chromium 91 | 92 | - name: Install Patchright Chromium Driver 93 | run: python -m patchright install chromium 94 | 95 | - name: Install HuggingFace Models 96 | run: | 97 | pip install -U "huggingface_hub[cli]" 98 | huggingface-cli download flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K 99 | huggingface-cli download CIDAS/clipseg-rd64-refined 100 | 101 | - name: Test with PyTest 102 | run: pytest --reruns 5 --only-rerun TimeoutError -v tests/test_recaptcha_sites.py -------------------------------------------------------------------------------- /.github/workflows/release_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Release Workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to publish (optional)' 8 | required: true 9 | 10 | permissions: 11 | actions: none 12 | attestations: none 13 | checks: none 14 | contents: write 15 | deployments: none 16 | id-token: write # For trusted Publishing 17 | issues: none 18 | discussions: none 19 | packages: none 20 | pages: none 21 | pull-requests: none 22 | repository-projects: none 23 | security-events: none 24 | statuses: none 25 | 26 | jobs: 27 | release-workflow: 28 | name: "Release Workflow, Release Automaticly with Patch Version Change or specify version" 29 | runs-on: ubuntu-latest 30 | environment: 31 | name: pypi 32 | url: https://pypi.org/p/recognizer 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Python 3.11 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: '3.11' 39 | 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install build 44 | pip install -r requirements.txt 45 | pip install -e . 46 | 47 | - name: Update version in package 48 | run: | 49 | VERSION_INPUT="${{ github.event.inputs.version }}" 50 | awk -v version="$VERSION_INPUT" '/^VERSION =/ { sub(/=.*/, "= \"" version "\"") } { print }' ./recognizer/__init__.py > temp && mv temp ./recognizer/__init__.py 51 | echo "Updated version to $VERSION_INPUT" 52 | 53 | - name: Verify version 54 | run: | 55 | VERSION_INPUT="${{ github.event.inputs.version }}" 56 | if ! grep -q "^VERSION = \"$VERSION_INPUT\"" ./recognizer/__init__.py; then 57 | echo "Version mismatch! Expected VERSION = \"$VERSION_INPUT\"" 58 | exit 1 59 | fi 60 | echo "Version verified successfully as $VERSION_INPUT" 61 | 62 | - name: Build the library 63 | run: | 64 | python -m build 65 | 66 | - name: Publish to PyPI 67 | uses: pypa/gh-action-pypi-publish@release/v1 68 | with: 69 | verbose: true 70 | 71 | - name: Commit and push version update 72 | uses: stefanzweifel/git-auto-commit-action@v5 73 | with: 74 | commit_message: "Chore: Update Version to ${{ github.event.inputs.version }} [skip ci]" 75 | file_pattern: "recognizer/__init__.py" 76 | commit_user_name: reCognizer Actions Bot 77 | commit_options: "--no-verify" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | .idea 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | # PyCharm 149 | .idea/ 150 | 151 | # YOLO 152 | *.pt 153 | 154 | user_data/ 155 | 156 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | Copyright (C) 2024 Vinyzu 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper mail. 650 | 651 | If the program does terminal interaction, make it output a short 652 | notice like this when it starts in an interactive mode: 653 | 654 | reCognizer Copyright (C) 2024 Vinyzu 655 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 656 | This is free software, and you are welcome to redistribute it 657 | under certain conditions; type `show c' for details. 658 | 659 | The hypothetical commands `show w' and `show c' should show the appropriate 660 | parts of the General Public License. Of course, your program's commands 661 | might be different; for a GUI interface, you would use an "about box". 662 | 663 | You should also get your employer (if you work as a programmer) or school, 664 | if any, to sign a "copyright disclaimer" for the program, if necessary. 665 | For more information on this, and how to apply and follow the GNU GPL, see 666 | . 667 | 668 | The GNU General Public License does not permit incorporating your program 669 | into proprietary programs. If your program is a subroutine library, you 670 | may consider it more useful to permit linking proprietary applications with 671 | the library. If this is what you want to do, use the GNU Lesser General 672 | Public License instead of this License. But first, please read 673 | . 674 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🎭 reCognizer 3 |

4 | 5 | 6 |

7 | 8 | Patchright Version 9 | 10 | 11 | 12 | 13 | 14 | GitHub Downloads (all assets, all releases) 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 | ![Accepted Formats](https://i.ibb.co/nztTD9Z/formats.png) 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 | ![Version](https://img.shields.io/pypi/v/reCognizer?display_name=release&label=reCognizer) 156 | ![License](https://img.shields.io/badge/License-GNU%20GPL-green) 157 | ![Python](https://img.shields.io/badge/Python-v3.x-lightgrey) 158 | 159 | [![my-discord](https://img.shields.io/badge/My_Discord-000?style=for-the-badge&logo=google-chat&logoColor=blue)](https://discordapp.com/users/935224495126487150) 160 | [![buy-me-a-coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-000?style=for-the-badge&logo=ko-fi&logoColor=brown)](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 | --------------------------------------------------------------------------------