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

View File

@@ -440,11 +440,19 @@ uv run python cli.py serve --port 8080
## HTTP 服务 (server.py可选) ## HTTP 服务 (server.py可选)
纯推理服务,不依赖 torch / 训练代码,仅需 onnxruntime + FastAPI。
```python ```python
# FastAPI 服务,提供 REST API # POST /solve - JSON base64 图片识别
# POST /solve - 上传图片,返回识别结果 # 请求: {"image": "<base64>", "type": "normal"} (type 可选)
# 请求: multipart/form-data字段名 image # 响应: {"type": "normal", "result": "A3B8", "raw": "A3B8", "time_ms": 12.3}
# 响应: {"type": "normal", "result": "A3B8", "confidence": 0.95, "time_ms": 45} #
# POST /solve/upload - multipart 文件上传识别
# 请求: multipart/form-data, 字段名 image, 可选 query param type
# 响应: 同上
#
# GET /health - 健康检查
# 响应: {"status": "ok", "models_loaded": true}
``` ```
## 关键约束和注意事项 ## 关键约束和注意事项

104
server.py
View File

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