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:
16
CLAUDE.md
16
CLAUDE.md
@@ -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
104
server.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user