feat(接口99): 网关改本机实现(纯CPU+单路排队锁),文档同步

- gateway/app.py: 接口99 从盲转发改为本机实现(同接口4 模式):
  显式 Form 三选一 + 惰性导入(缺依赖仅本接口报1007)+ run_in_threadpool
- 单路排队: _if99_lock 全局互斥——串行推理限制 CPU 峰值 +
  规避 MediaPipe 单例线程安全问题;实测 req2≈2×req1 确认串行
- 网关待改动 §9 重写: 本机实现部署步骤 + 依赖清单(mediapipe/opencv/
  numpy<2,torch CPU 版可选——不装则发际线5点null)+ 权重已入git说明
- 实现说明/接口文档/CLAUDE.md 同步: 接口4/99 网关本机实现不转发
- 实测: BiSeNet 13.3M参数/51MB;CPU单核 0.46-0.65s/张;网关路径
  70/70点;worker 路径保留(测试页直连);pytest 15 通过
This commit is contained in:
xsl
2026-09-06 23:37:01 +08:00
parent 63bd265386
commit 8b7bfe2023
5 changed files with 125 additions and 40 deletions
+73 -3
View File
@@ -588,7 +588,77 @@ async def head_band(request: Request):
return await _proxy(request, "/api/v1/head/band")
# 接口99 单路排队:网关机推理全局互斥。调用频率低,无需并发——串行既限制 CPU 峰值
# (MediaPipe + BiSeNet 全 CPU 推理),也规避检测器单例的线程安全风险。排队无超时,
# 请求方按需自带客户端超时。
_if99_lock = asyncio.Lock()
@app.post("/api/v1/face/special-points", tags=["人脸分析"])
async def face_special_points(request: Request):
"""接口99:人脸特殊点检测(固定关键点 + 发际线特殊点像素坐标)"""
return await _proxy(request, "/api/v1/face/special-points")
async def face_special_points(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带前缀)"),
):
"""接口99:人脸特殊点检测 — 网关本机实现(纯 CPU:MediaPipe + BiSeNet CPU),不经过 worker。
与 worker 同仓库,直接 import face_analysis.special_points;权重已入 git,clone 即用。
依赖惰性导入:网关缺依赖时仅本接口报 1007,不影响其他路由。
"""
import uuid as _uuid
rid = f"gw-{_uuid.uuid4().hex[:8]}"
def _err(code, message):
return JSONResponse(status_code=200, content={
"code": code, "message": message, "request_id": rid, "data": None})
# 三选一校验
provided = [x for x in (image_file, image_url, image_base64) if x]
if len(provided) != 1:
return _err(1007, "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个")
img_bytes = None
if image_file:
img_bytes = await image_file.read()
elif image_base64:
b64 = image_base64.split(",", 1)[1] if "," in image_base64 else image_base64
try:
img_bytes = base64.b64decode(b64)
except Exception:
return _err(1008, "图片格式不支持(base64 解码失败)")
else:
try:
import httpx
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as cli:
r = await cli.get(image_url)
r.raise_for_status()
img_bytes = r.content
except Exception:
return _err(1008, "图片下载失败(URL 不可达)")
try:
import cv2
import numpy as np # noqa: F401 (detect_special_points 依赖)
from face_analysis.special_points import (
detect_special_points, NoFaceError, NonFrontalError)
except ImportError as ex:
return _err(1007, f"网关依赖未安装(安装清单见 docs/网关待改动.md §9):{ex}")
image = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR)
if image is None:
return _err(1008, "图片格式不支持(仅 JPG / PNG)")
from fastapi.concurrency import run_in_threadpool
try:
async with _if99_lock: # 单路排队:同一时刻只跑一路推理
data = await run_in_threadpool(detect_special_points, image)
except NoFaceError:
return _err(1001, "无法识别人像")
except NonFrontalError:
return _err(1003, "角度问题,请上传正面照")
except Exception as ex: # noqa: BLE001
logger.exception("接口99 处理异常")
return _err(1007, f"处理失败:{ex}")
return JSONResponse(status_code=200, content={
"code": 0, "message": "success", "request_id": rid, "data": data})