""" FastAPI HTTP 推理服务 (纯推理,不依赖 torch/训练代码) 仅依赖: fastapi, uvicorn, python-multipart, onnxruntime, pillow, numpy API: POST /solve JSON base64 图片识别 POST /solve/upload multipart 文件上传识别 GET /health 健康检查 启动: uv sync --extra server python cli.py serve --port 8080 请求示例 (base64): curl -X POST http://localhost:8080/solve \ -H "Content-Type: application/json" \ -d '{"image": "", "type": "normal"}' 请求示例 (文件上传): curl -X POST http://localhost:8080/solve/upload -F "image=@captcha.png" 响应: {"type": "normal", "result": "A3B8", "raw": "A3B8", "time_ms": 12.3} """ import base64 def create_app(): """ 创建 FastAPI 应用实例(工厂函数)。 cli.py 的 cmd_serve 依赖此签名。 """ from typing import Optional from fastapi import FastAPI, File, Query, UploadFile from fastapi.responses import JSONResponse from pydantic import BaseModel from inference.pipeline import CaptchaPipeline app = FastAPI( title="CaptchaBreaker", description="验证码识别多模型系统 - HTTP 推理服务", version="0.1.0", ) pipeline: Optional[CaptchaPipeline] = None # ---- 启动时加载模型 ---- @app.on_event("startup") def _load_models(): nonlocal pipeline try: pipeline = CaptchaPipeline() except FileNotFoundError: pipeline = None # ---- 请求体定义 ---- class SolveRequest(BaseModel): image: str # base64 编码的图片 type: Optional[str] = None # 指定类型可跳过分类 # ---- 通用推理逻辑 ---- def _solve(image_bytes: bytes, captcha_type: Optional[str]) -> dict: if pipeline is None: return JSONResponse( status_code=503, content={"error": "模型未加载,请先训练并导出 ONNX 模型"}, ) if not image_bytes: return JSONResponse(status_code=400, content={"error": "空图片"}) try: result = pipeline.solve(image_bytes, captcha_type=captcha_type) except (RuntimeError, TypeError) as e: return JSONResponse(status_code=400, content={"error": str(e)}) except Exception as e: return JSONResponse(status_code=400, content={"error": f"图片解析失败: {e}"}) return { "type": result["type"], "result": result["result"], "raw": result["raw"], "time_ms": result["time_ms"], } # ---- 路由 ---- @app.get("/health") def health(): models_loaded = pipeline is not None return { "status": "ok" if models_loaded else "no_models", "models_loaded": models_loaded, } @app.post("/solve") async def solve_base64(req: SolveRequest): """JSON 请求,图片通过 base64 传输。""" try: image_bytes = base64.b64decode(req.image) except Exception: return JSONResponse( status_code=400, content={"error": "base64 解码失败"}, ) return _solve(image_bytes, req.type) @app.post("/solve/upload") async def solve_upload( image: UploadFile = File(...), type: Optional[str] = Query(None, description="指定类型跳过分类"), ): """multipart 文件上传。""" data = await image.read() return _solve(data, type) return app