File size: 6,334 Bytes
42eced1 0532bc6 1010cae 42eced1 c72b6b8 42eced1 89788d3 42eced1 89788d3 42eced1 89788d3 0532bc6 89788d3 42eced1 09c8554 1010cae 89788d3 42eced1 0532bc6 89788d3 09c8554 f1f87f1 09c8554 0532bc6 09c8554 0532bc6 f1f87f1 89788d3 09c8554 1010cae f1f87f1 1010cae 89788d3 1010cae 89788d3 1010cae 89788d3 42eced1 1010cae 89788d3 1010cae 5fd91a4 1010cae 0532bc6 42eced1 66d08e1 09c8554 66d08e1 42eced1 1010cae 42eced1 1010cae f1f87f1 42eced1 f1f87f1 66d08e1 f1f87f1 42eced1 66d08e1 09c8554 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import os
import io
import time
import base64
import secrets
import logging
import numpy as np
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Security, Depends
from fastapi.security.api_key import APIKeyHeader
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from PIL import Image
import torch
import onnxruntime as rt
from utils.tokenizer_base import Tokenizer
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
API_KEY = os.environ.get("API_KEY", "changeme")
IMG_SIZE = (128, 32)
VOCAB = r"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
MODEL_PATH = "./model/model.onnx"
ORT_INTRA_THREADS = int(os.getenv("ORT_INTRA_THREADS", "1"))
ORT_INTER_THREADS = int(os.getenv("ORT_INTER_THREADS", "1"))
# ββ Torch global optimizations ββββββββββββββββββββββββββββββββββββββββββββββββ
torch.set_grad_enabled(False) # no autograd overhead on tensor ops
torch.set_num_threads(1) # don't compete with ORT threads
torch.set_num_interop_threads(1) # no inter-op parallelism from torch
# ββ API Key Auth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
def verify_key(key: str = Security(api_key_header)):
if not key or not secrets.compare_digest(key, API_KEY):
raise HTTPException(status_code=401, detail="Invalid or missing API key")
return key
# ββ Load model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
logger.info("Loading ONNX model...")
tokenizer = Tokenizer(VOCAB)
opts = rt.SessionOptions()
opts.intra_op_num_threads = ORT_INTRA_THREADS
opts.inter_op_num_threads = ORT_INTER_THREADS
opts.execution_mode = rt.ExecutionMode.ORT_SEQUENTIAL
opts.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALL
opts.optimized_model_filepath = MODEL_PATH + ".opt"
opts.enable_mem_pattern = True
opts.enable_cpu_mem_arena = True
session = rt.InferenceSession(
MODEL_PATH,
sess_options=opts,
providers=["CPUExecutionProvider"]
)
input_name = session.get_inputs()[0].name
logger.info("β
ONNX model ready")
# ββ Inference βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def preprocess(image: Image.Image) -> np.ndarray:
image = image.convert("RGB").resize(IMG_SIZE, Image.BILINEAR)
x = np.ascontiguousarray(image, dtype=np.float32)
x = (x / 255.0 - 0.5) / 0.5
return x.transpose(2, 0, 1)[np.newaxis, :] # [1, 3, H, W]
def solve_image(image: Image.Image) -> str:
x = preprocess(image)
logits = session.run(None, {input_name: x})[0]
probs = torch.tensor(logits).softmax(-1)
preds, _ = tokenizer.decode(probs)
return preds[0]
# ββ Warmup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def warmup():
logger.info("Warming up model...")
dummy = Image.new("RGB", IMG_SIZE, color=(128, 128, 128))
for _ in range(3):
solve_image(dummy)
logger.info("β
Warmup complete β model is hot")
# ββ Lifespan ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@asynccontextmanager
async def lifespan(app: FastAPI):
warmup()
yield
# ββ Schemas βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class SolveRequest(BaseModel):
image_base64: str
class SolveResponse(BaseModel):
success: bool
text: str = ""
processing_time: float = 0.0
error: str = ""
# ββ FastAPI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
app = FastAPI(title="CAPTCHA Solver API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ββ Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.get("/health")
def health():
return {
"status": "ok",
"device": "cpu",
"model": "ONNX INT8",
"quantized": True,
"workers": os.getenv("WEB_CONCURRENCY", "1"),
"intra_threads": ORT_INTRA_THREADS,
}
@app.post("/solve-captcha-base64", response_model=SolveResponse)
def solve(req: SolveRequest, _: str = Depends(verify_key)):
start = time.time()
try:
raw = req.image_base64
if "," in raw:
raw = raw.split(",", 1)[1]
image = Image.open(io.BytesIO(base64.b64decode(raw)))
text = solve_image(image).strip()[:5]
elapsed = time.time() - start
logger.info(f"β
Solved: '{text}' in {elapsed:.3f}s")
return SolveResponse(success=True, text=text, processing_time=elapsed)
except Exception as e:
logger.error(f"Error: {e}")
return SolveResponse(
success=False,
error=str(e),
processing_time=time.time() - start
) |