From 513757cd6e12ee919bfce32df90e83c35966068a Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:36:03 +0800 Subject: [PATCH 01/14] Update pyproject.toml --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c8e85c0a6..16390a8054 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ in-project = true # https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" # Run `poetry install` in the project root opencv-python = "^4.8.0.76" @@ -49,7 +49,7 @@ regex = "*" pydantic = "^2.5.1" tenacity = "^8.2.3" # The following dependencies will not be installed -# when you just run `poetry install` or `pip install hcaptcha-challenger` +# when you just run `poetry install --all-extras` or `pip install hcaptcha-challenger` undetected-chromedriver = { version = "^3.5.3", optional = true } webdriver-manager = { version = "^4.0.1", optional = true } selenium = { version = "*", optional = true } @@ -82,7 +82,7 @@ filterwarnings = "ignore::DeprecationWarning" [tool.black] line-length = 100 -target-version = ["py38", "py39", "py310", "py311"] +target-version = ["py310", "py311", "py312"] skip-magic-trailing-comma = true [tool.poetry.extras] From aa77c3cf1eb95da4535546369b4537c6af0cadf8 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sun, 31 Mar 2024 22:54:45 +0800 Subject: [PATCH 02/14] 1 --- examples/lvm_challenge/vision_minor.ipynb | 48 +++++++++++++++++++++++ pyproject.toml | 3 ++ 2 files changed, 51 insertions(+) create mode 100644 examples/lvm_challenge/vision_minor.ipynb diff --git a/examples/lvm_challenge/vision_minor.ipynb b/examples/lvm_challenge/vision_minor.ipynb new file mode 100644 index 0000000000..86a060e7d9 --- /dev/null +++ b/examples/lvm_challenge/vision_minor.ipynb @@ -0,0 +1,48 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "collapsed": true, + "is_executing": true + }, + "outputs": [], + "source": [ + "# open ../../../static/img/brand/wordmark.png as base64 str\n", + "import base64\n", + "from pathlib import Path\n", + "\n", + "from IPython.display import HTML\n", + "\n", + "img_path = Path(\"../../../static/img/brand/wordmark.png\")\n", + "img_base64 = base64.b64encode(img_path.read_bytes()).decode(\"utf-8\")\n", + "\n", + "# display b64 image in notebook\n", + "HTML(f'')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 16390a8054..0d2c50abd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ ftfy = "*" regex = "*" pydantic = "^2.5.1" tenacity = "^8.2.3" +langchain = "^0.1.13" +langchain-anthropic = "^0.1.4" + # The following dependencies will not be installed # when you just run `poetry install --all-extras` or `pip install hcaptcha-challenger` undetected-chromedriver = { version = "^3.5.3", optional = true } From 30026bbfe4b7d31c754b83f8e11a68acae9748c5 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:29:39 +0800 Subject: [PATCH 03/14] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 61ee239a97..de2ae8749e 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,5 @@ assets/image_label_binary/off_road_vehicle/20** profile_pluggable_model.md ./*.png tests/record_json +docs/lvm_challenge/*.jpeg +docs/lvm_challenge/*.png \ No newline at end of file From 42006459fb48f8bd78fcbaaa2b1fb775ba3ae8e8 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:16:13 +0800 Subject: [PATCH 04/14] new-schema --- .gitignore | 5 +- automation/collector.py | 2 +- automation/sentinel.py | 5 +- backend/main.py | 24 +- backend/routers/__init__.py | 9 + backend/routers/challenge.py | 78 ++++ backend/routers/datalake.py | 18 + examples/demo_classifier_self_supervised.py | 26 +- examples/demo_find_unique_object.py | 2 +- examples/faker_client.py | 63 +++ examples/lvm_challenge/vision_minor.ipynb | 48 -- hcaptcha_challenger/__init__.py | 37 +- hcaptcha_challenger/agents/__init__.py | 4 +- .../agents/pipline/__init__.py | 5 - .../agents/playwright/control.py | 31 +- .../agents/playwright/dragon.py | 432 ++++++++++++++++++ .../{components/middleware.py => models.py} | 68 ++- .../{agents/pipline/control.py => pipline.py} | 16 +- .../{components => tools}/__init__.py | 0 .../{components => tools}/common.py | 4 +- .../cv_toolkit/__init__.py | 0 .../cv_toolkit/appears_only_once.py | 0 .../cv_toolkit/largest_animal.py | 0 .../{components => tools}/image_downloader.py | 0 .../image_label_area_select.py | 2 +- .../image_label_binary.py} | 10 +- .../{components => tools}/prompt_handler.py | 10 +- .../zero_shot_image_classifier.py | 2 +- pyproject.toml | 6 +- tests/__init__.py | 0 tests/test_downloader.py | 2 +- tests/test_normal_playwright.py | 2 +- tests/test_prompt_handler.py | 2 +- 33 files changed, 772 insertions(+), 141 deletions(-) create mode 100644 backend/routers/__init__.py create mode 100644 backend/routers/challenge.py create mode 100644 backend/routers/datalake.py create mode 100644 examples/faker_client.py delete mode 100644 examples/lvm_challenge/vision_minor.ipynb delete mode 100644 hcaptcha_challenger/agents/pipline/__init__.py create mode 100644 hcaptcha_challenger/agents/playwright/dragon.py rename hcaptcha_challenger/{components/middleware.py => models.py} (62%) rename hcaptcha_challenger/{agents/pipline/control.py => pipline.py} (97%) rename hcaptcha_challenger/{components => tools}/__init__.py (100%) rename hcaptcha_challenger/{components => tools}/common.py (97%) rename hcaptcha_challenger/{components => tools}/cv_toolkit/__init__.py (100%) rename hcaptcha_challenger/{components => tools}/cv_toolkit/appears_only_once.py (100%) rename hcaptcha_challenger/{components => tools}/cv_toolkit/largest_animal.py (100%) rename hcaptcha_challenger/{components => tools}/image_downloader.py (100%) rename hcaptcha_challenger/{components => tools}/image_label_area_select.py (97%) rename hcaptcha_challenger/{components/image_classifier.py => tools/image_label_binary.py} (96%) rename hcaptcha_challenger/{components => tools}/prompt_handler.py (84%) rename hcaptcha_challenger/{components => tools}/zero_shot_image_classifier.py (98%) create mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore index de2ae8749e..abb605fa13 100644 --- a/.gitignore +++ b/.gitignore @@ -156,4 +156,7 @@ profile_pluggable_model.md ./*.png tests/record_json docs/lvm_challenge/*.jpeg -docs/lvm_challenge/*.png \ No newline at end of file +docs/lvm_challenge/*.png +**/logs/** +examples/*.md +docs/logs/ \ No newline at end of file diff --git a/automation/collector.py b/automation/collector.py index 8cdd7f1c25..1ca3d6967a 100644 --- a/automation/collector.py +++ b/automation/collector.py @@ -245,7 +245,7 @@ async def _collete_datasets(self, context: ASyncContext, sitelink: str): await page.goto(sitelink) - await agent.handle_checkbox() + await agent.click_checkbox() for pth in range(1, self.per_times + 1): with suppress(Exception): diff --git a/automation/sentinel.py b/automation/sentinel.py index cbe12f1b29..e911122d1c 100644 --- a/automation/sentinel.py +++ b/automation/sentinel.py @@ -23,7 +23,8 @@ import hcaptcha_challenger as solver from hcaptcha_challenger import label_cleaning, split_prompt_message -from hcaptcha_challenger.agents import AgentT, QuestionResp, Malenia +from hcaptcha_challenger.agents import AgentT, Malenia +from hcaptcha_challenger.models import QuestionResp from hcaptcha_challenger.onnx.yolo import is_matched_ash_of_war from hcaptcha_challenger.utils import SiteKey @@ -228,7 +229,7 @@ async def collete_datasets(self, context: ASyncContext, sitekey: str, batch: int sitelink = SiteKey.as_sitelink(sitekey) await page.goto(sitelink) - await agent.handle_checkbox() + await agent.click_checkbox() for pth in range(1, batch + 1): try: diff --git a/backend/main.py b/backend/main.py index de488d5d9f..209977ff45 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,15 +1,25 @@ -from typing import Union - from fastapi import FastAPI +from fastapi.responses import RedirectResponse + +from routers import challenge_router, datalake_router app = FastAPI() +app.include_router(challenge_router, prefix="/challenge") +app.include_router(datalake_router, prefix="/datalake") + @app.get("/") -async def read_root(): - return {"Hello": "World"} +async def home(): + return RedirectResponse(url="https://github.com/QIN2DIM/hcaptcha-challenger") + + +@app.get("/ping", response_model=str) +async def ping(): + return "pong" + +if __name__ == "__main__": + import uvicorn -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} + uvicorn.run("main:app", host="0.0.0.0", port=33777) diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000000..bc7c2d9ba0 --- /dev/null +++ b/backend/routers/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Time : 2024/4/14 12:24 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +from .challenge import router as challenge_router +from .datalake import router as datalake_router + +__all__ = ["challenge_router", "datalake_router"] diff --git a/backend/routers/challenge.py b/backend/routers/challenge.py new file mode 100644 index 0000000000..6eea3d42c7 --- /dev/null +++ b/backend/routers/challenge.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Time : 2024/4/14 12:25 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +from fastapi import APIRouter + + +from typing import List + +from pydantic import BaseModel, Field, Base64Str + +router = APIRouter() + + +class SelfSupervisedPayload(BaseModel): + """hCaptcha payload of the image_label_binary challenge""" + + prompt: str = Field(..., description="challenge prompt") + challenge_images: List[Base64Str] = Field(default_factory=list) + positive_labels: List[str] | None = Field(default_factory=list) + negative_labels: List[str] | None = Field(default_factory=list) + + +class SelfSupervisedResponse(BaseModel): + """The binary classification result of the image, in the same order as the challenge_images.""" + + results: List[bool] = Field(default_factory=list) + + +import os +from pathlib import Path +from typing import List + +import hcaptcha_challenger as solver +from hcaptcha_challenger import handle, ModelHub, DataLake, register_pipline +import pandas as pd + +# Init local-side of the ModelHub +solver.install(upgrade=True, clip=True) + +images_dir = Path("tmp_dir/image_label_binary/streetlamp") + +prompt = "streetlamp" + +# Patch datalake maps not updated in time +datalake_post = { + # => prompt: sedan car + handle(prompt): {"positive_labels": ["streetlamp"], "negative_labels": ["duck", "shark"]} +} + + +def prelude_self_supervised_config(): + modelhub = ModelHub.from_github_repo() + modelhub.parse_objects() + for prompt_, serialized_binary in datalake_post.items(): + modelhub.datalake[prompt_] = DataLake.from_serialized(serialized_binary) + clip_model = register_pipline(modelhub, fmt="onnx") + + return modelhub, clip_model + + +def demo(image_paths: List[Path]): + modelhub, clip_model = prelude_self_supervised_config() + + classifier = solver.BinaryClassifier(modelhub=modelhub, clip_model=clip_model) + if results := classifier.execute(prompt, image_paths, self_supervised=True): + output = [ + {"image": f"![]({image_path})", "result": result} + for image_path, result in zip(image_paths, results) + ] + mdk = pd.DataFrame.from_records(output).to_markdown() + Path(f"result_{prompt}.md").write_text(mdk, encoding="utf8") + + +@router.post("/image_label_binary/clip", response_model=SelfSupervisedResponse) +async def read_item(payload: SelfSupervisedPayload): + return SelfSupervisedResponse() diff --git a/backend/routers/datalake.py b/backend/routers/datalake.py new file mode 100644 index 0000000000..ae419ccbe5 --- /dev/null +++ b/backend/routers/datalake.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Time : 2024/4/14 12:25 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/sync", response_model=str) +async def datalake_sync_from_github_repo(): + pass + + +@router.get("/list", response_model=list) +async def datalake_list(): + pass diff --git a/examples/demo_classifier_self_supervised.py b/examples/demo_classifier_self_supervised.py index be8bee2470..122e4a83ae 100644 --- a/examples/demo_classifier_self_supervised.py +++ b/examples/demo_classifier_self_supervised.py @@ -10,11 +10,12 @@ import hcaptcha_challenger as solver from hcaptcha_challenger import handle, ModelHub, DataLake, register_pipline + # Init local-side of the ModelHub solver.install(upgrade=True, clip=True) assets_dir = Path(__file__).parent.parent.joinpath("assets") -images_dir = assets_dir.joinpath("image_label_binary", "off_road_vehicle") +images_dir = assets_dir.joinpath("image_label_binary/off_road_vehicle") prompt = "Please click each image containing a sedan car" @@ -33,7 +34,7 @@ def prelude_self_supervised_config(): modelhub.parse_objects() for prompt_, serialized_binary in datalake_post.items(): modelhub.datalake[prompt_] = DataLake.from_serialized(serialized_binary) - clip_model = register_pipline(modelhub) + clip_model = register_pipline(modelhub, fmt="onnx") return modelhub, clip_model @@ -49,13 +50,30 @@ def get_test_images() -> List[Path]: def demo(): + def output_markdown_preview(): + """# pip install pandas tabulate""" + try: + import pandas as pd + import tabulate + except ImportError: + for image_path, result in zip(image_paths, results): + print(image_path, f"{result=}") + else: + output = [ + {"image": f"![]({image_path})", "result": result} + for image_path, result in zip(image_paths, results) + ] + mdk = pd.DataFrame.from_records(output).to_markdown() + mdk = f"- prompt: `{prompt}`\n\n{mdk}" + Path("result.md").write_text(mdk, encoding="utf8") + print(mdk) + modelhub, clip_model = prelude_self_supervised_config() image_paths = get_test_images() classifier = solver.BinaryClassifier(modelhub=modelhub, clip_model=clip_model) if results := classifier.execute(prompt, image_paths, self_supervised=True): - for image_path, result in zip(image_paths, results): - print(f"{image_path.name=} - {result=} {classifier.model_name=}") + output_markdown_preview() if __name__ == "__main__": diff --git a/examples/demo_find_unique_object.py b/examples/demo_find_unique_object.py index 0d1c776b29..8f863cba1b 100644 --- a/examples/demo_find_unique_object.py +++ b/examples/demo_find_unique_object.py @@ -7,7 +7,7 @@ from tqdm import tqdm import hcaptcha_challenger as solver -from hcaptcha_challenger.components.cv_toolkit.appears_only_once import ( +from hcaptcha_challenger.tools.cv_toolkit.appears_only_once import ( limited_radius, annotate_objects, find_unique_object, diff --git a/examples/faker_client.py b/examples/faker_client.py new file mode 100644 index 0000000000..75ede49800 --- /dev/null +++ b/examples/faker_client.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Time : 2024/4/1 21:10 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +from __future__ import annotations + +import asyncio +from pathlib import Path + +from playwright.async_api import async_playwright, BrowserContext + +from hcaptcha_challenger.agents import AgentV +from hcaptcha_challenger.agents import Malenia +from hcaptcha_challenger.onnx.modelhub import ModelHub +from hcaptcha_challenger.utils import SiteKey + + +# 1. You need to deploy sub-thread tasks and actively run `install(upgrade=True)` every 20 minutes +# 2. You need to make sure to run `install(upgrade=True, clip=True)` before each instantiation +# install(upgrade=True, clip=True) + + +async def main(headless: bool = False): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=headless) + context = await browser.new_context(locale="en-US") + await Malenia.apply_stealth(context) + await mime(context) + await context.close() + + +async def mime(context: BrowserContext): + page = await context.new_page() + + modelhub = ModelHub.from_github_repo() + modelhub.parse_objects() + + agent = AgentV.into_solver( + # page, the control handle of the Playwright Page + page=page, + # modelhub, Register modelhub externally, and the agent can patch custom configurations + modelhub=modelhub, + # tmp_dir, Mount the cache directory to the current working folder + tmp_dir=Path("tmp_dir"), + ) + + sitekey = SiteKey.user_easy + + if EXECUTION == "challenge": + sitelink = SiteKey.as_sitelink(sitekey) + await page.goto(sitelink) + await agent.ms.click_checkbox() + await agent.wait_for_challenge() + elif EXECUTION == "collect": + await agent.wait_for_collect(sitekey, batch=25) + + +if __name__ == "__main__": + EXECUTION = "collect" + # EXECUTION = "challenge" + + encrypted_resp = asyncio.run(main(headless=False)) diff --git a/examples/lvm_challenge/vision_minor.ipynb b/examples/lvm_challenge/vision_minor.ipynb deleted file mode 100644 index 86a060e7d9..0000000000 --- a/examples/lvm_challenge/vision_minor.ipynb +++ /dev/null @@ -1,48 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "initial_id", - "metadata": { - "collapsed": true, - "is_executing": true - }, - "outputs": [], - "source": [ - "# open ../../../static/img/brand/wordmark.png as base64 str\n", - "import base64\n", - "from pathlib import Path\n", - "\n", - "from IPython.display import HTML\n", - "\n", - "img_path = Path(\"../../../static/img/brand/wordmark.png\")\n", - "img_base64 = base64.b64encode(img_path.read_bytes()).decode(\"utf-8\")\n", - "\n", - "# display b64 image in notebook\n", - "HTML(f'')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/hcaptcha_challenger/__init__.py b/hcaptcha_challenger/__init__.py index 08b933f64d..0ab9676513 100644 --- a/hcaptcha_challenger/__init__.py +++ b/hcaptcha_challenger/__init__.py @@ -5,31 +5,30 @@ # Description: from __future__ import annotations -from dataclasses import dataclass from pathlib import Path from typing import Iterable from urllib.parse import urlparse -from hcaptcha_challenger.components.image_classifier import Classifier as BinaryClassifier -from hcaptcha_challenger.components.image_classifier import LocalBinaryClassifier -from hcaptcha_challenger.components.image_label_area_select import AreaSelector -from hcaptcha_challenger.components.middleware import QuestionResp, ChallengeResp, Answers, Status -from hcaptcha_challenger.components.prompt_handler import ( +from hcaptcha_challenger.models import QuestionResp, Answers, Status, ChallengeResp +from hcaptcha_challenger.onnx.modelhub import ModelHub +from hcaptcha_challenger.onnx.resnet import ResNetControl +from hcaptcha_challenger.onnx.yolo import YOLOv8 +from hcaptcha_challenger.onnx.yolo import YOLOv8Seg +from hcaptcha_challenger.tools.image_label_area_select import AreaSelector +from hcaptcha_challenger.tools.image_label_binary import Classifier as BinaryClassifier +from hcaptcha_challenger.tools.image_label_binary import LocalBinaryClassifier +from hcaptcha_challenger.tools.prompt_handler import ( label_cleaning, diagnose_task, split_prompt_message, prompt2task, handle, ) -from hcaptcha_challenger.components.zero_shot_image_classifier import ( +from hcaptcha_challenger.tools.zero_shot_image_classifier import ( ZeroShotImageClassifier, DataLake, register_pipline, ) -from hcaptcha_challenger.onnx.modelhub import ModelHub -from hcaptcha_challenger.onnx.resnet import ResNetControl -from hcaptcha_challenger.onnx.yolo import YOLOv8 -from hcaptcha_challenger.onnx.yolo import YOLOv8Seg from hcaptcha_challenger.utils import init_log __all__ = [ @@ -56,18 +55,10 @@ ] -@dataclass -class Project: - at_dir = Path(__file__).parent - logs = at_dir.joinpath("logs") - - -project = Project() - init_log( - runtime=project.logs.joinpath("runtime.log"), - error=project.logs.joinpath("error.log"), - serialize=project.logs.joinpath("serialize.log"), + runtime=Path("logs/runtime.log"), + error=Path("logs/error.log"), + serialize=Path("logs/serialize.log"), ) @@ -90,7 +81,7 @@ def install( modelhub.assets.flush_runtime_assets(upgrade=upgrade) if clip is True: - from hcaptcha_challenger.components.zero_shot_image_classifier import register_pipline + from hcaptcha_challenger.tools.zero_shot_image_classifier import register_pipline register_pipline(modelhub, install_only=True) diff --git a/hcaptcha_challenger/agents/__init__.py b/hcaptcha_challenger/agents/__init__.py index 773027ecd7..287c38a9d0 100644 --- a/hcaptcha_challenger/agents/__init__.py +++ b/hcaptcha_challenger/agents/__init__.py @@ -3,8 +3,8 @@ # Author : QIN2DIM # GitHub : https://github.com/QIN2DIM # Description: -from hcaptcha_challenger.agents.pipline.control import AgentR from hcaptcha_challenger.agents.playwright.control import AgentT +from hcaptcha_challenger.agents.playwright.dragon import AgentV from hcaptcha_challenger.agents.playwright.tarnished import Malenia, Tarnished -__all__ = ["AgentT", "Malenia", "Tarnished", "AgentR"] +__all__ = ["AgentT", "Malenia", "Tarnished", "AgentV"] diff --git a/hcaptcha_challenger/agents/pipline/__init__.py b/hcaptcha_challenger/agents/pipline/__init__.py deleted file mode 100644 index 5399e03a1a..0000000000 --- a/hcaptcha_challenger/agents/pipline/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# Time : 2023/11/17 18:54 -# Author : QIN2DIM -# GitHub : https://github.com/QIN2DIM -# Description: diff --git a/hcaptcha_challenger/agents/playwright/control.py b/hcaptcha_challenger/agents/playwright/control.py index fe0235eda4..b642d96984 100644 --- a/hcaptcha_challenger/agents/playwright/control.py +++ b/hcaptcha_challenger/agents/playwright/control.py @@ -20,36 +20,31 @@ from playwright.async_api import TimeoutError from tenacity import * -from hcaptcha_challenger.components.common import ( +from hcaptcha_challenger.models import Status, QuestionResp, ChallengeResp, RequestType +from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake +from hcaptcha_challenger.onnx.resnet import ResNetControl +from hcaptcha_challenger.onnx.yolo import ( + YOLOv8, + YOLOv8Seg, + is_matched_ash_of_war, + finetune_keypoint, +) +from hcaptcha_challenger.tools.common import ( match_model, match_datalake, rank_models, download_challenge_images, ) -from hcaptcha_challenger.components.cv_toolkit import ( +from hcaptcha_challenger.tools.cv_toolkit import ( find_unique_object, annotate_objects, find_unique_color, ) -from hcaptcha_challenger.components.middleware import ( - Status, - QuestionResp, - ChallengeResp, - RequestType, -) -from hcaptcha_challenger.components.prompt_handler import handle -from hcaptcha_challenger.components.zero_shot_image_classifier import ( +from hcaptcha_challenger.tools.prompt_handler import handle +from hcaptcha_challenger.tools.zero_shot_image_classifier import ( ZeroShotImageClassifier, register_pipline, ) -from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake -from hcaptcha_challenger.onnx.resnet import ResNetControl -from hcaptcha_challenger.onnx.yolo import ( - YOLOv8, - YOLOv8Seg, - is_matched_ash_of_war, - finetune_keypoint, -) @dataclass diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py new file mode 100644 index 0000000000..ef8f31efd2 --- /dev/null +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- +# Time : 2024/4/7 11:43 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +import abc +import asyncio +import hashlib +import re +import shutil +from abc import ABC +from asyncio import Queue +from contextlib import suppress +from dataclasses import dataclass +from dataclasses import field +from pathlib import Path +from typing import List + +from loguru import logger +from playwright.async_api import Page, Response, TimeoutError, expect + +from hcaptcha_challenger.models import ( + ChallengeResp, + QuestionResp, + RequestType, + Status, + ChallengeImage, + ToolExecution, + CollectibleType, + Collectible, +) +from hcaptcha_challenger.onnx.modelhub import ModelHub +from hcaptcha_challenger.tools.prompt_handler import handle + +HOOK_PURCHASE = "//div[@id='webPurchaseContainer']//iframe" +HOOK_CHECKBOX = "//iframe[contains(@title, 'checkbox for hCaptcha')]" +HOOK_CHALLENGE = "//iframe[contains(@title, 'hCaptcha challenge')]" + + +@dataclass +class MechanicalSkeleton: + page: Page + + async def click_checkbox(self): + try: + checkbox = self.page.frame_locator("//iframe[contains(@title,'checkbox')]") + await checkbox.locator("#checkbox").click() + except TimeoutError as err: + logger.warning("Failed to click checkbox", reason=err) + + async def refresh_challenge(self) -> bool | None: + try: + fl = self.page.frame_locator(HOOK_CHALLENGE) + await fl.locator("//div[@class='refresh button']").click() + return True + except TimeoutError as err: + logger.warning("Failed to click refresh button", reason=err) + + def switch_to_challenge_frame(self, window: str = "login"): + if window == "login": + frame_challenge = self.page.frame_locator(HOOK_CHALLENGE) + else: + frame_purchase = self.page.frame_locator(HOOK_PURCHASE) + frame_challenge = frame_purchase.frame_locator(HOOK_CHALLENGE) + + return frame_challenge + + +@dataclass +class OminousLand(ABC): + """不祥之地:base""" + + page: Page + tmp_dir: Path + ms: MechanicalSkeleton + image_queue: Queue + + typed_dir: Path = field(default_factory=Path) + canvas_screenshot_dir: Path = field(default_factory=Path) + + label: str = "" + prompt: str = "" + + tasklist: List[ChallengeImage] = field(default_factory=list) + examples: List[ChallengeImage] = field(default_factory=list) + + qr_data: dict | None = field(default_factory=dict) + qr: QuestionResp | None = field(default_factory=QuestionResp) + + encrypted_bytes: bytes | None = b"" + + @classmethod + def draws_from( + cls, page: Page, inputs: dict | bytes, tmp_dir: Path, image_queue: Queue, **kwargs + ): + # Cache images + if not isinstance(tmp_dir, Path): + tmp_dir = Path("tmp_dir") + + typed_dir = tmp_dir / "typed_dir" + canvas_screenshot_dir = tmp_dir / "canvas_screenshot" + + monster = cls( + page=page, + ms=MechanicalSkeleton(page), + tmp_dir=tmp_dir, + image_queue=image_queue, + typed_dir=typed_dir, + canvas_screenshot_dir=canvas_screenshot_dir, + ) + + if isinstance(inputs, dict): + monster.qr_data = inputs + elif isinstance(inputs, bytes): + monster.encrypted_bytes = inputs + + return monster + + def _init_imgdb(self, label: str, prompt: str): + """run after _get_captcha""" + self.tasklist.clear() + self.examples.clear() + + inv = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} + for c in inv: + label = label.replace(c, "") + prompt = prompt.replace(c, "") + label = label.strip() + + self.typed_dir = self.tmp_dir.joinpath(self.qr.request_type, label) + self.typed_dir.mkdir(parents=True, exist_ok=True) + + if self.qr.request_type != RequestType.ImageLabelBinary: + self.canvas_screenshot_dir = self.tmp_dir.joinpath(f"canvas_screenshot/{prompt}") + self.canvas_screenshot_dir.mkdir(parents=True, exist_ok=True) + + async def _recall_tasklist(self): + """run after _init_imgdb""" + frame_challenge = self.ms.switch_to_challenge_frame() + + if self.qr.request_type == RequestType.ImageLabelBinary: + images = frame_challenge.locator("//div[@class='task-grid']//div[@class='image']") + count = await images.count() + + background_urls = [] + challenge_images = {} + for i in range(count): + image = images.nth(i) + await expect(image).to_have_attribute( + "style", re.compile(r"url\(.+\)"), timeout=5000 + ) + style = await image.get_attribute("style") + datapoint_uri = style.split('"')[1] + background_urls.append(datapoint_uri) + + while not self.image_queue.empty(): + challenge_image: ChallengeImage = self.image_queue.get_nowait() + challenge_images[challenge_image.datapoint_uri] = challenge_image + + for url in background_urls: + challenge_image = challenge_images.get(url) + if challenge_image: + self.tasklist.append(challenge_image) + if not self.typed_dir.joinpath(challenge_image.filename).exists(): + shutil.move(src=challenge_image.runtime_fp, dst=self.typed_dir) + + elif self.qr.request_type == RequestType.ImageLabelAreaSelect: + # For the object detection task, tasklist is only used to collect datasets. + # The challenge in progress uses a canvas screenshot, not the challenge-image + canvas_bgk = frame_challenge.locator("//div[class='bounding-box-example']") + await expect(canvas_bgk).not_to_be_attached() + + # Expect only 1 image in the image_queue + while not self.image_queue.empty(): + challenge_image: ChallengeImage = self.image_queue.get_nowait() + if not self.typed_dir.joinpath(challenge_image.filename).exists(): + shutil.move(src=challenge_image.runtime_fp, dst=self.typed_dir) + # Cache image sequences for subsequent browser operations + self.tasklist.append(challenge_image) + + @abc.abstractmethod + async def _get_captcha(self, **kwargs): + raise NotImplementedError + + async def _solve_captcha(self, **kwargs): + frame_challenge = self.ms.switch_to_challenge_frame() + + match self.qr.request_type: + case RequestType.ImageLabelBinary: + pass + case RequestType.ImageLabelAreaSelect: + # Cache canvas to prepare for subsequent model processing + # canvas = frame_challenge.locator("//canvas") + # fp = self.canvas_screenshot_dir / f"{challenge_image.filename}.png" + # await canvas.screenshot(type="png", path=fp, scale="css") + pass + case RequestType.ImageLabelMultipleChoice: + pass + case _: + logger.warning("[INTERRUPT]", reason="Unknown type of challenge") + + async def _collect(self): + await self._get_captcha() + + logger.debug( + "task", + label=self.label, + type=self.qr.request_type, + requester_question=self.qr.requester_question, + trigger=self.__class__.__name__, + ) + + self._init_imgdb(self.label, self.prompt) + await self._recall_tasklist() + + async def _challenge(self): + await self._collect() + await self._solve_captcha() + + async def invoke(self, execution: ToolExecution = ToolExecution.CHALLENGE): + match execution: + case ToolExecution.COLLECT: + await self._collect() + case ToolExecution.CHALLENGE: + await self._challenge() + + +@dataclass +class ScarletWhisker(OminousLand): + """赤髯 (Chì Rán) ->> json""" + + async def _get_captcha(self, **kwargs): + self.qr = QuestionResp(**self.qr_data) + + self.prompt = self.qr.requester_question.get("en") + self.label = handle(self.prompt) + + +@dataclass +class DemonLordOfHuiYue(OminousLand): + """晦月魔君 ->> bytes""" + + async def _get_captcha(self, **kwargs): + self.qr = QuestionResp() + + # IMPORTANT + await self.page.wait_for_timeout(2000) + + frame_challenge = self.ms.switch_to_challenge_frame() + + # requester_question + prompt_element = frame_challenge.locator("//h2[@class='prompt-text']") + self.prompt = await prompt_element.text_content() + self.label = handle(self.prompt) + lang = await frame_challenge.locator("//html").get_attribute("lang") + self.qr.requester_question[lang] = self.prompt + + # request_type + if await frame_challenge.locator("//div[@class='task-grid']").count(): + self.qr.request_type = RequestType.ImageLabelBinary.value + has_exp = await frame_challenge.locator("//div[@class='challenge-example']").count() + elif await frame_challenge.locator("//div[contains(@class, 'bounding-box')]").count(): + self.qr.request_type = RequestType.ImageLabelAreaSelect.value + else: + # todo image_label_multiple_choice + self.qr.request_type = RequestType.ImageLabelMultipleChoice.value + + +@dataclass +class AgentV: + page: Page + + modelhub: ModelHub + + ms: MechanicalSkeleton = field(default_factory=MechanicalSkeleton) + + cr: ChallengeResp = field(default_factory=ChallengeResp) + + task_queue: Queue[Response] | None = None + cr_queue: Queue[ChallengeResp] = field(default_factory=Queue) + + tmp_dir: Path = field(default_factory=Path) + + image_queue: Queue = field(default_factory=Queue) + + _tool_type: ToolExecution | None = None + + def __post_init__(self): + # Control models + self.modelhub = self.modelhub or ModelHub.from_github_repo() + if not self.modelhub.label_alias: + self.modelhub.parse_objects() + + self.tmp_dir = self.tmp_dir or Path("tmp_dir") + self._cache_dir = self.tmp_dir / ".cache" + self._cache_dir.mkdir(parents=True, exist_ok=True) + + self._enable_evnet_listener(self.page) + + self.task_queue = Queue(maxsize=1) + + @classmethod + def into_solver(cls, page: Page, tmp_dir=None, modelhub: ModelHub | None = None, **kwargs): + return cls( + page=page, ms=MechanicalSkeleton(page), tmp_dir=tmp_dir, modelhub=modelhub, **kwargs + ) + + @property + def status(self): + return Status + + def _enable_evnet_listener(self, page: Page): + page.on("response", self._task_handler) + + @logger.catch + async def _task_handler(self, response: Response): + if "/getcaptcha/" in response.url: + # reset state + while not self.image_queue.empty(): + self.image_queue.get_nowait() + if self.task_queue.full(): + self.task_queue.get_nowait() + + # drop task + self.task_queue.put_nowait(response) + + # /cr 在 Submit Event 之后,cr 截至目前是明文数据 + elif "/checkcaptcha/" in response.url: + try: + metadata = await response.json() + self.cr_queue.put_nowait(ChallengeResp(**metadata)) + except Exception as err: + logger.exception(err) + + # Image GET Event 发生在 /GetCaptcha 之后,此时假设 prompt 和 label 已被正确初始化 + # _image_handler 和 _task_cr_handler 以协程方式运行,但在业务逻辑上,_task_cr_handler 先发生。 + elif response.url.startswith("https://imgs3.hcaptcha.com/tip/"): + image_bytes = await response.body() + mime_type = await response.header_value("content-type") + image_url = response.url + + suffix = ".jpeg" + if isinstance(mime_type, str): + _suffix = mime_type.split("/")[-1] + if _suffix in ["jpg", "jpeg", "png", "webp"]: + suffix = f".{_suffix}" + + fn = f"{hashlib.md5(image_bytes).hexdigest()}{suffix}" + fp = self._cache_dir / fn + fp.write_bytes(image_bytes) + + # waiting for lock + element = ChallengeImage( + datapoint_uri=image_url, filename=fn, body=image_bytes, runtime_fp=fp + ) + self.image_queue.put_nowait(element) + + @logger.catch + async def _tool_execution(self): + qr_data = await self.task_queue.get() + + driver_conf = {"page": self.page, "tmp_dir": self.tmp_dir, "image_queue": self.image_queue} + runnable: OminousLand | None = None + + match content_type := qr_data.headers.get("content-type"): + case "application/octet-stream": + driver_conf["inputs"] = await qr_data.body() + runnable = DemonLordOfHuiYue.draws_from(**driver_conf) + case "application/json": + data = await qr_data.json() + if data.get("pass"): + self.cr_queue.put_nowait(ChallengeResp(**data)) + else: + driver_conf["inputs"] = data + runnable = ScarletWhisker.draws_from(**driver_conf) + case _: + raise ValueError(f"Unknown Challenge Response Protocol - {content_type=}") + + if isinstance(runnable, OminousLand): + await runnable.invoke(execution=self._tool_type) + + async def wait_for_challenge( + self, execution_timeout: float = 150.0, response_timeout: float = 30.0 + ) -> Status: + self._tool_type = ToolExecution.CHALLENGE + + # CoroutineTask: Assigning human-computer challenge tasks to the main thread coroutine. + # Wait for the task to finish executing + try: + await asyncio.wait_for(self._tool_execution(), timeout=execution_timeout) + except asyncio.TimeoutError: + logger.error("Challenge execution timed out", timeout=execution_timeout) + return self.status.CHALLENGE_EXECUTION_TIMEOUT + + # CoroutineTask: Waiting for hCAPTCHA response processing result + # After the completion of the human-machine challenge workflow, + # it is expected to obtain a signal indicating whether the challenge was successful in the cr_queue. + self.cr = ChallengeResp() + try: + self.cr = await self.cr_queue.get() + except asyncio.TimeoutError: + logger.error("Timeout waiting for challenge response", timeout=response_timeout) + return self.status.CHALLENGE_RESPONSE_TIMEOUT + else: + logger.debug("[DONE]", **self.cr.model_dump(by_alias=True)) + + # Match: Timeout / Loss + if not self.cr or not self.cr.is_pass: + return self.status.CHALLENGE_RETRY + if self.cr.is_pass: + return self.status.CHALLENGE_SUCCESS + + async def wait_for_collect( + self, point: CollectibleType | None = None, *, batch: int = 20, timeout: float = 30.0 + ): + self._tool_type = ToolExecution.COLLECT + + sitelink = Collectible(point=point).fixed_sitelink + + await self.page.goto(sitelink) + await self.ms.click_checkbox() + + logger.debug("run collector", url=self.page.url) + + if batch >= 1: + for i in range(1, batch + 1): + with suppress(asyncio.TimeoutError): + await asyncio.wait_for(self._tool_execution(), timeout=timeout) + if not await self.ms.refresh_challenge(): + return await self.wait_for_collect(point=point, batch=batch - i) + + logger.success("The dataset collection is complete.", sitelink=sitelink) diff --git a/hcaptcha_challenger/components/middleware.py b/hcaptcha_challenger/models.py similarity index 62% rename from hcaptcha_challenger/components/middleware.py rename to hcaptcha_challenger/models.py index 3936435f15..a699bf6345 100644 --- a/hcaptcha_challenger/components/middleware.py +++ b/hcaptcha_challenger/models.py @@ -5,13 +5,15 @@ # Description: from __future__ import annotations +import base64 from enum import Enum from pathlib import Path -from typing import List, Dict, Any +from typing import List, Dict, Any, Union +from uuid import UUID -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, UUID4, AnyHttpUrl -from hcaptcha_challenger.components.prompt_handler import label_cleaning +from hcaptcha_challenger.tools.prompt_handler import label_cleaning class Status(str, Enum): @@ -21,6 +23,46 @@ class Status(str, Enum): CHALLENGE_RETRY = "retry" # (New Challenge) Types of challenges not yet scheduled CHALLENGE_BACKCALL = "backcall" + # Failed to pass the challenge within the specified time frame + CHALLENGE_EXECUTION_TIMEOUT = "challenge_execution_timeout" + CHALLENGE_RESPONSE_TIMEOUT = "challenge_response_timeout" + + +class Collectible(BaseModel): + point: UUID4 | AnyHttpUrl = Field(..., description="sitelink or sitekey") + + @field_validator("point") + def validate_point(cls, v: str): + def is_valid_uuid4(string): + try: + uuid_obj = UUID(string) + except ValueError: + return False + return uuid_obj.version == 4 + + _sitekey = "c86d730b-300a-444c-a8c5-5312e7a93628" + _sitelink = "https://accounts.hcaptcha.com/demo" + + if not isinstance(v, str): + v = f"{_sitelink}?sitekey={_sitekey}" + elif is_valid_uuid4(v): + v = f"{_sitelink}?sitekey={v}" + elif not v.startswith(_sitelink): + v = f"{_sitelink}?sitekey={_sitekey}" + + return v + + @property + def fixed_sitelink(self) -> str: + return self.point + + +CollectibleType = Union[UUID4, AnyHttpUrl, str] + + +class ToolExecution(str, Enum): + CHALLENGE = "challenge" + COLLECT = "collect" class ImageTask(BaseModel): @@ -142,3 +184,23 @@ class Answers(BaseModel): motionData: str = "" n: str = "" c: str = "" + + +class ChallengeImage(BaseModel): + datapoint_uri: str = Field(default="", description="图片的临时访问链接") + + filename: str = Field(default="challenge-image.jpeg", description="HASH 后的文件名,带有后缀") + + body: bytes = Field(default=b"", description="图片缓存字节") + + runtime_fp: Path = Field( + default_factory=Path, description="图片的临时缓存路径,fp = typed_dir / filename" + ) + + def save(self, typed_dir: Path) -> Path: + fp = typed_dir / self.filename + fp.write_bytes(self.body) + return fp + + def convert_body_to_base64(self) -> str: + return base64.b64encode(self.body).decode("utf8") diff --git a/hcaptcha_challenger/agents/pipline/control.py b/hcaptcha_challenger/pipline.py similarity index 97% rename from hcaptcha_challenger/agents/pipline/control.py rename to hcaptcha_challenger/pipline.py index 64ad5bd699..a7117e2cdc 100644 --- a/hcaptcha_challenger/agents/pipline/control.py +++ b/hcaptcha_challenger/pipline.py @@ -14,26 +14,26 @@ from PIL import Image from loguru import logger -from hcaptcha_challenger.components.common import ( +from hcaptcha_challenger.models import Status, QuestionResp, RequestType, Answers +from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake +from hcaptcha_challenger.onnx.resnet import ResNetControl +from hcaptcha_challenger.onnx.yolo import YOLOv8, is_matched_ash_of_war, YOLOv8Seg +from hcaptcha_challenger.tools.common import ( match_model, download_challenge_images, rank_models, match_datalake, ) -from hcaptcha_challenger.components.cv_toolkit import ( +from hcaptcha_challenger.tools.cv_toolkit import ( annotate_objects, find_unique_object, find_unique_color, ) -from hcaptcha_challenger.components.middleware import Status, QuestionResp, RequestType, Answers -from hcaptcha_challenger.components.prompt_handler import handle -from hcaptcha_challenger.components.zero_shot_image_classifier import ( +from hcaptcha_challenger.tools.prompt_handler import handle +from hcaptcha_challenger.tools.zero_shot_image_classifier import ( ZeroShotImageClassifier, register_pipline, ) -from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake -from hcaptcha_challenger.onnx.resnet import ResNetControl -from hcaptcha_challenger.onnx.yolo import YOLOv8, is_matched_ash_of_war, YOLOv8Seg @dataclass diff --git a/hcaptcha_challenger/components/__init__.py b/hcaptcha_challenger/tools/__init__.py similarity index 100% rename from hcaptcha_challenger/components/__init__.py rename to hcaptcha_challenger/tools/__init__.py diff --git a/hcaptcha_challenger/components/common.py b/hcaptcha_challenger/tools/common.py similarity index 97% rename from hcaptcha_challenger/components/common.py rename to hcaptcha_challenger/tools/common.py index 59859c47ae..ce6adafeee 100644 --- a/hcaptcha_challenger/components/common.py +++ b/hcaptcha_challenger/tools/common.py @@ -13,11 +13,11 @@ from pathlib import Path from typing import Literal, List, Tuple -from hcaptcha_challenger.components.image_downloader import Cirilla -from hcaptcha_challenger.components.middleware import QuestionResp +from hcaptcha_challenger.models import QuestionResp from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake from hcaptcha_challenger.onnx.resnet import ResNetControl from hcaptcha_challenger.onnx.yolo import YOLOv8 +from hcaptcha_challenger.tools.image_downloader import Cirilla def rank_models( diff --git a/hcaptcha_challenger/components/cv_toolkit/__init__.py b/hcaptcha_challenger/tools/cv_toolkit/__init__.py similarity index 100% rename from hcaptcha_challenger/components/cv_toolkit/__init__.py rename to hcaptcha_challenger/tools/cv_toolkit/__init__.py diff --git a/hcaptcha_challenger/components/cv_toolkit/appears_only_once.py b/hcaptcha_challenger/tools/cv_toolkit/appears_only_once.py similarity index 100% rename from hcaptcha_challenger/components/cv_toolkit/appears_only_once.py rename to hcaptcha_challenger/tools/cv_toolkit/appears_only_once.py diff --git a/hcaptcha_challenger/components/cv_toolkit/largest_animal.py b/hcaptcha_challenger/tools/cv_toolkit/largest_animal.py similarity index 100% rename from hcaptcha_challenger/components/cv_toolkit/largest_animal.py rename to hcaptcha_challenger/tools/cv_toolkit/largest_animal.py diff --git a/hcaptcha_challenger/components/image_downloader.py b/hcaptcha_challenger/tools/image_downloader.py similarity index 100% rename from hcaptcha_challenger/components/image_downloader.py rename to hcaptcha_challenger/tools/image_downloader.py diff --git a/hcaptcha_challenger/components/image_label_area_select.py b/hcaptcha_challenger/tools/image_label_area_select.py similarity index 97% rename from hcaptcha_challenger/components/image_label_area_select.py rename to hcaptcha_challenger/tools/image_label_area_select.py index fc9ec1b08d..c0e4c44bc9 100644 --- a/hcaptcha_challenger/components/image_label_area_select.py +++ b/hcaptcha_challenger/tools/image_label_area_select.py @@ -10,9 +10,9 @@ from loguru import logger -from hcaptcha_challenger.components.prompt_handler import handle from hcaptcha_challenger.onnx.modelhub import ModelHub from hcaptcha_challenger.onnx.yolo import YOLOv8 +from hcaptcha_challenger.tools.prompt_handler import handle class AreaSelector: diff --git a/hcaptcha_challenger/components/image_classifier.py b/hcaptcha_challenger/tools/image_label_binary.py similarity index 96% rename from hcaptcha_challenger/components/image_classifier.py rename to hcaptcha_challenger/tools/image_label_binary.py index 08b51e6218..c4a07bbe26 100644 --- a/hcaptcha_challenger/components/image_classifier.py +++ b/hcaptcha_challenger/tools/image_label_binary.py @@ -13,14 +13,14 @@ from PIL import Image from loguru import logger -from hcaptcha_challenger.components.common import rank_models -from hcaptcha_challenger.components.prompt_handler import handle -from hcaptcha_challenger.components.zero_shot_image_classifier import ( +from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake +from hcaptcha_challenger.onnx.resnet import ResNetControl +from hcaptcha_challenger.tools.common import rank_models +from hcaptcha_challenger.tools.prompt_handler import handle +from hcaptcha_challenger.tools.zero_shot_image_classifier import ( ZeroShotImageClassifier, register_pipline, ) -from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake -from hcaptcha_challenger.onnx.resnet import ResNetControl class Classifier: diff --git a/hcaptcha_challenger/components/prompt_handler.py b/hcaptcha_challenger/tools/prompt_handler.py similarity index 84% rename from hcaptcha_challenger/components/prompt_handler.py rename to hcaptcha_challenger/tools/prompt_handler.py index 153d11e537..46e9c032f7 100644 --- a/hcaptcha_challenger/components/prompt_handler.py +++ b/hcaptcha_challenger/tools/prompt_handler.py @@ -51,10 +51,16 @@ def split_prompt_message(prompt_message: str, lang: str) -> str: if "containing" in prompt_message: th = re.split(r"containing", prompt_message)[-1][1:].strip() return th[2:].strip() if th.startswith("a") else th + if prompt_message.startswith("please select all"): + prompt_message = prompt_message.replace("please select all ", "").strip() + return prompt_message + if prompt_message.startswith("please click on the"): + prompt_message = prompt_message.replace("please click on ", "").strip() + return prompt_message if prompt_message.startswith("select all") and "images" not in prompt_message: return prompt_message.split("select all")[-1].strip() - if "select all" in prompt_message: - return re.split(r"all (.*) images", prompt_message)[1].strip() + if "select all images of" in prompt_message: + return prompt_message.split("select all images of")[-1].strip() return prompt_message diff --git a/hcaptcha_challenger/components/zero_shot_image_classifier.py b/hcaptcha_challenger/tools/zero_shot_image_classifier.py similarity index 98% rename from hcaptcha_challenger/components/zero_shot_image_classifier.py rename to hcaptcha_challenger/tools/zero_shot_image_classifier.py index a7f05068c1..75f4332973 100644 --- a/hcaptcha_challenger/components/zero_shot_image_classifier.py +++ b/hcaptcha_challenger/tools/zero_shot_image_classifier.py @@ -13,10 +13,10 @@ import onnxruntime from PIL.Image import Image -from hcaptcha_challenger.components.prompt_handler import handle from hcaptcha_challenger.onnx.clip import MossCLIP from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake from hcaptcha_challenger.onnx.utils import is_cuda_pipline_available +from hcaptcha_challenger.tools.prompt_handler import handle def register_pipline( diff --git a/pyproject.toml b/pyproject.toml index 0d2c50abd9..5175370e57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ in-project = true # https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups +# pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 [tool.poetry.dependencies] python = "^3.10" @@ -48,8 +49,6 @@ ftfy = "*" regex = "*" pydantic = "^2.5.1" tenacity = "^8.2.3" -langchain = "^0.1.13" -langchain-anthropic = "^0.1.4" # The following dependencies will not be installed # when you just run `poetry install --all-extras` or `pip install hcaptcha-challenger` @@ -59,8 +58,7 @@ selenium = { version = "*", optional = true } playwright = { version = "*", optional = true } PyGithub = { version = "^1.59.1", optional = true } istockphoto = { version = "0.1.2", optional = true } -fastapi = { version = "*", optional = true } -uvicorn = { version = "*", extras = ["standard"], optional = true } +fastapi = { version = "*", extras=["all"], optional = true} [tool.poetry.group.test.dependencies] # https://docs.pytest.org/en/stable/reference/plugin_list.html#plugin-list diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_downloader.py b/tests/test_downloader.py index 63424a9379..1261182e0d 100644 --- a/tests/test_downloader.py +++ b/tests/test_downloader.py @@ -27,7 +27,7 @@ async def _downloader(): sitelink = SiteKey.as_sitelink(SiteKey.user_easy) await page.goto(sitelink) - await agent.handle_checkbox() + await agent.click_checkbox() await agent.collect() diff --git a/tests/test_normal_playwright.py b/tests/test_normal_playwright.py index f6c6f9a610..c7f7e36049 100644 --- a/tests/test_normal_playwright.py +++ b/tests/test_normal_playwright.py @@ -27,7 +27,7 @@ async def _normal_instance(): sitelink = SiteKey.as_sitelink(SiteKey.user_easy) await page.goto(sitelink) - await agent.handle_checkbox() + await agent.click_checkbox() await agent._reset_state() if not agent.qr.requester_question.keys(): diff --git a/tests/test_prompt_handler.py b/tests/test_prompt_handler.py index 16a971ad7e..abc2060050 100644 --- a/tests/test_prompt_handler.py +++ b/tests/test_prompt_handler.py @@ -11,7 +11,7 @@ import pytest from hcaptcha_challenger import split_prompt_message, label_cleaning, handle -from hcaptcha_challenger.components.prompt_handler import BAD_CODE +from hcaptcha_challenger.tools.prompt_handler import BAD_CODE pattern = re.compile(r"[^\x00-\x7F]") From 78e8c0090b1805db2c2858a0b3eba565ea58f9df Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sun, 14 Apr 2024 19:50:37 +0800 Subject: [PATCH 05/14] feat(backend): examples, payload design --- .gitignore | 3 +- .../0a250855340d6919d7d40e027e14ddb7.jpeg | Bin 0 -> 4341 bytes .../0dc5bf222566c789b1400017681e6634.jpeg | Bin 0 -> 4336 bytes .../1c4b5b2a29bd01c9e190203b13191de1.jpeg | Bin 0 -> 4175 bytes .../1f625f42bdc6a648b2311b2b8ac9a573.jpeg | Bin 0 -> 4365 bytes .../23006438f204b8032730e54d9c048f03.jpeg | Bin 0 -> 4402 bytes .../2b588e6d099ae24de6bd002373237016.jpeg | Bin 0 -> 4301 bytes .../2b86207da94d4a29f01325b01794aff1.jpeg | Bin 0 -> 4476 bytes .../2bef67821261920c3d1197c971d5db2c.jpeg | Bin 0 -> 3954 bytes .../31506b75a59a3ffb9c01038d9757aaf2.jpeg | Bin 0 -> 4250 bytes .../33d4fa8df6b13f89bf98225553f612c4.jpeg | Bin 0 -> 3918 bytes .../3590320e2af3c5b1a59d4f32d4646c74.jpeg | Bin 0 -> 4316 bytes .../396123559b43aed482cad3cd9b8ec94c.jpeg | Bin 0 -> 4425 bytes .../4528a9d72c1cfc8e906ffaa1dac031b4.jpeg | Bin 0 -> 4177 bytes .../50f0778136f0a88d4940bbd346d66aa0.jpeg | Bin 0 -> 4462 bytes .../5b6bfec1aeabd840a015126cc03d6518.jpeg | Bin 0 -> 4260 bytes .../6b9528ab5990660c6596018fbf8d7b7c.jpeg | Bin 0 -> 3655 bytes .../7260eb6e0ab45f696acf83dee9c2634d.jpeg | Bin 0 -> 4253 bytes .../7290a18a41e3346cafd06e7d6dbf1a26.jpeg | Bin 0 -> 3974 bytes .../8f38391ab5dd9cca78c91217f5b8aa27.jpeg | Bin 0 -> 4095 bytes .../8fdeb5c3338f0133f5fe1fe3b9216183.jpeg | Bin 0 -> 4150 bytes .../902d6009bb13dfa2cac585e2928efd29.jpeg | Bin 0 -> 4022 bytes .../9be25a532efedf77fd9e4d37ee2e301c.jpeg | Bin 0 -> 4318 bytes .../a698bd00f059ac0618a420fdf4765f17.jpeg | Bin 0 -> 4717 bytes .../a99e11c8196910a899c846f0a2bfa416.jpeg | Bin 0 -> 4115 bytes .../b6cdbafa182291f9d91c2474d16845d4.jpeg | Bin 0 -> 3751 bytes .../c03d11a827eed8fc1d6bb083a92dce92.jpeg | Bin 0 -> 4021 bytes .../c7ce3abce8d176af001975717d634b7e.jpeg | Bin 0 -> 4159 bytes .../d03f4f239191df50459ccd4d1316b9ab.jpeg | Bin 0 -> 4447 bytes .../d0a7e87990072185fc7b19fb5fe158f8.jpeg | Bin 0 -> 4270 bytes .../d40775690548d6fbd5b0594fde70451b.jpeg | Bin 0 -> 4610 bytes .../d76bff522ff571847549d8ee847075f6.jpeg | Bin 0 -> 4114 bytes .../dda8f3edc16e02e5241c21e6e27d6b4c.jpeg | Bin 0 -> 4087 bytes .../efb5927841ad1d6ec221129c13fe32a4.jpeg | Bin 0 -> 4217 bytes backend/routers/challenge.py | 93 +++++++++--------- examples/demo_classifier_self_supervised.py | 1 - examples/demo_find_unique_object.py | 4 +- examples/invoke_remote_solver.py | 64 ++++++++++++ 38 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 assets/image_label_binary/streetlamp/0a250855340d6919d7d40e027e14ddb7.jpeg create mode 100644 assets/image_label_binary/streetlamp/0dc5bf222566c789b1400017681e6634.jpeg create mode 100644 assets/image_label_binary/streetlamp/1c4b5b2a29bd01c9e190203b13191de1.jpeg create mode 100644 assets/image_label_binary/streetlamp/1f625f42bdc6a648b2311b2b8ac9a573.jpeg create mode 100644 assets/image_label_binary/streetlamp/23006438f204b8032730e54d9c048f03.jpeg create mode 100644 assets/image_label_binary/streetlamp/2b588e6d099ae24de6bd002373237016.jpeg create mode 100644 assets/image_label_binary/streetlamp/2b86207da94d4a29f01325b01794aff1.jpeg create mode 100644 assets/image_label_binary/streetlamp/2bef67821261920c3d1197c971d5db2c.jpeg create mode 100644 assets/image_label_binary/streetlamp/31506b75a59a3ffb9c01038d9757aaf2.jpeg create mode 100644 assets/image_label_binary/streetlamp/33d4fa8df6b13f89bf98225553f612c4.jpeg create mode 100644 assets/image_label_binary/streetlamp/3590320e2af3c5b1a59d4f32d4646c74.jpeg create mode 100644 assets/image_label_binary/streetlamp/396123559b43aed482cad3cd9b8ec94c.jpeg create mode 100644 assets/image_label_binary/streetlamp/4528a9d72c1cfc8e906ffaa1dac031b4.jpeg create mode 100644 assets/image_label_binary/streetlamp/50f0778136f0a88d4940bbd346d66aa0.jpeg create mode 100644 assets/image_label_binary/streetlamp/5b6bfec1aeabd840a015126cc03d6518.jpeg create mode 100644 assets/image_label_binary/streetlamp/6b9528ab5990660c6596018fbf8d7b7c.jpeg create mode 100644 assets/image_label_binary/streetlamp/7260eb6e0ab45f696acf83dee9c2634d.jpeg create mode 100644 assets/image_label_binary/streetlamp/7290a18a41e3346cafd06e7d6dbf1a26.jpeg create mode 100644 assets/image_label_binary/streetlamp/8f38391ab5dd9cca78c91217f5b8aa27.jpeg create mode 100644 assets/image_label_binary/streetlamp/8fdeb5c3338f0133f5fe1fe3b9216183.jpeg create mode 100644 assets/image_label_binary/streetlamp/902d6009bb13dfa2cac585e2928efd29.jpeg create mode 100644 assets/image_label_binary/streetlamp/9be25a532efedf77fd9e4d37ee2e301c.jpeg create mode 100644 assets/image_label_binary/streetlamp/a698bd00f059ac0618a420fdf4765f17.jpeg create mode 100644 assets/image_label_binary/streetlamp/a99e11c8196910a899c846f0a2bfa416.jpeg create mode 100644 assets/image_label_binary/streetlamp/b6cdbafa182291f9d91c2474d16845d4.jpeg create mode 100644 assets/image_label_binary/streetlamp/c03d11a827eed8fc1d6bb083a92dce92.jpeg create mode 100644 assets/image_label_binary/streetlamp/c7ce3abce8d176af001975717d634b7e.jpeg create mode 100644 assets/image_label_binary/streetlamp/d03f4f239191df50459ccd4d1316b9ab.jpeg create mode 100644 assets/image_label_binary/streetlamp/d0a7e87990072185fc7b19fb5fe158f8.jpeg create mode 100644 assets/image_label_binary/streetlamp/d40775690548d6fbd5b0594fde70451b.jpeg create mode 100644 assets/image_label_binary/streetlamp/d76bff522ff571847549d8ee847075f6.jpeg create mode 100644 assets/image_label_binary/streetlamp/dda8f3edc16e02e5241c21e6e27d6b4c.jpeg create mode 100644 assets/image_label_binary/streetlamp/efb5927841ad1d6ec221129c13fe32a4.jpeg create mode 100644 examples/invoke_remote_solver.py diff --git a/.gitignore b/.gitignore index abb605fa13..79a7771cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,5 @@ docs/lvm_challenge/*.jpeg docs/lvm_challenge/*.png **/logs/** examples/*.md -docs/logs/ \ No newline at end of file +docs/logs/ +docs/*.md diff --git a/assets/image_label_binary/streetlamp/0a250855340d6919d7d40e027e14ddb7.jpeg b/assets/image_label_binary/streetlamp/0a250855340d6919d7d40e027e14ddb7.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..064c0101861da7ead72f791e12bf6464651fede5 GIT binary patch literal 4341 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

r4vo7A@2zV<4gsj#+r10PKCm# z7RG!x=>$>Ql-gB|^F`O69K=Yvsrfu*X*zV#I}s(_&>lB_N( zuJnReTGW~hBPWttQrg>b57O->*(+I1oLFT{jS+ihPs_D@7#^FN6!ABWuSp)BD*w2E z6|YH5+p1WeA>8B+(rAPxHx=WS+L@(BwXAw=1I%cK<*2@`a!XzBM42ag-%QlqM$56| zIO?7_IvCN=Tb-BbDG_4sgRtCXjH3Mf(z_f+)dLMiMfg7G0iee zZMjc7=R=rxX$Bf9!AI(!T3fjaAfxrt0N4*sEFXBT}h{`f3NzaOukW)*>&!yW@$U!XACclSg87 z>>s>0>c^C#Xv52m^(fbK9b4N%b!0c&iSY4?^L%dDUiIjL>2q}UO2brmcD-MoXKY3EymI#v^Ez9xq8U#3c;r6-cOVts literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/0dc5bf222566c789b1400017681e6634.jpeg b/assets/image_label_binary/streetlamp/0dc5bf222566c789b1400017681e6634.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..45bda73c1931067532f9a122c62cdb5e71afccd0 GIT binary patch literal 4336 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/1c4b5b2a29bd01c9e190203b13191de1.jpeg b/assets/image_label_binary/streetlamp/1c4b5b2a29bd01c9e190203b13191de1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..80f3aa45444b9f433c442c4e3535a8d8a5be8f0b GIT binary patch literal 4175 zcmbW3c|6qLzsEmg>}1PsY$2hq81kLWO!j@5>>83SB8IVK-&4L!WHhLlv1Bb|3rV5u z%g9cMW`yht5$5XmyN`PxkNelX_kAAc{Bz!q^LRgB=Y1aMMVq9}16;=XM*08<1OTAZ z0nk1JIsh{xBNHP7GZPaN3kx$VJMS5GHa2!4p0k|1qQc^0qQVe}gtVfp#JP)75XgnU z}>3UXU+&hB_Wc~|8vk<0d8g>7KjCd&H;4X zATT$G_7M<2t&;)tuK@ltKy+Yw21X`k7FM>?gnBN34g?0%(SsQn=;=?hqfYk$dTs_D zNo6g@vlcE)=dircxb%ExDedYuKFd#A(kiY2@hq(T0)j%qGUsJ4$jPf-R{I-vMO{Z% zPv5}M$k@u-#@5dMI@-f#ieC+7#>h9?s9vK}QpO~DQUieC(E`D2DUirSg^W*34-u|zH z!#`Xg0Q_&P)AiqA|AUMBl#7m@9!$^lhYLg(ayr4>^bC^9j67NvOfJ~7=b&-SyxQsc z)om2NwVTBKt3}|K^$o*ukLF%L8)*8bA;^bMgT= zy;-ycL<5j97oMII@ON}8hLcw8+;N>t2<$=h)m*O%F>?SAe;ZLBe8`pMD)`)oN8jOh z3)6UQu$c!NPvpcEOcF<+gDb;iFIO$7m8rc0^NK&My0Ep z-fa^>q8hdynL`%^k^?Gmj8|zuq8r3^prtQp1f63(>_~uc>U52yRaI3{Y4#P%WeGbU8qkzo6(GTRl)ec zYuRApV1)fv82Do7YNyhFiPf7ra(?aJ zy?0O1+MkK$tao1qHHDsoNB@Z7(*^fZ@qrWLZ_69yzREjQwWlVb($VJ=zME9v^I8N= z@bTK!rerkjJn5FIw+&Xfzp}3BmjMKzz{AZ>{2Zi-$ z++_scQ2cZzF5hrrsBOkYkppdWs=H)i)V&UaS|-dDFk-*5MLoO7UEftf%sRfSjI3pb zSB@Qs{kZVJZPWG|&J&20f7#?Qt@0y$p9ZX2VDd7)38xq0joO@uU!Mll$d`5_kG;N~ zXJtmJKaIR~_biSRwN?0a($c)+PPTvqj&sGr0GN%RIju_cIMKX&_^Lw#%EffDcM`F& zlu84R!$V#t%0wyNF~cTzF=XaFg6jN+4s;k?wT z6tz8SY0_BmOqXa(@nVmXS9<0k1Us>Dj+3->@Zz0vxAf=b(PvgA%*C6{eT=>qS?ezA zZ*W=OK}t>tUb}DL7artbJ|?OEDSeJ;Xv;$vWjDHXXcgiB_JvEGKS0BqeG8#1kC&3i zTk5T%8SF+>ekap7mKu_yZ`yRmvmht5ZB zIJ8LUrmp3V0jTMaqo9C)A?czfD>nU8%;9cTV3CoqDt@rv;F^Ho{0Iq1iQ9;|n~mhik>r)!CTCndoA?ruJ*)CRA@AnPO||LNL_w_X~cELxlJ`CSaT$Wx`butKXu;$q%L z?wi$+K)>Akm0eThbGY^%rKP!?C~hg07}yI+LhSt^b5 z&R^&p6Kyj_YB}D>c>Z((Y_rLD}fhFP5L40I~g4HaA$qhUn|SYy=O?YLzp5OvN` zu7xA;qx!?qkm5OohFjVp^2A^3N98e$zdR_8aqHB;T3fG$AtJ1; zz8LuOU!Q7fvljeTyHREKv7-R)=7gjHtoI63Lp_#Woe(}5u+G&$%dQ(}HG~Arssywn zB3pwmTUWMMwrF;144u9&W75X!@g-TopELk5oRlJF=ifs?!Zr^lA|>+bt}I@4Svy*Q zNw`hM5am)5nbKyLm=GR`44T@!#Ho`@uIt)(Y4twHwQR<ihciNci(Q=g@%qxw1aObxbcV znxejQ_ieN9%icVySStlq`2b~`hB8b#c%p?&EBSR|LnsM#7rEC+H1&nQ;s8m+kjCH0 zq}-m4-dauFU3O8L{$cp7O0`Z`=Kc?I^gDtCTXPsp3Ku?{-Dc6+y&;Dvt5(eHKiq1g7h}9Ffth9uczsXc=|+d{uXun+2iMxBk+TDKHmK+>)KxKSFQEv zJrf=0mG*wB#&a{&^r_eFoJ}+ZuwR1FqpppP*(3oC}Y!05{*|iwd;9Rw?Q1!y!#n0bIEp*AGH7u-{lWgdgx@$ED-|q~i z+I>#`Tz8N+qc52Dq$Rq3BB6*C-@NQ;X?c%{=hG|SXU{QjA)K9$O51aCt9s*=464hz zos1wH315!B<@r5}`{9`{iaPD6md8y&h~rz57N$>p%0!u01}+5ggoQ5QzGX){0SBl# zjRni%A-1z*%`w!CM1^I|)f#QPZ?NBa!9|8!A~^uG>vu~GxnviMNT`drpxWJID()B4gs} z*oTyI${CKQya%~cJu_1R<1bam1=(BQArUODhTl%6#GClTg{d1yZXq)5by!6> z>*#2Z!mMeBh>GZo{j>^HQK+t_y?I{RO{-N$Xd=2w&n@&#(0Y`qgBj*z1#&T>Ng6xc zP~q8_*a&=)ac(U z-iY;AC^12nH8;neQ6KD(Vtv^pv6NaLR>(qd1fn-v!b61}%h#4Fte)8S<0l{A5)%B0 z^e%3%3Alm)uc8Cp*LSwN&{a@HMwH-pxbV+QcLPew%!V3zQjvOMCSjhUOyNXT2F<^E z5c8I~eTsAsTe^H~?uaIyACc$leI0<*Q#m7r*N1f)c7x|C42@j-MiAJE>2!gFI(H5` ze?cCM z6c@F%jtLm*w&4Wx_1UJ?>2H2R0|16I~r2I-9Ca10vopo@hMOT>f-r(EeTnpQdO$dE4`}- zh=7D5y^2&7P>LujGT!%{HM7=ynwj5T=X}|FopqjbPG?Tv0c^%NJPrVX008u-fYW(E zA7Er)fHKfCLZMJ5CdM-`P8Jw5GmMv=gOyW&4|!gI4}n0TWkgYeQbGuXn2NZR97a)5 z5h<#sr7Eu}qo64N&mkZtCMFm&jE9AVNB#ohg8ctEr!N3FBM=M3frZKLerzL+BZxj7(>k{|q#-0dycRm<|G_r-wlPOh^Ce0}wbp`vr_H1BaD6 zR4|xR{ucEyqmW)*H<$I;7qo&$2$|^&_cwh+C1n*=HFf=q1~@|_ys?d~ zoxOvj(t+&Cu>Qj3 z5i)j$8?Cq^y!Q|7U$XxljQszS{TJ-NU2^~o4EpnVU^svU)_02?S880~Ae zP{bc0tPHOl9OEJ@FH(*+YwQkoajbK~^V7kx_LZX9Woc`mBQM0^mS3ZF-;s9;jpCuz zI>K8nGIdY7?u?04ni#S3#aKg1Dr+ga-U6XK#F=NJpPn(IBff__HwcjJxi^*2cO;#> z$-h_PW8MiS*BnD_ZT$1H=3uYeP@r|V&5ZHEJQMoo`h(zpV?xf9PSu7a9EG}V?K>7w zr~CUYaw?5fgP(cyE&Q^zf(gSkP36LftDkRshuecRhU3Wy+=wWbm*KirmA8&d?^=qD z)kwTA&Nf{ldR)s)=1NGCta8L_8^zeram>h?__A-tDKO-43Ox7xCDAuQC~gMocSD$< zWuaC>eyNq3Hpa)%ajuEYliS=A9@&|;RUA1CXGgUI2TPsSA-6?tAo|T)C{?u#h7ZT% zZO{j)+j(C7=O0vzxg^Xa?eYgN3R1T`tK>WH`|*-@XpWhybB}W8ENf#M@2=1HSJcKw zpFJ`$N8Jo?PBAt&!OalrMwoj#;_vQR{O)3R*1Y?Tbaorr!Arxc7ZTL0H@t{2T>`*W zrsHK&0oK;JS7=~Otm$`8xDYSwFcTB@D9~mE5&q=asQ$H<{<7uYU9Onyx+@>8g|Vf! z`)GJ%yccb8-e{Ba(51VI+JVlw;6WR2(~01m%)s5NN24nNl_-yrWCiq$ZkBI~?yy-j z3GKl4a_>%{C|3S!am}uzU^>r1D`EaOmm<{5+!}2OLNS!#1JOA7B|>rj&7yWK<{k%! zcGY$_y{DvN0pd-QU=5*+&~wPGr*)?Q@!&+_0zzuR=|xRG83{oSmUlc&V;WyDWJ|r> z5%lC{I;@Vt=-CzKRPp6Uw{WmEqCN67*>Q({G601eHWZ&-YlRY&q@n(<{_L9`p+NR-ayBs#{jCLq*YNxt9IGYiZ?or95YXWu<1!Hk z&X1aYu<$xteQty|Q@VTZaq2at_QH!;jrcU{dE1?VTEDS^#!K970ZGh5p9Barqsyv3 zp484z=i|mKru8dc!E4k^rOV%9j>u|0_AIn5e+MfHjtAW%DDYiwwZ(MG@ltkxNK;V@ z3d7wP5yDw7wzi6}yWY_5dldPF%^`4c$0FaT?l{e~Qk7>v+>`QSqIpbfjE=508Y0p$ z_gtU3*rZ3@BCjIw8oeh)W@R*gavAxd{?BXA4FwbnE5oMoD~saQjZfdwA2_bXw!Wyj zu_!&J8Ou5I6}vF4yI$LSKnB?EsVLgE%&NXWO` zaE(wqnJWjaCH5*(c#QIl(->(O{EaG)%jMMX^)!P?!Q81yd+E}@LX(!t+RDl6r3twx zE-&P@HIMcbUg!DddTWgb&QFf-s{u&Ajku}3Mx}~?veYTt#l1P^pb0&N2- zP$`1p6@=3epVt^f6GxS&`PsEbcP!6s-WL`6JIaP6x61u4Idm;Grhvsi_&_padq`?( z#n8$1*cK0Ck{VB?)@A+BnI&#fx zCTsaFCj?hXZ2^wBQCHqgx-O{sIO%ZSRfbE4D{6;VTBxV=KT8Fh3ZNMthylD0?z$5MY<(qYjzd6G|acqP}!-_EzrR{phE?nd4eoF z@q@IsU$IkZki_&4Gjb{QNQF2ynDA|zrbhQ&951QwZua)5j$9rXwn^{0wQB-P8xro& z?8}o)|CF3*xwN>J9ugsJxZ5!LV`A!lg7my-)!5b7i8ECF$fW#-!M{24>ce&|Ld-O5 zT1|$+h?&_mgTTericS|8r z^`^W27bWWLv&6N*55+zaW<5gYUpij>o1%h0#1;#a;kOxXI^ zF~mFwKBua7SXOYzZ_ttR2BR2nU|=)g{sI{ns#18NGlybdPk?MWwOl!N&<&xE1j@7V zoa^TOVauT(xUPOoe7y=-leQH*ll|~AYm`^US(Y^!JiH=J=co4h+X&wvfLiG7#Osrb zHTi^PUqHqmdudO&hT`C0^XA@}+Fq@w#JijYGooP%c6+c~gX(M*X3#COaPx+W!|FM{*^@#hO40N%4 zxF&Aev+dlB8?$~aCf+=zK=N|M)s%*qW$Z~*+CE(XigATEUCQY23C#4ooEIM6jTPIu zL=2vVT(zT)qU;6$@a>1M@&g=@A&K8sA-m{$(^L;G!f?g#B|H%18aIAp-4qnl7Sa-=9g|q z)3FSxr~vdXK`thf(EdHse_buXdO2jrW26u(aWE^5&Pc2489;f3nrd^lwKungcqUnb zGj}CKttG>3i7kJ*j952*zZ^JYpi?yf%7V-8+|%4&SU*d6 zXVCL-gio1^^kbYXw_IsF&$KRmq2jpSbqZpv>$biOi}rMoEJ*?3sqy^B*D6w=KSx!6 zU}&2KQV#oOB4&2K>X^uH^nxyEE!h6*W62w^0E#^R)qa$nb6DM5=lNj;1GM}r-gCT~ ziLnIdOc9(~sW zrj}di_E=YFuF+7}%K?57n0L!nE$xhvW-%TWxzxy?@#XKgq}eta@^<6~WZt%u`UBk) z>onbS1?7XxgRKb0^+s^fTMrI-6px*X?GE^ap|@IdN&fx}+49Lbq3iXGkKOR7tj*kx zV#3m;`}ad)6VB||ufeINk;Nk%^D)aLUG{ASf*BLln9?u4+aq9bapl^fgaUqH=lP02 zb8Ex(H2Smq0^JrowW6=09QU6$DDJ4)v7+NYI6SnOKx7m~xDTLiNzw)Vl?ly{8<2l# zVVaZdpIazU9+tFdM>5x&*5@v>yVg%DF!0Bhy>4mHwEWD{=vhjnb>tXVBHB|j_nAhx zr0!IAux3B}vEvdjSDd>w(AtpS`?KbaAC(O=?1~X2zG39upYSM^&kt>ZrhF~RU+G2j zG<-Tp#`0O1pu)Vl6Z+mHqo_S7;BlWJ=S4rZhF5!=*c(lkoxJ-o&&EyxGyA-D!&FuY z!^87_q2@!u%?Z1>pOg0QA`KEWqBb61hY#^(+uKv>5JAzZ73)oDk*7d35ti@HlzA8Y z8*ECjOW-YTNaE)i9E^EVG#B@)@9Fi$^9w15p_CIpjR?q|-r2s@G@Y-SzcZCO<*O4r z7}i_n^WSmg>UwEDIzNiBM1zCYAs4Gr8`#+Nx8G z^rZyPZ*UfkD~Bw5x4F7YoB~Oz7vTaWNz_a4Md-hTgs1GL>y4J}CInhaU6=(es#� zrQ3-%C-clw#CzzWM1dH-7FAgQFTHWKE_y7JM@_rL&eMm&7!epc;XOf)Q7366|48JN zN^11{wuLckZy}p>lu4A{7CT{)2smr{WFYU(tKfX=A;$1l2^sBhmVMbr{jRi(_Lrs@DKPiOxFE*9f< literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/23006438f204b8032730e54d9c048f03.jpeg b/assets/image_label_binary/streetlamp/23006438f204b8032730e54d9c048f03.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..66fe89a44ae2edfd42c3560c8480b564c2f2b1da GIT binary patch literal 4402 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 M)KOc3<>zz%0WP8qBLDyZ literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/2b588e6d099ae24de6bd002373237016.jpeg b/assets/image_label_binary/streetlamp/2b588e6d099ae24de6bd002373237016.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..da365f1b414d689a9a70ccf3b14702cb3878465c GIT binary patch literal 4301 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 PlM|UZ5*Z?O^zA1$f!eZi5Qc`Su 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/2bef67821261920c3d1197c971d5db2c.jpeg b/assets/image_label_binary/streetlamp/2bef67821261920c3d1197c971d5db2c.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..749c943ac1feccf4b4d2e8f2d03f352c652a2628 GIT binary patch literal 3954 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/31506b75a59a3ffb9c01038d9757aaf2.jpeg b/assets/image_label_binary/streetlamp/31506b75a59a3ffb9c01038d9757aaf2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1e93b4a985d0fc5beaac1b5fc0c0c6914c1916bd GIT binary patch literal 4250 zcmbW(c{tSHzX0$LgRu-{%M`*GyOgnqvP8D*`%=mv+hhnK*|%vdGYvJeR+K&aAZ43D z%APQGA_fF&;9G(`##S(|D5-8p7-mV=Q-ybj~𝔨YRt^#M9M0H8Z* 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/33d4fa8df6b13f89bf98225553f612c4.jpeg b/assets/image_label_binary/streetlamp/33d4fa8df6b13f89bf98225553f612c4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3675bdb15943cf5f3371fa3b82960f13e8e35ce2 GIT binary patch literal 3918 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/3590320e2af3c5b1a59d4f32d4646c74.jpeg b/assets/image_label_binary/streetlamp/3590320e2af3c5b1a59d4f32d4646c74.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3c10415ba11641230b7f0100f73145ad17ec8877 GIT binary patch literal 4316 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/4528a9d72c1cfc8e906ffaa1dac031b4.jpeg b/assets/image_label_binary/streetlamp/4528a9d72c1cfc8e906ffaa1dac031b4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..28a3ae68b389a568b4058dad3dc7f9583ebc5d6d GIT binary patch literal 4177 zcmbW(XEfa1x&ZM17-RI_h7r9D5=M{Sh9P^8X#zod69n5DWx^L0kX<4G2sFy6yqk zZ|a1A{uRJ~1&9Dl2qA(JlaP|#Bs5V21RyY&fDjCU5E9;G2j1)hgftLZPB9fCIzxLX zmlr)UBq^VmTeYr>!D#FkkGO+(C19S=jK;df2`rxH-2tz?H?Q-9iN>3K0E*01p>hTX5Fm+ zhW#%W&5etIkPu7={o4g1@V}X08bS!C7!j?CA=KWBjtdz=Os|@hU)M##EpGIS!NGfs zl#xebg?ImN+P`G~H(2O@B>QjJ|G4G>axmzod0-kq3D}gkWYnj>Ph{|!jrfFNNX2bK zy`G^362htNgY#A7BVXc~B&}XW8{9RcH#;@#DHWL^B_>pmy`LXQ1Nl(0XJcrP>g_kx zmWOt1FpGhoX-1BgG@(S+!UdiV?#66w#@bBp(u;6lYW;F(boYd|%q~cK4Rhz&s=YE6 zD~xq)nhnqGKfP7M4|n;vB9=GG-{H-6QgPV!$xxaYLX(o)HX0(0f(rG2#Wa=%nklZ<-0?<4HtaReSKu3K$w!NhBTqTU)#0w4J5jt0vh8`_RcX>qlT zOS>~;X+IT?m60qUAuUQ|e>&wBK+9{OgGbBf|D~R7MV8lZF%TD1WH+or83@5Fa9LWO z`N%spyv>uYaWqhGJ?OK=994J@zMk`x0ijQdFQSYfSsRb9fqQMRz3FADk(ybF`uCaG z+uK67^=r#_g}qTR8wK`VT?&dhrX8YSQkoI0k2ccVGWHqQKT^K+(8SLgtSuv5@+Cq~ zehCQquu^o|p?tn*&C4 z&X2aUBNO$LU3n4Y&qMBv2&UjDN@IH5v%|1rBQlZ$zu#L8RHZL1@?bG956HuCd=skp zyYXCC`n5Q`JkG1D3ftc1VnSq{5|YNgV*OX-PVpNZ5v5G_G-^AsP+bp9{pCk`qd9Va z?1XiV4h4>DIr*srN_p9C1E8DP)MGkyeEU7l@eg9EmBJ&E{ou=|x5lBfXi~9OUq97O z#{|JS&caOo`QbiSFbDf`7f$OmG8nZq}k`=mw;c9KKxI9@Rh%dK;_A zg9Y)3GxMK8RSMI1@2X)1lL|Mcu3*H0D2M3P{x!f--0T-9AC~A`*5rG0hdR<_R(Z71 zONvdo4LWhzPpQUA-8!ObR42biPYr?_adj%^*TC_%suow4?l3Q2bw#tt+4DOqm?fXI z1fl<|NqVoEMPTPoCm>5JD$(TGlg`q;nDl7|TL$*iLyFlzBP&JHY2f8m%^b1}!!emA z>Ge*XqvsQ*P5RU}zx}8htvazN11*sWNu(~X-BfN^rRCf!|AR#b7m#;BlaC=rgJM+| z-X5DW>04R)2kl#mgJ!xQ>tJ#$jl0DbYX{M7C~jF{LDbzAhqq(i1caPAz&$MX(zWct z<}F+Yt5=+35vRdx60&F6T_ltErtak;mb8rf7fd>l86}ge>icwVJ^OGe}WTeH+<06gQ zUhbBX^LzvI&nTfs#b;ySFdu5g<=A9?Dy~X@(Kn8r-&_}}3)aW`xXXmJS}sf)#8=C- zyYinf)>kl)lIW$q-{(n|MT$DiomnT$?(RTjMN;EP)~|t2sgiz**h~#gB$W#Fm8mCw z><%B2yn2ecH%Qw;cv=eLgkMD3bWkzmpZFcZrdLgc1ISwb2)sP><5zcT@BX>YJQzYV zVorbgaE|wDwREr-%kq_Jsr!;T)-BTlW49)Fm1boc{V~m-S;i*VldUe+iYJ2qIfKZL z(0SE*S!=Ezgg;7}9$1OG6>`=S-g~xQ+}x5`waTZVBmnQBdQ^pBOsK^2!a^B9Tnm$d zx&jr9>ltkej+{{DxF_X9Jho`AAL}$5tmjR(T15xl!3B6A%4QL|qRSYcb7uDabSH6J z&$8ASoq7%6Gp~VawX>=1V8uyq4qkbe-TN3h>lTao?;1HK0I=B|9H>j9}Dt0xY`-|FkO`%WKX zps}V3hs09JL z8K)I*5KhfW1agci{EwOuvM&Z4oG#=mT0`{|5bu!BAjfJ2%GCzg(sT}_&!+JZ9qT~2 z^KyJAB*CK~Y16QmD%YF%^4#~_Waq-uW!Rf~Sk^=&Db~o(Kd3)XD==C8*eT8zy?ZtsnPbq zBbY!n%v=PYR+MyW)q-x2pAtBKQl4JlTfXt7_b@5=;d6fqd7Kr8MuLD*dO2>WET2E4 zxqaF%(a!p{_`-OF2!1fZ^V{=AnF=Ptz(@lN)MLd*MQXg=9gWev#%;=Ae}e23z0RiE z_10D=Y_h{|($0{C;wtOKZ=Zd+D))um!Y*+9uya#tZ~ZZd%}vWEO3qT@w~?C1CH&6w zw38}jOAHPzjP(q#PKsQ`_cbJI7@i!_C3~}t5+92rNcZSE$`r-wt{NoY8NnXb)>KelvGcXxt$s*Tm3d|WN9!!XW) zarb)`{d74_(k&-EQ66~k zho2=H#!ZYpVGC~9%z9j9r4lA+$fsbJwN{pV|Knp!unJPZ4N5!3#yy3U|w3EMR*aZ2J8V9J6kop~tJ zF|d*$7=EZX;Lnb5ih2v*)6h7Jy0<*J>+iX9FMVH}ZUl1;M3}{a6rt=N^*8l%xp^m- zEPJFrD%$U6nKizhle(8NgzZeM??oy_jQ%!#*TkRf&p$W|cpjZr#uPkFH`rvv8+_+r z8xgYgH4FKgAm3yC(nPYMHA}n4SqH_FVa1;km526#xx`9S8kC-ez!R&C4l)bV93z-f zIFb=TkM8xy{RJKkO4i44Q#r{tJt?Fu^G38L-!F!_-m&jqT$v}#(d-{nch@{Ov$EV| zIcU;Ca-;%i*^PBg-oz~ox5a~RUIUi(A~Qas?E*RtFA8S< z#Awy~W-DE*$W64iS~y0s7;HDgibaB%_fi&KeFz&8OnMpEt#6IN#s% z5zyWbu@#IY$-d**jt zofRd>n3|x)q8S}u_xK+%IDFThtlFbvJg6@hT{3SwsSql;&jngTd0lQB1yxa5rNAsQ z@7Ud5izCs|+(t+`EEP;ucIs(NaNedNUX1#939Ewwh!5Ds^=_?v6+*j+&XZu#a0(&kY2(k5N0^WfvGxcv>;em@bJ5;W6wV4iD0f zQ+`_oD30I!zsimwyi>0i<^?gssxW*y;fQh)>xE$rQ>`gK8Cx1*3bRd9sa<-Bi!)3h znEafV`l!^f@BS`Rt^b2U+wO>bMKpb=Fa)d_thoN6Tbl0jPwlk01yZOK$i<3E&?|J z3NkWsGExe1a&k&a3MweW6(}_|l$DO2hJllfn~Rf;gM$ZlU5JNIoS%b3SV2TwQc6}< zmRm?kO;K9)x{R#!KaYSYDJh}UP?jrKSfqJ5c%}c(MSKm=QUI|)ECj>{fN4PxS`cv< z;J%!b6!fnG{l>Tjx3&)se;xfkKKXO{_a7Gsfc%?v zdHx&rKU}nzE-(oRgoOMb7YK~F+z?t4QeG)CIxP!w_YitM={O1o?VO6%H}MFxjsH2mjFiCHvpO;{RW=|APIu>k|NlfG)#>&;lAj1$u;9A0al}(s=?rj7N8s z-u^wKM@dojVEAfBCi{&tjL}(6mabC~+-Zx`9buU+{Suop)sz%%MB}v% zm-RyMOY&*oht57W(#z>7eK^j3@CGgLc`J^jTs(-(?xR7&SS^9~2|3?;=tG9iwjR_; z=g5REbLnI~%c_S|adf}QllNlCA=vtDOMS*5U5hC3Bp7eLU6NW|c6qcOn07E82_JRz zdrM$4WYBhUt1bjLhSridS!F3zp;QAS&5zUr{TFK%gPnLNo|eBmuyKm8cTY3S3v+wa z6EdYF-*p8qwkX5u<}TjM{qSbV5rq3~mD}pu33yvY+zJ^P=1tMy(y!Uxm5Qw#OU5f{fZs-j<^&qpmu7kqd^$*D9$D+A46cyF#qeUoV zxgDHmYSuZ;OH*t4P30vAif*=(T3icC#GiI+DM&2UX3KZ*-Ybp@G?d^NYj}ZCQvEG! z#@8!MFP6KNP*N(RFZ;O3BX`@pREfW6j86K=cC%kaH7Dulm){RBqyxRb#58i_D@w5} zXT^~v6*6!x%nwuheLNJQ^}0d&b-Swx zOR4<+6$yHeR(msH?)#2C4bq=v`?BPdHFkfQJ3EOBFCxASk3#Pl)SemJhnCQu!0_xmPwq>XJ#(u@*BSbGBV#`x0)1T_0*=qE7f!kE5FN)Yl zJ&JqA3_nRML>6Ncf~-1WnlI9DYr~smgNO22t`HFDjEg^YKR5iC zL;2i8HEq=V8=O?#Sxh;Y-hli!irII@LhpA9d3V#diJjL0*olxGb3A+Dn5btg5qNz| zURR(zlheF6*U%#Cm62+fp2=7ibzHuMP56>uIFr3+J}lk*t8w089s1#$AiWO)pVdxi zt(-^JulxwYgV!Fkx0QC1ZP+Y@{fJe2`2J4?mQbGAuqsAb zXGwqAQdFWwn+%3Zq0%pzI@>zb$X6NW!Gxx;I;2%M1WJ(+b{{XMoHq{subUnyF-bR|L)^wc`|Wh2GD4d)oESU z8~n-FT$P(%)KH4 zx66q@;>PgVGzR`=>#Byy%&!q%xv+3o_h)BC9*1vqdD7IjIkiQ0ezL8)GtJK{&4-|( z>?DW)WZN+K^wR~`%cpIJRM@?j1!A@ak=<;ap`*&+n zUbLZRgu#~O<;BGz;Q||THuYN0+5SDt1FqIOeHhdC?7?rWlchka2C<;IR6hfrI=az0HAlrk9_1Fhqnl zvyU|RZ%V2hr#85t%g6oZ%-Sa7_AwXqMaQj%K=xqJFJ`50#rTV{59L4Z1QZehAI5Ps zY`37g9_Ra19cf0seG<}-*S3~>iTsUgD*Iz|;=LiGS~#p(s`g{y2JA(5YW+^Y_4jEw zj?KUL+jp)tJjr@LM)7)il(ESOpH+gRR2g_Ak@3}8>nn{&;HglM;%Zl;I?GnU?2FTd zfC_QLri>K^XHE8r_IPy)@))CU;W2E+oUigFa6S}vR8e!PqSmEp-E~2;m>BbYB9I)w zC5&f*hi9mw11`cx{d)(V)zk$-Zk;K5JCzt@-TTyQ~k;wSD7Fd8yG9M&0B)3)V}muY+q^M}u0Va*m$0 zom%d$bkMmfuGyjeK6AGAbg&NTZ}3B-wW@_@)@_ZxUblho>RsH3b16`&^5_pvt{Yih zHbPb#xcQhTHb*v%>|c~YCWio-UBV#R4DCBBV;tY?nQa35rW#8WZ zg0|CERg@J3#=-6Q=GK5EVPehB4u77N<;v>UX11n9Ezjr_+JYJVE_NBrFz)M2O-DP( zRyAP&*|L$;>up{2tBzzSbDovM zP+=Foy^tGt9?7(B4ksGIb5H zUq{zg<`h+bAG(>zM$d8GT7lkEBgvuk@14HIyXs;H(M6k?wGV4I1m;x~Ze*7;IiqH{ zZr=!uyH(aN1PYnA!HQb-*?H#Kc4lcfPw#~*e3~mY=VYFM5CQK-tuGn7p9;^cV-CW% z72HlEDg+`I=26LVkki(})V=~;pHG@FJKb6AgW3|I*iKuxlPrz1HYv zSFiEdHv&g4gZ8!_wXg;rZOrgu5H1p6gA5QJJCkx$4(?2|i)veY(xB=dB;K?FknuiJ z{E0`e%=;=F7J1_roDYT)(j)d?dep}a`^3fJYFBvgZ0Q)Ju&5s7`rqbKK(OVbM9yw} zJ(xvaWMEwrw&xae zBpN`QgQPNKan8dYtEt!Y(%CLO37wffe=baAf}TF4{9SWifMjLe^$5<)E(e+8?s2H- z`5na-@=aN0HdLR?hjjv8kB>1DLPxK+2 z-w7m0p`G@_&=64~kjZ7Nlh>N|Udi>P);skX*au8#ZnQC50?12fyy~<+=uEVFswZBA zW0Z>l{T45imnFJ9Smfbb%<5w7BdmD=w{dN4@Cma^nPDRT`vK7-*i1F<6@sEsrdjLw zQOIBL_xod#_@2vefDaL9vUnW+RDe~%j7cG!$1O(6YKj(Fqp?Mei5nLxAGxxq!gyZF9X zdemdVuEftve6J-F;VJwrT_UrF9FA2gWJ}mdy`3YN)dq^SDqpBK6^E){?EYgV@m^vn WfJ@IDRU2kfJ2wDZK9QIwF8l{-%TP4{ literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/5b6bfec1aeabd840a015126cc03d6518.jpeg b/assets/image_label_binary/streetlamp/5b6bfec1aeabd840a015126cc03d6518.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3937bb8da936603eef570789b438f3ae7b9831f2 GIT binary patch literal 4260 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/7260eb6e0ab45f696acf83dee9c2634d.jpeg b/assets/image_label_binary/streetlamp/7260eb6e0ab45f696acf83dee9c2634d.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..6eccdb552a1f8b80fb0a79bc9799a05d8e64c833 GIT binary patch literal 4253 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 WY~+XSlHZk6>%r3b89O0NL$)GWlE#v)OqQ~h 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/8f38391ab5dd9cca78c91217f5b8aa27.jpeg b/assets/image_label_binary/streetlamp/8f38391ab5dd9cca78c91217f5b8aa27.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c7215b1871825484eb06ca2e02d8f20ded269e12 GIT binary patch literal 4095 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 R5A7j5=b9hU2ggU5{|{n^gE;^I literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/902d6009bb13dfa2cac585e2928efd29.jpeg b/assets/image_label_binary/streetlamp/902d6009bb13dfa2cac585e2928efd29.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0611121f76d19ed16eff80ab6e58014acb1bb3d6 GIT binary patch literal 4022 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/9be25a532efedf77fd9e4d37ee2e301c.jpeg b/assets/image_label_binary/streetlamp/9be25a532efedf77fd9e4d37ee2e301c.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2165d853ece7b58a87aaf7a375e3c45a9fcfc938 GIT binary patch literal 4318 zcmbW(XEfZ~y9e;!7>wSDkbmvavIB@bd8U@$$f60&r=BfRL0h3?`;1E+vCR zp-}t?WpyQ4HEB7N>_3NqXlZE~=oz>f8M$QzVS=*%=a4@FtTaFr5CsMa0TiqtFe`}M z4eiEQ{TyHIl2dzbM%j)rQ^E74doFLMTm(@pyU-4m6TO8oL%m_y19D%@sor^d}da5PVUpZ{1-1v%gSF>RK9-O z(Ad;WXlZTh`qbUiOYH0aJT^Y@b#iKY=G)Tp%Iezs#$P`+_YV$_NXI9CpPv2W0s-KE zvwpAthW!s0>u(nYB_)^=@{bEd5&V0CSt+Rmk<@J377zysVm2TpF=6xV~E%OS}<>Pmcl zfSKKbGrVl}I4(@A_p*p;P=X!zNcJe3YxGN8e9X*NzHAaLriXj*8sm4S<;B}Q*a*4` z;UR<@Y>KkR!za#?Bl+>Rw>hW}(|0WfyHh>&mK{fAQgs_}OA$sDrEyQJZA-y}mcgPb zk!L?%NoFOi+#DN(0AFya$R{7GyE1(;oWcn-J^nDB`BYxKGJIc-|Kfp-hf42Id+4vF z$QC`uMqWBYmwQMxB3rD)#$n}?45?2BlD)nDvd{T0SVt17TqiXO=2VBm-(=VN3!&TIY{qXY?iD3Fs6D*P z;rf?^Re|Z^kx+a7`gP>w8XHn{1WLO>@tO>{eHUs-*q#>a=fmEw{w}P*vnCbS zyj!})kQeBc>}ODc3P5c;luc}17iMbZ&%1Vh|DfXTpQZJN#PDZ&LA zpuP0A_50-G740^1xqqW<&;j8 z_cVnz#I96(MPg8HW;#)=Eh~)I>o8gNbA`az0j`fXQo`(Q&Cq1ZHHOi+bP{Lr^QH->3V>@aPhc8V_urt?gIxI_-WJBVLTYd zxqu`C#CGA3r2(74K<3@Y60Wao$XK_pScu${(pt;9<`A*1eoy`&jRzB5U9p{DXEgWY% zv`_%6)=2=3&#RLyxucTO$>ovqDP-oMHti~DNN3lFC^`u{ix)n#I$>q#s*hWcYdL)k zrTiLN^G({$QP@8%J77w4$!uNKJ)mL2C3#_9(jWRpe^Iq~zj}o#LFvFxJ-<=jrTfHE zsN1;N^k~TObW9?1^;_{Pj{IGRpCdWz@>Zg1+tXIeM0p=KhXVHqGT?W8z{);1c_-z{ z8iRuCJ2Tu97gmvq%r(evhCX4UAQfxkESNRi@n9Y=fwi_3FibeF&+g|V-7NJBmXBXJ z_xS3sw1f<8aiLQ5p4ZFQzAkI%ot|Cf?2H$S*;B6~9_6K^r6s>L4Jv4!<93P(sl$z~ zpkZ|)9UV*>UK@#f%xp$!-1bigM~hK!ZhuO)2?z?7zA}BBjS-pU zT?E5ZRb1;1tAA1dxQ;UMEDJ3_`rL~R?60~W3f%qRPeani+|6UFeH`%=TY66^(2}TW zuG#e`bCIW;(}SxV#ZS*BeEspdP4GwBO3q;UtHy71S#$0czdSvxNKoX>s*E~b5wku>7-9}w<~+&){Xxje84AS zgOn>ol0=_Bo7^4HY&8d0o-&0%$pDYWSDt3dJ@Aguq#;_TBj6|eM!!V^}>z=!hqT4_%? z!CX4+tqMk-qF*&NgE9B#W;rOqIL){_ZH;;o17fGs6m|_G`xu30g^W89wKoQbE?j*e z>texP1%*~;)o*2Zf-?O+ISi;_AyfGy^$q)sU)!WcbiJP4f;XE=e(*FXGFlK8gfXY5 zty%VXs5Q_tJ$w^;Qa$d8BSd_zq?pxSYhZa;-$&|s_+z=x`0xH%c$E)vBk?Ts<-HH# z!9HmDr4Q_=q@)Ok>&?=0c+EfpX}IP_PA)1rr$_T~+p~GTa62YG?xgFL)P$|3BzJFO zu|JpAV+kxi{MPmIAA`p^grF~it+wgZy^Chb)HMq;_P3v`9&A4^gt4(~MefNX^S#C} zUOw{69Tw>U=jC4GKG6h?<+w@OtRMN+xR@Xw_xq zpf=TWK$kZk#jEes9X+5RLqP_#(KG8H7z16Obusrg<_`DX-}p73jfNN{boQ(uy!dmj zm9@8zPLzYN7}ChC+5}&p_~}8;zPHXFE(wyEhK>|&8dm7_=yc{XQ?KMvM6t)(s7}mV zkYTeIzGrRrONldNJxRa0u z2yJetK{agFm zqnUm3Dhu{i*`hv7G~bwyDptVT?hTiZ%nnK)*ddC_+(B%t2ZwripVhy5GO$k)^q-<{U&yfg ztcdq?95><%eKeJy-763{v&)MaztC*ZctrFrEuKFns{gDbj4vl_%pkJg>O?syS0PJ! zzAR(CD}5rSX(nV)a;Jhfap=3!v)DWpFI?LBs_VtM zHSJ=)*mpB!n5gXYfc}qV&N2P<`LRJKGuJqrqkv7Viu+BCSYDn@CbwmVXvt-j`e?0}^$j8P3;cxTJ1`aC^Li zOSQ+=cR_M&IiZWzr;gU+@I@S;nJO+H?!nZzMh5DS4iDuUHYU_n)HS})PTDR$RXOT} zq?LObWA83>Sm?lr$xCTrfn{yij!dL%Hy1)OdJPV%cvJDVB`qmVSuLO>#+M87?6%nA zQ%_`PAEObDIdkpptS^H~0?jY+7k>a6eJcp?NqqTt-3iA>J0uVyFYhdDtOFb_)8;I- zc2H%)6FP;AAoUBR%$+7nwJ=LUB6T1S^vwJHkY2UWPPXgD>9tG-LI&ytk+rp4#y zJHQFFrhKkY^u$wRVOD8iA;=-@qWj@yR z-nj|KNm3Ij7h!0ZyRtU~dOw9gp|J6p31zhU8yB_sj5tJ982f|7L##c@^bVDlkOZfx zN2t}WlnK)GOa3@&Ya%w#j5MZ%3;Y5mwQ1j%J>`Yt?Af~Btd{v>4yb2 z@6pUv6Lv9H0%Y^1{r#*}<}nKk!*L)0mJ$bF)>Gns>8<}wes_k#PA52r&nLqQ0H|olfEpUBI}uP^csOr0TDGJ5l4~fsHTG;*9?0h; z7KkTvIelX7ji|z?Lo8yJkd$#2+`hu<5tWhkfs|ktabiuS=H%l_@#!8mcxGMB+V~f9 zT6kuU2OIGCvQ6;-wLZFQmNV>2aE^YH(-Ux(ZzQ!#S7OU+$6ctx7m#7#EJ{M+?~Tgk zy}vhfu!^(%G;lk4{~d@7ypOZZG${~#-vhBf;EptQZ^{9QvurQGA4+oda=X^5Mq;?) zS`ia?(!PgG*f1gES$T!MK%(qy`o)p~7#_B7Q=Vw&crZv056|m$7H%c_>PQfd!=ihh zGEOB_zU$4uZ4jd|qWSZ^rm=Iq6hV<+NY0liP`vhsb+!+SVSBKtZ9pM|<&bwvE1o{H zHHk4bQ4?*nv!xT3Zhs>^frZGYb+=9Zvb+(|86B_EmbPYv8SrAN{}j-%k_R;dHYRIK zvHjj9PZO*b848^(!r{y|u1$?M12hEfCAZI~%XKmgP-`3v*qoowUKB||hJx&*SH1_c g2#LP%vyd*r=up)|Lq4Z#ku*1IP&20Aipewo0gNZ!+5i9m literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/a698bd00f059ac0618a420fdf4765f17.jpeg b/assets/image_label_binary/streetlamp/a698bd00f059ac0618a420fdf4765f17.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..851be59ae51f661e85f719c090ca5e306bf23538 GIT binary patch literal 4717 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 K{)XPskN*IvNYerU literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/a99e11c8196910a899c846f0a2bfa416.jpeg b/assets/image_label_binary/streetlamp/a99e11c8196910a899c846f0a2bfa416.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..09230a1f2c56b3f5c02673f7e22a935a01b9d4d9 GIT binary patch literal 4115 zcmbW&XHe5kw*c^e2ojnF1RhEt2!YTI)r1xVr72a4ph#~DJkq5%5k;g$kY)nX1f?ku zs(_$$f>h~XC?bX)APD60yziZRXYQwa@7dY?vO7EHw>xJJ?K^EAxS+4CrwxEW00252 z0Bs7;0vH(>APi?1ArJ@?6C*P#$5~bu7FHhii|0A`d0_(lyilkhTm~U1Bqa=mid_+x zLdq&AD8LYCHI$sHjJ$%}KbL@*n3z~uSh>%h<(9hyy(IU44q7X~#t1|LkzkMzK*t6G zvw>(G0PHl+GoXJ3@Sg#q1Jj>jfG{#Kvz#8Nxd70Cz+gIh@R>98^rxr8PWJ(NwlnOP zWUn$@G_i*Wd2`4`rQ|URYgV;!ntt1a%RBf)Gcj}h#m&PjB8m_bmrzhtx`IM0YhAmp zt)r`_f6L6=!t%D2wWE`>i>sUaeP2KSfWSvV!7-0xpTs?V7N3fJnU?-4BQxtwenDYT z@!OKp4h2jD9vK}Q|2{Ff@Po9tw7l|j_1D(+&Mswd|A2b< zj|&8V|IIpG{|);eF1Aw_9X&mm9`cV1L>G8E!EE$rF3B>mUp0Z)dtVfii(=%^Ov$Ti zV-l7(-Q;xe`NqrzS6C3)`iJ%}+5Zj}{r{5v7wo@XlK?9ibozN1-&0s{Um@S`^fCy21b(MqF=W~!PizWSLWB%P#44I=YxtZCLJ$=5-?@!v*>uM* zLtbQWtJ45O$t&)YhT1yTX+?{Y*R2=V)vvLI=7#v1pg{!jZdItCXpX2u-C{b5p|&d#?^HYRxB5%mUT`4Jp!j0;r{%A-7uj;*fko>MQqiwpKhaR*_DYp_t zNldJbe=-PxwV&mlm{QM^AP1?NtLL}ul(bxJwC+&HY!tPK)lSvp2VW=^th4Ti31d|q z!$PvmmMy6Awa@oNt2SxC6PMKOP^CwPG~nvZ>aCU7%Y{O21uv%JT+oQY<)U%Nbs4s!Xye!`r8Iksez(R^nJ`-IVti zUOM?Gf8@jlM5YLj^uN?vmiP;j4599kbtgk*m{skge(R_;uB^@-x#>$9V*>_vMz%Qr zoMBhu2rWp-@g**tz+w#oUEds}H1+1d7adV;2r6ucQaZ6x6x+*Dmm$G&eGVJ|4uwK?{GXDjBp~=gt%+*eI4qx2tyMMwkD%E}^-AbcN z_=pBr?^X}_ON#J`T09G84<=0&Juq>7DWF{Zi7f-`NvsyTwWKC$`C%@C(*4CYhHgk2O0Rl~<+(`qb=ItrHe$(=}u)|T&ELcqew9i=KyBkLlQTj8UCcWsT0(I_)z?D5j4N$4bzw4wknm4O z=f^(O7%|+r8@<&eaoeYLf9+_CYAN~d5w@Lcpx6Fl&y~O7P`Y3mU^G@~Y1P=&)OYa) zudwq=bQ^yQajZDKFLYqs*$RFEFO9sNDvNs^bmDKzDz|p}NNnmgianm0&y3IM!4D~? zb*S%RIFpaSB$uJ_nx-|WaLX610r#$6(g_`w`KxaLsYCBQys9q@cuDoNI@v`iKY}g> zkgx`;r7#KWmv=FJXqFr@m}+UhkR8U8;N8!)I_>mPMq~H=Bl3%mUS8H55^A}T%<}mM z+#qK8*T%5~U)^uq`{v@wOF25OZd_N}B+#9(k@%nMHGJop+`|L%l_B8u6(04N@fu&8 z;8uCc{1oP+6ΞqLepqzWGskN$CRt(Gh_kzpS9L8?WDDa!X#!M$YUh9r=@}M|uJ5 zbI7TVPKTK2VBfo1dXj{UfxhEzwFuscB#oE3?x1c}>_O7bdIYn`(8L8!zKlI+1(gP< z?|N3g?|WSDadvwC1YKOyi`{QX2$Psv^{VUg*lR_XwzzmWiK~QU6)(!%D~NE%hR^pE z?B6m~+}*Aq3L{~Dvi-7o&UYPg)nPB^_c!7#*Lmea#D#;rk#N`XqeDxZ;$B9x?+@{h zA~8lQcQs|W6N5-eD|ZgfQsc~xjZhw87~-nIJ<_(zOb^|9pSs-_?`aBYo48^k<(dAo zKSv_*VBP!Jec#~OlCcld*Wm%A_lcqai?|1Le~%bk=vgk3?{(H@}0He}>T~Mf`ZjqRiMWT(V$9UDdfP$0SO2ae2+A zx*_L&GgWqgiYAWulMlxpn`kFjJp+4!9y`q|F$Ck(AWOf+>8c!lKcoS3zcEDWzWKsd zwD*o(01a>p4~uhLXL@6yRWpi3Of)FTd4G&tIY@&3aoRf!ai}Mp)0coKFj?>4eNK`8@19Crbl4lE=5ZJ450zo_-8(g@~kEEjx99cPLx)1pwgh6PpQ$>({xMho$S7F zGYEoHhNo7wY%F4$(?55`xrH9J4;KYT#A+vUUmQ4D`J_BNE}J*k?LrcLqlf(Siasi_ z>DXIO=Ew6-@2gXu*k&ZKP5Ychz9<`{!{|T6_8G)hlIk{Y2uldc+(7QFT`y-gP`PdI zb71yoW=FqLW}1P-4~ZyLf`YnwFHUd8XKyGic|n%Nf7LSS6R#ni!j5n#qPK3m6@h?4 zwVr70D3s6IyS4&}@^f>Oc(1l8xaUJgQjywqu3swE6^;nwSw+dHWLZf^L}D=wC^PW@ z@Glox7k3pMO|?&k>YV?~`Z|K=@=CvCjyqKJ&xFKVf6B$%Z|Ryzf|BJsy-kapu-KiG zjJKo&O~AKpbDavQNB~rlI$NkmeD&Sm zK^FyP^f+uYE{DZw-`<97h0SdiN)cu5k`XDm5uvBIe8UFjNm%`FUL7f6KH|?auh@Ty zZc#e`9}ds71Yn+u|7<>~x$~gQ7pG$!JQB^pET;=e&VOB9VLzkeW1IP$&IedLw_Dck j#E$9`NaK}-qQMlvEJb{pZwaL;{#(r*F~avNf;RCVpFzp# literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/b6cdbafa182291f9d91c2474d16845d4.jpeg b/assets/image_label_binary/streetlamp/b6cdbafa182291f9d91c2474d16845d4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..bd85c781b10586734993d4e7846afb175c1ee712 GIT binary patch literal 3751 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/c03d11a827eed8fc1d6bb083a92dce92.jpeg b/assets/image_label_binary/streetlamp/c03d11a827eed8fc1d6bb083a92dce92.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d5ecc590eb724695686409e261c0d1e8c40740b5 GIT binary patch literal 4021 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/d03f4f239191df50459ccd4d1316b9ab.jpeg b/assets/image_label_binary/streetlamp/d03f4f239191df50459ccd4d1316b9ab.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ce5629522ea9f29db26138c6953fc736a4b6154c GIT binary patch literal 4447 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 J^*LV7{Rc7ID!c#y literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/d0a7e87990072185fc7b19fb5fe158f8.jpeg b/assets/image_label_binary/streetlamp/d0a7e87990072185fc7b19fb5fe158f8.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..64290433b6f9e347286433473e412ecbf1f2fe7a GIT binary patch literal 4270 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 literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/d40775690548d6fbd5b0594fde70451b.jpeg b/assets/image_label_binary/streetlamp/d40775690548d6fbd5b0594fde70451b.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..49cad9d7cba021be510255dea2b42174e178047c GIT binary patch literal 4610 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�F(Jf^1QAJh-Hg17$s-;ywywX8_Ja96;Zm30;LvZO?pG(sb(E6Ej(a>zCA}dk_ zP0;y%TU%%o$*l1mpZ2t?qe7R#9u$fxt`K;I0~`jG%3{IJ9z&gr2(qZFkChs zl3ENdaL6tf$Xd0p3+26@mPPRPN$LeEumC6Rc><1fFWuZ#e072WkTXt5n8;}E!dFet zRdTr(k&luMZFocMi=K;3nonuRVw-KHAPrOfh2U@TmNJrZNuej^wt6ed6$-Vx>OtX+ zQ*>kJf(4_{Olj(wl@Ja?YSJ)Ivy797y6fw;XNDVrvTN7V6bS@XZW^<5E}!o4vi+wO N!{N~^tr^DTe*lvua;*RW literal 0 HcmV?d00001 diff --git a/assets/image_label_binary/streetlamp/efb5927841ad1d6ec221129c13fe32a4.jpeg b/assets/image_label_binary/streetlamp/efb5927841ad1d6ec221129c13fe32a4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..92169f136379f896b050d1af091bc0a62f7ec8ac GIT binary patch literal 4217 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 literal 0 HcmV?d00001 diff --git a/backend/routers/challenge.py b/backend/routers/challenge.py index 6eea3d42c7..5c5938fbbe 100644 --- a/backend/routers/challenge.py +++ b/backend/routers/challenge.py @@ -3,21 +3,42 @@ # Author : QIN2DIM # GitHub : https://github.com/QIN2DIM # Description: +from io import BytesIO +from typing import List + +from PIL import Image from fastapi import APIRouter +from loguru import logger +from pydantic import BaseModel, Field, Base64Bytes +import hcaptcha_challenger as solver +from hcaptcha_challenger import ( + handle, + ModelHub, + DataLake, + register_pipline, + ZeroShotImageClassifier, +) -from typing import List +router = APIRouter() -from pydantic import BaseModel, Field, Base64Str +# Init local-side of the ModelHub +solver.install(upgrade=True, clip=True) -router = APIRouter() +modelhub = ModelHub.from_github_repo() +modelhub.parse_objects() + +clip_model = register_pipline(modelhub, fmt="onnx") +logger.success( + "register clip_model", tool=clip_model.__class__.__name__, modelhub=modelhub.__class__.__name__ +) class SelfSupervisedPayload(BaseModel): """hCaptcha payload of the image_label_binary challenge""" prompt: str = Field(..., description="challenge prompt") - challenge_images: List[Base64Str] = Field(default_factory=list) + challenge_images: List[Base64Bytes] = Field(default_factory=list) positive_labels: List[str] | None = Field(default_factory=list) negative_labels: List[str] | None = Field(default_factory=list) @@ -28,51 +49,33 @@ class SelfSupervisedResponse(BaseModel): results: List[bool] = Field(default_factory=list) -import os -from pathlib import Path -from typing import List - -import hcaptcha_challenger as solver -from hcaptcha_challenger import handle, ModelHub, DataLake, register_pipline -import pandas as pd - -# Init local-side of the ModelHub -solver.install(upgrade=True, clip=True) - -images_dir = Path("tmp_dir/image_label_binary/streetlamp") - -prompt = "streetlamp" - -# Patch datalake maps not updated in time -datalake_post = { - # => prompt: sedan car - handle(prompt): {"positive_labels": ["streetlamp"], "negative_labels": ["duck", "shark"]} -} - +def invoke_clip_tool(payload: SelfSupervisedPayload) -> List[bool]: + label = handle(payload.prompt) -def prelude_self_supervised_config(): - modelhub = ModelHub.from_github_repo() - modelhub.parse_objects() - for prompt_, serialized_binary in datalake_post.items(): - modelhub.datalake[prompt_] = DataLake.from_serialized(serialized_binary) - clip_model = register_pipline(modelhub, fmt="onnx") + if any(payload.positive_labels) and any(payload.negative_labels): + serialized = { + "positive_labels": payload.positive_labels, + "negative_labels": payload.negative_labels, + } + modelhub.datalake[label] = DataLake.from_serialized(serialized) - return modelhub, clip_model + if not (dl := modelhub.datalake.get(label)): + dl = DataLake.from_challenge_prompt(label) + tool = ZeroShotImageClassifier.from_datalake(dl) + # Default to `RESNET.OPENAI` perf_counter 1.794s + model = clip_model or register_pipline(modelhub) -def demo(image_paths: List[Path]): - modelhub, clip_model = prelude_self_supervised_config() + response: List[bool] = [] + for image_data in payload.challenge_images: + results = tool(model, image=Image.open(BytesIO(image_data))) + trusted = results[0]["label"] in tool.positive_labels + response.append(trusted) - classifier = solver.BinaryClassifier(modelhub=modelhub, clip_model=clip_model) - if results := classifier.execute(prompt, image_paths, self_supervised=True): - output = [ - {"image": f"![]({image_path})", "result": result} - for image_path, result in zip(image_paths, results) - ] - mdk = pd.DataFrame.from_records(output).to_markdown() - Path(f"result_{prompt}.md").write_text(mdk, encoding="utf8") + return response -@router.post("/image_label_binary/clip", response_model=SelfSupervisedResponse) -async def read_item(payload: SelfSupervisedPayload): - return SelfSupervisedResponse() +@router.post("/image_label_binary", response_model=SelfSupervisedResponse) +async def challenge_image_label_binary(payload: SelfSupervisedPayload): + results = invoke_clip_tool(payload) + return SelfSupervisedResponse(results=results) diff --git a/examples/demo_classifier_self_supervised.py b/examples/demo_classifier_self_supervised.py index 122e4a83ae..1819d9105b 100644 --- a/examples/demo_classifier_self_supervised.py +++ b/examples/demo_classifier_self_supervised.py @@ -10,7 +10,6 @@ import hcaptcha_challenger as solver from hcaptcha_challenger import handle, ModelHub, DataLake, register_pipline - # Init local-side of the ModelHub solver.install(upgrade=True, clip=True) diff --git a/examples/demo_find_unique_object.py b/examples/demo_find_unique_object.py index 8f863cba1b..0853f225d6 100644 --- a/examples/demo_find_unique_object.py +++ b/examples/demo_find_unique_object.py @@ -7,14 +7,14 @@ from tqdm import tqdm import hcaptcha_challenger as solver +from hcaptcha_challenger.onnx.modelhub import ModelHub +from hcaptcha_challenger.onnx.yolo import YOLOv8Seg from hcaptcha_challenger.tools.cv_toolkit.appears_only_once import ( limited_radius, annotate_objects, find_unique_object, find_unique_color, ) -from hcaptcha_challenger.onnx.modelhub import ModelHub -from hcaptcha_challenger.onnx.yolo import YOLOv8Seg solver.install(upgrade=True) diff --git a/examples/invoke_remote_solver.py b/examples/invoke_remote_solver.py new file mode 100644 index 0000000000..a2abb00dfe --- /dev/null +++ b/examples/invoke_remote_solver.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Time : 2022/9/23 17:28 +# Author : QIN2DIM +# Github : https://github.com/QIN2DIM +# Description: +import base64 +import random +from pathlib import Path +from typing import List + +# pip install pandas tabulate +import pandas as pd +from httpx import Client + +BASE_URL = "http://localhost:33777" +client = Client(base_url=BASE_URL, timeout=30) + + +def invoke_remove_tool(self_supervised_payload: dict): + response = client.post("/challenge/image_label_binary", json=self_supervised_payload) + response.raise_for_status() + results = response.json()["results"] + + return results + + +def show_and_cache(image_paths: List[Path], results: List[str], prompt: str): + output = [ + {"image": f"![]({image_path})", "result": result} + for image_path, result in zip(image_paths, results) + ] + mdk = pd.DataFrame.from_records(output).to_markdown() + mdk = f"- prompt: `{prompt}`\n\n{mdk}" + print(mdk) + + fp = Path(f"results {prompt}.md") + fp.write_text(mdk, encoding="utf8") + print(f"\nsaved ->> {fp.resolve()}") + + +def run(): + images_dir = Path(__file__).parent.parent.joinpath("assets/image_label_binary/streetlamp") + image_paths = list(images_dir.glob("*.jpeg")) + if not image_paths: + return + random.shuffle(image_paths) + image_paths = image_paths[:5] + + prompt = "streetlamp" + challenge_images = [base64.b64encode(fp.read_bytes()).decode() for fp in image_paths] + self_supervised_payload = { + "prompt": prompt, + "challenge_images": challenge_images, + "positive_labels": ["streetlamp", "light"], + "negative_labels": ["duck", "shark", "swan"], + } + + results = invoke_remove_tool(self_supervised_payload) + + show_and_cache(image_paths, results, prompt) + + +if __name__ == "__main__": + run() From 2df91aac2e5820443105a513832c5b9955aba888 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sun, 14 Apr 2024 21:02:59 +0800 Subject: [PATCH 06/14] Update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 79a7771cfb..e818d8578a 100644 --- a/.gitignore +++ b/.gitignore @@ -157,7 +157,9 @@ profile_pluggable_model.md tests/record_json docs/lvm_challenge/*.jpeg docs/lvm_challenge/*.png -**/logs/** +logs examples/*.md docs/logs/ docs/*.md +node_modules +pnpm-lock.yaml \ No newline at end of file From e8f202b8fc528e46d7320ca325934684046a2141 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Mon, 15 Apr 2024 08:17:44 +0800 Subject: [PATCH 07/14] x1 --- backend/readme.md | 5 +- backend/routers/challenge.py | 57 +----- examples/faker_client.py | 17 +- .../agents/playwright/dragon.py | 179 ++++++++++++++++-- hcaptcha_challenger/models.py | 21 +- .../tools/zero_shot_image_classifier.py | 36 +++- pyproject.toml | 2 + 7 files changed, 222 insertions(+), 95 deletions(-) diff --git a/backend/readme.md b/backend/readme.md index 2d03adce3c..ce960fc378 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -1,10 +1,9 @@ ```bash -pip install fastapi -pip install uvicorn[standard] +pip install fastapi[all] ``` ```bash -uvicorn main:app --reload +python main.py ``` ```markdown diff --git a/backend/routers/challenge.py b/backend/routers/challenge.py index 5c5938fbbe..4819155297 100644 --- a/backend/routers/challenge.py +++ b/backend/routers/challenge.py @@ -3,22 +3,14 @@ # Author : QIN2DIM # GitHub : https://github.com/QIN2DIM # Description: -from io import BytesIO -from typing import List -from PIL import Image from fastapi import APIRouter from loguru import logger -from pydantic import BaseModel, Field, Base64Bytes import hcaptcha_challenger as solver -from hcaptcha_challenger import ( - handle, - ModelHub, - DataLake, - register_pipline, - ZeroShotImageClassifier, -) +from hcaptcha_challenger import ModelHub, register_pipline +from hcaptcha_challenger.models import SelfSupervisedResponse, SelfSupervisedPayload +from hcaptcha_challenger.tools.zero_shot_image_classifier import invoke_clip_tool router = APIRouter() @@ -34,48 +26,7 @@ ) -class SelfSupervisedPayload(BaseModel): - """hCaptcha payload of the image_label_binary challenge""" - - prompt: str = Field(..., description="challenge prompt") - challenge_images: List[Base64Bytes] = Field(default_factory=list) - positive_labels: List[str] | None = Field(default_factory=list) - negative_labels: List[str] | None = Field(default_factory=list) - - -class SelfSupervisedResponse(BaseModel): - """The binary classification result of the image, in the same order as the challenge_images.""" - - results: List[bool] = Field(default_factory=list) - - -def invoke_clip_tool(payload: SelfSupervisedPayload) -> List[bool]: - label = handle(payload.prompt) - - if any(payload.positive_labels) and any(payload.negative_labels): - serialized = { - "positive_labels": payload.positive_labels, - "negative_labels": payload.negative_labels, - } - modelhub.datalake[label] = DataLake.from_serialized(serialized) - - if not (dl := modelhub.datalake.get(label)): - dl = DataLake.from_challenge_prompt(label) - tool = ZeroShotImageClassifier.from_datalake(dl) - - # Default to `RESNET.OPENAI` perf_counter 1.794s - model = clip_model or register_pipline(modelhub) - - response: List[bool] = [] - for image_data in payload.challenge_images: - results = tool(model, image=Image.open(BytesIO(image_data))) - trusted = results[0]["label"] in tool.positive_labels - response.append(trusted) - - return response - - @router.post("/image_label_binary", response_model=SelfSupervisedResponse) async def challenge_image_label_binary(payload: SelfSupervisedPayload): - results = invoke_clip_tool(payload) + results = invoke_clip_tool(modelhub, payload, clip_model) return SelfSupervisedResponse(results=results) diff --git a/examples/faker_client.py b/examples/faker_client.py index 75ede49800..2d282a7b1d 100644 --- a/examples/faker_client.py +++ b/examples/faker_client.py @@ -12,7 +12,6 @@ from hcaptcha_challenger.agents import AgentV from hcaptcha_challenger.agents import Malenia -from hcaptcha_challenger.onnx.modelhub import ModelHub from hcaptcha_challenger.utils import SiteKey @@ -33,17 +32,7 @@ async def main(headless: bool = False): async def mime(context: BrowserContext): page = await context.new_page() - modelhub = ModelHub.from_github_repo() - modelhub.parse_objects() - - agent = AgentV.into_solver( - # page, the control handle of the Playwright Page - page=page, - # modelhub, Register modelhub externally, and the agent can patch custom configurations - modelhub=modelhub, - # tmp_dir, Mount the cache directory to the current working folder - tmp_dir=Path("tmp_dir"), - ) + agent = AgentV.into_solver(page=page, tmp_dir=Path("tmp_dir")) sitekey = SiteKey.user_easy @@ -57,7 +46,7 @@ async def mime(context: BrowserContext): if __name__ == "__main__": - EXECUTION = "collect" - # EXECUTION = "challenge" + # EXECUTION = "collect" + EXECUTION = "challenge" encrypted_resp = asyncio.run(main(headless=False)) diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py index ef8f31efd2..b73f18fc16 100644 --- a/hcaptcha_challenger/agents/playwright/dragon.py +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -6,6 +6,7 @@ import abc import asyncio import hashlib +import os import re import shutil from abc import ABC @@ -16,6 +17,8 @@ from pathlib import Path from typing import List +import dotenv +import httpx from loguru import logger from playwright.async_api import Page, Response, TimeoutError, expect @@ -28,19 +31,96 @@ ToolExecution, CollectibleType, Collectible, + SelfSupervisedPayload, ) +from hcaptcha_challenger.onnx.clip import MossCLIP from hcaptcha_challenger.onnx.modelhub import ModelHub from hcaptcha_challenger.tools.prompt_handler import handle +from hcaptcha_challenger.tools.zero_shot_image_classifier import invoke_clip_tool, register_pipline +from cachetools import TTLCache +from asyncache import cached + +dotenv.load_dotenv() HOOK_PURCHASE = "//div[@id='webPurchaseContainer']//iframe" HOOK_CHECKBOX = "//iframe[contains(@title, 'checkbox for hCaptcha')]" HOOK_CHALLENGE = "//iframe[contains(@title, 'hCaptcha challenge')]" +datalake_post = { + "animals possessing wings": { + "positive_labels": ["bird"], + "negative_labels": ["lion", "elephant", "bear"], + }, + "something for drinking": { + "positive_labels": ["cup", "something for drinking"], + "negative_labels": ["streetlamp", "animal"], + }, + "something used for transportation": { + "positive_labels": ["tractor"], + "negative_labels": ["cat", "clock", "eagle"], + }, + "streetlamp": {"positive_labels": ["streetlamp"], "negative_labels": ["shark", "duck", "swan"]}, +} + +_cached_ping_result = TTLCache(maxsize=10, ttl=60) + + +@cached(_cached_ping_result) +async def is_solver_edge_worker_available() -> bool: + solver_base_url = os.getenv("SOLVER_BASE_URL") + if not solver_base_url: + return False + + try: + client = httpx.AsyncClient(base_url=solver_base_url, timeout=1) + response = await client.get("/ping") + response.raise_for_status() + return True + except (httpx.HTTPStatusError, httpx.ReadTimeout) as err: + logger.warning("Failed to connect SolverEdgeWorker", base_url=solver_base_url, err=err) + return False + + +@dataclass +class SolverEdgeWorker: + solver_base_url: str | None = os.getenv("SOLVER_BASE_URL") + """ + Default to http://localhost:33777 + """ + + def __init__(self): + self.client = None + if self.solver_base_url: + self.client = httpx.AsyncClient(base_url=self.solver_base_url) + + async def invoke_clip_tool(self, payload: dict) -> List[bool]: + _service_point = "/challenge/image_label_binary" + + response = await self.client.post(_service_point, json=payload) + response.raise_for_status() + results = response.json()["results"] + + return results + + @dataclass class MechanicalSkeleton: page: Page + sew: SolverEdgeWorker = field(default_factory=SolverEdgeWorker) + + modelhub: ModelHub | None = None + + clip_model: MossCLIP | None = None + + def __post_init__(self): + self.sew = SolverEdgeWorker() + + if not self.modelhub: + self.modelhub = ModelHub.from_github_repo() + self.modelhub.parse_objects() + async def click_checkbox(self): try: checkbox = self.page.frame_locator("//iframe[contains(@title,'checkbox')]") @@ -65,6 +145,45 @@ def switch_to_challenge_frame(self, window: str = "login"): return frame_challenge + async def challenge_image_label_binary( + self, label: str, challenge_images: List[ChallengeImage] + ): + frame_challenge = self.switch_to_challenge_frame() + + challenge_images = [i.into_base64bytes() for i in challenge_images] + patched_model_prompt = datalake_post.get(label) + self_supervised_payload = { + "prompt": label, + "challenge_images": challenge_images, + **patched_model_prompt, + } + + # {{< IMAGE CLASSIFICATION >}} + if await is_solver_edge_worker_available(): + results: List[bool] = await self.sew.invoke_clip_tool(self_supervised_payload) + else: + payload = SelfSupervisedPayload(**self_supervised_payload) + results: List[bool] = invoke_clip_tool(self.modelhub, payload, self.clip_model) + + # {{< DRIVE THE BROWSER TO TAKE ON THE CHALLENGE >}} + samples = frame_challenge.locator("//div[@class='task-image']") + count = await samples.count() + positive_cases = 0 + for i in range(count): + sample = samples.nth(i) + if results[i]: + positive_cases += 1 + with suppress(TimeoutError): + await sample.click(delay=200) + elif positive_cases == 0 and i == count - 1: + await sample.click(delay=200) + + # {{< Verify >}} + with suppress(TimeoutError): + await self.page.pause() + fl = frame_challenge.locator("//div[@class='button-submit button']") + await fl.click() + @dataclass class OminousLand(ABC): @@ -75,6 +194,8 @@ class OminousLand(ABC): ms: MechanicalSkeleton image_queue: Queue + crumb_count = 1 + typed_dir: Path = field(default_factory=Path) canvas_screenshot_dir: Path = field(default_factory=Path) @@ -91,7 +212,12 @@ class OminousLand(ABC): @classmethod def draws_from( - cls, page: Page, inputs: dict | bytes, tmp_dir: Path, image_queue: Queue, **kwargs + cls, + page: Page, + inputs: dict | bytes, + tmp_dir: Path, + image_queue: Queue, + ms: MechanicalSkeleton | None = None, ): # Cache images if not isinstance(tmp_dir, Path): @@ -100,9 +226,10 @@ def draws_from( typed_dir = tmp_dir / "typed_dir" canvas_screenshot_dir = tmp_dir / "canvas_screenshot" + ms = ms or MechanicalSkeleton(page=page) monster = cls( page=page, - ms=MechanicalSkeleton(page), + ms=ms, tmp_dir=tmp_dir, image_queue=image_queue, typed_dir=typed_dir, @@ -134,6 +261,14 @@ def _init_imgdb(self, label: str, prompt: str): self.canvas_screenshot_dir = self.tmp_dir.joinpath(f"canvas_screenshot/{prompt}") self.canvas_screenshot_dir.mkdir(parents=True, exist_ok=True) + async def _recall_crumb(self): + frame_challenge = self.ms.switch_to_challenge_frame() + crumbs = frame_challenge.locator("//div[@class='Crumb']") + if await crumbs.first.is_visible(): + self.crumb_count = 2 + else: + self.crumb_count = 1 + async def _recall_tasklist(self): """run after _init_imgdb""" frame_challenge = self.ms.switch_to_challenge_frame() @@ -182,20 +317,20 @@ async def _recall_tasklist(self): async def _get_captcha(self, **kwargs): raise NotImplementedError - async def _solve_captcha(self, **kwargs): - frame_challenge = self.ms.switch_to_challenge_frame() - + async def _solve_captcha(self): match self.qr.request_type: case RequestType.ImageLabelBinary: - pass + await self.ms.challenge_image_label_binary( + label=self.label, challenge_images=self.tasklist + ) case RequestType.ImageLabelAreaSelect: # Cache canvas to prepare for subsequent model processing # canvas = frame_challenge.locator("//canvas") # fp = self.canvas_screenshot_dir / f"{challenge_image.filename}.png" # await canvas.screenshot(type="png", path=fp, scale="css") - pass + await self.ms.refresh_challenge() case RequestType.ImageLabelMultipleChoice: - pass + await self.ms.refresh_challenge() case _: logger.warning("[INTERRUPT]", reason="Unknown type of challenge") @@ -215,6 +350,7 @@ async def _collect(self): async def _challenge(self): await self._collect() + await self._recall_crumb() await self._solve_captcha() async def invoke(self, execution: ToolExecution = ToolExecution.CHALLENGE): @@ -258,7 +394,7 @@ async def _get_captcha(self, **kwargs): # request_type if await frame_challenge.locator("//div[@class='task-grid']").count(): self.qr.request_type = RequestType.ImageLabelBinary.value - has_exp = await frame_challenge.locator("//div[@class='challenge-example']").count() + # has_exp = await frame_challenge.locator("//div[@class='challenge-example']").count() elif await frame_challenge.locator("//div[contains(@class, 'bounding-box')]").count(): self.qr.request_type = RequestType.ImageLabelAreaSelect.value else: @@ -270,8 +406,6 @@ async def _get_captcha(self, **kwargs): class AgentV: page: Page - modelhub: ModelHub - ms: MechanicalSkeleton = field(default_factory=MechanicalSkeleton) cr: ChallengeResp = field(default_factory=ChallengeResp) @@ -286,11 +420,6 @@ class AgentV: _tool_type: ToolExecution | None = None def __post_init__(self): - # Control models - self.modelhub = self.modelhub or ModelHub.from_github_repo() - if not self.modelhub.label_alias: - self.modelhub.parse_objects() - self.tmp_dir = self.tmp_dir or Path("tmp_dir") self._cache_dir = self.tmp_dir / ".cache" self._cache_dir.mkdir(parents=True, exist_ok=True) @@ -300,10 +429,9 @@ def __post_init__(self): self.task_queue = Queue(maxsize=1) @classmethod - def into_solver(cls, page: Page, tmp_dir=None, modelhub: ModelHub | None = None, **kwargs): - return cls( - page=page, ms=MechanicalSkeleton(page), tmp_dir=tmp_dir, modelhub=modelhub, **kwargs - ) + def into_solver(cls, page: Page, tmp_dir=None, clip_model: MossCLIP | None = None, **kwargs): + ms = MechanicalSkeleton(page=page, clip_model=clip_model) + return cls(page=page, ms=ms, tmp_dir=tmp_dir, **kwargs) @property def status(self): @@ -359,7 +487,12 @@ async def _task_handler(self, response: Response): async def _tool_execution(self): qr_data = await self.task_queue.get() - driver_conf = {"page": self.page, "tmp_dir": self.tmp_dir, "image_queue": self.image_queue} + driver_conf = { + "page": self.page, + "tmp_dir": self.tmp_dir, + "image_queue": self.image_queue, + "ms": self.ms, + } runnable: OminousLand | None = None match content_type := qr_data.headers.get("content-type"): @@ -384,6 +517,10 @@ async def wait_for_challenge( ) -> Status: self._tool_type = ToolExecution.CHALLENGE + if not self.ms.clip_model: + modelhub = ModelHub.from_github_repo() + self.ms.clip_model = register_pipline(modelhub, fmt="onnx") + # CoroutineTask: Assigning human-computer challenge tasks to the main thread coroutine. # Wait for the task to finish executing try: diff --git a/hcaptcha_challenger/models.py b/hcaptcha_challenger/models.py index a699bf6345..f67a2fd032 100644 --- a/hcaptcha_challenger/models.py +++ b/hcaptcha_challenger/models.py @@ -11,7 +11,7 @@ from typing import List, Dict, Any, Union from uuid import UUID -from pydantic import BaseModel, Field, field_validator, UUID4, AnyHttpUrl +from pydantic import BaseModel, Field, field_validator, UUID4, AnyHttpUrl, Base64Bytes from hcaptcha_challenger.tools.prompt_handler import label_cleaning @@ -202,5 +202,20 @@ def save(self, typed_dir: Path) -> Path: fp.write_bytes(self.body) return fp - def convert_body_to_base64(self) -> str: - return base64.b64encode(self.body).decode("utf8") + def into_base64bytes(self) -> str: + return base64.b64encode(self.body).decode() + + +class SelfSupervisedPayload(BaseModel): + """hCaptcha payload of the image_label_binary challenge""" + + prompt: str = Field(..., description="challenge prompt") + challenge_images: List[Base64Bytes] = Field(default_factory=list) + positive_labels: List[str] | None = Field(default_factory=list) + negative_labels: List[str] | None = Field(default_factory=list) + + +class SelfSupervisedResponse(BaseModel): + """The binary classification result of the image, in the same order as the challenge_images.""" + + results: List[bool] = Field(default_factory=list) diff --git a/hcaptcha_challenger/tools/zero_shot_image_classifier.py b/hcaptcha_challenger/tools/zero_shot_image_classifier.py index 75f4332973..46559bc1db 100644 --- a/hcaptcha_challenger/tools/zero_shot_image_classifier.py +++ b/hcaptcha_challenger/tools/zero_shot_image_classifier.py @@ -7,16 +7,18 @@ from dataclasses import dataclass from dataclasses import field +from io import BytesIO from pathlib import Path from typing import List, Literal, Iterable, Tuple import onnxruntime -from PIL.Image import Image +from PIL import Image from hcaptcha_challenger.onnx.clip import MossCLIP from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake from hcaptcha_challenger.onnx.utils import is_cuda_pipline_available from hcaptcha_challenger.tools.prompt_handler import handle +from hcaptcha_challenger.models import SelfSupervisedPayload def register_pipline( @@ -39,6 +41,10 @@ def register_pipline( """ if fmt in ["transformers", None]: fmt = "transformers" if is_cuda_pipline_available else "onnx" + try: + import huggingface_hub # type:ignore + except ImportError: + fmt = "onnx" if fmt in ["onnx"]: v_net, t_net = None, None @@ -138,3 +144,31 @@ def __call__(self, detector: MossCLIP, image: Image, *args, **kwargs): image = [image] predictions = detector(image, candidate_labels=self.candidate_labels) return predictions + + +def invoke_clip_tool( + modelhub: ModelHub, payload: SelfSupervisedPayload, clip_model: MossCLIP | None = None +) -> List[bool]: + label = handle(payload.prompt) + + if any(payload.positive_labels) and any(payload.negative_labels): + serialized = { + "positive_labels": payload.positive_labels, + "negative_labels": payload.negative_labels, + } + modelhub.datalake[label] = DataLake.from_serialized(serialized) + + if not (dl := modelhub.datalake.get(label)): + dl = DataLake.from_challenge_prompt(label) + tool = ZeroShotImageClassifier.from_datalake(dl) + + # Default to `RESNET.OPENAI` perf_counter 1.794s + model = clip_model or register_pipline(modelhub) + + response: List[bool] = [] + for image_data in payload.challenge_images: + results = tool(model, image=Image.open(BytesIO(image_data))) + trusted = results[0]["label"] in tool.positive_labels + response.append(trusted) + + return response diff --git a/pyproject.toml b/pyproject.toml index 5175370e57..fe0283d5e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,8 @@ playwright = { version = "*", optional = true } PyGithub = { version = "^1.59.1", optional = true } istockphoto = { version = "0.1.2", optional = true } fastapi = { version = "*", extras=["all"], optional = true} +cachetools = "^5.3.3" +asyncache = "^0.3.1" [tool.poetry.group.test.dependencies] # https://docs.pytest.org/en/stable/reference/plugin_list.html#plugin-list From 29447f5d086420260d757c8f22b0330c55b4bd1f Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:30:59 +0800 Subject: [PATCH 08/14] update: wait_for_challenge --- .../agents/playwright/dragon.py | 43 ++++++++++++++++--- hcaptcha_challenger/tools/prompt_handler.py | 3 ++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py index b73f18fc16..21026072f0 100644 --- a/hcaptcha_challenger/agents/playwright/dragon.py +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -61,6 +61,18 @@ "negative_labels": ["cat", "clock", "eagle"], }, "streetlamp": {"positive_labels": ["streetlamp"], "negative_labels": ["shark", "duck", "swan"]}, + "similar to the following silhouette": { + "positive_labels": ["duck"], + "negative_labels": ["cat", "dog", "frog"], + }, + "please click on objects or entities related to work": { + "positive_labels": ["glass"], + "negative_labels": ["excavator", "tree", "nature"], + }, + "similar to the following pattern": { + "positive_labels": ["raccoon"], + "negative_labels": ["duck", "apple"], + }, } _cached_ping_result = TTLCache(maxsize=10, ttl=60) @@ -180,7 +192,6 @@ async def challenge_image_label_binary( # {{< Verify >}} with suppress(TimeoutError): - await self.page.pause() fl = frame_challenge.locator("//div[@class='button-submit button']") await fl.click() @@ -338,7 +349,7 @@ async def _collect(self): await self._get_captcha() logger.debug( - "task", + "Invoke task", label=self.label, type=self.qr.request_type, requester_question=self.qr.requester_question, @@ -351,7 +362,11 @@ async def _collect(self): async def _challenge(self): await self._collect() await self._recall_crumb() - await self._solve_captcha() + + for i in range(self.crumb_count): + if i != 0: + await self._recall_tasklist() + await self._solve_captcha() async def invoke(self, execution: ToolExecution = ToolExecution.CHALLENGE): match execution: @@ -513,10 +528,14 @@ async def _tool_execution(self): await runnable.invoke(execution=self._tool_type) async def wait_for_challenge( - self, execution_timeout: float = 150.0, response_timeout: float = 30.0 + self, + execution_timeout: float = 90, + response_timeout: float = 30.0, + retry_on_failure: bool = True, ) -> Status: self._tool_type = ToolExecution.CHALLENGE + # Initialize CLIP model if not self.ms.clip_model: modelhub = ModelHub.from_github_repo() self.ms.clip_model = register_pipline(modelhub, fmt="onnx") @@ -528,6 +547,14 @@ async def wait_for_challenge( except asyncio.TimeoutError: logger.error("Challenge execution timed out", timeout=execution_timeout) return self.status.CHALLENGE_EXECUTION_TIMEOUT + logger.debug("Invoke done", _tool_type=self._tool_type) + + # CoroutineTask: Assigned a new task + # The possible reason is that the challenge was **manually** refreshed during the task. + while self.cr_queue.empty(): + if not self.task_queue.empty(): + return await self.wait_for_challenge(execution_timeout, response_timeout) + await asyncio.sleep(0.01) # CoroutineTask: Waiting for hCAPTCHA response processing result # After the completion of the human-machine challenge workflow, @@ -539,12 +566,16 @@ async def wait_for_challenge( logger.error("Timeout waiting for challenge response", timeout=response_timeout) return self.status.CHALLENGE_RESPONSE_TIMEOUT else: - logger.debug("[DONE]", **self.cr.model_dump(by_alias=True)) - # Match: Timeout / Loss if not self.cr or not self.cr.is_pass: + if retry_on_failure: + logger.error("Invoke verification", **self.cr.model_dump(by_alias=True)) + return await self.wait_for_challenge( + execution_timeout, response_timeout, retry_on_failure=retry_on_failure + ) return self.status.CHALLENGE_RETRY if self.cr.is_pass: + logger.success("Invoke verification", **self.cr.model_dump(by_alias=True)) return self.status.CHALLENGE_SUCCESS async def wait_for_collect( diff --git a/hcaptcha_challenger/tools/prompt_handler.py b/hcaptcha_challenger/tools/prompt_handler.py index 46e9c032f7..a53f41cead 100644 --- a/hcaptcha_challenger/tools/prompt_handler.py +++ b/hcaptcha_challenger/tools/prompt_handler.py @@ -57,6 +57,9 @@ def split_prompt_message(prompt_message: str, lang: str) -> str: if prompt_message.startswith("please click on the"): prompt_message = prompt_message.replace("please click on ", "").strip() return prompt_message + if prompt_message.startswith("please click on all entities similar"): + prompt_message = prompt_message.replace("please click on all entities ", "").strip() + return prompt_message if prompt_message.startswith("select all") and "images" not in prompt_message: return prompt_message.split("select all")[-1].strip() if "select all images of" in prompt_message: From 401695d0f0efd5db40cd670d4a0f2267f9036cf1 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Tue, 16 Apr 2024 00:05:32 +0800 Subject: [PATCH 09/14] fix(solve_captcha): runtime error --- hcaptcha_challenger/agents/playwright/dragon.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py index 21026072f0..b4354c7556 100644 --- a/hcaptcha_challenger/agents/playwright/dragon.py +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -47,6 +47,7 @@ HOOK_CHALLENGE = "//iframe[contains(@title, 'hCaptcha challenge')]" +# todo move to datalake.json datalake_post = { "animals possessing wings": { "positive_labels": ["bird"], @@ -331,9 +332,13 @@ async def _get_captcha(self, **kwargs): async def _solve_captcha(self): match self.qr.request_type: case RequestType.ImageLabelBinary: - await self.ms.challenge_image_label_binary( - label=self.label, challenge_images=self.tasklist - ) + try: + await self.ms.challenge_image_label_binary( + label=self.label, challenge_images=self.tasklist + ) + except Exception as err: + logger.error(f"An error occurred while processing the challenge task", err=err) + await self.ms.refresh_challenge() case RequestType.ImageLabelAreaSelect: # Cache canvas to prepare for subsequent model processing # canvas = frame_challenge.locator("//canvas") @@ -569,7 +574,7 @@ async def wait_for_challenge( # Match: Timeout / Loss if not self.cr or not self.cr.is_pass: if retry_on_failure: - logger.error("Invoke verification", **self.cr.model_dump(by_alias=True)) + logger.error("Invoke verification", is_pass=self.cr.is_pass) return await self.wait_for_challenge( execution_timeout, response_timeout, retry_on_failure=retry_on_failure ) From 34a6d47d9cbe999931d15d214eac7e30b4f782fe Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Thu, 18 Apr 2024 00:37:40 +0800 Subject: [PATCH 10/14] f1 --- examples/faker_client.py | 4 ++-- hcaptcha_challenger/agents/playwright/dragon.py | 6 +++--- hcaptcha_challenger/tools/prompt_handler.py | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/faker_client.py b/examples/faker_client.py index 2d282a7b1d..ee4800f0c3 100644 --- a/examples/faker_client.py +++ b/examples/faker_client.py @@ -46,7 +46,7 @@ async def mime(context: BrowserContext): if __name__ == "__main__": - # EXECUTION = "collect" - EXECUTION = "challenge" + EXECUTION = "collect" + # EXECUTION = "challenge" encrypted_resp = asyncio.run(main(headless=False)) diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py index b4354c7556..9632dd46ee 100644 --- a/hcaptcha_challenger/agents/playwright/dragon.py +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -66,9 +66,9 @@ "positive_labels": ["duck"], "negative_labels": ["cat", "dog", "frog"], }, - "please click on objects or entities related to work": { - "positive_labels": ["glass"], - "negative_labels": ["excavator", "tree", "nature"], + "related to work": { + "positive_labels": ["excavator"], + "negative_labels": ["glass", "tree", "nature"], }, "similar to the following pattern": { "positive_labels": ["raccoon"], diff --git a/hcaptcha_challenger/tools/prompt_handler.py b/hcaptcha_challenger/tools/prompt_handler.py index a53f41cead..3b63a98e78 100644 --- a/hcaptcha_challenger/tools/prompt_handler.py +++ b/hcaptcha_challenger/tools/prompt_handler.py @@ -60,6 +60,9 @@ def split_prompt_message(prompt_message: str, lang: str) -> str: if prompt_message.startswith("please click on all entities similar"): prompt_message = prompt_message.replace("please click on all entities ", "").strip() return prompt_message + if prompt_message.startswith("please click on objects or entities"): + prompt_message = prompt_message.replace("please click on objects or entities", "") + return prompt_message.strip() if prompt_message.startswith("select all") and "images" not in prompt_message: return prompt_message.split("select all")[-1].strip() if "select all images of" in prompt_message: From 59ec3f4d1c01a13c916ce5d51663fb817234548b Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Fri, 19 Apr 2024 02:44:19 +0800 Subject: [PATCH 11/14] f2 --- {backend => api}/main.py | 0 {backend => api}/readme.md | 0 {backend => api}/routers/__init__.py | 0 {backend => api}/routers/challenge.py | 0 {backend => api}/routers/datalake.py | 0 docker/Dockerfile | 0 docker/docker-compose.yaml | 0 examples/clip_datalake.json | 0 examples/faker_client.py | 2 + hcaptcha_challenger/{ => agents}/pipline.py | 14 +- .../agents/playwright/control.py | 14 +- .../agents/playwright/dragon.py | 7 +- hcaptcha_challenger/constant.py | 35 +++++ hcaptcha_challenger/models.py | 10 +- hcaptcha_challenger/tools/__init__.py | 26 ++++ hcaptcha_challenger/tools/common.py | 142 ------------------ hcaptcha_challenger/tools/image_downloader.py | 62 ++++++++ .../tools/image_label_binary.py | 46 +++++- hcaptcha_challenger/tools/match_model.py | 34 +++++ hcaptcha_challenger/tools/prompt_handler.py | 32 +--- src/clip_datalake.json | 0 tests/test_prompt_handler.py | 2 +- 22 files changed, 225 insertions(+), 201 deletions(-) rename {backend => api}/main.py (100%) rename {backend => api}/readme.md (100%) rename {backend => api}/routers/__init__.py (100%) rename {backend => api}/routers/challenge.py (100%) rename {backend => api}/routers/datalake.py (100%) create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yaml create mode 100644 examples/clip_datalake.json rename hcaptcha_challenger/{ => agents}/pipline.py (98%) create mode 100644 hcaptcha_challenger/constant.py delete mode 100644 hcaptcha_challenger/tools/common.py create mode 100644 hcaptcha_challenger/tools/match_model.py create mode 100644 src/clip_datalake.json diff --git a/backend/main.py b/api/main.py similarity index 100% rename from backend/main.py rename to api/main.py diff --git a/backend/readme.md b/api/readme.md similarity index 100% rename from backend/readme.md rename to api/readme.md diff --git a/backend/routers/__init__.py b/api/routers/__init__.py similarity index 100% rename from backend/routers/__init__.py rename to api/routers/__init__.py diff --git a/backend/routers/challenge.py b/api/routers/challenge.py similarity index 100% rename from backend/routers/challenge.py rename to api/routers/challenge.py diff --git a/backend/routers/datalake.py b/api/routers/datalake.py similarity index 100% rename from backend/routers/datalake.py rename to api/routers/datalake.py diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/clip_datalake.json b/examples/clip_datalake.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/faker_client.py b/examples/faker_client.py index ee4800f0c3..8126841da2 100644 --- a/examples/faker_client.py +++ b/examples/faker_client.py @@ -8,12 +8,14 @@ import asyncio from pathlib import Path +import dotenv from playwright.async_api import async_playwright, BrowserContext from hcaptcha_challenger.agents import AgentV from hcaptcha_challenger.agents import Malenia from hcaptcha_challenger.utils import SiteKey +dotenv.load_dotenv() # 1. You need to deploy sub-thread tasks and actively run `install(upgrade=True)` every 20 minutes # 2. You need to make sure to run `install(upgrade=True, clip=True)` before each instantiation diff --git a/hcaptcha_challenger/pipline.py b/hcaptcha_challenger/agents/pipline.py similarity index 98% rename from hcaptcha_challenger/pipline.py rename to hcaptcha_challenger/agents/pipline.py index a7117e2cdc..c05e810193 100644 --- a/hcaptcha_challenger/pipline.py +++ b/hcaptcha_challenger/agents/pipline.py @@ -18,19 +18,15 @@ from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake from hcaptcha_challenger.onnx.resnet import ResNetControl from hcaptcha_challenger.onnx.yolo import YOLOv8, is_matched_ash_of_war, YOLOv8Seg -from hcaptcha_challenger.tools.common import ( +from hcaptcha_challenger.tools import ( + match_datalake, match_model, - download_challenge_images, rank_models, - match_datalake, -) -from hcaptcha_challenger.tools.cv_toolkit import ( + download_challenge_images, + find_unique_color, annotate_objects, find_unique_object, - find_unique_color, -) -from hcaptcha_challenger.tools.prompt_handler import handle -from hcaptcha_challenger.tools.zero_shot_image_classifier import ( + handle, ZeroShotImageClassifier, register_pipline, ) diff --git a/hcaptcha_challenger/agents/playwright/control.py b/hcaptcha_challenger/agents/playwright/control.py index b642d96984..a12937aa0c 100644 --- a/hcaptcha_challenger/agents/playwright/control.py +++ b/hcaptcha_challenger/agents/playwright/control.py @@ -29,19 +29,15 @@ is_matched_ash_of_war, finetune_keypoint, ) -from hcaptcha_challenger.tools.common import ( - match_model, +from hcaptcha_challenger.tools import ( match_datalake, + match_model, rank_models, download_challenge_images, -) -from hcaptcha_challenger.tools.cv_toolkit import ( - find_unique_object, - annotate_objects, find_unique_color, -) -from hcaptcha_challenger.tools.prompt_handler import handle -from hcaptcha_challenger.tools.zero_shot_image_classifier import ( + annotate_objects, + find_unique_object, + handle, ZeroShotImageClassifier, register_pipline, ) diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py index 9632dd46ee..e3d468d3b8 100644 --- a/hcaptcha_challenger/agents/playwright/dragon.py +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -19,6 +19,8 @@ import dotenv import httpx +from asyncache import cached +from cachetools import TTLCache from loguru import logger from playwright.async_api import Page, Response, TimeoutError, expect @@ -35,10 +37,7 @@ ) from hcaptcha_challenger.onnx.clip import MossCLIP from hcaptcha_challenger.onnx.modelhub import ModelHub -from hcaptcha_challenger.tools.prompt_handler import handle -from hcaptcha_challenger.tools.zero_shot_image_classifier import invoke_clip_tool, register_pipline -from cachetools import TTLCache -from asyncache import cached +from hcaptcha_challenger.tools import handle, register_pipline, invoke_clip_tool dotenv.load_dotenv() diff --git a/hcaptcha_challenger/constant.py b/hcaptcha_challenger/constant.py new file mode 100644 index 0000000000..0baa4bd85b --- /dev/null +++ b/hcaptcha_challenger/constant.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Time : 2024/4/19 2:29 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +BAD_CODE = { + "а": "a", + "е": "e", + "e": "e", + "i": "i", + "і": "i", + "ο": "o", + "с": "c", + "ԁ": "d", + "ѕ": "s", + "һ": "h", + "у": "y", + "р": "p", + "ϳ": "j", + "х": "x", + "\u0405": "S", + "\u0042": "B", + "\u0052": "R", + "\u0049": "I", + "\u0043": "C", + "\u004b": "K", + "\u039a": "K", + "\u0053": "S", + "\u0421": "C", + "\u006c": "l", + "\u0399": "I", + "\u0392": "B", + "ー": "一", + "土": "士", +} diff --git a/hcaptcha_challenger/models.py b/hcaptcha_challenger/models.py index f67a2fd032..bb2ef7df1b 100644 --- a/hcaptcha_challenger/models.py +++ b/hcaptcha_challenger/models.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, Field, field_validator, UUID4, AnyHttpUrl, Base64Bytes -from hcaptcha_challenger.tools.prompt_handler import label_cleaning +from hcaptcha_challenger.constant import BAD_CODE class Status(str, Enum): @@ -131,12 +131,16 @@ def check_requester_question_example(cls, v: str | List[str]): def cache(self, tmp_dir: Path): shape_type = self.request_config.get("shape_type", "") - requester_question = label_cleaning(self.requester_question.get("en", "")) + # label cleaning + requester_question = self.requester_question.get("en", "") + for c in BAD_CODE: + requester_question = requester_question.replace(c, BAD_CODE[c]) + answer_keys = list(self.requester_restricted_answer_set.keys()) ak = f".{answer_keys[0]}" if len(answer_keys) > 0 else "" fn = f"{self.request_type}.{shape_type}.{requester_question}{ak}.json" - inv = {"\\", "/", ":", "*", "?", "<", ">", "|"} + inv = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} for c in inv: fn = fn.replace(c, "") diff --git a/hcaptcha_challenger/tools/__init__.py b/hcaptcha_challenger/tools/__init__.py index de089d3940..6df50f747d 100644 --- a/hcaptcha_challenger/tools/__init__.py +++ b/hcaptcha_challenger/tools/__init__.py @@ -3,3 +3,29 @@ # Author : QIN2DIM # GitHub : https://github.com/QIN2DIM # Description: +from .cv_toolkit import ( + annotate_objects, + find_unique_object, + find_similar_objects, + find_unique_color, +) +from .image_downloader import download_challenge_images +from .image_label_binary import rank_models, match_datalake +from .match_model import match_model +from .prompt_handler import handle +from .zero_shot_image_classifier import ZeroShotImageClassifier, register_pipline, invoke_clip_tool + +__all__ = [ + "download_challenge_images", + "rank_models", + "match_datalake", + "annotate_objects", + "find_unique_object", + "find_similar_objects", + "handle", + "ZeroShotImageClassifier", + "register_pipline", + "match_model", + "find_unique_color", + "invoke_clip_tool", +] diff --git a/hcaptcha_challenger/tools/common.py b/hcaptcha_challenger/tools/common.py deleted file mode 100644 index ce6adafeee..0000000000 --- a/hcaptcha_challenger/tools/common.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- -# Time : 2023/11/18 22:38 -# Author : QIN2DIM -# GitHub : https://github.com/QIN2DIM -# Description: -from __future__ import annotations - -import asyncio -import hashlib -import shutil -import time -from contextlib import suppress -from pathlib import Path -from typing import Literal, List, Tuple - -from hcaptcha_challenger.models import QuestionResp -from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake -from hcaptcha_challenger.onnx.resnet import ResNetControl -from hcaptcha_challenger.onnx.yolo import YOLOv8 -from hcaptcha_challenger.tools.image_downloader import Cirilla - - -def rank_models( - nested_models: List[str], example_paths: List[Path], modelhub: ModelHub -) -> Tuple[ResNetControl, str] | None: - # {{< Rank ResNet Models >}} - rank_ladder = [] - - for example_path in example_paths: - img_stream = example_path.read_bytes() - for model_name in reversed(nested_models): - if (net := modelhub.match_net(focus_name=model_name)) is None: - return - control = ResNetControl.from_pluggable_model(net) - result_, proba = control.execute(img_stream, proba=True) - if result_ and proba[0] > 0.68: - rank_ladder.append([control, model_name, proba]) - if proba[0] > 0.87: - break - - # {{< Catch-all Rules >}} - if rank_ladder: - alts = sorted(rank_ladder, key=lambda x: x[-1][0], reverse=True) - best_model, model_name = alts[0][0], alts[0][1] - return best_model, model_name - - -def match_datalake(modelhub: ModelHub, label: str) -> DataLake: - # prelude datalake - if dl := modelhub.datalake.get(label): - return dl - - # prelude clip_candidates - for ket in reversed(modelhub.clip_candidates.keys()): - if ket in label: - candidates = modelhub.clip_candidates[ket] - if candidates and len(candidates) > 2: - dl = DataLake.from_binary_labels(candidates[:1], candidates[1:]) - return dl - - # catch-all - dl = DataLake.from_challenge_prompt(raw_prompt=label) - return dl - - -def match_model( - label: str, ash: str, modelhub: ModelHub, select: Literal["yolo", "resnet"] = None -) -> ResNetControl | YOLOv8: - """match solution after `tactical_retreat`""" - focus_label = modelhub.label_alias.get(label, "") - - # Match YOLOv8 model - if not focus_label or select == "yolo": - focus_name, classes = modelhub.apply_ash_of_war(ash=ash) - session = modelhub.match_net(focus_name=focus_name) - detector = YOLOv8.from_pluggable_model(session, classes) - return detector - - # Match ResNet model - focus_name = focus_label - if not focus_name.endswith(".onnx"): - focus_name = f"{focus_name}.onnx" - net = modelhub.match_net(focus_name=focus_name) - control = ResNetControl.from_pluggable_model(net) - return control - - -async def download_challenge_images( - qr: QuestionResp, label: str, tmp_dir: Path, ignore_examples: bool = False -): - request_type = qr.request_type - ks = list(qr.requester_restricted_answer_set.keys()) - - inv = {"\\", "/", ":", "*", "?", "<", ">", "|"} - for c in inv: - label = label.replace(c, "") - label = label.strip() - - if len(ks) > 0: - typed_dir = tmp_dir.joinpath(request_type, label, ks[0]) - else: - typed_dir = tmp_dir.joinpath(request_type, label) - typed_dir.mkdir(parents=True, exist_ok=True) - - ciri = Cirilla() - container = [] - tasks = [] - for i, tk in enumerate(qr.tasklist): - challenge_img_path = typed_dir.joinpath(f"{time.time()}.{i}.png") - context = (challenge_img_path, tk.datapoint_uri) - container.append(context) - tasks.append(asyncio.create_task(ciri.elder_blood(context))) - - examples = [] - if not ignore_examples: - with suppress(Exception): - for i, uri in enumerate(qr.requester_question_example): - example_img_path = typed_dir.joinpath(f"{time.time()}.exp.{i}.png") - context = (example_img_path, uri) - examples.append(context) - tasks.append(asyncio.create_task(ciri.elder_blood(context))) - - await asyncio.gather(*tasks) - - # Optional deduplication - _img_paths = [] - for src, _ in container: - cache = src.read_bytes() - dst = typed_dir.joinpath(f"{hashlib.md5(cache).hexdigest()}.png") - shutil.move(src, dst) - _img_paths.append(dst) - - # Optional deduplication - _example_paths = [] - if examples: - for src, _ in examples: - cache = src.read_bytes() - dst = typed_dir.joinpath(f"{hashlib.md5(cache).hexdigest()}.png") - shutil.move(src, dst) - _example_paths.append(dst) - - return _img_paths, _example_paths diff --git a/hcaptcha_challenger/tools/image_downloader.py b/hcaptcha_challenger/tools/image_downloader.py index 082221a4b2..0eaac3aa73 100644 --- a/hcaptcha_challenger/tools/image_downloader.py +++ b/hcaptcha_challenger/tools/image_downloader.py @@ -6,14 +6,19 @@ from __future__ import annotations import asyncio +import hashlib +import shutil import sys +import time from abc import ABC, abstractmethod +from contextlib import suppress from pathlib import Path from typing import Any, Tuple, List import httpx from httpx import AsyncClient from tenacity import * +from hcaptcha_challenger.models import QuestionResp DownloadList = List[Tuple[Path, str]] @@ -99,3 +104,60 @@ def common_download(container: DownloadList): for img_path, url in container: resp = httpx.get(url) img_path.write_bytes(resp.content) + + +async def download_challenge_images( + qr: QuestionResp, label: str, tmp_dir: Path, ignore_examples: bool = False +): + request_type = qr.request_type + ks = list(qr.requester_restricted_answer_set.keys()) + + inv = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} + for c in inv: + label = label.replace(c, "") + label = label.strip() + + if len(ks) > 0: + typed_dir = tmp_dir.joinpath(request_type, label, ks[0]) + else: + typed_dir = tmp_dir.joinpath(request_type, label) + typed_dir.mkdir(parents=True, exist_ok=True) + + ciri = Cirilla() + container = [] + tasks = [] + for i, tk in enumerate(qr.tasklist): + challenge_img_path = typed_dir.joinpath(f"{time.time()}.{i}.png") + context = (challenge_img_path, tk.datapoint_uri) + container.append(context) + tasks.append(asyncio.create_task(ciri.elder_blood(context))) + + examples = [] + if not ignore_examples: + with suppress(Exception): + for i, uri in enumerate(qr.requester_question_example): + example_img_path = typed_dir.joinpath(f"{time.time()}.exp.{i}.png") + context = (example_img_path, uri) + examples.append(context) + tasks.append(asyncio.create_task(ciri.elder_blood(context))) + + await asyncio.gather(*tasks) + + # Optional deduplication + _img_paths = [] + for src, _ in container: + cache = src.read_bytes() + dst = typed_dir.joinpath(f"{hashlib.md5(cache).hexdigest()}.png") + shutil.move(src, dst) + _img_paths.append(dst) + + # Optional deduplication + _example_paths = [] + if examples: + for src, _ in examples: + cache = src.read_bytes() + dst = typed_dir.joinpath(f"{hashlib.md5(cache).hexdigest()}.png") + shutil.move(src, dst) + _example_paths.append(dst) + + return _img_paths, _example_paths diff --git a/hcaptcha_challenger/tools/image_label_binary.py b/hcaptcha_challenger/tools/image_label_binary.py index c4a07bbe26..6eec557a5a 100644 --- a/hcaptcha_challenger/tools/image_label_binary.py +++ b/hcaptcha_challenger/tools/image_label_binary.py @@ -7,7 +7,7 @@ from contextlib import suppress from pathlib import Path -from typing import List, Dict +from typing import List, Dict, Tuple import cv2 from PIL import Image @@ -15,7 +15,6 @@ from hcaptcha_challenger.onnx.modelhub import ModelHub, DataLake from hcaptcha_challenger.onnx.resnet import ResNetControl -from hcaptcha_challenger.tools.common import rank_models from hcaptcha_challenger.tools.prompt_handler import handle from hcaptcha_challenger.tools.zero_shot_image_classifier import ( ZeroShotImageClassifier, @@ -154,3 +153,46 @@ def __init__(self, model_path: Path): def parse_once(self, image: bytes) -> bool | None: with suppress(Exception): return self.model.execute(image) + + +def rank_models( + nested_models: List[str], example_paths: List[Path], modelhub: ModelHub +) -> Tuple[ResNetControl, str] | None: + # {{< Rank ResNet Models >}} + rank_ladder = [] + + for example_path in example_paths: + img_stream = example_path.read_bytes() + for model_name in reversed(nested_models): + if (net := modelhub.match_net(focus_name=model_name)) is None: + return + control = ResNetControl.from_pluggable_model(net) + result_, proba = control.execute(img_stream, proba=True) + if result_ and proba[0] > 0.68: + rank_ladder.append([control, model_name, proba]) + if proba[0] > 0.87: + break + + # {{< Catch-all Rules >}} + if rank_ladder: + alts = sorted(rank_ladder, key=lambda x: x[-1][0], reverse=True) + best_model, model_name = alts[0][0], alts[0][1] + return best_model, model_name + + +def match_datalake(modelhub: ModelHub, label: str) -> DataLake: + # prelude datalake + if dl := modelhub.datalake.get(label): + return dl + + # prelude clip_candidates + for ket in reversed(modelhub.clip_candidates.keys()): + if ket in label: + candidates = modelhub.clip_candidates[ket] + if candidates and len(candidates) > 2: + dl = DataLake.from_binary_labels(candidates[:1], candidates[1:]) + return dl + + # catch-all + dl = DataLake.from_challenge_prompt(raw_prompt=label) + return dl diff --git a/hcaptcha_challenger/tools/match_model.py b/hcaptcha_challenger/tools/match_model.py new file mode 100644 index 0000000000..37c4bc6b8b --- /dev/null +++ b/hcaptcha_challenger/tools/match_model.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Time : 2023/11/18 22:38 +# Author : QIN2DIM +# GitHub : https://github.com/QIN2DIM +# Description: +from __future__ import annotations + +from typing import Literal + +from hcaptcha_challenger.onnx.modelhub import ModelHub +from hcaptcha_challenger.onnx.resnet import ResNetControl +from hcaptcha_challenger.onnx.yolo import YOLOv8 + + +def match_model( + label: str, ash: str, modelhub: ModelHub, select: Literal["yolo", "resnet"] = None +) -> ResNetControl | YOLOv8: + """match solution after `tactical_retreat`""" + focus_label = modelhub.label_alias.get(label, "") + + # Match YOLOv8 model + if not focus_label or select == "yolo": + focus_name, classes = modelhub.apply_ash_of_war(ash=ash) + session = modelhub.match_net(focus_name=focus_name) + detector = YOLOv8.from_pluggable_model(session, classes) + return detector + + # Match ResNet model + focus_name = focus_label + if not focus_name.endswith(".onnx"): + focus_name = f"{focus_name}.onnx" + net = modelhub.match_net(focus_name=focus_name) + control = ResNetControl.from_pluggable_model(net) + return control diff --git a/hcaptcha_challenger/tools/prompt_handler.py b/hcaptcha_challenger/tools/prompt_handler.py index 3b63a98e78..3e232a6f9f 100644 --- a/hcaptcha_challenger/tools/prompt_handler.py +++ b/hcaptcha_challenger/tools/prompt_handler.py @@ -4,37 +4,7 @@ # GitHub : https://github.com/QIN2DIM # Description: import re - -BAD_CODE = { - "а": "a", - "е": "e", - "e": "e", - "i": "i", - "і": "i", - "ο": "o", - "с": "c", - "ԁ": "d", - "ѕ": "s", - "һ": "h", - "у": "y", - "р": "p", - "ϳ": "j", - "х": "x", - "\u0405": "S", - "\u0042": "B", - "\u0052": "R", - "\u0049": "I", - "\u0043": "C", - "\u004b": "K", - "\u039a": "K", - "\u0053": "S", - "\u0421": "C", - "\u006c": "l", - "\u0399": "I", - "\u0392": "B", - "ー": "一", - "土": "士", -} +from hcaptcha_challenger.constant import BAD_CODE def split_prompt_message(prompt_message: str, lang: str) -> str: diff --git a/src/clip_datalake.json b/src/clip_datalake.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_prompt_handler.py b/tests/test_prompt_handler.py index abc2060050..4d5d631989 100644 --- a/tests/test_prompt_handler.py +++ b/tests/test_prompt_handler.py @@ -11,7 +11,7 @@ import pytest from hcaptcha_challenger import split_prompt_message, label_cleaning, handle -from hcaptcha_challenger.tools.prompt_handler import BAD_CODE +from hcaptcha_challenger.constant import BAD_CODE pattern = re.compile(r"[^\x00-\x7F]") From 46aa578901456218cf9e1b27700e2d886851dcea Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Fri, 19 Apr 2024 06:09:46 +0800 Subject: [PATCH 12/14] f3 --- hcaptcha_challenger/onnx/modelhub.py | 94 +++++++++++++++++--------- src/clip_datalake.json | 0 src/{objects.yaml => objects2023.yaml} | 0 src/objects2024.yaml | 31 +++++++++ 4 files changed, 92 insertions(+), 33 deletions(-) delete mode 100644 src/clip_datalake.json rename src/{objects.yaml => objects2023.yaml} (100%) create mode 100644 src/objects2024.yaml diff --git a/hcaptcha_challenger/onnx/modelhub.py b/hcaptcha_challenger/onnx/modelhub.py index ac46954692..69a1e2e875 100644 --- a/hcaptcha_challenger/onnx/modelhub.py +++ b/hcaptcha_challenger/onnx/modelhub.py @@ -6,6 +6,7 @@ from __future__ import annotations import gc +import inspect import json import os import shutil @@ -14,7 +15,7 @@ from datetime import datetime, timedelta from json import JSONDecodeError from pathlib import Path -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Any, Literal from urllib.parse import urlparse import cv2 @@ -27,11 +28,18 @@ from tenacity import * from tqdm import tqdm -from hcaptcha_challenger.utils import from_dict_to_model - DEFAULT_KEYPOINT_MODEL = "COCO2020_yolov8m.onnx" +def from_dict_to_model(cls, data: Dict[str, Any]): + return cls( + **{ + key: (data[key] if val.default == val.empty else data.get(key, val.default)) + for key, val in inspect.signature(cls).parameters.items() + } + ) + + @logger.catch @retry( retry=retry_if_exception_type((httpx.ConnectTimeout, httpx.ConnectError)), @@ -252,6 +260,7 @@ class ModelHub: lang: str = "en" label_alias: Dict[str, str] = field(default_factory=dict) + model_slots: Dict[str, ModelSlot] = field(default_factory=dict) """ Image classification --- @@ -262,6 +271,7 @@ class ModelHub: yolo_names: List[str] = field(default_factory=list) ashes_of_war: Dict[str, List[str]] = field(default_factory=dict) + yolo_modelscope: Dict[str, YOLOModelscope] = field(default_factory=dict) """ Object Detection --- @@ -277,7 +287,7 @@ class ModelHub: "find the {z} pictures most similar to {y} in the {x_i} pictures" """ - circle_segment_model: str = field(default=str) + circle_segment_model: str = "appears_only_once_2309_yolov8s-seg.onnx" """ Image Segmentation --- @@ -327,9 +337,22 @@ def __post_init__(self): self.assets_dir.mkdir(mode=0o777, parents=True, exist_ok=True) @classmethod - def from_github_repo(cls, username: str = "QIN2DIM", lang: str = "en", **kwargs): - release_url = f"https://api.github.com/repos/{username}/hcaptcha-challenger/releases" - objects_url = f"https://raw.githubusercontent.com/{username}/hcaptcha-challenger/main/src/objects.yaml" + def from_github_repo( + cls, + username: str = "QIN2DIM", + lang: str = "en", + repo: str = "hcaptcha-challenger", + conf_: str = "objects2024.yaml", + **kwargs, + ): + release_url = ( + kwargs.get("release_url", os.getenv("RELEASE_URL")) + or f"https://api.github.com/repos/{username}/{repo}/releases" + ) + objects_url = ( + kwargs.get("objects_url", os.getenv("OBJECTS_URL")) + or f"https://raw.githubusercontent.com/{username}/{repo}/main/src/{conf_}" + ) instance = cls(release_url=release_url, objects_url=objects_url, lang=lang) instance.assets = Assets.from_release_url(release_url) @@ -356,34 +379,20 @@ def parse_objects(self): os.remove(self.objects_path) return - label_to_i18n_mapping: dict = data.get("label_alias", {}) - if label_to_i18n_mapping: - for model_name, lang_to_prompts in label_to_i18n_mapping.items(): - for lang, prompts in lang_to_prompts.items(): - if lang != self.lang: - continue - self.label_alias.update({prompt.strip(): model_name for prompt in prompts}) - - yolo2names: Dict[str, List[str]] = data.get("ashes_of_war", {}) - if yolo2names: - self.yolo_names = [cl for cc in yolo2names.values() for cl in cc] - self.ashes_of_war = yolo2names - - nested_categories = data.get("nested_categories", {}) - self.nested_categories = nested_categories or {} - - self.circle_segment_model = data.get( - "circle_seg", "appears_only_once_2309_yolov8s-seg.onnx" - ) + # Match model slots + for slot in data.get("model_slots", []): + if prompt_ := slot.get("requester_question"): + self.model_slots[prompt_] = ModelSlot(**slot) - datalake = data.get("datalake", {}) - if datalake: - for prompt, serialized_binary in datalake.items(): - datalake[prompt] = DataLake.from_serialized(serialized_binary) - self.datalake = datalake or {} + # Match YOLO models + for ym in data.get("yolo_modelscope", []): + if model_name_ := ym.get("model"): + self.yolo_modelscope[model_name_] = YOLOModelscope(**ym) - clip_candidates = data.get("clip_candidates", {}) - self.clip_candidates = clip_candidates or {} + # Match CLIP selections + for selection in data.get("clip_selections", []): + if prompt_ := selection.get("requester_question"): + self.datalake[prompt_] = DataLake.from_clip_selection(**selection) def pull_model(self, focus_name: str): """ @@ -593,3 +602,22 @@ def from_serialized(cls, fields: Dict[str, List[str]]): @classmethod def from_binary_labels(cls, positive_labels: List[str], negative_labels: List[str]): return cls(positive_labels=positive_labels, negative_labels=negative_labels) + + @classmethod + def from_clip_selection(cls, requester_question: str, positive: List[str], negative: List[str]): + return cls( + positive_labels=positive, negative_labels=negative, raw_prompt=requester_question + ) + + +@dataclass +class ModelSlot: + requester_question: str + request_type: Literal["image_label_binary", "image_label_area_select"] + related_models: List[str] = field(default_factory=list) + + +@dataclass +class YOLOModelscope: + model: str + labels: List[str] = field(default_factory=list) diff --git a/src/clip_datalake.json b/src/clip_datalake.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/objects.yaml b/src/objects2023.yaml similarity index 100% rename from src/objects.yaml rename to src/objects2023.yaml diff --git a/src/objects2024.yaml b/src/objects2024.yaml new file mode 100644 index 0000000000..61168d311d --- /dev/null +++ b/src/objects2024.yaml @@ -0,0 +1,31 @@ +$ref: + release_url: https://api.github.com/repos/QIN2DIM/hcaptcha-challenger/releases + objects_url: https://raw.githubusercontent.com/QDIN2DIM/hcaptcha-challenger/main/src/objects2024.yaml +model_slots: + - requester_question: Please click on the turtle's head + request_type: image_label_area_select + related_models: + - head_of_the_animal_turtle_2309_yolov8s.onnx + - requester_question: Please click all images contain electronic device + request_type: image_label_binary + related_models: + - electronic_device2316.onnx + - nested_electronic_device_mouse2309.onnx + - nested_electronic_device_circuit_board2309.onnx + - requester_question: Please click all images contain plant + request_type: image_label_binary + related_models: + - nested_plant2311.onnx + - plant2319.onnx +yolo_modelscope: + - model: head_of_the_animal_turtle_2309_yolov8s.onnx + labels: + - turtle-head +clip_selections: + - requester_question: Please click all images contain football stadium + positive: + - football stadium + negative: + - mouse + - natural landscape + - river From 1b0440347ff2f6290222b45ecead808cb5a2c4d1 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sat, 20 Apr 2024 15:29:55 +0800 Subject: [PATCH 13/14] f3 --- automation/collector.py | 4 +-- automation/sentinel.py | 4 +-- examples/faker_client.py | 5 +-- hcaptcha_challenger/__init__.py | 17 ++++----- hcaptcha_challenger/onnx/modelhub.py | 13 +++---- hcaptcha_challenger/tools/prompt_handler.py | 40 +++++---------------- tests/test_prompt_handler.py | 4 +-- 7 files changed, 30 insertions(+), 57 deletions(-) diff --git a/automation/collector.py b/automation/collector.py index 1ca3d6967a..7c7f2a7160 100644 --- a/automation/collector.py +++ b/automation/collector.py @@ -27,7 +27,7 @@ from loguru import logger from playwright.async_api import BrowserContext as ASyncContext, async_playwright -from hcaptcha_challenger import split_prompt_message, label_cleaning +from hcaptcha_challenger import regularize_prompt_message, label_cleaning from hcaptcha_challenger.agents import AgentT, Malenia TEMPLATE_BINARY_DATASETS = """ @@ -81,7 +81,7 @@ def __post_init__(self): self.mixed_label = self.issue.title.split(" ")[1].strip() self.parent_prompt = self.issue.title.split("@")[-1].strip() else: - self.mixed_label = split_prompt_message(self.challenge_prompt, lang="en") + self.mixed_label = regularize_prompt_message(self.challenge_prompt) self.parent_prompt = "image_label_binary" @classmethod diff --git a/automation/sentinel.py b/automation/sentinel.py index e911122d1c..6c06818528 100644 --- a/automation/sentinel.py +++ b/automation/sentinel.py @@ -22,7 +22,7 @@ from playwright.async_api import BrowserContext as ASyncContext, async_playwright, Page import hcaptcha_challenger as solver -from hcaptcha_challenger import label_cleaning, split_prompt_message +from hcaptcha_challenger import label_cleaning, regularize_prompt_message from hcaptcha_challenger.agents import AgentT, Malenia from hcaptcha_challenger.models import QuestionResp from hcaptcha_challenger.onnx.yolo import is_matched_ash_of_war @@ -169,7 +169,7 @@ def _bypass_motion(self): since=datetime.now() - timedelta(days=14), assignee=self.assignees[0], ): - mixed_label = split_prompt_message(self.issue_prompt, lang="en") + mixed_label = regularize_prompt_message(self.issue_prompt) if issue.created_at + timedelta(hours=24) > datetime.now(): issue.add_to_labels("🏹 ci: sentinel") if mixed_label in issue.title.lower(): diff --git a/examples/faker_client.py b/examples/faker_client.py index 8126841da2..0cfe12a2d2 100644 --- a/examples/faker_client.py +++ b/examples/faker_client.py @@ -14,6 +14,7 @@ from hcaptcha_challenger.agents import AgentV from hcaptcha_challenger.agents import Malenia from hcaptcha_challenger.utils import SiteKey +from hcaptcha_challenger import install dotenv.load_dotenv() @@ -48,7 +49,7 @@ async def mime(context: BrowserContext): if __name__ == "__main__": - EXECUTION = "collect" - # EXECUTION = "challenge" + # EXECUTION = "collect" + EXECUTION = "challenge" encrypted_resp = asyncio.run(main(headless=False)) diff --git a/hcaptcha_challenger/__init__.py b/hcaptcha_challenger/__init__.py index 0ab9676513..df7ef16cd3 100644 --- a/hcaptcha_challenger/__init__.py +++ b/hcaptcha_challenger/__init__.py @@ -20,7 +20,7 @@ from hcaptcha_challenger.tools.prompt_handler import ( label_cleaning, diagnose_task, - split_prompt_message, + regularize_prompt_message, prompt2task, handle, ) @@ -44,7 +44,7 @@ "ChallengeResp", "label_cleaning", "diagnose_task", - "split_prompt_message", + "regularize_prompt_message", "prompt2task", "handle", "ModelHub", @@ -56,19 +56,20 @@ init_log( - runtime=Path("logs/runtime.log"), - error=Path("logs/error.log"), - serialize=Path("logs/serialize.log"), + runtime=Path("logs/{time:YYYY-MM-DD}/runtime.log"), + error=Path("logs/{time:YYYY-MM-DD}/error.log"), + serialize=Path("logs/{time:YYYY-MM-DD}/serialize.log"), ) def install( upgrade: bool | None = False, - username: str = "QIN2DIM", - lang: str = "en", flush_yolo: bool | Iterable[str] = False, pypi: bool = False, clip: bool = False, + username: str = "QIN2DIM", + repo: str = "hcaptcha-challenger", + conf_="objects2024.yaml", **kwargs, ): if pypi is True: @@ -76,7 +77,7 @@ def install( PyPI("hcaptcha-challenger").install() - modelhub = ModelHub.from_github_repo(username=username, lang=lang) + modelhub = ModelHub.from_github_repo(username=username, repo=repo, conf_=conf_) modelhub.pull_objects(upgrade=upgrade) modelhub.assets.flush_runtime_assets(upgrade=upgrade) diff --git a/hcaptcha_challenger/onnx/modelhub.py b/hcaptcha_challenger/onnx/modelhub.py index 69a1e2e875..aafef727a0 100644 --- a/hcaptcha_challenger/onnx/modelhub.py +++ b/hcaptcha_challenger/onnx/modelhub.py @@ -257,8 +257,6 @@ class ModelHub: assets_dir = models_dir.joinpath("_assets") objects_path = models_dir.joinpath("objects.yaml") - lang: str = "en" - label_alias: Dict[str, str] = field(default_factory=dict) model_slots: Dict[str, ModelSlot] = field(default_factory=dict) """ @@ -319,7 +317,7 @@ class ModelHub: clip_candidates: Dict[str, List[str]] = field(default_factory=dict) """ - CLIP self-supervised candidates + [DEPRECATED] CLIP self-supervised candidates """ release_url: str = "" @@ -334,13 +332,13 @@ class ModelHub: """ def __post_init__(self): - self.assets_dir.mkdir(mode=0o777, parents=True, exist_ok=True) + self.assets = Assets.from_release_url(self.release_url) + self.assets_dir.mkdir(parents=True, exist_ok=True) @classmethod def from_github_repo( cls, username: str = "QIN2DIM", - lang: str = "en", repo: str = "hcaptcha-challenger", conf_: str = "objects2024.yaml", **kwargs, @@ -354,10 +352,7 @@ def from_github_repo( or f"https://raw.githubusercontent.com/{username}/{repo}/main/src/{conf_}" ) - instance = cls(release_url=release_url, objects_url=objects_url, lang=lang) - instance.assets = Assets.from_release_url(release_url) - - return instance + return cls(release_url=release_url, objects_url=objects_url) def pull_objects(self, upgrade: bool = False): """Network request""" diff --git a/hcaptcha_challenger/tools/prompt_handler.py b/hcaptcha_challenger/tools/prompt_handler.py index 3e232a6f9f..5ee4fdcb06 100644 --- a/hcaptcha_challenger/tools/prompt_handler.py +++ b/hcaptcha_challenger/tools/prompt_handler.py @@ -7,36 +7,12 @@ from hcaptcha_challenger.constant import BAD_CODE -def split_prompt_message(prompt_message: str, lang: str) -> str: +def regularize_prompt_message(prompt_message: str) -> str: """Detach label from challenge prompt""" - if lang.startswith("zh"): - if "中包含" in prompt_message or "上包含" in prompt_message: - return re.split(r"击|(的每)", prompt_message)[2] - if "的每" in prompt_message: - return re.split(r"(包含)|(的每)", prompt_message)[3] - if "包含" in prompt_message: - return re.split(r"(包含)|(的图)", prompt_message)[3] - elif lang.startswith("en"): - prompt_message = prompt_message.replace(".", "").lower() - if "containing" in prompt_message: - th = re.split(r"containing", prompt_message)[-1][1:].strip() - return th[2:].strip() if th.startswith("a") else th - if prompt_message.startswith("please select all"): - prompt_message = prompt_message.replace("please select all ", "").strip() - return prompt_message - if prompt_message.startswith("please click on the"): - prompt_message = prompt_message.replace("please click on ", "").strip() - return prompt_message - if prompt_message.startswith("please click on all entities similar"): - prompt_message = prompt_message.replace("please click on all entities ", "").strip() - return prompt_message - if prompt_message.startswith("please click on objects or entities"): - prompt_message = prompt_message.replace("please click on objects or entities", "") - return prompt_message.strip() - if prompt_message.startswith("select all") and "images" not in prompt_message: - return prompt_message.split("select all")[-1].strip() - if "select all images of" in prompt_message: - return prompt_message.split("select all images of")[-1].strip() + prompt_message = prompt_message.lower() + if prompt_message.endswith("."): + prompt_message = prompt_message[:-1] + prompt_message = prompt_message.strip() return prompt_message @@ -71,12 +47,12 @@ def diagnose_task(words: str) -> str: return words -def prompt2task(prompt: str, lang: str = "en") -> str: - prompt = split_prompt_message(prompt, lang) +def prompt2task(prompt: str) -> str: + prompt = regularize_prompt_message(prompt) prompt = label_cleaning(prompt) prompt = diagnose_task(prompt) return prompt def handle(x): - return split_prompt_message(label_cleaning(x), "en") + return regularize_prompt_message(label_cleaning(x)) diff --git a/tests/test_prompt_handler.py b/tests/test_prompt_handler.py index 4d5d631989..d09b9c7c73 100644 --- a/tests/test_prompt_handler.py +++ b/tests/test_prompt_handler.py @@ -10,7 +10,7 @@ import pytest -from hcaptcha_challenger import split_prompt_message, label_cleaning, handle +from hcaptcha_challenger import regularize_prompt_message, label_cleaning, handle from hcaptcha_challenger.constant import BAD_CODE pattern = re.compile(r"[^\x00-\x7F]") @@ -23,7 +23,7 @@ @pytest.mark.parametrize("prompt", prompts) def test_split_prompt_message(prompt: str): - result = split_prompt_message(prompt, lang="en") + result = regularize_prompt_message(prompt) assert result != prompt From cfc745458ccb4d954dd59b3bb1cd41083fc61d13 Mon Sep 17 00:00:00 2001 From: QIN2DIM <62018067+QIN2DIM@users.noreply.github.com> Date: Sat, 20 Apr 2024 19:13:52 +0800 Subject: [PATCH 14/14] f3af5 --- examples/faker_client.py | 9 +- .../agents/playwright/dragon.py | 125 +++++++----------- hcaptcha_challenger/constant.py | 2 + hcaptcha_challenger/models.py | 15 ++- hcaptcha_challenger/onnx/modelhub.py | 33 ++--- hcaptcha_challenger/tools/image_downloader.py | 4 +- hcaptcha_challenger/tools/prompt_handler.py | 6 +- src/objects2024.yaml | 20 ++- 8 files changed, 96 insertions(+), 118 deletions(-) diff --git a/examples/faker_client.py b/examples/faker_client.py index 0cfe12a2d2..53aaeeca25 100644 --- a/examples/faker_client.py +++ b/examples/faker_client.py @@ -14,12 +14,12 @@ from hcaptcha_challenger.agents import AgentV from hcaptcha_challenger.agents import Malenia from hcaptcha_challenger.utils import SiteKey -from hcaptcha_challenger import install dotenv.load_dotenv() # 1. You need to deploy sub-thread tasks and actively run `install(upgrade=True)` every 20 minutes # 2. You need to make sure to run `install(upgrade=True, clip=True)` before each instantiation +# from hcaptcha_challenger import install # install(upgrade=True, clip=True) @@ -29,6 +29,7 @@ async def main(headless: bool = False): context = await browser.new_context(locale="en-US") await Malenia.apply_stealth(context) await mime(context) + await context.close() @@ -45,11 +46,11 @@ async def mime(context: BrowserContext): await agent.ms.click_checkbox() await agent.wait_for_challenge() elif EXECUTION == "collect": - await agent.wait_for_collect(sitekey, batch=25) + await agent.wait_for_collect(sitekey, batch=2) if __name__ == "__main__": - # EXECUTION = "collect" - EXECUTION = "challenge" + EXECUTION = "collect" + # EXECUTION = "challenge" encrypted_resp = asyncio.run(main(headless=False)) diff --git a/hcaptcha_challenger/agents/playwright/dragon.py b/hcaptcha_challenger/agents/playwright/dragon.py index e3d468d3b8..5ab6063169 100644 --- a/hcaptcha_challenger/agents/playwright/dragon.py +++ b/hcaptcha_challenger/agents/playwright/dragon.py @@ -9,6 +9,7 @@ import os import re import shutil +import uuid from abc import ABC from asyncio import Queue from contextlib import suppress @@ -24,6 +25,7 @@ from loguru import logger from playwright.async_api import Page, Response, TimeoutError, expect +from hcaptcha_challenger.constant import INV from hcaptcha_challenger.models import ( ChallengeResp, QuestionResp, @@ -46,35 +48,6 @@ HOOK_CHALLENGE = "//iframe[contains(@title, 'hCaptcha challenge')]" -# todo move to datalake.json -datalake_post = { - "animals possessing wings": { - "positive_labels": ["bird"], - "negative_labels": ["lion", "elephant", "bear"], - }, - "something for drinking": { - "positive_labels": ["cup", "something for drinking"], - "negative_labels": ["streetlamp", "animal"], - }, - "something used for transportation": { - "positive_labels": ["tractor"], - "negative_labels": ["cat", "clock", "eagle"], - }, - "streetlamp": {"positive_labels": ["streetlamp"], "negative_labels": ["shark", "duck", "swan"]}, - "similar to the following silhouette": { - "positive_labels": ["duck"], - "negative_labels": ["cat", "dog", "frog"], - }, - "related to work": { - "positive_labels": ["excavator"], - "negative_labels": ["glass", "tree", "nature"], - }, - "similar to the following pattern": { - "positive_labels": ["raccoon"], - "negative_labels": ["duck", "apple"], - }, -} - _cached_ping_result = TTLCache(maxsize=10, ttl=60) @@ -121,10 +94,8 @@ class MechanicalSkeleton: page: Page sew: SolverEdgeWorker = field(default_factory=SolverEdgeWorker) - - modelhub: ModelHub | None = None - - clip_model: MossCLIP | None = None + modelhub: ModelHub = field(default_factory=ModelHub) + clip_model: MossCLIP = field(default_factory=MossCLIP) def __post_init__(self): self.sew = SolverEdgeWorker() @@ -162,12 +133,18 @@ async def challenge_image_label_binary( ): frame_challenge = self.switch_to_challenge_frame() + # {{< Reload SELF-SUPERVISED CONFIGURATION >}} + if not (model_slot := self.modelhub.model_slots.get(label)): + return + if not (clip_selection := model_slot.clip_selection): + return + challenge_images = [i.into_base64bytes() for i in challenge_images] - patched_model_prompt = datalake_post.get(label) + clip_selection = clip_selection.model_dump() self_supervised_payload = { "prompt": label, "challenge_images": challenge_images, - **patched_model_prompt, + **clip_selection, } # {{< IMAGE CLASSIFICATION >}} @@ -177,7 +154,7 @@ async def challenge_image_label_binary( payload = SelfSupervisedPayload(**self_supervised_payload) results: List[bool] = invoke_clip_tool(self.modelhub, payload, self.clip_model) - # {{< DRIVE THE BROWSER TO TAKE ON THE CHALLENGE >}} + # {{< DRIVE THE BROWSER TO WORK ON THE CHALLENGE >}} samples = frame_challenge.locator("//div[@class='task-image']") count = await samples.count() positive_cases = 0 @@ -254,23 +231,21 @@ def draws_from( return monster - def _init_imgdb(self, label: str, prompt: str): + def _init_imgdb(self, label: str): """run after _get_captcha""" self.tasklist.clear() self.examples.clear() - inv = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} - for c in inv: + for c in INV: label = label.replace(c, "") - prompt = prompt.replace(c, "") - label = label.strip() self.typed_dir = self.tmp_dir.joinpath(self.qr.request_type, label) self.typed_dir.mkdir(parents=True, exist_ok=True) - if self.qr.request_type != RequestType.ImageLabelBinary: - self.canvas_screenshot_dir = self.tmp_dir.joinpath(f"canvas_screenshot/{prompt}") - self.canvas_screenshot_dir.mkdir(parents=True, exist_ok=True) + self.canvas_screenshot_dir = self.tmp_dir.joinpath( + f"canvas_screenshot/{self.qr.request_type}/{label}" + ) + self.canvas_screenshot_dir.mkdir(parents=True, exist_ok=True) async def _recall_crumb(self): frame_challenge = self.ms.switch_to_challenge_frame() @@ -280,7 +255,7 @@ async def _recall_crumb(self): else: self.crumb_count = 1 - async def _recall_tasklist(self): + async def _recall_tasklist(self, capture_screenshot: bool = True): """run after _init_imgdb""" frame_challenge = self.ms.switch_to_challenge_frame() @@ -299,16 +274,21 @@ async def _recall_tasklist(self): datapoint_uri = style.split('"')[1] background_urls.append(datapoint_uri) + logger.debug(f"{self.image_queue.qsize()=}") while not self.image_queue.empty(): challenge_image: ChallengeImage = self.image_queue.get_nowait() + challenge_image.move_to(self.typed_dir) challenge_images[challenge_image.datapoint_uri] = challenge_image for url in background_urls: - challenge_image = challenge_images.get(url) + challenge_image: ChallengeImage = challenge_images.get(url) if challenge_image: self.tasklist.append(challenge_image) - if not self.typed_dir.joinpath(challenge_image.filename).exists(): - shutil.move(src=challenge_image.runtime_fp, dst=self.typed_dir) + + if capture_screenshot: + canvas = frame_challenge.locator("//div[@class='challenge-container']") + fp = self.canvas_screenshot_dir / f"{uuid.uuid4()}.png" + await canvas.screenshot(type="png", path=fp, scale="css") elif self.qr.request_type == RequestType.ImageLabelAreaSelect: # For the object detection task, tasklist is only used to collect datasets. @@ -319,11 +299,14 @@ async def _recall_tasklist(self): # Expect only 1 image in the image_queue while not self.image_queue.empty(): challenge_image: ChallengeImage = self.image_queue.get_nowait() - if not self.typed_dir.joinpath(challenge_image.filename).exists(): - shutil.move(src=challenge_image.runtime_fp, dst=self.typed_dir) - # Cache image sequences for subsequent browser operations + challenge_image.move_to(self.typed_dir) self.tasklist.append(challenge_image) + if self.image_queue.qsize() == 0: + canvas = frame_challenge.locator("//canvas") + fp = self.canvas_screenshot_dir / f"{challenge_image.filename}.png" + await canvas.screenshot(type="png", path=fp, scale="css") + @abc.abstractmethod async def _get_captcha(self, **kwargs): raise NotImplementedError @@ -339,17 +322,13 @@ async def _solve_captcha(self): logger.error(f"An error occurred while processing the challenge task", err=err) await self.ms.refresh_challenge() case RequestType.ImageLabelAreaSelect: - # Cache canvas to prepare for subsequent model processing - # canvas = frame_challenge.locator("//canvas") - # fp = self.canvas_screenshot_dir / f"{challenge_image.filename}.png" - # await canvas.screenshot(type="png", path=fp, scale="css") await self.ms.refresh_challenge() case RequestType.ImageLabelMultipleChoice: await self.ms.refresh_challenge() case _: logger.warning("[INTERRUPT]", reason="Unknown type of challenge") - async def _collect(self): + async def _collect(self, capture_screenshot: bool = True): await self._get_captcha() logger.debug( @@ -360,8 +339,8 @@ async def _collect(self): trigger=self.__class__.__name__, ) - self._init_imgdb(self.label, self.prompt) - await self._recall_tasklist() + self._init_imgdb(self.label) + await self._recall_tasklist(capture_screenshot=capture_screenshot) async def _challenge(self): await self._collect() @@ -426,15 +405,12 @@ class AgentV: page: Page ms: MechanicalSkeleton = field(default_factory=MechanicalSkeleton) - cr: ChallengeResp = field(default_factory=ChallengeResp) - - task_queue: Queue[Response] | None = None - cr_queue: Queue[ChallengeResp] = field(default_factory=Queue) - tmp_dir: Path = field(default_factory=Path) - image_queue: Queue = field(default_factory=Queue) + cr_queue: Queue[ChallengeResp] = field(default_factory=Queue) + _image_queue: Queue[ChallengeImage] = field(default_factory=Queue) + _task_queue: Queue[Response] = field(default_factory=Queue) _tool_type: ToolExecution | None = None @@ -442,11 +418,10 @@ def __post_init__(self): self.tmp_dir = self.tmp_dir or Path("tmp_dir") self._cache_dir = self.tmp_dir / ".cache" self._cache_dir.mkdir(parents=True, exist_ok=True) + self._task_queue = Queue(maxsize=1) self._enable_evnet_listener(self.page) - self.task_queue = Queue(maxsize=1) - @classmethod def into_solver(cls, page: Page, tmp_dir=None, clip_model: MossCLIP | None = None, **kwargs): ms = MechanicalSkeleton(page=page, clip_model=clip_model) @@ -463,13 +438,13 @@ def _enable_evnet_listener(self, page: Page): async def _task_handler(self, response: Response): if "/getcaptcha/" in response.url: # reset state - while not self.image_queue.empty(): - self.image_queue.get_nowait() - if self.task_queue.full(): - self.task_queue.get_nowait() + while not self._image_queue.empty(): + self._image_queue.get_nowait() + if self._task_queue.full(): + self._task_queue.get_nowait() # drop task - self.task_queue.put_nowait(response) + self._task_queue.put_nowait(response) # /cr 在 Submit Event 之后,cr 截至目前是明文数据 elif "/checkcaptcha/" in response.url: @@ -500,16 +475,16 @@ async def _task_handler(self, response: Response): element = ChallengeImage( datapoint_uri=image_url, filename=fn, body=image_bytes, runtime_fp=fp ) - self.image_queue.put_nowait(element) + self._image_queue.put_nowait(element) @logger.catch async def _tool_execution(self): - qr_data = await self.task_queue.get() + qr_data = await self._task_queue.get() driver_conf = { "page": self.page, "tmp_dir": self.tmp_dir, - "image_queue": self.image_queue, + "image_queue": self._image_queue, "ms": self.ms, } runnable: OminousLand | None = None @@ -556,7 +531,7 @@ async def wait_for_challenge( # CoroutineTask: Assigned a new task # The possible reason is that the challenge was **manually** refreshed during the task. while self.cr_queue.empty(): - if not self.task_queue.empty(): + if not self._task_queue.empty(): return await self.wait_for_challenge(execution_timeout, response_timeout) await asyncio.sleep(0.01) diff --git a/hcaptcha_challenger/constant.py b/hcaptcha_challenger/constant.py index 0baa4bd85b..f99678f4e9 100644 --- a/hcaptcha_challenger/constant.py +++ b/hcaptcha_challenger/constant.py @@ -33,3 +33,5 @@ "ー": "一", "土": "士", } + +INV = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} diff --git a/hcaptcha_challenger/models.py b/hcaptcha_challenger/models.py index bb2ef7df1b..12a01d09e3 100644 --- a/hcaptcha_challenger/models.py +++ b/hcaptcha_challenger/models.py @@ -6,6 +6,7 @@ from __future__ import annotations import base64 +import shutil from enum import Enum from pathlib import Path from typing import List, Dict, Any, Union @@ -13,7 +14,7 @@ from pydantic import BaseModel, Field, field_validator, UUID4, AnyHttpUrl, Base64Bytes -from hcaptcha_challenger.constant import BAD_CODE +from hcaptcha_challenger.constant import BAD_CODE, INV class Status(str, Enum): @@ -140,8 +141,7 @@ def cache(self, tmp_dir: Path): ak = f".{answer_keys[0]}" if len(answer_keys) > 0 else "" fn = f"{self.request_type}.{shape_type}.{requester_question}{ak}.json" - inv = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} - for c in inv: + for c in INV: fn = fn.replace(c, "") if tmp_dir and tmp_dir.exists(): @@ -209,14 +209,19 @@ def save(self, typed_dir: Path) -> Path: def into_base64bytes(self) -> str: return base64.b64encode(self.body).decode() + def move_to(self, dst: Path): + if dst.is_dir(): + dst = dst / self.filename + return shutil.move(self.runtime_fp, dst=dst) + class SelfSupervisedPayload(BaseModel): """hCaptcha payload of the image_label_binary challenge""" prompt: str = Field(..., description="challenge prompt") challenge_images: List[Base64Bytes] = Field(default_factory=list) - positive_labels: List[str] | None = Field(default_factory=list) - negative_labels: List[str] | None = Field(default_factory=list) + positive_labels: List[str] | None = Field(default_factory=list, alias="positive") + negative_labels: List[str] | None = Field(default_factory=list, alias="negative") class SelfSupervisedResponse(BaseModel): diff --git a/hcaptcha_challenger/onnx/modelhub.py b/hcaptcha_challenger/onnx/modelhub.py index aafef727a0..f26ff1f12d 100644 --- a/hcaptcha_challenger/onnx/modelhub.py +++ b/hcaptcha_challenger/onnx/modelhub.py @@ -27,6 +27,7 @@ from onnxruntime import InferenceSession from tenacity import * from tqdm import tqdm +from pydantic import BaseModel, Field DEFAULT_KEYPOINT_MODEL = "COCO2020_yolov8m.onnx" @@ -376,19 +377,15 @@ def parse_objects(self): # Match model slots for slot in data.get("model_slots", []): - if prompt_ := slot.get("requester_question"): - self.model_slots[prompt_] = ModelSlot(**slot) + if not (requester_question := slot.get("requester_question")): + continue + self.model_slots[requester_question] = ModelSlot(**slot) # Match YOLO models for ym in data.get("yolo_modelscope", []): if model_name_ := ym.get("model"): self.yolo_modelscope[model_name_] = YOLOModelscope(**ym) - # Match CLIP selections - for selection in data.get("clip_selections", []): - if prompt_ := selection.get("requester_question"): - self.datalake[prompt_] = DataLake.from_clip_selection(**selection) - def pull_model(self, focus_name: str): """ 1. node_id: Record the insertion point @@ -605,14 +602,18 @@ def from_clip_selection(cls, requester_question: str, positive: List[str], negat ) -@dataclass -class ModelSlot: - requester_question: str - request_type: Literal["image_label_binary", "image_label_area_select"] - related_models: List[str] = field(default_factory=list) +class CLIPSelection(BaseModel): + positive: List[str] = Field(default_factory=list) + negative: List[str] = Field(default_factory=list) -@dataclass -class YOLOModelscope: - model: str - labels: List[str] = field(default_factory=list) +class ModelSlot(BaseModel): + requester_question: str = Field(...) + request_type: Literal["image_label_binary", "image_label_area_select"] = Field(...) + related_models: List[str] | None = Field(default_factory=list) + clip_selection: CLIPSelection | None = Field(None) + + +class YOLOModelscope(BaseModel): + model: str = Field(...) + labels: List[str] = Field(default_factory=list) diff --git a/hcaptcha_challenger/tools/image_downloader.py b/hcaptcha_challenger/tools/image_downloader.py index 0eaac3aa73..dc5a6865b1 100644 --- a/hcaptcha_challenger/tools/image_downloader.py +++ b/hcaptcha_challenger/tools/image_downloader.py @@ -19,6 +19,7 @@ from httpx import AsyncClient from tenacity import * from hcaptcha_challenger.models import QuestionResp +from hcaptcha_challenger.constant import INV DownloadList = List[Tuple[Path, str]] @@ -112,8 +113,7 @@ async def download_challenge_images( request_type = qr.request_type ks = list(qr.requester_restricted_answer_set.keys()) - inv = {"\\", "/", ":", "*", "?", "<", ">", "|", "\n"} - for c in inv: + for c in INV: label = label.replace(c, "") label = label.strip() diff --git a/hcaptcha_challenger/tools/prompt_handler.py b/hcaptcha_challenger/tools/prompt_handler.py index 5ee4fdcb06..2b88045ce1 100644 --- a/hcaptcha_challenger/tools/prompt_handler.py +++ b/hcaptcha_challenger/tools/prompt_handler.py @@ -3,8 +3,7 @@ # Author : QIN2DIM # GitHub : https://github.com/QIN2DIM # Description: -import re -from hcaptcha_challenger.constant import BAD_CODE +from hcaptcha_challenger.constant import BAD_CODE, INV def regularize_prompt_message(prompt_message: str) -> str: @@ -30,8 +29,7 @@ def diagnose_task(words: str) -> str: raise TypeError(f"({words})TASK should be string type data") # Filename contains illegal characters - inv = {"\\", "/", ":", "*", "?", "<", ">", "|"} - if s := set(words) & inv: + if s := set(words) & INV: raise TypeError(f"({words})TASK contains invalid characters({s})") # Normalized separator diff --git a/src/objects2024.yaml b/src/objects2024.yaml index 61168d311d..d277bebf8f 100644 --- a/src/objects2024.yaml +++ b/src/objects2024.yaml @@ -12,20 +12,16 @@ model_slots: - electronic_device2316.onnx - nested_electronic_device_mouse2309.onnx - nested_electronic_device_circuit_board2309.onnx - - requester_question: Please click all images contain plant + - requester_question: please select the objects that can be cut with a knife or scissors request_type: image_label_binary - related_models: - - nested_plant2311.onnx - - plant2319.onnx + clip_selection: + positive: + - eggplant + negative: + - glass + - truck + - drill yolo_modelscope: - model: head_of_the_animal_turtle_2309_yolov8s.onnx labels: - turtle-head -clip_selections: - - requester_question: Please click all images contain football stadium - positive: - - football stadium - negative: - - mouse - - natural landscape - - river