diff --git a/docs/README.md b/docs/README.md
index a01a53a..201f07c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,6 +7,7 @@
|------|------|
| [实现说明.md](实现说明.md) | **实现总览**:架构、五个接口怎么实现、base64→URL 映射、错误码、部署/环境要点。先读这份。 |
| [接口文档.md](接口文档.md) | 对外 API 契约(字段 / 错误码)。字段命名的唯一权威。 |
+| [接口52网关接入.md](接口52网关接入.md) | 52号请求透传、图片URL与空字段约定、公开测试页及网关部署步骤。 |
| [旷视具体需求.md](旷视具体需求.md) | 原始需求。 |
| [../OFFLINE_ASSETS.md](../OFFLINE_ASSETS.md) | worker 离线模型权重/字体清单(内网部署)。 |
| [最近一周调用统计_20260915-20260921.md](最近一周调用统计_20260915-20260921.md) | 2026-09-15~09-21 网关调用量、接口构成、成功率。 |
diff --git a/docs/实现说明.md b/docs/实现说明.md
index 522aa89..752313a 100644
--- a/docs/实现说明.md
+++ b/docs/实现说明.md
@@ -98,6 +98,11 @@
- **做什么**:入参同接口2(`gender` + 多选 `hair_style` 必填)。对每个选中发型 → `middle`/`high`/`low` 三档发际线叠图 + 生发图 + 首个选中发型的面部中间点坐标。
- **怎么实现**:复用接口2 的 502 点渲染管线,三档分别用 `hairline_texture[/_high|/_low]` 同名贴图渲染叠图;生发同接口2(ComfyUI inpaint),**黑模板固定取 `hairline_texture_black/`(middle)**,每发型 1 张生发图。`best_hairline_center_point`=眉心 x × 首个选中发型 middle 档发际线 y。
+### 接口52 发际线新规则 `/api/v1/hairline/generate-52`(网关透传)
+- `main`新增网关路由和`static/test_interface52.html`。GPU worker负责中下庭均值定位、扶正裁切及生发;主备worker均需部署52号实现,网关只透传表单并附加内部Token。
+- 非空图片使用原有`*_base64`→`*_url`落盘改写;空图片保留`*_base64: null`。metadata和坐标原样返回,middle叠加到`input_image_url`。旧接口、5号队列优先级及网关IP策略保持原样。
+- 参数、空字段与部署步骤见[接口52网关接入](接口52网关接入.md)。worker分支的IP限制与算法不合入本次网关改动。
+
### 接口9 头发遮罩 `/api/v1/head/mask`(worker)
- **做什么**:人像照 → 头发遮罩 + 全流程分步可视化。
- **怎么实现**(`face_analysis/head_mask.py`):MediaPipe 底部分割线(21/68/104/69/108/151/337/299/333/298/251 连线,21/251 水平延伸)→ 线以上为上半区 → BiSeNet 与 SegFormer 双路头发分割、逐列向下填充到分割线得闭合区域(头发+额头)→ 外缘朝 151 内缩 `erode_cm`(默认1.2cm,虹膜标定) → 最终遮罩。返回 `steps_common` + `bisenet`/`segformer` 两组各四张图。
diff --git a/docs/接口52网关接入.md b/docs/接口52网关接入.md
new file mode 100644
index 0000000..f83b78d
--- /dev/null
+++ b/docs/接口52网关接入.md
@@ -0,0 +1,65 @@
+# 接口52:网关接入与部署
+
+`main`用于网关部署,本次只加入网关调用52号所需的路由、公开测试页及契约。GPU worker继续使用已部署的`ubuntu3090_debug`版本(`1c51f3e`及后续兼容版本),负责算法和本机IP限制;本次不把worker的算法、启动参数或IP中间件合并到网关。
+
+## 调用流程
+
+客户端 → `POST /api/v1/hairline/generate-52` → 网关现有队列和worker池 → worker同路径 → 网关落盘非空图片并返回公网URL。
+
+- 请求体原样转发,网关附加配置中的`X-Internal-Token`;客户端和公开测试页不携带内部密码。
+- 沿用现有主备派发、隔离期、重试、日志和全局并发限制。原有5号插队优先级保持原样,52号走默认队列。
+- 5号`/api/v1/hairline/generate`和其他接口保持原样;网关不新增IP限制。
+- worker的中下庭规则、扶正、3:4裁切和生发结果由同一次worker响应提供,网关不重新计算坐标。
+
+## 请求
+
+使用`multipart/form-data`;图片三选一:`image_file`、`image_url`、`image_base64`。必填`gender=male/female`和`hair_style=1..7`(支持逗号分隔多选)。
+
+可选参数全部透传:
+
+| 参数 | 默认值 | 说明 |
+|---|---|---|
+| normalize_image | false | true先扶正9→152并按当前规则裁切;网关测试页主动开启 |
+| generate_grow_image | true | false跳过ComfyUI,快速检查曲线和坐标 |
+| use_mask | true | 是否使用生发遮罩 |
+| prompt | 填充遮罩区域的头发,人物增加一点磨皮 | 生发提示词 |
+
+当前标准裁切高约`(3÷0.9)L`、宽`2.5L`,L为9→152像素距离;标准窗口不足时长图尝试保留人脸裁成3:4,其余仅扶正。具体状态以`framing`为准。
+
+## 响应及空图片
+
+worker始终返回`*_base64`。网关沿用原有公共改写,不添加52号专用的空字段改名规则:
+
+| worker字段 | 网关字段 |
+|---|---|
+| image_middle_base64(非空PNG) | image_middle_url |
+| input_image_base64、original_image_base64(非空JPEG) | input_image_url、original_image_url |
+| grown_image_base64(非空JPEG) | grown_image_url |
+| image_high_base64、image_low_base64(null) | 原字段保留,值为null |
+| grown_image_base64(null) | 原字段保留,值为null;没有grown_image_url |
+
+`height_rule`、每个样式的`height_placement`、`guide_landmarks`、`face_measure`、`framing`、`width/height`和中心点原样返回。开启预处理时`coordinate_space=processed_image`,middle必须叠加到`input_image_url`,原始图`original_image_url`仅供对照。
+
+公开测试页:`/static/test_interface52.html`,按原图、middle、矩形裁切发际线叠加、生发图排列。矩形左/右边过162/389,底边过9,高为9→152的70%。worker的30案例调试接口和对比报告不在本次网关同步范围。
+
+## 部署步骤
+
+1. 网关拉取`main`,保留原有`gateway/config.json`。
+2. 确保池中所有可能被选中的主/备worker都已提供`/api/v1/hairline/generate-52`;仅更新网关无法让旧worker获得52号功能。网关出口IP须在对应worker白名单内,当前GPU worker已允许`49.232.51.171`。无需在网关配置IP白名单。
+3. 重启网关服务(本仓库示例名`hair-gateway`)。若线上使用`nginx/hair-gateway.conf`,同步其中的读取超时并检查、重载Nginx。单张高分辨率生发已观察到超过100秒,本次示例从90秒改为660秒,覆盖默认600秒上游单次请求超时;多样式或重试时按总耗时另行调整。
+4. 先以`generate_grow_image=false`检查请求转发和URL,再在实际需要时验证生发。`/monitor`、worker主备配置和旧接口无需调整。
+
+```bash
+curl -X POST https://hair.xiangsilian.com/api/v1/hairline/generate-52 \
+ -F 'image_file=@/path/to/photo.jpg' \
+ -F 'gender=male' -F 'hair_style=1' \
+ -F 'normalize_image=true' -F 'generate_grow_image=false'
+```
+
+成功时`code=0`,检查`input_image_url`和`hairline_images[].image_middle_url`可打开、尺寸一致;high/low仍为`*_base64: null`。访问测试页时无需输入内部Token。
+
+## 验证
+
+`tests/test_gateway_interface52.py`覆盖5/52路由分离、完整表单透传、由网关注入内部Token、真实公共转发函数的图片落盘与URL可访问、空字段保持原样和队列槽释放。现有pool、serialization、monitor、reqlog测试用于防止回归。
+
+2026-11-04验证结果:上述网关测试共42项通过。另起本机临时网关连接当前真实GPU worker,浏览器验证旧5号仍返回三档,52号女性7型均返回可访问的图片URL,长图输出1062×1416,矩形叠加与空字段正常,页面请求不携带内部Token,无脚本错误。本轮关闭生发以验证网关链路,未重新运行ComfyUI生发;临时网关验证后关闭。
diff --git a/docs/接口文档.md b/docs/接口文档.md
index 6d57521..4605ccb 100644
--- a/docs/接口文档.md
+++ b/docs/接口文档.md
@@ -3,7 +3,7 @@
> 本文档依据《旷视具体需求.docx》整理,描述各接口的**输入**与**输出**。
> 文中标记为 **【待确认】** 的内容为需求文档中尚未明确、需与对方对齐的点。
-> **当前为第一版 Mock 服务**:所有接口已上线,但**不做真实算法计算**,统一返回下文「响应示例」中的默认值;所有图片字段均返回示例图 `https://hair.xiangsilian.com/static/sample.jpg`。真实逻辑后续接入。
+> 下文示例用于说明响应结构。接口52由网关转发至GPU worker执行真实计算,具体接入与部署见[接口52网关接入](接口52网关接入.md)。
---
@@ -20,6 +20,7 @@
| 3 B 端生发 | POST | `/api/v1/hair/grow-b` |
| 4 用户特征 | POST | `/api/v1/face/features` |
| 5 发际线 PNG 生成 | POST | `/api/v1/hairline/generate` |
+| 52 发际线新规则 | POST | `/api/v1/hairline/generate-52` |
---
@@ -519,6 +520,153 @@
---
+## 接口 52:发际线 PNG 生成接口
+
+**说明**:入参同接口2(先选性别、再多选发型)。对每个选中发型返回一张 `middle` 设计叠图与生发图。**目标上庭 =(中庭 + 下庭)÷2**,从眉心向上定位;所有形状的中点对齐同一目标。`high` / `low` 已停用,旧字段保留为 `null`。
+
+**请求**:`POST /api/v1/hairline/generate-52`(网关透传至同路径GPU worker)
+
+**职责**:GPU worker返回base64并完成算法处理,网关负责透传参数、附加内部Token、保存非空图片及返回公网URL。网关不增加IP限制;网关出口IP须已在worker白名单内。部署与完整字段约定见[接口52网关接入](接口52网关接入.md)。
+
+### 图片扶正与标准构图
+
+显式传`normalize_image=true`时,先扶正9→152,再按**高(3÷0.9)×L≈3.3333×L、宽2.5×L(3:4)**裁切,L为9→152的直线像素距离。相对原3L规则,裁切框宽高均扩大约11.11%,同样显示尺寸下人物缩小约10%。高度取最接近的4的倍数,宽度取高度的3/4。眉心定位沿用已扶正参考图:9号点相对位置约(49.0061%,34.4136%)。参考图仍为786×1048,两点X均为385.18784。
+
+目标窗口完全在原图有效区域内才进行标准裁切。标准窗口不足时,偏长图片(宽/高<3/4)采用原图有效范围内最大的3:4窗口,尽量贴近眉心目标位置并保留完整人脸,`status=aspect_crop`;人脸放不进3:4窗口时回退仅扶正并返回原因。其余图片空间不足时仍仅扶正、去除旋转空角。不放大小图。**所有叠图与坐标使用响应中的处理后底图 `input_image_*`**,调用方应使用该底图叠加;`normalize_image=false` 可关闭此预处理。
+
+`framing`包含处理前后尺寸、旋转角、是否标准裁切、跳过原因、`source_to_processed`/`processed_to_source`仿射矩阵。`status`区分`reference_crop`(标准窗口)、`aspect_crop`(长图3:4裁切)和`rotation_only`(仅扶正);`standard_crop=false`时仍可能有`aspect_crop=true`。`aspect_crop_skip_reason`说明长图无法保留人脸时的回退原因。`original_image_*`保留EXIF方向修正后的上传图,原图坐标可通过逆矩阵还原。
+
+### 输入
+
+图片参数见「通用约定 → 图片传参字段」。专属参数:
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 7 / male 7)。缺失/非法返回 `1004` |
+| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),决定返回哪些发际线类型。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal, 7=wave。缺失/越界/非法返回 `1007` |
+| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
+| prompt | string | 否 | ComfyUI 提示词,默认「填充遮罩区域的头发,人物增加一点磨皮」,会替换工作流节点 60 的文本 |
+| normalize_image | bool | 否 | 新增可选开关,默认false:保留原图尺寸与坐标。传true开启扶正和裁切;网关测试页显式传true |
+| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回设计叠图、中心点与高度规则数据,可大幅降低耗时 |
+
+> 生发划线与遮罩直接取新位置的 middle 曲线;旧规则的头发边界与模板用于保留形状和倾斜,不决定新中心高度。计算使用未取整的像素高度,与四庭测量采用同一 Y 方向;测量主流程失败时用发际线管线的同编号关键点计算,`measurement_source` 标明来源。目标越出图片或无法对齐时返回 1007,不静默回退旧规则。
+
+### 高度规则与对比
+
+- **参数与字段**:原有8个请求参数、响应外层结构和所有旧返回字段均保留。`use_mask=true`、默认prompt、`generate_grow_image=true`保持原值。新功能只新增可选参数和返回字段;旧请求未传`normalize_image`时保留原图坐标,high/low按已约定保留null,worker始终返回`*_base64`;网关沿用原有规则,有图片时改为`*_url`,空图片保留`*_base64: null`。
+- `height_rule`:`middle_height_px`、`lower_height_px`、`target_upper_height_px`、`target_upper_height_cm`(可空)、`target_center`、`brow_center`、`nose_bottom`、`chin_tip`、`measurement_source`。
+- 每项 `height_placement`:`old_center`、`new_center`、`shift_x_px`、`shift_y_px`(正=下移)、`target_error_px`。浮点坐标保留精度,旧中心点字段继续返回取整像素。
+- `face_measure` 为处理后底图的实际测量值,未被设计目标覆盖。
+- `guide_landmarks`:9、150、152、162、251、389 号 MediaPipe 点的图片像素坐标,供对比页绘制参考框。参考框左/右边分别经过162/389,底边经过9,高度为9→152距离的70%;两侧均固定平行9→152。
+- 网关测试页:`/static/test_interface52.html`。worker本机另有旧/新规则对比页,本次不向网关开放调试接口。
+- 规则页每个发型依次展示未裁切原图、middle、裁切发际线叠加、生发图。“裁切发际线叠加”由页面按`guide_landmarks`生成:在相同处理后底图上叠加矩形内的middle PNG,矩形外保持透明;左/右边过162/389,底边过9,高度0.7×9→152,左右边平行9→152。
+
+### 网关输出(data)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| hairline_images | object[] | **选中发型**列表,**数量 = 所选发型数**,元素见下表 |
+| best_hairline_center_point | object \| null | **首个选中发型**的 **middle 档**发际线曲线「面部中间点」坐标,当前底图像素:`{ "x": number, "y": number }` |
+| high_hairline_center_point | object \| null | 已停用,固定 `null` |
+| low_hairline_center_point | object \| null | 已停用,固定 `null` |
+| input_image_url | string | 当前底图JPEG的URL,发际线叠图、参考点和测量值均使用该图的坐标系 |
+| original_image_url | string | EXIF方向修正后的上传原图JPEG的URL |
+| width / height | int | 当前底图的实际像素宽 / 高 |
+| coordinate_space | string | 开启预处理时为 `processed_image`,关闭时为 `original_image` |
+| framing | object \| null | 裁切状态、旋转角、前后尺寸、轴线误差及双向仿射矩阵;关闭预处理时为 `null` |
+| height_rule | object | 中庭/下庭、目标上庭高度、目标中心及测量来源,详见“高度规则与对比” |
+| guide_landmarks | object | 9、150、152、162、251、389 号点的当前底图像素坐标;参考框实际使用9、152、162、389 |
+| face_measure | object \| null | **复用接口1**的四庭七眼测量**数值**(不含标注图)。独立流程,测量失败(无人脸/非正面/分割失败)时为 `null`,不影响发际线主结果。字段结构见下表 |
+
+`hairline_images` 元素:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`/`wave`(male) |
+| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加 `input_image_url` 显示) |
+| image_high_base64 | null | 已停用,保留字段为 `null` |
+| image_low_base64 | null | 已停用,保留字段为 `null` |
+| grown_image_url | string | 生发成功时的完整人像JPEG URL |
+| grown_image_base64 | null | 生发失败或`generate_grow_image=false`时返回;此时没有`grown_image_url` |
+| order | int | 发型序号(= 传入的 hair_style 值) |
+| height_placement | object | 旧/新中心、XY移动距离、目标误差,均为未取整的像素值 |
+
+> 上表为网关响应字段。worker直连时所有图片字段均为`*_base64`;网关仅对有图片内容的字段生成`*_url`,high/low等空字段保留`*_base64: null`,沿用既有行为。
+>
+> middle透明PNG叠加到`input_image_url`,生发图直接显示。网关测试页为`/static/test_interface52.html`,页面不包含内部Token。
+
+`face_measure` 元素(与[接口1](#接口-1四庭七眼测量标注接口)的 `data` 同构,**不含** `annotated_image_*` 标注图字段):
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| face_total_height_cm | number | 全脸总高度(cm)= 四庭之和 |
+| four_courts | object | 四庭数据(顶/上/中/下庭 cm + 占比 ratios),结构同接口1 |
+| seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距 cm + 占比 ratios + eye1~eye7 从左到右 7 段宽度),结构同接口1 |
+| landmarks | object | 5 个纵向关键点像素坐标(hair_top/hairline/brow_center/nose_bottom/chin_tip),结构同接口1 |
+| hairline_source | string | 发际线来源:`segmentation`(真实分割)/ `estimated`(比例估算)/ `corrected`(发际线贴近头顶、分割不可靠,已按 0.95×中庭反推修正) |
+| head_pose | object | 头部姿态角度(yaw/pitch/roll,单位:度) |
+| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),当前底图像素:`{ "x": int, "y": int }` |
+| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),当前底图像素:`{ "x": int, "y": int }` |
+
+> `eye1`~`eye7` 为从左到右共 7 段宽度,eye1=左耳外段、eye7=右耳外段,某侧耳朵不可见时对应段为 `null`。详见接口1说明。
+
+### 网关响应结构示例(normalize_image=false)
+
+此例未开启裁切,坐标基于原图;`face_measure=null`时仍可按发际线关键点计算目标。有图片内容的字段已落盘为公网URL;空图片字段仍返回null,保留base64字段名。
+
+```json
+{
+ "code": 0,
+ "message": "success",
+ "request_id": "mock-request-id",
+ "data": {
+ "hairline_images": [
+ {
+ "hairline_type": "ellipse",
+ "image_middle_url": "https://hair.xiangsilian.com/static/annotations/middle.png",
+ "image_high_base64": null,
+ "image_low_base64": null,
+ "grown_image_url": "https://hair.xiangsilian.com/static/annotations/grown.jpg",
+ "order": 1,
+ "height_placement": {
+ "old_center": {"x": 540.0, "y": 450.0},
+ "new_center": {"x": 540.0, "y": 430.0},
+ "shift_x_px": 0.0, "shift_y_px": -20.0, "target_error_px": 0.0
+ }
+ }
+ ],
+ "best_hairline_center_point": {"x": 540, "y": 430},
+ "high_hairline_center_point": null,
+ "low_hairline_center_point": null,
+ "input_image_url": "https://hair.xiangsilian.com/static/annotations/input.jpg",
+ "original_image_url": "https://hair.xiangsilian.com/static/annotations/original.jpg",
+ "width": 1080, "height": 1440,
+ "coordinate_space": "original_image",
+ "framing": null,
+ "height_rule": {
+ "name": "court_average", "formula": "上庭 = (中庭 + 下庭) / 2",
+ "measurement_source": "hairline_landmarks",
+ "brow_center": {"x": 540.0, "y": 740.0},
+ "nose_bottom": {"x": 540.0, "y": 1050.0},
+ "chin_tip": {"x": 540.0, "y": 1360.0},
+ "middle_height_px": 310.0, "lower_height_px": 310.0,
+ "target_upper_height_px": 310.0, "target_upper_height_cm": null,
+ "target_center": {"x": 540.0, "y": 430.0}
+ },
+ "guide_landmarks": {
+ "9": {"x": 540.0, "y": 740.0}, "150": {"x": 470.0, "y": 1340.0},
+ "152": {"x": 540.0, "y": 1360.0}, "162": {"x": 300.0, "y": 680.0},
+ "251": {"x": 770.0, "y": 630.0}, "389": {"x": 780.0, "y": 680.0}
+ },
+ "face_measure": null
+ }
+}
+```
+
+开启`normalize_image=true`后,`coordinate_space=processed_image`,`framing`包含前述裁切状态和坐标变换,所有发际线图片须叠加到`input_image_url`。原始图片仅用于展示和坐标回溯。
+
+---
+
## 接口 9:头发遮罩生成接口
**说明**:输入一张含人头的照片,生成头发遮罩,并返回**每一步的可视化图**(供对比调试)。
diff --git a/docs/网关待改动.md b/docs/网关待改动.md
index eee02c5..12f6341 100644
--- a/docs/网关待改动.md
+++ b/docs/网关待改动.md
@@ -5,6 +5,14 @@
---
+## 接口52(2026-11-04 已接入main)
+
+新增`POST /api/v1/hairline/generate-52`,由网关转发到GPU worker同路径。5号保持原样,52号的`normalize_image`等参数原样透传;非空图片沿用公共base64→URL改写,空图片仍保留`*_base64: null`。不在网关增加IP限制,不合入worker算法。
+
+测试页`/static/test_interface52.html`不包含内部密码。部署前确认主/备worker均支持52号,并检查生发请求的上游超时。详见[接口52网关接入](接口52网关接入.md)。
+
+---
+
## 1. ✅【功能必需】base64→URL 落盘扩展名按内容嗅探(支持 JPG)
**背景**:接口 **2/3/5 的返回图改成了 JPG**(体积约小 9×),**接口 1 标注图仍是 PNG**(含透明)。
diff --git a/gateway/app.py b/gateway/app.py
index e187b89..450a3e9 100644
--- a/gateway/app.py
+++ b/gateway/app.py
@@ -250,6 +250,7 @@ async def index():
"if3_hair_grow_b": "/static/test_interface3.html",
"if4_features": "/static/test_interface4.html",
"if5_hairline": "/static/test_interface5.html",
+ "if52_hairline": "/static/test_interface52.html",
"if6_measure_v2": "/static/test_interface6.html",
"if7_hair_grow_v2": "/static/test_interface7.html",
"if9_head_mask": "/static/test_interface9.html",
@@ -671,6 +672,17 @@ async def hairline_generate(request: Request):
return await _proxy(request, "/api/v1/hairline/generate")
+@app.post("/api/v1/hairline/generate-52", tags=["人脸分析"],
+ summary="接口52:发际线新规则与人像裁切")
+async def hairline_generate_52(request: Request):
+ """透传图片三选一、gender、hair_style及normalize_image等参数至GPU worker。
+
+ worker负责中下庭均值定位、扶正裁切和生发;网关沿用原有派发、鉴权与图片URL改写。
+ 空图片保持原有 *_base64: null 字段。接口5保持原样。
+ """
+ return await _proxy(request, "/api/v1/hairline/generate-52")
+
+
@app.post("/api/v1/hair/grow-v2", tags=["生发"])
async def hair_grow_v2(request: Request):
"""接口7:C端生发 v2(add_hair2 工作流)"""
diff --git a/nginx/hair-gateway.conf b/nginx/hair-gateway.conf
index 575764c..90e66b4 100644
--- a/nginx/hair-gateway.conf
+++ b/nginx/hair-gateway.conf
@@ -21,7 +21,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
# 网关内部转发可能耗时较长(worker 处理 + 排队)
- proxy_read_timeout 90s;
+ proxy_read_timeout 660s;
proxy_send_timeout 90s;
}
diff --git a/static/hairline_reference_box.js b/static/hairline_reference_box.js
new file mode 100644
index 0000000..8d38b6a
--- /dev/null
+++ b/static/hairline_reference_box.js
@@ -0,0 +1,62 @@
+"use strict";
+// Reference geometry is in input-image pixels; no screen-vertical assumptions.
+window.hairlineReferenceBox = function(points) {
+ const p = id => {
+ const value = points[String(id)];
+ if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y)) throw new Error(`缺少 ${id} 号关键点`);
+ return [value.x, value.y];
+ };
+ const add = (a,b) => [a[0]+b[0],a[1]+b[1]];
+ const sub = (a,b) => [a[0]-b[0],a[1]-b[1]];
+ const mul = (a,s) => [a[0]*s,a[1]*s];
+ const dot = (a,b) => a[0]*b[0]+a[1]*b[1];
+ const brow=p(9),chin=p(152),left=p(162),right=p(389);
+ const delta=sub(chin,brow),length=Math.hypot(...delta);
+ if(length<1)throw new Error("9→152 方向无法确定");
+ const up=mul(delta,-1/length),horizontal=[-up[1],up[0]],height=.7*length;
+ const sidePoint=(anchor,direction,level)=>{
+ const denominator=dot(direction,up);
+ if(Math.abs(denominator)<1e-6)throw new Error("参考框侧边与底边平行");
+ return add(anchor,mul(direction,(level-dot(sub(anchor,brow),up))/denominator));
+ };
+ const corners=[sidePoint(left,up,0),sidePoint(right,up,0),
+ sidePoint(right,up,height),sidePoint(left,up,height)];
+ if(dot(sub(corners[1],corners[0]),horizontal)<=0)throw new Error("参考框左右位置异常");
+ return {corners,height,faceLength:length,
+ landmarks:{9:brow,152:chin,162:left,389:right}};
+};
+
+window.clipHairlineToReferenceBox = function(image, points) {
+ const box=window.hairlineReferenceBox(points);
+ const width=image.naturalWidth||image.width,height=image.naturalHeight||image.height;
+ if(!width||!height)throw new Error("发际线图片尚未加载");
+ const canvas=document.createElement("canvas");canvas.width=width;canvas.height=height;
+ const ctx=canvas.getContext("2d");
+ ctx.beginPath();box.corners.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p));ctx.closePath();
+ ctx.clip();ctx.drawImage(image,0,0,width,height);
+ return canvas;
+};
+
+window.drawHairlineReferenceBox = function(ctx, points, crop, width, height) {
+ const box=window.hairlineReferenceBox(points);
+ const [x0,y0,x1,y1]=crop;
+ const screen=p=>[(p[0]-x0)/(x1-x0)*width,(p[1]-y0)/(y1-y0)*height];
+ const color="#e33399";
+ ctx.save();ctx.strokeStyle=color;ctx.lineWidth=Math.max(2.5,width/280);ctx.lineJoin="round";
+ ctx.beginPath();box.corners.forEach((p,i)=>{const q=screen(p);i?ctx.lineTo(...q):ctx.moveTo(...q);});ctx.closePath();ctx.stroke();
+ ctx.lineWidth=1.4;ctx.setLineDash([7,6]);ctx.beginPath();ctx.moveTo(...screen(box.landmarks[9]));ctx.lineTo(...screen(box.landmarks[152]));ctx.stroke();ctx.setLineDash([]);
+ const fontSize=Math.max(13,width/43);ctx.font=`600 ${fontSize}px sans-serif`;
+ for(const id of [9,152,162,389]){
+ const [x,y]=screen(box.landmarks[id]);
+ if(x<0||x>width||y<0||y>height)continue;
+ ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,Math.max(3,width/160),0,Math.PI*2);ctx.fill();
+ const label=String(id),tw=ctx.measureText(label).width;
+ const tx=Math.max(3,Math.min(width-tw-8,x+7)),ty=Math.max(fontSize+4,Math.min(height-5,y-7));
+ ctx.fillStyle="rgba(255,255,255,.9)";ctx.fillRect(tx-3,ty-fontSize,tw+6,fontSize+4);ctx.fillStyle="#a51868";ctx.fillText(label,tx,ty);
+ }
+ const top=screen([(box.corners[2][0]+box.corners[3][0])/2,(box.corners[2][1]+box.corners[3][1])/2]);
+ const text="70% · 9→152",tw=ctx.measureText(text).width;
+ const tx=Math.max(4,Math.min(width-tw-6,top[0]-tw/2)),ty=Math.max(fontSize+4,top[1]-8);
+ ctx.fillStyle="rgba(255,255,255,.9)";ctx.fillRect(tx-3,ty-fontSize,tw+6,fontSize+4);ctx.fillStyle="#a51868";ctx.fillText(text,tx,ty);
+ ctx.restore();
+};
diff --git a/static/img_downscale.js b/static/img_downscale.js
index f01502f..e175a00 100644
--- a/static/img_downscale.js
+++ b/static/img_downscale.js
@@ -52,4 +52,34 @@
}
window.downscaleImageFile = downscaleImageFile;
+ /**
+ * 将接口返回的图片字段规范成 可用值。
+ * 同时兼容:
+ * - 网关改写后的 *_url(http/https/相对路径)
+ * - worker 直连的 *_base64(原始 base64,或已带 data:image/...;base64, 前缀)
+ * @param {string|null|undefined} url
+ * @param {string|null|undefined} b64
+ * @param {string} [mimeHint='image/jpeg'] 原始 base64 时的 MIME 提示(会按魔数再嗅探)
+ * @returns {string}
+ */
+ function resolveImgSrc(url, b64, mimeHint) {
+ var v = url || b64 || '';
+ if (!v || typeof v !== 'string') return '';
+ v = v.trim();
+ if (!v) return '';
+ // 已是可用 URL / data URI / blob / 协议相对 URL
+ if (/^(https?:|blob:|data:|\/\/)/i.test(v)) return v;
+ // 同站相对路径(仅匹配明确的 /static/...;勿把 JPEG base64 的 /9j/ 前缀当路径)
+ if (/^\/static\//.test(v)) return v;
+ // 原始 base64:去掉可能残留的 data URI 前缀后再拼,避免双重前缀导致无法显示
+ var raw = v.replace(/^data:image\/[a-zA-Z0-9+.-]+;base64,/i, '');
+ if (!raw) return '';
+ var mime = mimeHint || 'image/jpeg';
+ if (raw.indexOf('iVBOR') === 0) mime = 'image/png';
+ else if (raw.indexOf('/9j/') === 0) mime = 'image/jpeg';
+ else if (raw.indexOf('R0lGOD') === 0) mime = 'image/gif';
+ else if (raw.indexOf('UklGR') === 0) mime = 'image/webp';
+ return 'data:' + mime + ';base64,' + raw;
+ }
+ window.resolveImgSrc = resolveImgSrc;
})();
diff --git a/static/integration.html b/static/integration.html
index e83a35e..49fd429 100644
--- a/static/integration.html
+++ b/static/integration.html
@@ -56,6 +56,7 @@
接口3
接口4
接口5
+ 接口52
接口6
错误码
在线测试
@@ -263,6 +264,16 @@ console.log(features.gene_style); // "自然型"
+
/api/v1/hairline/generate-52网关透传至GPU worker执行。中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。
+基础图片参数、gender、hair_style、use_mask、prompt、generate_grow_image沿用接口5。新增可选normalize_image,默认false,传true开启扶正与裁切;测试页显式开启。
非空图片返回*_url,空图片沿用*_base64: null。另有height_rule、framing、guide_landmarks、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。
/api/v1/face/measure-v2 v2上传正面照,选择性别与发型,查看按中下庭均值定位的设计曲线和生发效果。
+ + +