docs: 架构拆分为外网网关+高性能worker(GPU)

- 新增 系统架构-网关与高性能后端.md: 网关薄代理(健康检查/空闲派发/鉴权/
  base64落盘改URL/无后端→1007) + worker(GPU跑完整app)职责划分、配置文件、
  鉴权(共享密码可轮换)、标注图base64流程、部署、安全注意
- 关键约束: 接口文档不变; 每worker并发=1, 总并发=健康worker数; 多worker可配置
- 技术方案: 加运行位置横幅; torch改GPU(CUDA); handler返回annotated_image_base64
- 任务书v1.4: 标注差异说明; 阶段八返base64+鉴权中间件; 阶段十worker(GPU)部署;
  新增§16网关工作流; DoD拆分worker侧/网关侧
- OFFLINE_ASSETS: 区分worker(GPU torch)与网关(轻量)依赖
This commit is contained in:
xsl
2026-06-14 13:55:45 +08:00
parent 7493f48992
commit 2e789d4efa
4 changed files with 387 additions and 43 deletions
@@ -1,8 +1,15 @@
# 接口 1:四庭七眼测量 — 开发任务书(AI Agent 执行版)
> 配套技术方案:[`接口1-四庭七眼测量-技术实现方案.md`](接口1-四庭七眼测量-技术实现方案.md)
> 系统架构:[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)
> 执行者:AI coding agent。请**严格按阶段顺序**执行,每个阶段完成后运行该阶段的「验证方法」,**通过后再进入下一阶段**。
> 📌 **架构拆分(务必先读架构文档)**:系统分为「外网网关」+「高性能 worker(GPU)」两层。
> - **本任务书的阶段一~十(§2–§11)都在 worker 上实现**——worker 跑完整 `app.py` + `face_analysis`,用 GPU。
> - **网关是独立工作流,见 §16**(薄代理:健康检查 / 派发 / 鉴权 / 标注图落盘改 URL)。
> - 与单机版的两处关键差异:① torch 用 **GPU(CUDA)** 版;② 阶段八 handler **返回 `annotated_image_base64` 而非保存到 static + URL**(落盘改 URL 由网关做)。
> - 接口文档(对外契约)**完全不变**。
---
## 0. 背景与目标
@@ -219,38 +226,40 @@ print('numpy', numpy.__version__); print('mediapipe', mediapipe.__version__)"
## 9. 阶段八:接入 app.py
**开发步骤**
1.`app.py` 替换 `/api/v1/face/measure` 的 Mock 实现:
1.`app.py`**worker 侧**替换 `/api/v1/face/measure` 的 Mock 实现:
- 解析三选一图片输入(沿用现有 URL/base64/file 处理;URL 需下载,base64 需去前缀解码)。
- 校验:大小 ≤1MB(1006)、可解码(1008)、分辨率用**短边/长边**判断(1002,技术方案 §8.3 修订版)。**门槛做成可配置**:读环境变量 `MIN_SHORT_SIDE`(默认 1080)、`MIN_LONG_SIDE`(默认 1920),不要硬编码(见 §14 分辨率门槛说明)。
- 校验:大小 ≤1MB(1006)、可解码(1008)、分辨率用**短边/长边**判断(1002,技术方案 §8.3 修订版)。**门槛做成可配置**:读环境变量 `MIN_SHORT_SIDE`(默认 600)、`MIN_LONG_SIDE`(默认 800),不要硬编码(见 §14)。
- `detector.detect` → None 则 1001。
- `check_frontal_face` → False 则 1003。
- `hair_segmenter` 取 mask(失败传 None,由 measure 内部兜底)。
- `measure_face``create_annotated_image` → 保存到 `static/annotations/{uuid}.png` → 拼出 URL(用 `SAMPLE_IMAGE_URL` 同源的 base,即 `https://hair.xiangsilian.com/static/annotations/{uuid}.png`
- `return ok(result.to_response())`data 内含 `annotated_image_url`
2. 模型单例在模块加载时初始化(detector、segmenter),避免每请求重建。
3. 异常兜底:未预期异常返回 `err(1007, ...)`(按文档错误码定义对齐)。
- `measure_face``create_annotated_image`
- **⚠️ 拆分架构:返回 base64,不落盘不拼 URL**。`data["annotated_image_base64"] = base64(png)``return ok(data)`。落盘成 `annotated_image_url` 由网关完成(见架构文档 §9)。**worker 不写 static、不拼 hair.xiangsilian.com URL**
2. 模型单例在模块加载时初始化(detector、segmenter),避免每请求重建BiSeNet `.to('cuda' if available)`
3. **鉴权中间件**worker 增加校验 `X-Internal-Token` 的依赖/中间件,密码来自 worker 配置文件 `accept_passwords` 列表,不匹配返回 HTTP 401(架构文档 §7)。`/health` 不校验(供网关探测)。
4. 异常兜底:未预期异常返回 `err(1007, ...)`
**交付物**:更新后的 `app.py`
**验证方法**(本地起服务
> 默认门槛已是 600/800`frontal.jpg` 直接放行,无需绕过校验
**验证方法**(本地起 worker
> 默认门槛已是 600/800`frontal.jpg` 直接放行。worker 已加鉴权,需带 `X-Internal-Token` 头(值取 worker 配置的密码;本地测试可设一个测试密码)
```bash
./venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000 &
F=http://127.0.0.1:8000/api/v1/face/measure
# 0) 正常图 → code==0data 含 four_courts/seven_eyes/annotated_image_url/hairline_source/head_pose
curl -s -X POST $F -F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
# 1002) 低分辨率
curl -s -X POST $F -F image_file=@tests/fixtures/lowres.png
# 1001) 非人脸风景
curl -s -X POST $F -F image_file=@tests/fixtures/landscape.jpg
# 1008) 损坏文件
curl -s -X POST $F -F image_file=@tests/fixtures/corrupt.bin
# 1006) 超大图(>1MB,临时生成不入库)
H="X-Internal-Token: testpass" # 与本地 worker 配置一致
# 0) 正常图 → code==0data 含 four_courts/seven_eyes/annotated_image_base64/hairline_source/head_pose
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
# 1002/1001/1008/1006 同样带 -H "$H"
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/lowres.png # 1002
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/landscape.jpg # 1001
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/corrupt.bin # 1008
head -c 1100000 /dev/urandom > /tmp/oversize.bin
curl -s -X POST $F -F image_file=@/tmp/oversize.bin
curl -s -H "$H" -X POST $F -F image_file=@/tmp/oversize.bin # 1006
# 鉴权) 不带 token → HTTP 401
curl -s -o /dev/null -w "%{http_code}\n" -X POST $F -F image_file=@tests/fixtures/frontal.jpg # 401
# 1003) 侧脸:仓库无素材,用 mock 大 yaw 在单测中覆盖
```
- 访问返回的 `annotated_image_url` 对应的本地文件存在
- 正常用例 data 含 `annotated_image_base64`**不是** URL——落盘改 URL 由网关做,见 §16);base64 解码后是合法 PNG
- 不带 token 返回 401`/health` 不需要 token。
- `/docs` Swagger 正常加载,该接口 schema 未破坏。
**完成标准**0/1002/1001/1008/1006 五类用例返回正确 code(有现成夹具);1003 用 mock 覆盖;正常用例 data 结构与文档一致。
@@ -280,37 +289,52 @@ curl -s -X POST $F -F image_file=@/tmp/oversize.bin
---
## 11. 阶段十:部署与冒烟
## 11. 阶段十:worker 部署与冒烟(GPU 机 :28187
**开发步骤**
1. 确认 `hair.service`(systemd)无需改动即可加载新依赖;若新增 torch 导致启动变慢,记录冷启动耗时
2. 部署脚本补一步 `scripts/download_weights.sh`(生产机拉权重)
3. 重启服务,跑线上冒烟
1. 在 GPU 机部署完整 `app.py` + `face_analysis` + 模型权重(见 `OFFLINE_ASSETS.md`);torch 用 CUDA 版
2. resnet18 骨干放入 torch 缓存(`~/.cache/torch/hub/checkpoints/`),避免联网下载
3. worker 配置文件写好 `accept_passwords`uvicorn 监听 `0.0.0.0:28187`;防火墙只放行网关 IP
4. systemd 管理 worker 进程;确认 `/health` 模型就绪后才返回 200。
**交付物**更新的部署说明(写进 `CLAUDE.md``docs/`
**交付物**worker 部署说明 + systemd unit + worker 配置文件示例
**验证方法**
**验证方法**(在网关机或被放行的机器上)
```bash
sudo systemctl restart hair && sudo systemctl status hair
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/measure \
TOK="X-Internal-Token: <worker配置的密码>"
# 0) 正常请求(直连 worker)→ code==0data 含 annotated_image_base64
curl -s -H "$TOK" -X POST http://<worker>:28187/api/v1/face/measure \
-F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
# 期望 code==0annotated_image_url 可公网访问(curl -I 返回 200)
journalctl -u hair -n 50 # 无 ERROR/Traceback
# 鉴权) 不带 token → 401
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://<worker>:28187/api/v1/face/measure -F image_file=@tests/fixtures/frontal.jpg
# 就绪) /health → 200
curl -s http://<worker>:28187/health
nvidia-smi # 确认推理时 GPU 被占用
```
**完成标准**线上接口返回真实数据,标注图可访问,日志无异常
**完成标准**worker 直连返回真实数据(base64 图)、鉴权生效、`/health` 就绪、GPU 在用
> 端到端(经网关的 HTTPS 冒烟)见 §16 网关工作流的验证。
---
## 12. 总交付清单(Definition of Done
- [ ] `requirements.txt` / `.gitignore` / `scripts/download_weights.sh`
**worker 侧(算法)**
- [ ] `requirements.txt`torch CUDA 版)/ `.gitignore` / `scripts/download_weights.sh`
- [ ] `face_analysis/``detector.py``pose.py``calibration.py``hair_segmenter.py``measure.py``annotation.py``face_mesh_landmarks.py``fonts/``weights/`
- [ ] `app.py``/api/v1/face/measure` 真实实现(移除该接口 Mock
- [ ] `tests/`fixtures + 单元 + 集成 + 数值回归,`pytest` 全绿
- [ ] 线上冒烟通过,标注图可公网访问
- [ ] 文档:实测基线数值表 + 部署说明更新
- [ ] 返回 data 含新增字段 `hairline_source``head_pose`其余字段与 `docs/接口文档.md` 对齐
- [ ] `app.py``/api/v1/face/measure` 真实实现(移除该接口 Mock**返回 `annotated_image_base64`**
- [ ] worker 鉴权中间件(`X-Internal-Token`+ 配置文件 + `/health` 就绪态
- [ ] `tests/`fixtures + 单元 + 集成 + 数值回归 + §15 三层精度,`pytest` 全绿
- [ ] workerGPU 机 :28187)部署冒烟通过,GPU 在用
- [ ] worker 返回 data 含 `annotated_image_base64``hairline_source``head_pose`业务字段与 `docs/接口文档.md` 对齐
**网关侧(见 §16**
- [ ] 网关代理 5 个接口,对外契约/字段/URL 与接口文档**完全一致**
- [ ] worker 池健康检查 + 空闲派发(并发=worker 数)+ 全忙排队/无后端→1007
- [ ] 共享密码鉴权(配置文件,可轮换)
- [ ] base64 → 落盘 `static/annotations/` → 改写 `annotated_image_url`
- [ ] 端到端 HTTPS 冒烟:`https://hair.xiangsilian.com/...` 返回真实数据、`annotated_image_url` 可公网访问
---
@@ -476,4 +500,50 @@ def test_landmark_overlay():
---
> **任务书版本**: v1.3 **创建日期**: 2026-06-13v1.3:新增 §15 三层精度验证策略 + 合成真值生成器)| 配套技术方案 v2.0
## 16. 网关工作流(独立于上面 worker 阶段)
> 完整设计见 [`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)。网关是**新建的薄代理应用**,与 worker 算法解耦,可并行开发。worker 至少完成阶段八(能直连返回 base64)后即可联调。
### 16.1 网关骨架与配置
**开发步骤**
1. 新建 `gateway/`(独立 FastAPI/asgi 应用):`gateway/app.py``gateway/config.example.json``gateway/pool.py`(健康池+派发)、`gateway/forward.py`(转发+图片改写)。
2. 加载 `config.json`(架构文档 §4 schema):worker 列表、密码、健康检查/派发参数。`config.json``.gitignore`,仓库只放 `config.example.json`
**验证**:启动网关,加载配置无误,打印解析出的 worker 列表与参数。
### 16.2 健康检查 + 派发(并发=worker 数)
**开发步骤**
1. 后台任务周期轮询每个 worker `/health`(带 token),按 `unhealthy/healthy_threshold` 维护在线池(架构 §5)。
2. 每个 worker 维护忙/闲状态;新请求选一个**空闲健康** worker 派发,并发=1/worker;全忙则排队至 `queue_wait_seconds`,超时→1007;池空→1007(架构 §6/§10)。
**验证**
- 配 2 个 worker,并发打 3 个请求:前 2 个并行、第 3 个排队;返回均正常。
- 停掉 1 个 worker:健康检查在 `interval×threshold` 内将其下线,请求自动只走存活的。
- 全停:请求返回 `code==1007`
### 16.3 鉴权 + 转发 + 标注图改写
**开发步骤**
1. 转发:原样透传客户端请求体,附加 `X-Internal-Token`(架构 §7/§8)。worker 的 10011008 原样透传给客户端。
2. **图片改写**worker 响应里的 `*_base64` 字段 → 解码落盘 `static/annotations/{uuid}.png` → 删除 base64、写入对应 `*_url`=`https://hair.xiangsilian.com/static/...`(架构 §9)。映射表以接口文档为准(接口1:`annotated_image`)。
3. 网关托管 `/static/*`;加标注图定期清理任务。
4. 转发失败按 `max_retries` 换 worker 重试;耗尽→1007。
**验证(端到端,最终冒烟)**
```bash
# 经网关 HTTPS(对外,无需 token——token 是网关→worker 内部的)
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/measure \
-F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
# 期望:与接口文档完全一致——data 含 annotated_image_url(不是 base64
curl -sI https://hair.xiangsilian.com/static/annotations/<uuid>.png # 200
```
- **对外响应字段/URL 与接口文档逐一一致**(客户端无感知拆分)。
- 直接打 worker 不带 token → 401;经网关正常。
**网关完成标准**:5 接口代理正常;多 worker 并发与排队符合预期;后端全挂→1007;标注图经网关落盘且公网可访问;**对外契约零变化**。
---
> **任务书版本**: v1.4 **创建日期**: 2026-06-13v1.4:适配网关+worker 拆分架构——worker 返 base64/GPU/鉴权,新增 §16 网关工作流)| 配套技术方案 v2.0 / 系统架构 v1.0