From 1c51f3eaabfa3395d80bbc10563cd67103a15891 Mon Sep 17 00:00:00 2001 From: xsl Date: Wed, 4 Nov 2026 10:45:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=8652=E5=8F=B7=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E9=99=90=E5=AE=9A=E4=B8=BAGPU=20worker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 撤回52号网关路由、图片字段改名和mock改动,gateway目录恢复a3fddbb。worker保持base64和null响应,更新接口说明和回归测试。 验证:worker API、IP限制与网关兼容性共54项测试通过。 --- app.py | 8 +++--- docs/实现说明.md | 6 ++--- docs/接口52图片裁切扶正规划.md | 7 ++--- docs/接口文档.md | 38 +++++++++++++-------------- gateway/app.py | 8 ------ gateway/forward.py | 18 +++---------- gateway/stub_worker.py | 10 ------- static/integration.html | 10 +++---- tests/test_gateway_hairline_routes.py | 16 ++++++----- tests/test_gateway_nullable_images.py | 25 +++++++----------- tests/test_ip_access.py | 4 +-- 11 files changed, 58 insertions(+), 92 deletions(-) diff --git a/app.py b/app.py index 96b4f28..202a448 100644 --- a/app.py +++ b/app.py @@ -1804,16 +1804,16 @@ async def _generate_hairline52_data(image, gender, hair_styles, use_mask=True, **返回说明**: - `hairline_images`:**选中发型**的列表,数量 = 所选发型数,`order` = 发型序号。每项含: - - `image_middle_url`:新高度规则的透明发际线 PNG;`image_high_url` / `image_low_url` 固定 null。 + - `image_middle_base64`:新高度规则的透明发际线 PNG;`image_high_base64` / `image_low_base64` 固定 null。 - `height_placement`:旧/新中心坐标、移动距离和目标误差(像素)。 - - `grown_image_url`:该发型的**生发图**(生发失败时为 `null`)。 + - `grown_image_base64`:该发型的**生发图**(生发失败时为 `null`)。 - `hairline_type`:发际线类型 key。 - worker 返回 `*_base64`,网关落盘后改写为 `*_url`。 + 本接口仅部署在GPU worker,图片始终返回 `*_base64`;空图片保留字段为 null。 - `best_hairline_center_point`:**首个选中发型**的 **middle 档**发际线曲线**面部中间点**坐标, 以**当前底图像素**为基准(左上角为原点,x 向右,y 向下)。 - `high_hairline_center_point` / `low_hairline_center_point`:旧字段,固定 null。 - `height_rule`:中庭、下庭、目标上庭高度、目标中心与测量来源;`face_measure` 为当前底图测量。 -- `input_image_*`:处理后底图,须用它叠加发际线;`original_image_*`:处理前图片。 +- `input_image_base64`:处理后底图,须用它叠加发际线;`original_image_base64`:处理前图片。 - `coordinate_space`:默认 `original_image`,开启预处理后为 `processed_image`。 - `framing`:是否标准裁切、旋转角、前后尺寸及双向坐标变换;关闭预处理时为 null。 """, diff --git a/docs/实现说明.md b/docs/实现说明.md index f68061d..052cf9c 100644 --- a/docs/实现说明.md +++ b/docs/实现说明.md @@ -37,7 +37,7 @@ | 2 | `results[].image_base64` / `results[].grown_image_base64`(可空) | `results[].image_url` / `results[].grown_image_url` | | 3 | `hair_growth_image_base64`(可空) | `hair_growth_image_url` | | 5 | `hairline_images[].image_{middle,high,low}_base64` / `grown_image_base64`(可空) | `hairline_images[].image_{middle,high,low}_url` / `grown_image_url` | -| 52 | `hairline_images[].image_{middle,high,low}_base64` / `grown_image_base64`、`input_image_base64` / `original_image_base64` | 同名 `*_url`,空图片也改为`*_url: null` | +| 52 | `hairline_images[].image_{middle,high,low}_base64` / `grown_image_base64`、`input_image_base64` / `original_image_base64` | 仅GPU worker,本次不新增网关路由;直接返回`*_base64` | | 9 | `steps_common.*_base64`、`bisenet|segformer.{hair,closed_region,final_overlay,mask}_mask_base64` 等 | 同名 `*_url` | | 10 | `steps_common.*_base64`、`bisenet|segformer.{base_mask,contour,band_overlay,mask}_base64` 等 | 同名 `*_url` | | 11 | `steps.{input,baseline,hair_mask,mask_overlay,mask,swap_raw,alpha,final}_base64` | `steps.*_url` | @@ -45,7 +45,7 @@ | 4 | (网关本机产出,无图片字段,`features` 为 JSON 字符串) | — | > 实现建议:递归遍历 data,凡 key 以 `_base64` 结尾就落盘改 `_url`,自动覆盖嵌套/新增字段。 -> 兼容约定:接口52显式开启空图片字段改名;接口2/3/5等旧接口仍保留空图片的`*_base64: null`字段名。有图片时均落盘为`*_url`。 +> 兼容约定:网关保持原样,有图片时落盘为`*_url`,空图片保留`*_base64: null`。接口52仅在worker返回base64,与网关的图片改名无关。 > **图片格式**:接口1 标注图含透明用 **PNG**;接口2/3/5 是不透明照片用 **JPG**(小很多,~9×)。 > 网关落盘按内容嗅探扩展名(PNG 头→`.png`,否则 `.jpg`)。 @@ -108,7 +108,7 @@ - **模板定位**:原有502点/贴图生成形状,`hairline/height_rule.py` 将每种 middle 曲线沿面部向上方向平移、校准中心 Y,保留曲线形状与倾斜。此规则仅接口52使用,接口2保持原逻辑。 - **生发**:移动后的预览曲线直接生成黑线和重绘遮罩,再送ComfyUI,避免预览与黑模板位置不一致。 - **裁切叠加预览**:测试页新增“裁切发际线叠加”。`static/hairline_reference_box.js`共享同一个162/389/9/152参考框几何,按原图像素裁切middle透明图层,再叠加到处理后人像;四栏顺序为原图、middle、裁切发际线叠加、生发图。 -- **兼容**:high/low不再生成,图片与中心点字段保留为null。网关`gateway/forward.py`仅为接口52开启空图片`*_base64`→`*_url: null`,旧接口空字段名保持原样。部署52路由时同步该改写;网关不增加IP限制。`face_measure`为当前底图测量;新增`height_rule`与各项`height_placement`说明设计目标与实际误差。 +- **兼容**:high/low不再生成,图片与中心点字段保留为null。接口52始终返回`*_base64`,空图片保留字段并返回null。本次不修改网关路由、落盘、字段改名或IP策略。`face_measure`为当前底图测量;新增`height_rule`与各项`height_placement`说明设计目标与实际误差。 - **测试**:规则页`static/test_interface52.html`,对比页`static/test_interface52_compare.html`;运行`scripts/compare_hairline_height.py`默认保存全部30张×男女各7型,共420组对比。每人一行,顺序为未裁切原图、单张旧规则图、全部新规则样式,支持位置对照、额头特写、同图叠线和上传新图。新图可叠加162/389两侧、底边经过9、高度为9→152距离70%的参考框。原图与叠图按需加载。 ### 接口9 头发遮罩 `/api/v1/head/mask`(worker) diff --git a/docs/接口52图片裁切扶正规划.md b/docs/接口52图片裁切扶正规划.md index 0bd0917..2247b6c 100644 --- a/docs/接口52图片裁切扶正规划.md +++ b/docs/接口52图片裁切扶正规划.md @@ -6,7 +6,7 @@ - 接口5已按11月2日前最新提交`a3fddbb`恢复:`POST /api/v1/hairline/generate`,返回middle/high/low三档,测试页`/static/test_interface5.html`。 - 当前新规则完整迁至接口52:`POST /api/v1/hairline/generate-52`,测试页`/static/test_interface52.html`,30案例对比`/static/test_interface52_compare.html`。接口52保留迁移前所有参数和返回字段,页面显式开启`normalize_image=true`。 -- 原`test_interface5_compare.html`书签跳转至52号对比页;公开5号与52号的处理路由、生成函数和网关转发均独立。 +- 原`test_interface5_compare.html`书签跳转至52号对比页;5号与52号的worker处理路由和生成函数独立;52号仅部署在GPU worker,本次不新增网关转发。 ## 已确定的要求 @@ -112,8 +112,9 @@ ## 2026-11-04 提交前 Review 与验证 - 接口5的请求处理函数、三档PNG生成函数与`a3fddbb`逐项AST比较一致,测试页逐字节一致;新高度规则、扶正裁切和矩形叠加均在接口52。 -- 修复网关空图片字段改名影响旧接口的兼容性问题:仅接口52开启`*_base64: null`→`*_url: null`,接口2/3/5等旧接口保留原字段名;已增加递归改写和真实转发函数的回归测试。 +- 范围修正:接口52仅属于GPU worker,返回`*_base64`及null;网关路由、落盘、字段改名、mock worker均恢复`a3fddbb`,没有“仅52号改名”的特殊逻辑。测试确认旧网关行为保持原样且未暴露52号路由。 +- 范围修正后的回归检查:`./venv/bin/pytest tests/test_ip_access.py tests/test_gateway_hairline_routes.py tests/test_gateway_nullable_images.py tests/test_api.py -q`,54项通过。 - GPU worker所有动态接口启用直连IP白名单,本机和网关`49.232.51.171`可访问;网关侧不启用IP限制。配置与部署见[接口IP白名单](接口IP白名单.md)。 -- `./venv/bin/pytest -q`:113项通过、1项已有失败。`tests/test_pipeline.py::test_numeric_regression`期望全脸总高24.01cm,当前实测22.47cm;从未修改的`a3fddbb`导出代码、夹具后独立运行,同样失败。此次保留历史断言,未通过修改基线掩盖差异。 +- 上一轮`./venv/bin/pytest -q`:113项通过、1项已有失败(撤回网关新增功能前的测试数)。`tests/test_pipeline.py::test_numeric_regression`期望全脸总高24.01cm,当前实测22.47cm;从未修改的`a3fddbb`导出代码、夹具后独立运行,同样失败。此次保留历史断言,未通过修改基线掩盖差异。 - 浏览器实测:接口5返回middle/high/low且保持868×1166;接口52长图案例7返回1062×1416、`status=aspect_crop`,四栏图和裁切边框正常;对比页显示30个案例、378组结果,旧书签可跳转52,未出现页面脚本错误。定位验证均关闭ComfyUI生发,本轮未重复运行耗时的真实生发。 - 批量结果与上传图位于被Git忽略的`static/annotations/`及`tests/output/`,不随提交分发;新环境执行`scripts/compare_hairline_height.py`重建。420是计划组合数,378为当前成功结果,失败组及原因保留在报告中。 diff --git a/docs/接口文档.md b/docs/接口文档.md index a1e549b..2300eb6 100644 --- a/docs/接口文档.md +++ b/docs/接口文档.md @@ -526,7 +526,7 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso **说明**:入参同接口2(先选性别、再多选发型)。对每个选中发型返回一张 `middle` 设计叠图与生发图。**目标上庭 =(中庭 + 下庭)÷2**,从眉心向上定位;所有形状的中点对齐同一目标。`high` / `low` 已停用,旧字段保留为 `null`。 -**请求**:`POST /api/v1/hairline/generate-52` +**请求**:`POST /api/v1/hairline/generate-52`(仅GPU worker,端口8187;本次不在网关新增路由) **IP限制**:GPU worker的所有动态接口统一仅允许本机回环地址、本机网卡地址及白名单;目前白名单含网关IP `49.232.51.171`,其他直连来源需加入`ip_access.json`的`allowed_ips`。该文件支持单个IP与CIDR,修改后立即生效;拒绝时返回HTTP403、code=1010。网关不启用此限制,已有鉴权仍保留。详见[接口IP白名单](接口IP白名单.md)。 @@ -555,7 +555,7 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso ### 高度规则与对比 -- **参数与字段**:原有8个请求参数、响应外层结构和所有旧返回字段均保留。`use_mask=true`、默认prompt、`generate_grow_image=true`保持原值。新功能只新增可选参数和返回字段;旧请求未传`normalize_image`时保留原图坐标,high/low按已约定保留null,worker仍返回`*_base64`,网关仍返回对应`*_url`。 +- **参数与字段**:原有8个请求参数、响应外层结构和所有旧返回字段均保留。`use_mask=true`、默认prompt、`generate_grow_image=true`保持原值。新功能只新增可选参数和返回字段;旧请求未传`normalize_image`时保留原图坐标,high/low按已约定保留null,worker始终返回`*_base64`,不负责保存图片、生成公网URL或改名;网关保持原样。 - `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` 为处理后底图的实际测量值,未被设计目标覆盖。 @@ -573,8 +573,8 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso | 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 | 当前底图,发际线叠图、参考点和测量值均使用该图的坐标系 | -| original_image_url | string | EXIF方向修正后的上传原图 | +| input_image_base64 | string | 当前底图的JPEG base64,发际线叠图、参考点和测量值均使用该图的坐标系 | +| original_image_base64 | string | EXIF方向修正后的上传原图JPEG base64 | | width / height | int | 当前底图的实际像素宽 / 高 | | coordinate_space | string | 开启预处理时为 `processed_image`,关闭时为 `original_image` | | framing | object \| null | 裁切状态、旋转角、前后尺寸、轴线误差及双向仿射矩阵;关闭预处理时为 `null` | @@ -587,16 +587,16 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso | 字段 | 类型 | 说明 | |------|------|------| | 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_url | null | 已停用,保留字段为 `null` | -| image_low_url | null | 已停用,保留字段为 `null` | -| grown_image_url | string \| null | **生发后图片** URL(ComfyUI「植发」效果图,完整人像照片,生发失败或 `generate_grow_image=false` 时为 `null`) | +| image_middle_base64 | string | middle 档发际线曲线**透明 PNG** base64(仅曲线,透明底,**不含人物**,需叠加 `input_image_base64` 显示) | +| image_high_base64 | null | 已停用,保留字段为 `null` | +| image_low_base64 | null | 已停用,保留字段为 `null` | +| grown_image_base64 | string \| null | **生发后图片** JPEG base64(ComfyUI「植发」效果图,完整人像照片,生发失败或 `generate_grow_image=false` 时为 `null`) | | order | int | 发型序号(= 传入的 hair_style 值) | | height_placement | object | 旧/新中心、XY移动距离、目标误差,均为未取整的像素值 | -> worker 侧返回 `image_middle_base64` / `image_high_base64` / `image_low_base64` / `grown_image_base64`,网关落盘后改写为上表对应的 `*_url`。 +> 上表为GPU worker的实际响应字段;所有图片使用base64,空图片保留原字段并返回null。网关的落盘与URL改写属于网关职责,本次不修改。 > -> 💡 `image_middle_url` 为透明底 PNG,前端需用绝对定位叠加到 `input_image_url` 上显示(参考[测试页](https://hair.xiangsilian.com/static/test_interface52.html)的 `.img-stack` 叠加结构)。`grown_image_url` 是完整人像照片,直接显示即可。worker返回上述图片的 `*_base64`,网关改写为 `*_url`。 +> 前端将base64转为data URI后显示。middle透明图叠加到`input_image_base64`,生发图直接显示。GPU worker本机测试页为`http://localhost:8187/static/test_interface52.html`。 `face_measure` 元素(与[接口1](#接口-1四庭七眼测量标注接口)的 `data` 同构,**不含** `annotated_image_*` 标注图字段): @@ -613,9 +613,9 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso > `eye1`~`eye7` 为从左到右共 7 段宽度,eye1=左耳外段、eye7=右耳外段,某侧耳朵不可见时对应段为 `null`。详见接口1说明。 -### 网关响应结构示例(normalize_image=false) +### GPU worker响应结构示例(normalize_image=false) -此例未开启裁切,坐标基于原图;`face_measure=null`时仍可按发际线关键点计算目标。网关对接口52的所有空图片字段均返回`*_url: null`。worker直连时图片字段名对应为`*_base64`,图片内容为base64。 +此例未开启裁切,坐标基于原图;`face_measure=null`时仍可按发际线关键点计算目标。图片内容以缩略base64字符串示意,空图片字段返回null,字段名不变。 ```json { @@ -626,10 +626,10 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso "hairline_images": [ { "hairline_type": "ellipse", - "image_middle_url": "https://hair.xiangsilian.com/static/annotations/mid1.png", - "image_high_url": null, - "image_low_url": null, - "grown_image_url": "https://hair.xiangsilian.com/static/annotations/grown1.jpg", + "image_middle_base64": "iVBORw0KGgo...", + "image_high_base64": null, + "image_low_base64": null, + "grown_image_base64": "/9j/4AAQ...", "order": 1, "height_placement": { "old_center": {"x": 540.0, "y": 450.0}, @@ -641,8 +641,8 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso "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", + "input_image_base64": "/9j/4AAQ...", + "original_image_base64": "/9j/4AAQ...", "width": 1080, "height": 1440, "coordinate_space": "original_image", "framing": null, @@ -666,7 +666,7 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso } ``` -开启`normalize_image=true`后,`coordinate_space=processed_image`,`framing`包含前述裁切状态和坐标变换,所有发际线图片须叠加到`input_image_url`。原始图片仅用于展示和坐标回溯。 +开启`normalize_image=true`后,`coordinate_space=processed_image`,`framing`包含前述裁切状态和坐标变换,所有发际线图片须叠加到`input_image_base64`。原始图片仅用于展示和坐标回溯。 --- diff --git a/gateway/app.py b/gateway/app.py index 56d396b..b96cc37 100644 --- a/gateway/app.py +++ b/gateway/app.py @@ -208,8 +208,6 @@ 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", - "if52_compare": "/static/test_interface52_compare.html", "if6_measure_v2": "/static/test_interface6.html", "if7_hair_grow_v2": "/static/test_interface7.html", "if9_head_mask": "/static/test_interface9.html", @@ -593,12 +591,6 @@ async def hairline_generate(request: Request): return await _proxy(request, "/api/v1/hairline/generate") -@app.post("/api/v1/hairline/generate-52", tags=["人脸分析"]) -async def hairline_generate_52(request: Request): - """接口52:新高度规则、扶正裁切与发际线生成""" - 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/gateway/forward.py b/gateway/forward.py index 704c35b..2e6aa40 100644 --- a/gateway/forward.py +++ b/gateway/forward.py @@ -88,8 +88,6 @@ def rewrite_base64_to_url( obj: Any, public_base_url: str, static_dir: str, - *, - rewrite_null_images: bool = False, ) -> Any: """递归遍历响应 JSON,将所有 *_base64 字段改写为 *_url。 @@ -97,16 +95,10 @@ def rewrite_base64_to_url( - 支持 data: URI 和原始 base64 两种格式 - 解码 base64 → 保存到 static_dir/{uuid}.png - 删除 *_base64 字段,新增 *_url 字段指向公网 URL - - 接口52显式开启 rewrite_null_images,空图片也返回 *_url: null;旧接口保持原空字段 """ if isinstance(obj, dict): new_dict: Dict[str, Any] = {} for key, value in obj.items(): - if rewrite_null_images and key.endswith("_base64") and value is None: - # Deprecated/optional image fields must keep their external - # *_url names even when no image is generated. - new_dict[key[:-7] + "_url"] = None - continue if key.endswith("_base64") and isinstance(value, str): img_bytes = _decode_base64_value(value) if img_bytes is not None: @@ -126,12 +118,10 @@ def rewrite_base64_to_url( logger.warning("字段 %s 的值无法解码为图片,保留", key) new_dict[key] = value else: - new_dict[key] = rewrite_base64_to_url( - value, public_base_url, static_dir, rewrite_null_images=rewrite_null_images) + new_dict[key] = rewrite_base64_to_url(value, public_base_url, static_dir) return new_dict elif isinstance(obj, list): - return [rewrite_base64_to_url(item, public_base_url, static_dir, - rewrite_null_images=rewrite_null_images) for item in obj] + return [rewrite_base64_to_url(item, public_base_url, static_dir) for item in obj] else: return obj @@ -258,9 +248,7 @@ async def proxy_request(request: Request, path: str) -> JSONResponse: ) # 递归改写 base64 图片字段 - rewritten = rewrite_base64_to_url( - worker_json, public_base_url, static_dir, - rewrite_null_images=path == "/api/v1/hairline/generate-52") + rewritten = rewrite_base64_to_url(worker_json, public_base_url, static_dir) return JSONResponse( status_code=resp.status_code, diff --git a/gateway/stub_worker.py b/gateway/stub_worker.py index 3aa05ad..8745d6c 100644 --- a/gateway/stub_worker.py +++ b/gateway/stub_worker.py @@ -7,7 +7,6 @@ - POST /api/v1/hair/grow-b → mock 响应 - POST /api/v1/face/features → mock 响应 - POST /api/v1/hairline/generate → mock 响应 -- POST /api/v1/hairline/generate-52 → 新规则 mock 响应 鉴权:校验 X-Internal-Token 头,不匹配返回 401。 并发模拟:可通过 ?delay=N 让接口 sleep N 秒(默认 1)。 @@ -214,7 +213,6 @@ def create_app(port: int, password: str) -> FastAPI: # --- 接口5 --- @app.post("/api/v1/hairline/generate") - @app.post("/api/v1/hairline/generate-52") async def hairline_generate( request: Request, x_internal_token: Optional[str] = Header(default=None), @@ -247,14 +245,6 @@ def create_app(port: int, password: str) -> FastAPI: ], "best_hairline_center_point": {"x": 540, "y": 430}, } - if request.url.path.endswith("generate-52"): - for item in data["hairline_images"]: - item["image_high_base64"] = None - item["image_low_base64"] = None - data.update(high_hairline_center_point=None, low_hairline_center_point=None, - face_measure=None, framing=None, height_rule=None, - guide_landmarks={}, input_image_base64=_png, original_image_base64=_png, - coordinate_space="original_image", width=1, height=1) return mock_ok(data, delay=delay) return app diff --git a/static/integration.html b/static/integration.html index 8166019..4a850db 100644 --- a/static/integration.html +++ b/static/integration.html @@ -265,10 +265,10 @@ console.log(features.gene_style); // "自然型"

52. 发际线新规则与裁切  POST  /api/v1/hairline/generate-52

-

中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。

+

仅部署在GPU worker(8187),本次不新增网关路由。中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。

基础图片参数、gender、hair_style、use_mask、prompt、generate_grow_image沿用接口5。新增可选normalize_image,默认false,传true开启扶正与裁切;测试页显式开启。

-

返回height_rule、framing、guide_landmarks、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。

-

52号规则测试页 · 52号30案例对比页

+

图片返回*_base64,空图片保留字段并返回null。另有height_rule、framing、guide_landmarks、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。

+

52号规则测试页 · 52号30案例对比页

@@ -334,8 +334,8 @@ console.log(features.gene_style); // "自然型" 3. B端生发/static/test_interface3.html划线图上传 → 生发效果图 4. 用户特征/static/test_interface4.html上传照片 → 6项面部特征 + 原始JSON 5. 发际线PNG/static/test_interface5.html上传+性别 → 发际线方案+中心点坐标 - 52. 发际线新规则/static/test_interface52.html中下庭定位、扶正裁切、矩形发际线叠加及生发 - 52. 规则对比/static/test_interface52_compare.html30个案例,原图与旧/新规则样式对照 + 52. 发际线新规则/static/test_interface52.html中下庭定位、扶正裁切、矩形发际线叠加及生发 + 52. 规则对比/static/test_interface52_compare.html30个案例,原图与旧/新规则样式对照 6. 四庭七眼 v2/static/test_interface6.html同接口1,去顶庭 · 竖线发际线→下巴 · 无头部端线

diff --git a/tests/test_gateway_hairline_routes.py b/tests/test_gateway_hairline_routes.py index 96b8986..bf592dc 100644 --- a/tests/test_gateway_hairline_routes.py +++ b/tests/test_gateway_hairline_routes.py @@ -1,16 +1,12 @@ -"""The legacy interface 5 and new interface 52 forward to separate worker paths.""" +"""Interface 52 belongs to the GPU worker; the gateway keeps its legacy routes.""" import asyncio import importlib -import pytest from starlette.requests import Request -@pytest.mark.parametrize("name,path", [ - ("hairline_generate", "/api/v1/hairline/generate"), - ("hairline_generate_52", "/api/v1/hairline/generate-52"), -]) -def test_distinct_hairline_forwarding(monkeypatch, name, path): +def test_legacy_hairline_forwarding(monkeypatch): + name, path = "hairline_generate", "/api/v1/hairline/generate" gateway = importlib.import_module("gateway.app") request = Request({"type": "http", "method": "POST", "path": path, "headers": []}) calls = [] @@ -25,3 +21,9 @@ def test_distinct_hairline_forwarding(monkeypatch, name, path): assert next(route for route in gateway.app.routes if getattr(route, "path", None) == path).endpoint is endpoint assert asyncio.run(endpoint(request)) == {"forwarded_to": path} assert calls == [path] + + +def test_interface52_is_not_exposed_by_gateway(): + gateway = importlib.import_module("gateway.app") + assert not any(getattr(route, "path", None) == "/api/v1/hairline/generate-52" + for route in gateway.app.routes) diff --git a/tests/test_gateway_nullable_images.py b/tests/test_gateway_nullable_images.py index 5493c3f..a712bc4 100644 --- a/tests/test_gateway_nullable_images.py +++ b/tests/test_gateway_nullable_images.py @@ -1,4 +1,4 @@ -"""Nullable/deprecated worker images retain their documented gateway keys.""" +"""Keep the gateway's historical image-field conversion unchanged.""" import asyncio import json from types import SimpleNamespace @@ -11,14 +11,12 @@ from starlette.requests import Request from gateway.forward import rewrite_base64_to_url -def test_nullable_image_fields_keep_external_names(tmp_path): +def test_nullable_image_fields_keep_original_names(tmp_path): value = {"hairline_images": [{"image_high_base64": None, "image_low_base64": None, "grown_image_base64": None}], "high_hairline_center_point": None, "face_measure": None} - actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path), rewrite_null_images=True) - assert actual == {"hairline_images": [{"image_high_url": None, "image_low_url": None, - "grown_image_url": None}], - "high_hairline_center_point": None, "face_measure": None} + actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path)) + assert actual == value assert not list(tmp_path.iterdir()) @@ -30,23 +28,18 @@ def test_nonempty_images_still_rewrite_alongside_null_fields(tmp_path): Image.new("RGBA", (2, 2), (255, 255, 255, 0)).save(buf, format="PNG") value = {"image_middle_base64": base64.b64encode(buf.getvalue()).decode(), "image_high_base64": None} - actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path), rewrite_null_images=True) + actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path)) assert actual["image_middle_url"].startswith("https://example.test/static/annotations/") - assert actual["image_high_url"] is None + assert actual["image_high_base64"] is None + assert "image_high_url" not in actual assert len(list(tmp_path.iterdir())) == 1 -def test_legacy_nullable_image_field_names_remain_unchanged(tmp_path): - value = {"hairline_images": [{"grown_image_base64": None}], "face_measure": None} - assert rewrite_base64_to_url(value, "https://example.test", str(tmp_path)) == value - assert not list(tmp_path.iterdir()) - - @pytest.mark.parametrize("path,expected_key", [ ("/api/v1/hairline/generate", "grown_image_base64"), - ("/api/v1/hairline/generate-52", "grown_image_url"), + ("/api/v1/hair/grow", "grown_image_base64"), ]) -def test_proxy_rewrites_null_images_only_for_interface52(tmp_path, monkeypatch, path, expected_key): +def test_proxy_keeps_original_null_image_fields(tmp_path, monkeypatch, path, expected_key): import gateway.config as config import gateway.forward as forward diff --git a/tests/test_ip_access.py b/tests/test_ip_access.py index 9ddad1a..4adc2cb 100644 --- a/tests/test_ip_access.py +++ b/tests/test_ip_access.py @@ -94,8 +94,8 @@ def test_gateway_does_not_apply_worker_ip_policy(policy, monkeypatch): monkeypatch.setattr(gateway, '_proxy', proxy) # Deliberately skip lifespan: no workers or configuration are needed. client = TestClient(gateway.app, client=('203.0.113.9', 50000)) - assert client.post('/api/v1/hairline/generate-52').json()['code'] == 0 + assert client.post('/api/v1/hairline/generate-52').status_code == 404 assert client.post('/api/v1/hairline/generate').json()['code'] == 0 assert client.post('/api/v1/hair/grow').json()['code'] == 0 assert client.get('/static/test_interface52.html').status_code == 200 - assert calls == ['/api/v1/hairline/generate-52', '/api/v1/hairline/generate', '/api/v1/hair/grow'] + assert calls == ['/api/v1/hairline/generate', '/api/v1/hair/grow']