captchakraken 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -9
- package/LICENSE +165 -145
- package/NOTICE +4 -4
- package/README.md +10 -11
- package/dist/cli-invocation.d.ts +6 -3
- package/dist/cli-invocation.js +26 -10
- package/dist/humanize.d.ts +37 -1
- package/dist/humanize.js +143 -9
- package/dist/index.d.ts +13 -12
- package/dist/index.js +25 -33
- package/dist/kinds.d.ts +15 -2
- package/dist/kinds.js +12 -4
- package/dist/playwright-types.d.ts +3 -0
- package/dist/selectors.d.ts +96 -11
- package/dist/selectors.js +226 -112
- package/dist/solver.d.ts +93 -13
- package/dist/solver.js +579 -208
- package/dist/types.d.ts +53 -8
- package/dist/verdicts.d.ts +3 -0
- package/dist/verdicts.js +6 -0
- package/package.json +4 -4
- package/python/AGENTS.md +7 -11
- package/python/LICENSE +165 -145
- package/python/NOTICE +4 -4
- package/python/README.md +10 -9
- package/python/pyproject.toml +27 -13
- package/python/src/captchakraken/__init__.py +43 -46
- package/python/src/captchakraken/action_types.py +32 -1
- package/python/src/captchakraken/captchakraken.config.json +416 -0
- package/python/src/captchakraken/cli.py +66 -30
- package/python/src/captchakraken/clipvideo.py +258 -0
- package/python/src/captchakraken/humanize.py +141 -10
- package/python/src/captchakraken/image_processor.py +3 -7
- package/python/src/captchakraken/keyframes.py +96 -14
- package/python/src/captchakraken/kinds.py +14 -2
- package/python/src/captchakraken/models.json +28 -3
- package/python/src/captchakraken/page_solver.py +621 -259
- package/python/src/captchakraken/planner.py +298 -47
- package/python/src/captchakraken/pointer/__init__.py +20 -0
- package/python/src/captchakraken/pointer/detector.py +333 -0
- package/python/src/captchakraken/pointer/elements.py +273 -0
- package/python/src/captchakraken/pointer/heads.py +163 -0
- package/python/src/captchakraken/pointer/layout.py +319 -0
- package/python/src/captchakraken/pointer/marks.py +184 -0
- package/python/src/captchakraken/pointer/server.py +146 -0
- package/python/src/captchakraken/pointer/service.py +553 -0
- package/python/src/captchakraken/pointer/vllm_plugin.py +109 -0
- package/python/src/captchakraken/prompts.py +111 -34
- package/python/src/captchakraken/selectors.py +224 -131
- package/python/src/captchakraken/server_manager.py +18 -0
- package/python/src/captchakraken/solver.py +169 -88
- package/python/src/captchakraken/tool_calls/board_painted.py +11 -6
- package/python/src/captchakraken/tool_calls/fill_tracker.py +152 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +14 -59
- package/python/src/captchakraken/tool_calls/find_grid.py +273 -1191
- package/python/src/captchakraken/tool_calls/slide_tracker.py +1102 -0
- package/python/src/captchakraken/verdicts.py +16 -0
- package/dist/slide-geometry.d.ts +0 -6
- package/dist/slide-geometry.js +0 -38
- package/python/src/captchakraken/tool_calls/track_piece.py +0 -120
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The /pointer HTTP endpoint: one board per request, generation 3's answer back.
|
|
2
|
+
|
|
3
|
+
python -m captchakraken.pointer.server --model /models/pointer-0.8b \\
|
|
4
|
+
--detector /models/detector_r18.pt --port 8001
|
|
5
|
+
|
|
6
|
+
POST /v1/pointer
|
|
7
|
+
{"image": "<base64 PNG/JPEG>"} a still, as captured
|
|
8
|
+
{"frames": ["<base64 JPEG>", ...], "fps": 5.0} a video, `clipvideo`-cut
|
|
9
|
+
{"marked": false} optional: skip the detector
|
|
10
|
+
{"avoid": [answer, ...]} optional: answers already refused on this
|
|
11
|
+
board; the likeliest other ones come back
|
|
12
|
+
-> {"actions": [{...}, {...}, {...}], the three likeliest distinct answers, best first
|
|
13
|
+
"answer": {...}, the first action, kept for older callers
|
|
14
|
+
"elements": [{"id", "class", "box", "score"}], "size": [W, H]}
|
|
15
|
+
|
|
16
|
+
`elements` are in the pixels of the board as the model saw it (`size`), numbered
|
|
17
|
+
as the answer's ids are. The client sends no prompt: the server owns it.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import base64
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import threading
|
|
27
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
28
|
+
from typing import Dict
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
from PIL import Image
|
|
32
|
+
|
|
33
|
+
from . import layout as L
|
|
34
|
+
|
|
35
|
+
PATH = "/v1/pointer"
|
|
36
|
+
#: Refused answers one request may name: more than a solve's loops ever perform on one board.
|
|
37
|
+
MAX_AVOID = 32
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _image(b64: str) -> Image.Image:
|
|
41
|
+
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse(body: Dict) -> Dict:
|
|
45
|
+
"""A request body -> PointerService.solve's arguments. Raises ValueError on a bad one."""
|
|
46
|
+
if ("image" in body) == ("frames" in body):
|
|
47
|
+
raise ValueError("send exactly one of image or frames")
|
|
48
|
+
marked = bool(body.get("marked", True))
|
|
49
|
+
avoid = body.get("avoid") or []
|
|
50
|
+
if not isinstance(avoid, list) or not all(isinstance(a, dict) for a in avoid):
|
|
51
|
+
raise ValueError("avoid is a list of earlier answers")
|
|
52
|
+
if len(avoid) > MAX_AVOID:
|
|
53
|
+
raise ValueError(f"{len(avoid)} answers to avoid; at most {MAX_AVOID}")
|
|
54
|
+
history = body.get("history") or []
|
|
55
|
+
if "image" in body:
|
|
56
|
+
if len(history) > L.MAX_HISTORY:
|
|
57
|
+
raise ValueError(f"{len(history)} earlier screens; at most {L.MAX_HISTORY}")
|
|
58
|
+
return {"image": _image(body["image"]), "marked": marked, "avoid": avoid,
|
|
59
|
+
"history": [_image(h) for h in history]}
|
|
60
|
+
if history:
|
|
61
|
+
raise ValueError("a video board carries no earlier screens")
|
|
62
|
+
from .. import clipvideo as CV
|
|
63
|
+
if abs(float(body.get("fps", CV.FPS)) - CV.FPS) > 1e-6:
|
|
64
|
+
raise ValueError(f"frames must be cut at {CV.FPS} fps by clipvideo, got {body.get('fps')}")
|
|
65
|
+
frames = [np.asarray(Image.open(io.BytesIO(base64.b64decode(f))).convert("RGB")) for f in body["frames"]]
|
|
66
|
+
if len(frames) < 2:
|
|
67
|
+
raise ValueError("a video needs at least two frames")
|
|
68
|
+
return {"frames": frames, "marked": False, "avoid": avoid}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def reply(result: Dict) -> Dict:
|
|
72
|
+
return {"actions": result.get("answers") or [result["answer"]],
|
|
73
|
+
"answer": result["answer"], "size": list(result["size"]),
|
|
74
|
+
"elements": [{"id": n, "class": e["class"], "box": [round(v, 1) for v in e["box"]],
|
|
75
|
+
"score": e["score"]} for n, e in enumerate(result["elements"], 1)]}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def make_handler(service):
|
|
79
|
+
lock = threading.Lock() # one engine; vLLM batches inside a call, not across threads
|
|
80
|
+
|
|
81
|
+
class Handler(BaseHTTPRequestHandler):
|
|
82
|
+
protocol_version = "HTTP/1.1"
|
|
83
|
+
|
|
84
|
+
def _send(self, code: int, obj: Dict) -> None:
|
|
85
|
+
raw = json.dumps(obj).encode()
|
|
86
|
+
self.send_response(code)
|
|
87
|
+
self.send_header("Content-Type", "application/json")
|
|
88
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
89
|
+
self.end_headers()
|
|
90
|
+
self.wfile.write(raw)
|
|
91
|
+
|
|
92
|
+
def _authorized(self) -> bool:
|
|
93
|
+
key = os.getenv("VLLM_API_KEY", "")
|
|
94
|
+
return not key or self.headers.get("Authorization") == f"Bearer {key}"
|
|
95
|
+
|
|
96
|
+
def do_GET(self): # noqa: N802 — http.server's name
|
|
97
|
+
path = self.path.rstrip("/")
|
|
98
|
+
if path == "/health":
|
|
99
|
+
return self._send(200, {})
|
|
100
|
+
if not self._authorized():
|
|
101
|
+
return self._send(401, {"error": "Unauthorized"})
|
|
102
|
+
if path == "/v1/models":
|
|
103
|
+
model = os.getenv("CAPTCHA_LORA_NAME", "captcha-pointer")
|
|
104
|
+
return self._send(200, {"object": "list", "data": [
|
|
105
|
+
{"id": model, "object": "model", "created": 0, "owned_by": "captchakraken"},
|
|
106
|
+
]})
|
|
107
|
+
return self._send(404, {"error": {"message": f"no route {self.path}"}})
|
|
108
|
+
|
|
109
|
+
def do_POST(self): # noqa: N802 — http.server's name
|
|
110
|
+
if self.path.rstrip("/") != PATH:
|
|
111
|
+
return self._send(404, {"error": {"message": f"no route {self.path}"}})
|
|
112
|
+
if not self._authorized():
|
|
113
|
+
return self._send(401, {"error": "Unauthorized"})
|
|
114
|
+
try:
|
|
115
|
+
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)))
|
|
116
|
+
args = parse(body)
|
|
117
|
+
except (ValueError, KeyError, OSError) as e:
|
|
118
|
+
return self._send(400, {"error": {"message": str(e), "type": "invalid_request"}})
|
|
119
|
+
with lock:
|
|
120
|
+
result = service.solve(**args)
|
|
121
|
+
return self._send(200, reply(result))
|
|
122
|
+
|
|
123
|
+
def log_message(self, fmt, *args): # quiet: one line per request is the caller's to log
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
return Handler
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main():
|
|
130
|
+
ap = argparse.ArgumentParser()
|
|
131
|
+
ap.add_argument("--model", required=True, help="an exported pointer checkpoint")
|
|
132
|
+
ap.add_argument("--detector", default=None, help="the element detector; without it every board goes plain")
|
|
133
|
+
ap.add_argument("--letters", default=None,
|
|
134
|
+
help="the letter detector: a typed code is the one it and the model read together")
|
|
135
|
+
ap.add_argument("--host", default="127.0.0.1")
|
|
136
|
+
ap.add_argument("--port", type=int, default=8001)
|
|
137
|
+
ap.add_argument("--gpu-memory-utilization", type=float, default=0.45)
|
|
138
|
+
a = ap.parse_args()
|
|
139
|
+
from .service import PointerService
|
|
140
|
+
svc = PointerService(a.model, detector=a.detector, letters=a.letters,
|
|
141
|
+
gpu_memory_utilization=a.gpu_memory_utilization)
|
|
142
|
+
ThreadingHTTPServer((a.host, a.port), make_handler(svc)).serve_forever()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|