- worker: POST /api/v1/face/special-points,60 个固定编号关键点(6组) + 5 个发际线特殊点(沿指定关键点方向射线步进,与 BiSeNet 头发 mask 首次相交; 双射线取中点,left_left=mid(333→332, 298→284) 与右侧镜像) - 网关: 同路径盲转发路由(调用频率低,维持转发不占网关依赖; 响应无 *_base64 字段,改写逻辑天然跳过) - 测试页 static/test_interface99.html:分组着色小点 + 点ID标注(可开关) + 发际线点大圆标签 + JSON 展示 - 文档: CLAUDE.md 重写至当前架构;实现说明/接口文档补接口 9~12/99 契约; 网关待改动补 §7(9/10已加) §8(11/12待加) §9(接口99部署步骤)
223 lines
10 KiB
Markdown
223 lines
10 KiB
Markdown
# 网关侧改动清单(worker 近期变更引发)
|
||
|
||
> 给网关开发:以下是 worker/契约近期变化里**与网关有关**的点。标 ✅ 的我已在本仓库 `gateway/`
|
||
> 改好(你 review/拉取即可);标 🔲 的是**建议你确认或改**。功能必需只有第 1 条。
|
||
|
||
---
|
||
|
||
## 1. ✅【功能必需】base64→URL 落盘扩展名按内容嗅探(支持 JPG)
|
||
|
||
**背景**:接口 **2/3/5 的返回图改成了 JPG**(体积约小 9×),**接口 1 标注图仍是 PNG**(含透明)。
|
||
网关把 `*_base64` 落盘时若**硬编码 `.png`**,JPG 会被存成 `.png`(内容是 JPG、扩展名错)。
|
||
|
||
**改动**(`gateway/forward.py` 的 `rewrite_base64_to_url`,已改):
|
||
```python
|
||
# 原:filename = f"{uuid.uuid4().hex}.png"
|
||
ext = "png" if img_bytes[:8] == b"\x89PNG\r\n\x1a\n" else "jpg" # 按内容嗅探
|
||
filename = f"{uuid.uuid4().hex}.{ext}"
|
||
```
|
||
> 这样接口1 存 `.png`、接口2/3/5 存 `.jpg`,对外 URL 后缀也就正确。**若你的网关是独立部署/独立代码,按上面这两行改一下即可。**
|
||
|
||
---
|
||
|
||
## 2. 🔲【建议】生发接口超时调大
|
||
|
||
接口 **2(一次 N 张 Flux,~18s)/ 3(~6s)** 经 ComfyUI 同步出图较慢。
|
||
`gateway/config.json` 的 `dispatch.request_timeout_seconds` 建议 **≥ 120**,否则网关会先超时换 worker 重试。
|
||
|
||
---
|
||
|
||
## 3. 🔲【确认】base64→URL 通用改写仍覆盖这些场景
|
||
|
||
- **数组里的图片字段**:接口2 `results[].image_base64` / `results[].grown_image_base64`、
|
||
接口5 `hairline_images[].image_base64` 在数组元素内——改写要**递归进数组**(你现有的递归实现已覆盖)。
|
||
- **可空字段**:接口2/3 的生发图(ComfyUI 没起/失败时)`*_base64` 为 **null** → 保留 null、不落盘。
|
||
|
||
---
|
||
|
||
## 4. 🔲【可选·仅影响 /docs】OpenAPI 表单声明
|
||
|
||
纯文档展示,不影响转发功能(网关是盲转发)。若想让网关 `/docs` 准确:
|
||
- 接口2 `/hair/grow`、接口5 `/hairline/generate` 入参**新增必填 `gender`**(male/female)。
|
||
- 接口3 `/hair/grow-b` 入参**只剩 `marked_image_*`**(已去掉 `original_image_*`)。
|
||
- (`gateway/app.py` 里的 `_*_FORMS` 字典当前未被路由引用,所以不改也不影响实际行为。)
|
||
|
||
---
|
||
|
||
## 5. 🔲【新增】接口7 C端生发 v2(`/api/v1/hair/grow-v2`)
|
||
|
||
**背景**:worker 侧已新增接口 7,功能与接口 2 完全一致,区别仅在于 ComfyUI 工作流使用 `add_hair2.json`(而非 `add_hair.json`)。
|
||
|
||
**网关需新增一个路由**,代理转发到 worker 的同路径:
|
||
|
||
```python
|
||
# gateway/app.py
|
||
|
||
@app.post("/api/v1/hair/grow-v2", tags=["生发"])
|
||
async def hair_grow_v2(request: Request):
|
||
"""接口7:C端生发 v2(add_hair2 工作流)"""
|
||
return await _proxy(request, "/api/v1/hair/grow-v2")
|
||
```
|
||
|
||
**无需额外改动**:
|
||
- 请求:multipart/form-data,参数与接口 2 完全相同(`image_file/url/base64` 三选一 + `gender` + `hair_style` + `beauty_enabled` + `use_mask` + `prompt`),网关盲转发即可
|
||
- 响应:结构与接口 2 完全一致,`results[].image_base64` / `results[].grown_image_base64` 经现有 `rewrite_base64_to_url` 自动改写为 URL
|
||
- base64→URL:数组内图片字段递归改写已覆盖,无需修改
|
||
|
||
### 入参(与接口 2 一致)
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| image_file / image_url / image_base64 | — | **三选一** | 用户正面照 |
|
||
| gender | string | **是** | `male` / `female` |
|
||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`)。female / male 均为 1–7 |
|
||
| beauty_enabled | bool | 否 | 美颜开关(本期不生效) |
|
||
| use_mask | bool | 否 | 默认 `true`,`false` 跳过遮罩 |
|
||
| prompt | string | 否 | ComfyUI 提示词 |
|
||
|
||
### 出参(与接口 2 一致)
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "success",
|
||
"request_id": "gw-xxxxxxxx",
|
||
"data": {
|
||
"results": [
|
||
{
|
||
"image_url": "https://hair.xiangsilian.com/static/annotations/xxx.jpg",
|
||
"grown_image_url": "https://hair.xiangsilian.com/static/annotations/xxx.jpg",
|
||
"hairline_type": "ellipse",
|
||
"order": 1
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### worker 侧信息
|
||
|
||
| 项目 | 值 |
|
||
|------|-----|
|
||
| worker 路径 | `/api/v1/hair/grow-v2` |
|
||
| 工作流文件 | `add_hair2.json` |
|
||
| 输入/遮罩节点 | 26(LoadImage) |
|
||
| 输出节点 | 75(SaveImage,自动检测) |
|
||
| 提示词节点 | 60(JjkText) |
|
||
|
||
### 🔲【可选·仅影响 /docs】OpenAPI 表单声明
|
||
|
||
若想让网关 `/docs` 展示准确,在 `gateway/app.py` 新增 `_GROW_V2_FORMS`(或复用 `_GROW_FORMS` 并补充 `gender`/`hair_style` 字段),然后将路由函数签名改为显式声明 Form 参数(参考接口 4 的写法)。不改也不影响实际转发。
|
||
|
||
|
||
|
||
---
|
||
|
||
## 6. ✅【已完成】接口6 四庭七眼测量 v2(`/api/v1/face/measure-v2`)
|
||
|
||
**背景**:worker 侧接口 6 是接口 1 的**去顶庭变体**(不再完全一致):不画头顶横线/不返回顶庭数据、竖线范围发际线→下巴尖、不画人头最左/最右端线。worker 侧与接口 1 共用 `_face_measure_impl(variant="v6")`。
|
||
|
||
**网关已新增路由**(`gateway/app.py` 已实现,网关机器 pull 后生效):
|
||
|
||
```python
|
||
# gateway/app.py
|
||
|
||
@app.post("/api/v1/face/measure-v2", tags=["人脸分析"])
|
||
async def face_measure_v2(request: Request):
|
||
"""接口6:四庭七眼测量 v2(去顶庭 + 去头部端线)"""
|
||
return await _proxy(request, "/api/v1/face/measure-v2")
|
||
```
|
||
|
||
**无需额外改动**:
|
||
- 入参:与接口 1 完全相同(image_file/url/base64 三选一)
|
||
- 出参:`annotated_image_base64` → 经现有 `rewrite_base64_to_url` 自动改写为 `annotated_image_url`
|
||
- worker 侧 v6 差异(去顶庭字段、变体标注)由 `_face_measure_impl` 内部处理,网关透明转发
|
||
|
||
---
|
||
|
||
## 7. ✅【已完成】接口9/10 头部遮罩(`/api/v1/head/mask`、`/api/v1/head/band`)
|
||
|
||
`gateway/app.py` 已新增两条盲转发路由(同接口6 的写法):
|
||
|
||
```python
|
||
@app.post("/api/v1/head/mask", tags=["人脸分析"])
|
||
@app.post("/api/v1/head/band", tags=["人脸分析"])
|
||
```
|
||
|
||
- 入参:图片三选一 + `erode_cm`(接口10 另有 `dilate_cm`)。
|
||
- 出参:`steps_common.*_base64` 与 `bisenet`/`segformer` 组内的 `*_base64` 字段——现有递归
|
||
`rewrite_base64_to_url` 已覆盖(嵌套 dict/数组),无需额外改动。
|
||
- 两个接口响应较慢(双分割模型跑两遍),注意 `request_timeout_seconds`。
|
||
|
||
---
|
||
|
||
## 8. 🔲【新增】接口11/12 发际线生发/带重绘(`/api/v1/hairline/grow*`)
|
||
|
||
worker 侧已上线 4 条路由,**网关尚无对应转发路由**(当前仅测试页直连 worker)。如需对外提供,参考接口9 加法:
|
||
|
||
```python
|
||
@app.post("/api/v1/hairline/grow", tags=["生发"]) # 接口11
|
||
@app.post("/api/v1/hairline/grow_v2", tags=["生发"]) # 接口12 全参数
|
||
@app.post("/api/v1/hairline/grow_v2_final", tags=["生发"]) # 接口12 final 精简版
|
||
@app.post("/api/v1/hairline/grow_v2_final_v2", tags=["生发"])
|
||
```
|
||
|
||
- 入参:图片三选一 + `hairline_id`(接口12 final 两变体仅需这两个)。接口11/12 全参数版还有
|
||
20+ 个调优 Form 参数,网关盲转发即可,无需逐个声明。
|
||
- 出参:`data.steps.*_base64`(input/final/redraw_full/redraw_band 等各步图)→ 现有递归改写覆盖。
|
||
- ⚠️ 依赖 worker 机的 change_hair(:8801) 与 ComfyUI(:8188) 都在跑;单请求 10s+,超时建议 ≥120s。
|
||
- 契约细节见 [`接口文档.md`](接口文档.md) 接口11/12 章节。
|
||
|
||
---
|
||
|
||
## 9. ✅【新增】接口99 人脸特殊点(`/api/v1/face/special-points`)
|
||
|
||
**背景**:worker 侧新增接口99——固定编号 MediaPipe 关键点(6 组 60 点)+ 5 个发际线特殊点,
|
||
返回原图像素坐标 JSON(无图片字段)。
|
||
|
||
**网关代码已在本仓库 `gateway/app.py` 改好**(同接口1 的盲转发写法,pull 后重启即生效):
|
||
|
||
```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")
|
||
```
|
||
|
||
**网关机部署/调试步骤**:
|
||
|
||
```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
|
||
```
|
||
|
||
**无需额外改动**:
|
||
- 请求:multipart/form-data,仅图片三选一参数,盲转发即可。
|
||
- 响应:`data.points` 全是坐标(`{"x":..,"y":..}`),**没有任何 `*_base64` 字段**,
|
||
现有 `rewrite_base64_to_url` 递归改写天然跳过,原样返回。
|
||
- 性能:纯本地推理(MediaPipe + BiSeNet,秒级),不依赖 ComfyUI/change_hair,
|
||
`request_timeout_seconds` 无需调大。
|
||
|
||
**注意**:
|
||
- worker 侧测试页 `static/test_interface99.html` 是 worker 本机直连的(带 token 头),
|
||
经网关调试请用 curl 或网关 `/docs`(网关只盲转发 `/api/*`,不代理 worker 的 `/static`)。
|
||
- 契约详见 [`接口文档.md`](接口文档.md) 接口99 章节;worker 错误码 1001(无人脸)/1003(非正面)
|
||
/1007/1008 原样透传。
|
||
|
||
---
|
||
|
||
## 已经做好、无需再动的
|
||
|
||
- **接口4 在网关本机实现**(调豆包,不转发 worker)——已完成;config 里配 `ark`。
|
||
- **接口4 业务错误 HTTP 状态**已统一为 200(与其余接口一致)。
|
||
- **接口3 去 original / best_hairline**——网关盲转发,无需改(映射表里也没有 best_hairline)。
|
||
|
||
---
|
||
|
||
> 对外字段映射总表见 [`实现说明.md`](实现说明.md) §1;契约以 [`接口文档.md`](接口文档.md) 为准。
|