diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..14a6325 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,33 @@ +# 文档索引(按开发机器划分) + +系统拆分为两台机器开发:**外网网关** 和 **高性能 worker(GPU)**。下面标清每台机器该读哪些文档。 + +## 🌐 两端共享(都要读) + +| 文档 | 作用 | +|------|------| +| [系统架构-网关与高性能后端.md](系统架构-网关与高性能后端.md) | 两层拆分的总设计与契约:拓扑、职责、配置、鉴权、base64→URL、错误处理。**两端的接口约定,先读。** | +| [接口文档.md](接口文档.md) | 对外 API 契约(字段/错误码)。**拆分后保持不变**,是字段命名的唯一权威。 | +| [旷视具体需求.md](旷视具体需求.md) | 原始需求 | + +## 🖥️ 高性能 worker 机(GPU) + +跑完整 `app.py` + `face_analysis`,做真正的算法。 + +| 文档 | 作用 | +|------|------| +| [接口1-四庭七眼测量-技术实现方案.md](接口1-四庭七眼测量-技术实现方案.md) | 四庭七眼算法方案(MediaPipe / BiSeNet / 标定 / 标注图 / 误差) | +| [接口1-四庭七眼测量-开发任务书.md](接口1-四庭七眼测量-开发任务书.md) | worker 侧开发任务书(阶段一~十 + 精度验证),AI agent 执行 | +| [../OFFLINE_ASSETS.md](../OFFLINE_ASSETS.md) | 离线模型权重/字体清单(内网部署)。worker 需要这些权重 | + +## 🚪 外网网关机 + +薄反向代理,不跑算法。 + +| 文档 | 作用 | +|------|------| +| [网关-开发任务书.md](网关-开发任务书.md) | 网关侧开发任务书(健康检查 / 派发 / 鉴权 / base64→URL / 部署),AI agent 执行 | + +--- + +> 开发顺序建议:worker 先跑通(本机即可开发验证)→ worker 部署到 GPU 机 → 网关接入联调。网关在 worker 就绪前可用本地 stub worker 独立开发。 diff --git a/docs/接口1-四庭七眼测量-开发任务书.md b/docs/接口1-四庭七眼测量-开发任务书.md index 932d529..fc47c06 100644 --- a/docs/接口1-四庭七眼测量-开发任务书.md +++ b/docs/接口1-四庭七眼测量-开发任务书.md @@ -1,13 +1,14 @@ -# 接口 1:四庭七眼测量 — 开发任务书(AI Agent 执行版) +# 接口 1:四庭七眼测量 — 开发任务书(worker 侧 · AI Agent 执行版) +> **在高性能 worker(GPU 机)上开发。** 本任务书只负责 worker 侧算法逻辑。 > 配套技术方案:[`接口1-四庭七眼测量-技术实现方案.md`](接口1-四庭七眼测量-技术实现方案.md) -> 系统架构:[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md) +> 系统架构(两端共享契约,先读):[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.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 由网关做)。 +> 📌 **本机职责**:worker 跑完整 `app.py` + `face_analysis`,用 GPU。与单机版的两处关键差异: +> - ① torch 用 **GPU(CUDA)** 版; +> - ② 阶段八 handler **返回 `annotated_image_base64`,不保存到 static、不拼 URL**(落盘改 URL 由网关做)。 > - 接口文档(对外契约)**完全不变**。 --- @@ -258,7 +259,7 @@ curl -s -H "$H" -X POST $F -F image_file=@/tmp/oversize.bin # 1006 curl -s -o /dev/null -w "%{http_code}\n" -X POST $F -F image_file=@tests/fixtures/frontal.jpg # 401 # 1003) 侧脸:仓库无素材,用 mock 大 yaw 在单测中覆盖 ``` -- 正常用例 data 含 `annotated_image_base64`(**不是** URL——落盘改 URL 由网关做,见 §16);base64 解码后是合法 PNG。 +- 正常用例 data 含 `annotated_image_base64`(**不是** URL——落盘改 URL 由网关做,见网关任务书);base64 解码后是合法 PNG。 - 不带 token 返回 401;`/health` 不需要 token。 - `/docs` Swagger 正常加载,该接口 schema 未破坏。 @@ -314,13 +315,12 @@ nvidia-smi # 确认推理时 GPU 被占用 **完成标准**:worker 直连返回真实数据(base64 图)、鉴权生效、`/health` 就绪、GPU 在用。 -> 端到端(经网关的 HTTPS 冒烟)见 §16 网关工作流的验证。 +> 端到端(经网关的 HTTPS 冒烟)见 [`网关-开发任务书.md`](网关-开发任务书.md) 的验证。 --- -## 12. 总交付清单(Definition of Done) +## 12. 总交付清单(worker 侧 Definition of Done) -**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),**返回 `annotated_image_base64`** @@ -329,12 +329,7 @@ nvidia-smi # 确认推理时 GPU 被占用 - [ ] worker(GPU 机 :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` 可公网访问 +> 网关侧的交付清单在 [`网关-开发任务书.md`](网关-开发任务书.md)(另一台机器开发),本任务书不含。 --- @@ -500,50 +495,4 @@ def test_landmark_overlay(): --- -## 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 的 1001–1008 原样透传给客户端。 -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/.png # 200 -``` -- **对外响应字段/URL 与接口文档逐一一致**(客户端无感知拆分)。 -- 直接打 worker 不带 token → 401;经网关正常。 - -**网关完成标准**:5 接口代理正常;多 worker 并发与排队符合预期;后端全挂→1007;标注图经网关落盘且公网可访问;**对外契约零变化**。 - ---- - -> **任务书版本**: v1.4 | **创建日期**: 2026-06-13(v1.4:适配网关+worker 拆分架构——worker 返 base64/GPU/鉴权,新增 §16 网关工作流)| 配套技术方案 v2.0 / 系统架构 v1.0 +> **任务书版本**: v1.5 | **创建日期**: 2026-06-13(v1.5:拆出网关任务书到独立文档,本书聚焦 worker 侧)| 配套技术方案 v2.0 / 系统架构 v1.0 / 网关任务书 v1.0 diff --git a/docs/网关-开发任务书.md b/docs/网关-开发任务书.md new file mode 100644 index 0000000..7f27e53 --- /dev/null +++ b/docs/网关-开发任务书.md @@ -0,0 +1,198 @@ +# 外网网关 — 开发任务书(AI Agent 执行版) + +> 在 **外网机(`hair.xiangsilian.com`)** 上开发。本任务书自包含,只负责**网关**这一层。 +> 配套:[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)(两端共享的契约,**先读**)。 +> worker(GPU 机)侧算法由另一份任务书负责:`接口1-四庭七眼测量-开发任务书.md`,本机不涉及。 + +> 执行者:AI coding agent。**严格按阶段顺序**执行,每阶段跑完「验证方法」通过后再进入下一阶段。 + +--- + +## 0. 网关是什么 / 不是什么 + +- **是**:一个无状态的**薄反向代理**。对外保持 HTTPS 接口与接口文档**完全不变**;对内把请求转发给高性能 worker 池,做健康检查、空闲派发、鉴权、把 worker 返回的 base64 标注图落盘成对外 URL。 +- **不是**:不跑任何算法(无 MediaPipe / torch / BiSeNet);不解析业务入参;不持有业务状态。所有业务逻辑和错误码(1001–1008)都来自 worker,网关原样透传。 + +**网关唯一自行产生的错误**:无可用 worker / 全忙排队超时 → `code: 1007`(复用系统错误,不新增错误码)。 + +--- + +## 1. 总体约束 + +1. **对外契约零变化**:客户端看到的 URL、请求方式(multipart/form-data,三选一图片)、响应结构 `{code,message,request_id,data}`、字段名、错误码,全部与 `docs/接口文档.md` 一致。客户端**无感知**拆分。 +2. **代理全部 5 个接口**:`/api/v1/face/measure`、`/hair/grow`、`/hair/grow-b`、`/face/features`、`/hairline/generate`。worker 跑完整 app,网关统一代理。 +3. **技术栈**:Python + FastAPI + `httpx`(异步转发)+ uvicorn。**不引入** torch/mediapipe/opencv 等重依赖,网关保持轻量。 +4. **无状态**:除"健康池 + worker 忙闲状态"这点运行时状态外,不持久化业务数据。 +5. 密码、worker 列表等走**配置文件**,不硬编码、不入 git。 + +--- + +## 2. 配置文件 + +`gateway/config.json`(运维维护,**入 `.gitignore`**;仓库只放 `gateway/config.example.json`): + +```json +{ + "workers": [ + "http://hair.xiangsilian.com:28187", + "http://10.0.0.12:28187" + ], + "shared_password": "REPLACE_ME_ROTATE_PERIODICALLY", + "public_base_url": "https://hair.xiangsilian.com", + "static_dir": "static/annotations", + "health_check": { + "path": "/health", + "interval_seconds": 8, + "timeout_seconds": 3, + "unhealthy_threshold": 2, + "healthy_threshold": 1 + }, + "dispatch": { + "per_worker_concurrency": 1, + "queue_wait_seconds": 30, + "request_timeout_seconds": 60, + "retry_on_failure": true, + "max_retries": 1 + } +} +``` + +字段含义见架构文档 §4。要点:`workers` 手动增删(**加一个就多一路并发**);`per_worker_concurrency` 固定 1;`shared_password` 网关调用 worker 时通过 `X-Internal-Token` 头发送。 + +--- + +## 3. 阶段一:骨架 + 配置加载 + +**开发步骤** +1. 新建目录: + ``` + gateway/ + ├── app.py # FastAPI 应用 + 5 接口代理路由 + ├── config.py # 读取/校验 config.json + ├── config.example.json + ├── pool.py # 健康池 + worker 忙闲状态 + 派发 + ├── forward.py # httpx 转发 + base64→URL 改写 + └── __init__.py + static/annotations/ # 标注图落盘目录(.gitkeep) + ``` +2. `config.py`:加载 `config.json`,缺字段给默认值,启动时校验 `workers` 非空、密码非占位值(占位值打 WARNING)。 +3. `app.py`:建 FastAPI 应用,挂载 `/static`,加 `/gateway-health`(网关自身健康,区别于 worker 的 `/health`)。 +4. 更新根 `.gitignore`:忽略 `gateway/config.json`、`static/annotations/*`(保留 `.gitkeep`)。 + +**验证** +```bash +./venv/bin/uvicorn gateway.app:app --host 127.0.0.1 --port 8080 & +curl -s http://127.0.0.1:8080/gateway-health # 200 +``` +启动日志打印解析出的 worker 列表与派发参数。 + +**完成标准**:网关起得来,配置正确加载,无 worker 时也不崩。 + +--- + +## 4. 阶段二:健康检查 + 空闲派发(并发=worker 数) + +**开发步骤** +1. `pool.py`: + - 后台 asyncio 任务,每 `interval_seconds` 给每个 worker 发 `GET {worker}/health`(带 `X-Internal-Token`,`timeout_seconds` 超时)。 + - 连续失败 `unhealthy_threshold` 次 → 下线;重新连续成功 `healthy_threshold` 次 → 上线。维护在线集合。 + - 每个 worker 一个 `asyncio.Lock`(或容量=1 的信号量)表示忙闲(`per_worker_concurrency=1`)。 +2. 派发 `acquire_worker()`:从在线池里挑一个**空闲** worker 占用;全忙则 `await` 等待,最多 `queue_wait_seconds`,超时抛 `NoWorkerAvailable`;在线池为空也抛该异常。用完 `release`。 +3. 路由层捕获 `NoWorkerAvailable` → 返回 `{code:1007, message:"后端服务暂不可用,请稍后重试", ...}`。 + +**验证** +- 配 2 个**假 worker**(本地起两个返回 `/health` 200 + 简单 echo 的 stub):并发打 3 个请求 → 前 2 个并行、第 3 个排队后成功。 +- 停掉 1 个 stub → `interval×unhealthy_threshold` 内自动下线,请求只走存活的。 +- 全停 → 请求返回 `code==1007`。 +```bash +# 可用 python 起两个 stub:每个监听不同端口,/health 返回200,业务接口 sleep 1s 再回 +``` + +**完成标准**:健康检查上下线正确;并发=在线 worker 数;全忙排队、池空→1007。 + +--- + +## 5. 阶段三:鉴权头 + 转发 + 标注图改写 + +**开发步骤** +1. `forward.py`:用 `httpx.AsyncClient` 把客户端请求**原样转发**到选中的 worker 对应路径: + - 透传 method、multipart/form body、查询参数;附加 `X-Internal-Token: ` 头。 + - `request_timeout_seconds` 超时;连接失败/超时/5xx/401 视为该 worker 异常 → 标记不健康,按 `max_retries` 换 worker 重试;耗尽 → 1007。 + - worker 返回的 1001–1008 业务响应**原样透传**给客户端(不要改 code)。 +2. **base64 → URL 改写**:拿到 worker 的 JSON 响应后,对约定的图片字段: + - worker 用 `*_base64` 承载 PNG(如接口1 `annotated_image_base64`)。 + - 网关:解码 → 存 `static/annotations/{uuid}.png` → **删除 `*_base64`,新增 `*_url`** = `{public_base_url}/static/annotations/{uuid}.png`。 + - 维护一张「接口路径 → 图片字段名」映射表(接口1:`annotated_image`;接口3/5 待其实现后补;**以接口文档字段名为准**)。 +3. 5 个接口路由统一走「选 worker → 转发 → 改写 → 返回」一条链路。 + +**验证(端到端,最终冒烟)** +```bash +# 经网关(对外 HTTPS;客户端不需要 token——token 是网关→worker 内部的) +curl -s -X POST https://hair.xiangsilian.com/api/v1/face/measure \ + -F image_file=@<一张人像> | python -m json.tool +# 期望:与接口文档完全一致——data 含 annotated_image_url(不是 base64) +curl -sI https://hair.xiangsilian.com/static/annotations/.png # 200 +``` +- 对外响应字段/URL 与 `docs/接口文档.md` **逐一一致**。 +- 响应里**不应**出现 `*_base64`(已被网关消化)。 +- 直接打 worker 不带 token → 401(worker 侧行为);经网关正常。 + +**完成标准**:5 接口代理通;base64 落盘改 URL 正确;对外契约零变化;失败重试与 1007 生效。 + +--- + +## 6. 阶段四:静态托管 + 清理 + +**开发步骤** +1. 网关 `app.mount("/static", StaticFiles(directory="static"), ...)` 托管落盘的标注图。 +2. 加**定期清理**:`static/annotations/` 会持续增长,按时间(如保留 N 天)或容量清理。可用进程内定时任务或外部 cron(任选,记录方案)。 + +**验证** +- 生成的图能经 `https://hair.xiangsilian.com/static/annotations/.png` 访问(200)。 +- 清理任务按规则删除过期文件,不误删近期文件。 + +**完成标准**:静态图可公网访问;清理任务可控。 + +--- + +## 7. 阶段五:部署 + +**开发步骤** +1. nginx:443 → 网关进程(uvicorn)。沿用现有 `nginx/hair.conf` 风格。 +2. systemd 管理网关进程(可继续用 `hair.service`,或新建 `hair-gateway.service`)。 +3. `config.json` 就位(worker 列表 + 密码);`static/annotations/` 可写。 + +**验证** +```bash +sudo systemctl restart hair-gateway && sudo systemctl status hair-gateway +# 端到端冒烟(同阶段三);journalctl 无 ERROR +``` + +**完成标准**:线上经 HTTPS 走通全链路,标注图可访问,日志无异常。 + +--- + +## 8. 交付清单(网关侧 DoD) + +- [ ] `gateway/`:`app.py`、`config.py`、`pool.py`、`forward.py`、`config.example.json` +- [ ] 代理 5 个接口,对外契约/字段/URL 与接口文档**完全一致** +- [ ] worker 池健康检查(上下线)+ 空闲派发(并发=在线 worker 数)+ 全忙排队/无后端→1007 +- [ ] 共享密码鉴权(`X-Internal-Token`,配置文件,可轮换) +- [ ] base64 → 落盘 `static/annotations/` → 改写 `*_url`,响应无 `*_base64` 残留 +- [ ] 静态托管 + 定期清理 +- [ ] 端到端 HTTPS 冒烟通过,`annotated_image_url` 公网可访问 +- [ ] `config.json` 入 `.gitignore`,仓库只留 `config.example.json` + +--- + +## 9. 风险与注意 + +1. **联调依赖 worker**:阶段四之前可用**本地 stub worker**(返回 `/health` 200 + 假的 base64 图)独立开发;worker 真机就绪后再换真实地址端到端联调。 +2. **接口文档是字段唯一权威**:图片字段映射表、对外字段名都以 `docs/接口文档.md` 为准,冲突时以文档为准并在 PR 说明指出。 +3. **安全(先跑通后处理,已知项)**:`:28187` 当前 HTTP 明文 + 公网可达,密码明文传输;后续建议加 TLS / 内网 / IP 白名单(见架构文档 §13/§14)。本阶段不阻塞。 +4. **图片字段未实现的接口**:接口 2/4/5 worker 暂为 mock,网关先按"无图片字段或透传"处理,待 worker 实现对应图片后再补映射。 + +--- + +> **文档版本**: v1.0 | **创建日期**: 2026-06-14 | 配套:系统架构 v1.0 +> **关键约束**: 接口文档不变;不跑算法;每 worker 并发=1;无后端→1007;base64→落盘→URL