Refactor server.py: add base64 support, separate from training

- POST /solve accepts JSON with base64 encoded image
- POST /solve/upload keeps multipart file upload compatibility
- Server only depends on inference code (onnxruntime), no torch
- Catch invalid image errors with proper 400 response
- Update CLAUDE.md with new API documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hua
2026-03-11 19:28:09 +08:00
parent 788ddcae1a
commit 68ab86e6b9
2 changed files with 80 additions and 40 deletions

104
server.py
View File

@@ -1,21 +1,30 @@
"""
FastAPI HTTP 推理服务
FastAPI HTTP 推理服务 (纯推理,不依赖 torch/训练代码)
提供 REST API 识别验证码:
POST /solve - 上传图片,返回识别结果
GET /health - 健康检查
仅依赖: fastapi, uvicorn, python-multipart, onnxruntime, pillow, numpy
启动方式:
uv run python cli.py serve --port 8080
API:
POST /solve JSON base64 图片识别
POST /solve/upload multipart 文件上传识别
GET /health 健康检查
请求示例:
curl -X POST http://localhost:8080/solve -F "image=@captcha.png"
启动:
uv sync --extra server
python cli.py serve --port 8080
响应示例:
{"type": "normal", "result": "A3B8", "confidence": 0.95, "time_ms": 45}
请求示例 (base64):
curl -X POST http://localhost:8080/solve \
-H "Content-Type: application/json" \
-d '{"image": "<base64>", "type": "normal"}'
请求示例 (文件上传):
curl -X POST http://localhost:8080/solve/upload -F "image=@captcha.png"
响应:
{"type": "normal", "result": "A3B8", "raw": "A3B8", "time_ms": 12.3}
"""
from __future__ import annotations
import base64
def create_app():
@@ -23,10 +32,12 @@ def create_app():
创建 FastAPI 应用实例(工厂函数)。
cli.py 的 cmd_serve 依赖此签名。
需要安装 server 可选依赖: uv sync --extra server
"""
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
@@ -36,47 +47,38 @@ def create_app():
version="0.1.0",
)
pipeline: CaptchaPipeline | None = None
pipeline: Optional[CaptchaPipeline] = None
# ---- 启动时加载模型 ----
@app.on_event("startup")
def _load_models():
nonlocal pipeline
try:
pipeline = CaptchaPipeline()
except FileNotFoundError:
# 模型未导出时允许启动,但 /solve 会返回 503
pipeline = None
@app.get("/health")
def health():
models_loaded = pipeline is not None
return {"status": "ok" if models_loaded else "no_models", "models_loaded": models_loaded}
# ---- 请求体定义 ----
class SolveRequest(BaseModel):
image: str # base64 编码的图片
type: Optional[str] = None # 指定类型可跳过分类
@app.post("/solve")
async def solve(
image: UploadFile = File(...),
type: str | None = Query(None, description="指定类型跳过分类"),
):
# ---- 通用推理逻辑 ----
def _solve(image_bytes: bytes, captcha_type: Optional[str]) -> dict:
if pipeline is None:
return JSONResponse(
status_code=503,
content={"error": "模型未加载,请先训练并导出 ONNX 模型"},
)
data = await image.read()
if not data:
return JSONResponse(
status_code=400,
content={"error": "空文件"},
)
if not image_bytes:
return JSONResponse(status_code=400, content={"error": "空图片"})
try:
result = pipeline.solve(data, captcha_type=type)
except RuntimeError as e:
return JSONResponse(
status_code=400,
content={"error": str(e)},
)
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"],
@@ -85,4 +87,34 @@ def create_app():
"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