phpwind-captcha-ocr / scripts /label_captchas.py
FlanChanXwO's picture
Upload scripts/label_captchas.py with huggingface_hub
6b0e0fc verified
Raw
History Blame Contribute Delete
4.29 kB
#!/usr/bin/env python3
"""用 pie-xian command-a-vision 批量识别验证码:3x放大 + 每张2次读取(不一致补第3次) + 多数投票。
用法: label_captchas.py <图片目录> <输出.json> [workers]
输出: {filename: {label, votes:[...], count, agreed}}
"""
import base64
import concurrent.futures
import glob
import json
import os
import re
import ssl
import sys
import tempfile
import urllib.error
import urllib.request
from collections import Counter
from PIL import Image
API = "https://api.pie-xian.com/v1/chat/completions"
KEY = os.environ.get("PIE_XIAN_API_KEY", "sk-Rel8iERHrmu9NDbvQhnhwTowEUnGqVvCFXMatASH8q5O4gGP")
MODEL = "command-a-vision-07-2025"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36"
PROMPT = "识别这张验证码图片中的字符。只输出字符本身,不要任何解释、标点或额外文字。这是登录验证码,可能包含1-4个数字。"
_CTX = ssl.create_default_context()
_CTX.check_hostname = False
_CTX.verify_mode = ssl.CERT_NONE
def call_vision(big_path):
b64 = base64.b64encode(open(big_path, "rb").read()).decode()
body = json.dumps({
"model": MODEL,
"messages": [{"role": "user", "content": [
{"type": "text", "text": PROMPT},
{"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}},
]}],
"max_tokens": 20, "temperature": 0,
}).encode()
req = urllib.request.Request(API, data=body, headers={
"Authorization": "Bearer " + KEY, "Content-Type": "application/json", "User-Agent": UA})
d = json.loads(urllib.request.urlopen(req, timeout=90, context=_CTX).read())
code = re.sub(r"[^0-9A-Za-z]", "", d["choices"][0]["message"]["content"])
if not code:
raise RuntimeError("empty output")
return code
def make_big(src, bigdir):
name = os.path.basename(src)
dst = os.path.join(bigdir, name)
im = Image.open(src).convert("RGB")
im = im.resize((im.width * 3, im.height * 3), Image.LANCZOS)
im.save(dst)
return dst
def label_one(big):
votes = []
for _ in range(2):
for attempt in range(3):
try:
votes.append(call_vision(big))
break
except Exception:
continue
c = Counter(votes)
if len(votes) >= 2 and c.most_common(1)[0][1] >= 2:
label, n = c.most_common(1)[0]
return {"label": label, "votes": votes, "count": n, "agreed": True}
for attempt in range(3):
try:
votes.append(call_vision(big))
break
except Exception:
continue
if not votes:
return {"label": None, "votes": [], "count": 0, "agreed": False, "error": "all reads failed"}
c = Counter(votes)
label, n = c.most_common(1)[0]
return {"label": label, "votes": votes, "count": n, "agreed": n == len(votes)}
def main():
d = sys.argv[1]
out = sys.argv[2]
workers = int(sys.argv[3]) if len(sys.argv) > 3 else 8
with tempfile.TemporaryDirectory(prefix="sp_big_") as bigdir:
files = sorted(glob.glob(os.path.join(d, "*.png")))
print(f"images={len(files)} workers={workers}", flush=True)
bigs = {f: make_big(f, bigdir) for f in files}
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futs = {ex.submit(label_one, bigs[f]): f for f in files}
done = 0
for fut in concurrent.futures.as_completed(futs):
f = futs[fut]
try:
results[os.path.basename(f)] = fut.result()
except Exception as e:
results[os.path.basename(f)] = {"label": None, "votes": [], "count": 0, "error": str(e)}
done += 1
if done % 10 == 0 or done == len(files):
print(f"progress {done}/{len(files)}", flush=True)
with open(out, "w") as fh:
json.dump(results, fh, ensure_ascii=False, indent=1)
ok = sum(1 for v in results.values() if v.get("label"))
agree = sum(1 for v in results.values() if v.get("agreed"))
print(f"DONE labeled={ok}/{len(files)} full-agree={agree} -> {out}")
if __name__ == "__main__":
main()