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:
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
旷视接口服务 — FastAPI 人像分析/生发服务,**一个仓库、两种部署角色**:
|
||||
|
||||
- **worker**(`app.py` + `face_analysis/` + `hairline/`):跑真实算法(本地模型 + ComfyUI + change_hair),监听 `0.0.0.0:8187`。**本机(GPU 机)即 worker 角色**,不跑网关。
|
||||
- **网关**(`gateway/`):外网机器上的薄反向代理,负责鉴权/健康检查/派发,并把 worker 返回的 `*_base64` 落盘改写为 `*_url`。接口4(豆包)在网关本机实现,不转发 worker。
|
||||
- **网关**(`gateway/`):外网机器上的薄反向代理,负责鉴权/健康检查/派发,并把 worker 返回的 `*_base64` 落盘改写为 `*_url`。接口4(豆包)和接口99(人脸特殊点,纯 CPU)在网关本机实现,不转发 worker。
|
||||
|
||||
> 历史包袱:`app.py` 的 FastAPI `description` 和 `docs/接口文档.md` 部分示例仍写「Mock 第一版」字样,实际全部接口已是真实算法实现。**架构以 `docs/实现说明.md` 为准,契约以 `docs/接口文档.md` 为准。**
|
||||
|
||||
@@ -51,7 +51,7 @@ cd /home/ubuntu/change_hair && bash start_all.sh start # 换发型三件套 88
|
||||
| 12 | `POST /api/v1/hairline/grow_v2` | 同上 | 接口11 final + 发际线重绘带 → Flux-2 保色重绘 |
|
||||
| 12f | `POST /api/v1/hairline/grow_v2_final` | 同上(`_V2_FINAL_DEFAULTS`) | 精简版:仅图片 + hairline_id,取整帧重绘 |
|
||||
| 12f2 | `POST /api/v1/hairline/grow_v2_final_v2` | 同上 | 同上,取 B 局部加发+全脸美颜 |
|
||||
| 99 | `POST /api/v1/face/special-points` | `face_analysis/special_points.py` | 固定编号关键点 + 发际线特殊点(射线求交头发 mask)像素坐标 |
|
||||
| 99 | `POST /api/v1/face/special-points` | `face_analysis/special_points.py`(网关直接 import 本机实现 + `_if99_lock` 单路排队;worker 保留同路径备份) | 固定编号关键点 + 发际线特殊点(射线求交头发 mask)像素坐标 |
|
||||
| 代理 | `POST /api/swapHair/v1`、`/hairColor/v2` | httpx 转发 127.0.0.1:8801 | 解决测试页 CORS |
|
||||
| 工具 | `POST /api/v1/redraw` | `hairline/redraw.py` | 直接 ComfyUI 重绘(图+遮罩+prompt) |
|
||||
| 调试 | `POST /api/v1/debug/grow-timing`、`GET /api/v1/debug/hairline_log` | — | 免鉴权,分步计时/下载日志 |
|
||||
|
||||
+5
-4
@@ -13,7 +13,7 @@
|
||||
```
|
||||
客户端 ──HTTPS──> 外网网关(gateway/) ──HTTP(X-Internal-Token)──> worker(GPU 机, app.py)
|
||||
│ 薄代理 + 落盘改URL │ 跑算法(本地模型/ComfyUI/change_hair)
|
||||
└ 接口4 本机直接调豆包(不转发) └ 接口1/2/3/5/6/7/9/10/11/12/99 + 工具路由
|
||||
└ 接口4(豆包)/99(特殊点) 本机实现(不转发) └ 接口1/2/3/5/6/7/9/10/11/12 + 工具路由
|
||||
```
|
||||
|
||||
- **worker**(`app.py` + `face_analysis/` + `hairline/`):跑真正的算法,除 ComfyUI/change_hair 走本机
|
||||
@@ -22,7 +22,8 @@
|
||||
另有 worker 本机的工具路由:`/api/swapHair/v1`、`/hairColor/v2`(CORS 代理转发 change_hair:8801)、
|
||||
`/api/v1/redraw`(直接 ComfyUI 重绘)、`/api/v1/debug/*`(免鉴权调试)。
|
||||
- **网关**(`gateway/`):薄反向代理,健康检查/派发/鉴权/把 worker 的 `*_base64` 落盘改成 `*_url`。
|
||||
唯一例外是**接口4 在网关本机直接实现**(调外网豆包,不转发 worker)。
|
||||
网关本机实现的接口(不转发 worker):**接口4**(调外网豆包)与**接口99**(纯 CPU 特殊点检测,
|
||||
直接 import 同仓库 `face_analysis/special_points`,`_if99_lock` 单路排队)。
|
||||
- **图片三选一**:所有接口图片入参 `image_file`/`image_url`/`image_base64` 严格三选一(接口3 是 `marked_image_*`)。
|
||||
- **响应信封**:`{code, message, request_id, data}`;业务错误用 `code`(HTTP 一律 200)。
|
||||
|
||||
@@ -114,9 +115,9 @@
|
||||
- **三个变体**:`grow_v2` 全参数暴露(接口11 参数 + `comfyui_prompt`/`beauty_alpha`/`band_lo_mult`/`band_hi_mult`);`grow_v2_final` / `grow_v2_final_v2` 精简生产版——仅图片 + `hairline_id`,其余固化 `_V2_FINAL_DEFAULTS`(push=0.8cm / two_stage / color_match=关 / beauty_alpha=0.6 等),两者后端计算完全一致(同一次输出含 A/B 两版),差异只是测试页展示哪版。接口2 女性 1..5 的生发图即复用此管线。
|
||||
- ⚠️ 依赖 ComfyUI(:8188);重绘失败在 `data.redraw.c_error` 报告。
|
||||
|
||||
### 接口99 人脸特殊点 `/api/v1/face/special-points`(worker)
|
||||
### 接口99 人脸特殊点 `/api/v1/face/special-points`(**网关本机**,worker 保留同路径备份)
|
||||
- **做什么**:人脸照 → 60 个固定编号 MediaPipe 关键点(6 组:eyebrow/eye/mouth/nose_midline/cheek/face_edge)+ 5 个发际线特殊点,原图像素坐标 JSON(字段 `<组名>_<点ID>`,如 `eyebrow_107`)。
|
||||
- **怎么实现**(`face_analysis/special_points.py`):MediaPipe 单例取关键点;发际线点=接口1 方案B 的 BiSeNet 头发 mask 上沿指定关键点方向(如 151→10)射线步进取首个头发像素,双射线的(right/right_right/left/left_left)取中点。无图片字段,网关纯盲转发。
|
||||
- **怎么实现**(`face_analysis/special_points.py`):MediaPipe 单例取关键点;发际线点=接口1 方案B 的 BiSeNet 头发 mask 上沿指定关键点方向(如 151→10)射线步进取首个头发像素,双射线的(right/right_right/left/left_left)取中点。**纯 CPU 可跑**(BiSeNet 单核 <1s),网关直接 import 本机实现(`_if99_lock` 单路排队);worker 侧保留同路径接口供测试页直连。部署依赖清单见 `网关待改动.md` §9。
|
||||
- 测试页:`static/test_interface99.html`(canvas 可视化 + JSON,worker 本机直连)。
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -656,7 +656,7 @@
|
||||
|
||||
## 接口 99:人脸特殊点检测接口
|
||||
|
||||
**说明**:输入一张人脸照片,返回一组**固定编号 MediaPipe Face Mesh 关键点**的像素坐标,外加 5 个**发际线特殊点**(射线与头发分割发际线的交点)。
|
||||
**说明**:输入一张人脸照片,返回一组**固定编号 MediaPipe Face Mesh 关键点**的像素坐标,外加 5 个**发际线特殊点**(射线与头发分割发际线的交点)。**生产路径:网关本机实现(纯 CPU,不转发 worker,同接口4 模式)**;worker 侧保留同路径接口供测试页直连。
|
||||
|
||||
**请求**:`POST /api/v1/face/special-points`
|
||||
|
||||
@@ -706,7 +706,7 @@
|
||||
| 10 外缘膨胀带 | `/api/v1/head/band` | 用户照片 | 头部外缘带状遮罩 + 分步可视化 |
|
||||
| 11 发际线生发 | `/api/v1/hairline/grow` | 用户照片 + hairline_id | 压低发际线图 + 全步骤可视化(不含重绘) |
|
||||
| 12 发际线带重绘 | `/api/v1/hairline/grow_v2` | 同接口11 + 重绘带参数 | final 基底 + Flux-2 保色重绘(整帧/局部两版);精简版 `grow_v2_final[_v2]` 仅需图片+hairline_id |
|
||||
| 99 人脸特殊点 | `/api/v1/face/special-points` | 用户照片 | 60 个固定编号关键点 + 5 个发际线特殊点像素坐标 JSON |
|
||||
| 99 人脸特殊点 | `/api/v1/face/special-points` | 用户照片 | 60 个固定编号关键点 + 5 个发际线特殊点像素坐标 JSON(**网关本机实现**,同接口4) |
|
||||
|
||||
> 另有 worker 本机工具路由(不在对外契约内):`/api/swapHair/v1`、`/hairColor/v2`(CORS 代理 change_hair)、`/api/v1/redraw`(直接 ComfyUI 重绘)、`/api/v1/debug/*`。
|
||||
|
||||
|
||||
+43
-29
@@ -170,44 +170,58 @@ worker 侧已上线 4 条路由,**网关尚无对应转发路由**(当前仅
|
||||
|
||||
---
|
||||
|
||||
## 9. ✅【新增】接口99 人脸特殊点(`/api/v1/face/special-points`)
|
||||
## 9. ✅【新增】接口99 人脸特殊点(`/api/v1/face/special-points`)— **网关本机实现,不转发 worker**
|
||||
|
||||
**背景**:worker 侧新增接口99——固定编号 MediaPipe 关键点(6 组 60 点)+ 5 个发际线特殊点,
|
||||
返回原图像素坐标 JSON(无图片字段)。
|
||||
**背景**:接口99(固定编号 MediaPipe 关键点 6 组 60 点 + 5 个发际线特殊点,返回像素坐标 JSON)
|
||||
**纯 CPU 可跑**(MediaPipe 本就 CPU;BiSeNet 分割单核实测 <1s)且调用频率低。
|
||||
按约定:**不用 GPU、低频的接口默认网关本机实现**,不占 worker 队列
|
||||
(worker 池 `per_worker_concurrency=1`,转发会排在生发任务后面等)。
|
||||
|
||||
**网关代码已在本仓库 `gateway/app.py` 改好**(同接口1 的盲转发写法,pull 后重启即生效):
|
||||
**网关代码已在本仓库 `gateway/app.py` 改好**——与接口4 同款本机实现(显式 Form 参数 +
|
||||
惰性导入 + `run_in_threadpool` + `gw-` request_id),直接 import 同仓库的
|
||||
`face_analysis/special_points.detect_special_points`。**单路排队**:`_if99_lock` 全局互斥,
|
||||
同一时刻只跑一路推理(限制 CPU 峰值 + 规避 MediaPipe 单例线程安全问题),请求 FIFO 排队。
|
||||
|
||||
```python
|
||||
@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")
|
||||
```
|
||||
### 网关机部署前检查
|
||||
|
||||
**网关机部署/调试步骤**:
|
||||
1. **Python 3.10~3.12**(mediapipe 0.10.14 无 3.13 wheel)。
|
||||
2. **安装依赖**(网关 venv,国内镜像):
|
||||
|
||||
```bash
|
||||
cd /path/to/hair && git pull # 拉取本分支
|
||||
sudo systemctl restart hair-gateway # 或项目实际的网关服务名(uvicorn gateway.app:app)
|
||||
# 验证路由已注册(应能在输出里看到 /api/v1/face/special-points)
|
||||
curl -s http://127.0.0.1:8080/openapi.json | python3 -m json.tool | grep special-points
|
||||
# 冒烟:经网关调一次(multipart 传图,无需 X-Internal-Token,网关会注入)
|
||||
curl -s -X POST http://127.0.0.1:8080/api/v1/face/special-points \
|
||||
-F "image_file=@/tmp/face.jpg" | python3 -m json.tool | head -30
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
mediapipe==0.10.14 opencv-python==4.10.0.84 numpy==1.26.4
|
||||
|
||||
# 可选:BiSeNet 头发分割(发际线 5 点需要)。CPU 版 torch 仅 ~200MB:
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
torch==2.2.2 torchvision==0.17.2 --index-url https://download.pytorch.org/whl/cpu
|
||||
# 不装 torch 也能跑:60 个关键点正常返回,发际线 5 点为 null(hairline_source="unavailable")
|
||||
```
|
||||
|
||||
**无需额外改动**:
|
||||
- 请求:multipart/form-data,仅图片三选一参数,盲转发即可。
|
||||
- 响应:`data.points` 全是坐标(`{"x":..,"y":..}`),**没有任何 `*_base64` 字段**,
|
||||
现有 `rewrite_base64_to_url` 递归改写天然跳过,原样返回。
|
||||
- 性能:纯本地推理(MediaPipe + BiSeNet,秒级),不依赖 ComfyUI/change_hair,
|
||||
`request_timeout_seconds` 无需调大。
|
||||
3. **权重已入 git**,`git clone/pull` 自动到位:`face_analysis/weights/79999_iter.pth`(51MB)。
|
||||
无需手动下载任何模型文件。
|
||||
|
||||
**注意**:
|
||||
- worker 侧测试页 `static/test_interface99.html` 是 worker 本机直连的(带 token 头),
|
||||
经网关调试请用 curl 或网关 `/docs`(网关只盲转发 `/api/*`,不代理 worker 的 `/static`)。
|
||||
- 契约详见 [`接口文档.md`](接口文档.md) 接口99 章节;worker 错误码 1001(无人脸)/1003(非正面)
|
||||
/1007/1008 原样透传。
|
||||
### 部署与冒烟
|
||||
|
||||
```bash
|
||||
cd /path/to/hair && git pull
|
||||
sudo systemctl restart hair-gateway # 或网关实际服务名(uvicorn gateway.app:app)
|
||||
|
||||
# 冒烟(multipart 传图;经网关无需 X-Internal-Token)
|
||||
curl -s -X POST http://127.0.0.1:8080/api/v1/face/special-points \
|
||||
-F "image_file=@/tmp/face.jpg" | python3 -m json.tool | head -30
|
||||
# 期望 code=0;data.points 含 eyebrow_107 等 60 个关键点 + hair_line_* 5 点
|
||||
# 缺依赖时返回 code=1007 且 message 提示"网关依赖未安装"
|
||||
```
|
||||
|
||||
### 注意
|
||||
|
||||
- **超时无需调大**:单路 CPU 推理 <1s;即使排队,低频下积压可忽略。排队本身无超时,
|
||||
调用方按需自带客户端超时。
|
||||
- **错误码透传契约**:1001(无人脸)/ 1003(非正面)/ 1007(参数/处理失败/缺依赖)/
|
||||
1008(图片格式/下载失败),HTTP 一律 200。
|
||||
- **worker 侧同路径接口保留**(`app.py`),供测试页 `static/test_interface99.html`
|
||||
直连调试(需 token);生产路径是网关本机实现。
|
||||
- 契约详见 [`接口文档.md`](接口文档.md) 接口99 章节。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+73
-3
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user