fix: 将52号接口范围限定为GPU worker
撤回52号网关路由、图片字段改名和mock改动,gateway目录恢复a3fddbb。worker保持base64和null响应,更新接口说明和回归测试。 验证:worker API、IP限制与网关兼容性共54项测试通过。
This commit is contained in:
@@ -1804,16 +1804,16 @@ async def _generate_hairline52_data(image, gender, hair_styles, use_mask=True,
|
|||||||
**返回说明**:
|
**返回说明**:
|
||||||
|
|
||||||
- `hairline_images`:**选中发型**的列表,数量 = 所选发型数,`order` = 发型序号。每项含:
|
- `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`:旧/新中心坐标、移动距离和目标误差(像素)。
|
- `height_placement`:旧/新中心坐标、移动距离和目标误差(像素)。
|
||||||
- `grown_image_url`:该发型的**生发图**(生发失败时为 `null`)。
|
- `grown_image_base64`:该发型的**生发图**(生发失败时为 `null`)。
|
||||||
- `hairline_type`:发际线类型 key。
|
- `hairline_type`:发际线类型 key。
|
||||||
worker 返回 `*_base64`,网关落盘后改写为 `*_url`。
|
本接口仅部署在GPU worker,图片始终返回 `*_base64`;空图片保留字段为 null。
|
||||||
- `best_hairline_center_point`:**首个选中发型**的 **middle 档**发际线曲线**面部中间点**坐标,
|
- `best_hairline_center_point`:**首个选中发型**的 **middle 档**发际线曲线**面部中间点**坐标,
|
||||||
以**当前底图像素**为基准(左上角为原点,x 向右,y 向下)。
|
以**当前底图像素**为基准(左上角为原点,x 向右,y 向下)。
|
||||||
- `high_hairline_center_point` / `low_hairline_center_point`:旧字段,固定 null。
|
- `high_hairline_center_point` / `low_hairline_center_point`:旧字段,固定 null。
|
||||||
- `height_rule`:中庭、下庭、目标上庭高度、目标中心与测量来源;`face_measure` 为当前底图测量。
|
- `height_rule`:中庭、下庭、目标上庭高度、目标中心与测量来源;`face_measure` 为当前底图测量。
|
||||||
- `input_image_*`:处理后底图,须用它叠加发际线;`original_image_*`:处理前图片。
|
- `input_image_base64`:处理后底图,须用它叠加发际线;`original_image_base64`:处理前图片。
|
||||||
- `coordinate_space`:默认 `original_image`,开启预处理后为 `processed_image`。
|
- `coordinate_space`:默认 `original_image`,开启预处理后为 `processed_image`。
|
||||||
- `framing`:是否标准裁切、旋转角、前后尺寸及双向坐标变换;关闭预处理时为 null。
|
- `framing`:是否标准裁切、旋转角、前后尺寸及双向坐标变换;关闭预处理时为 null。
|
||||||
""",
|
""",
|
||||||
|
|||||||
+3
-3
@@ -37,7 +37,7 @@
|
|||||||
| 2 | `results[].image_base64` / `results[].grown_image_base64`(可空) | `results[].image_url` / `results[].grown_image_url` |
|
| 2 | `results[].image_base64` / `results[].grown_image_base64`(可空) | `results[].image_url` / `results[].grown_image_url` |
|
||||||
| 3 | `hair_growth_image_base64`(可空) | `hair_growth_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` |
|
| 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` |
|
| 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` |
|
| 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` |
|
| 11 | `steps.{input,baseline,hair_mask,mask_overlay,mask,swap_raw,alpha,final}_base64` | `steps.*_url` |
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
| 4 | (网关本机产出,无图片字段,`features` 为 JSON 字符串) | — |
|
| 4 | (网关本机产出,无图片字段,`features` 为 JSON 字符串) | — |
|
||||||
|
|
||||||
> 实现建议:递归遍历 data,凡 key 以 `_base64` 结尾就落盘改 `_url`,自动覆盖嵌套/新增字段。
|
> 实现建议:递归遍历 data,凡 key 以 `_base64` 结尾就落盘改 `_url`,自动覆盖嵌套/新增字段。
|
||||||
> 兼容约定:接口52显式开启空图片字段改名;接口2/3/5等旧接口仍保留空图片的`*_base64: null`字段名。有图片时均落盘为`*_url`。
|
> 兼容约定:网关保持原样,有图片时落盘为`*_url`,空图片保留`*_base64: null`。接口52仅在worker返回base64,与网关的图片改名无关。
|
||||||
> **图片格式**:接口1 标注图含透明用 **PNG**;接口2/3/5 是不透明照片用 **JPG**(小很多,~9×)。
|
> **图片格式**:接口1 标注图含透明用 **PNG**;接口2/3/5 是不透明照片用 **JPG**(小很多,~9×)。
|
||||||
> 网关落盘按内容嗅探扩展名(PNG 头→`.png`,否则 `.jpg`)。
|
> 网关落盘按内容嗅探扩展名(PNG 头→`.png`,否则 `.jpg`)。
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
- **模板定位**:原有502点/贴图生成形状,`hairline/height_rule.py` 将每种 middle 曲线沿面部向上方向平移、校准中心 Y,保留曲线形状与倾斜。此规则仅接口52使用,接口2保持原逻辑。
|
- **模板定位**:原有502点/贴图生成形状,`hairline/height_rule.py` 将每种 middle 曲线沿面部向上方向平移、校准中心 Y,保留曲线形状与倾斜。此规则仅接口52使用,接口2保持原逻辑。
|
||||||
- **生发**:移动后的预览曲线直接生成黑线和重绘遮罩,再送ComfyUI,避免预览与黑模板位置不一致。
|
- **生发**:移动后的预览曲线直接生成黑线和重绘遮罩,再送ComfyUI,避免预览与黑模板位置不一致。
|
||||||
- **裁切叠加预览**:测试页新增“裁切发际线叠加”。`static/hairline_reference_box.js`共享同一个162/389/9/152参考框几何,按原图像素裁切middle透明图层,再叠加到处理后人像;四栏顺序为原图、middle、裁切发际线叠加、生发图。
|
- **裁切叠加预览**:测试页新增“裁切发际线叠加”。`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%的参考框。原图与叠图按需加载。
|
- **测试**:规则页`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)
|
### 接口9 头发遮罩 `/api/v1/head/mask`(worker)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
- 接口5已按11月2日前最新提交`a3fddbb`恢复:`POST /api/v1/hairline/generate`,返回middle/high/low三档,测试页`/static/test_interface5.html`。
|
- 接口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`。
|
- 当前新规则完整迁至接口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 与验证
|
## 2026-11-04 提交前 Review 与验证
|
||||||
|
|
||||||
- 接口5的请求处理函数、三档PNG生成函数与`a3fddbb`逐项AST比较一致,测试页逐字节一致;新高度规则、扶正裁切和矩形叠加均在接口52。
|
- 接口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)。
|
- 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生发,本轮未重复运行耗时的真实生发。
|
- 浏览器实测:接口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为当前成功结果,失败组及原因保留在报告中。
|
- 批量结果与上传图位于被Git忽略的`static/annotations/`及`tests/output/`,不随提交分发;新环境执行`scripts/compare_hairline_height.py`重建。420是计划组合数,378为当前成功结果,失败组及原因保留在报告中。
|
||||||
|
|||||||
+19
-19
@@ -526,7 +526,7 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso
|
|||||||
|
|
||||||
**说明**:入参同接口2(先选性别、再多选发型)。对每个选中发型返回一张 `middle` 设计叠图与生发图。**目标上庭 =(中庭 + 下庭)÷2**,从眉心向上定位;所有形状的中点对齐同一目标。`high` / `low` 已停用,旧字段保留为 `null`。
|
**说明**:入参同接口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)。
|
**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_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`。浮点坐标保留精度,旧中心点字段继续返回取整像素。
|
- 每项 `height_placement`:`old_center`、`new_center`、`shift_x_px`、`shift_y_px`(正=下移)、`target_error_px`。浮点坐标保留精度,旧中心点字段继续返回取整像素。
|
||||||
- `face_measure` 为处理后底图的实际测量值,未被设计目标覆盖。
|
- `face_measure` 为处理后底图的实际测量值,未被设计目标覆盖。
|
||||||
@@ -573,8 +573,8 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso
|
|||||||
| best_hairline_center_point | object \| null | **首个选中发型**的 **middle 档**发际线曲线「面部中间点」坐标,当前底图像素:`{ "x": number, "y": number }` |
|
| best_hairline_center_point | object \| null | **首个选中发型**的 **middle 档**发际线曲线「面部中间点」坐标,当前底图像素:`{ "x": number, "y": number }` |
|
||||||
| high_hairline_center_point | object \| null | 已停用,固定 `null` |
|
| high_hairline_center_point | object \| null | 已停用,固定 `null` |
|
||||||
| low_hairline_center_point | object \| null | 已停用,固定 `null` |
|
| low_hairline_center_point | object \| null | 已停用,固定 `null` |
|
||||||
| input_image_url | string | 当前底图,发际线叠图、参考点和测量值均使用该图的坐标系 |
|
| input_image_base64 | string | 当前底图的JPEG base64,发际线叠图、参考点和测量值均使用该图的坐标系 |
|
||||||
| original_image_url | string | EXIF方向修正后的上传原图 |
|
| original_image_base64 | string | EXIF方向修正后的上传原图JPEG base64 |
|
||||||
| width / height | int | 当前底图的实际像素宽 / 高 |
|
| width / height | int | 当前底图的实际像素宽 / 高 |
|
||||||
| coordinate_space | string | 开启预处理时为 `processed_image`,关闭时为 `original_image` |
|
| coordinate_space | string | 开启预处理时为 `processed_image`,关闭时为 `original_image` |
|
||||||
| framing | object \| null | 裁切状态、旋转角、前后尺寸、轴线误差及双向仿射矩阵;关闭预处理时为 `null` |
|
| 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) |
|
| 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_middle_base64 | string | middle 档发际线曲线**透明 PNG** base64(仅曲线,透明底,**不含人物**,需叠加 `input_image_base64` 显示) |
|
||||||
| image_high_url | null | 已停用,保留字段为 `null` |
|
| image_high_base64 | null | 已停用,保留字段为 `null` |
|
||||||
| image_low_url | null | 已停用,保留字段为 `null` |
|
| image_low_base64 | null | 已停用,保留字段为 `null` |
|
||||||
| grown_image_url | string \| null | **生发后图片** URL(ComfyUI「植发」效果图,完整人像照片,生发失败或 `generate_grow_image=false` 时为 `null`) |
|
| grown_image_base64 | string \| null | **生发后图片** JPEG base64(ComfyUI「植发」效果图,完整人像照片,生发失败或 `generate_grow_image=false` 时为 `null`) |
|
||||||
| order | int | 发型序号(= 传入的 hair_style 值) |
|
| order | int | 发型序号(= 传入的 hair_style 值) |
|
||||||
| height_placement | object | 旧/新中心、XY移动距离、目标误差,均为未取整的像素值 |
|
| 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_*` 标注图字段):
|
`face_measure` 元素(与[接口1](#接口-1四庭七眼测量标注接口)的 `data` 同构,**不含** `annotated_image_*` 标注图字段):
|
||||||
|
|
||||||
@@ -613,9 +613,9 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso
|
|||||||
|
|
||||||
> `eye1`~`eye7` 为从左到右共 7 段宽度,eye1=左耳外段、eye7=右耳外段,某侧耳朵不可见时对应段为 `null`。详见接口1说明。
|
> `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
|
```json
|
||||||
{
|
{
|
||||||
@@ -626,10 +626,10 @@ GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.jso
|
|||||||
"hairline_images": [
|
"hairline_images": [
|
||||||
{
|
{
|
||||||
"hairline_type": "ellipse",
|
"hairline_type": "ellipse",
|
||||||
"image_middle_url": "https://hair.xiangsilian.com/static/annotations/mid1.png",
|
"image_middle_base64": "iVBORw0KGgo...",
|
||||||
"image_high_url": null,
|
"image_high_base64": null,
|
||||||
"image_low_url": null,
|
"image_low_base64": null,
|
||||||
"grown_image_url": "https://hair.xiangsilian.com/static/annotations/grown1.jpg",
|
"grown_image_base64": "/9j/4AAQ...",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"height_placement": {
|
"height_placement": {
|
||||||
"old_center": {"x": 540.0, "y": 450.0},
|
"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},
|
"best_hairline_center_point": {"x": 540, "y": 430},
|
||||||
"high_hairline_center_point": null,
|
"high_hairline_center_point": null,
|
||||||
"low_hairline_center_point": null,
|
"low_hairline_center_point": null,
|
||||||
"input_image_url": "https://hair.xiangsilian.com/static/annotations/input.jpg",
|
"input_image_base64": "/9j/4AAQ...",
|
||||||
"original_image_url": "https://hair.xiangsilian.com/static/annotations/original.jpg",
|
"original_image_base64": "/9j/4AAQ...",
|
||||||
"width": 1080, "height": 1440,
|
"width": 1080, "height": 1440,
|
||||||
"coordinate_space": "original_image",
|
"coordinate_space": "original_image",
|
||||||
"framing": null,
|
"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`。原始图片仅用于展示和坐标回溯。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -208,8 +208,6 @@ async def index():
|
|||||||
"if3_hair_grow_b": "/static/test_interface3.html",
|
"if3_hair_grow_b": "/static/test_interface3.html",
|
||||||
"if4_features": "/static/test_interface4.html",
|
"if4_features": "/static/test_interface4.html",
|
||||||
"if5_hairline": "/static/test_interface5.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",
|
"if6_measure_v2": "/static/test_interface6.html",
|
||||||
"if7_hair_grow_v2": "/static/test_interface7.html",
|
"if7_hair_grow_v2": "/static/test_interface7.html",
|
||||||
"if9_head_mask": "/static/test_interface9.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")
|
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=["生发"])
|
@app.post("/api/v1/hair/grow-v2", tags=["生发"])
|
||||||
async def hair_grow_v2(request: Request):
|
async def hair_grow_v2(request: Request):
|
||||||
"""接口7:C端生发 v2(add_hair2 工作流)"""
|
"""接口7:C端生发 v2(add_hair2 工作流)"""
|
||||||
|
|||||||
+3
-15
@@ -88,8 +88,6 @@ def rewrite_base64_to_url(
|
|||||||
obj: Any,
|
obj: Any,
|
||||||
public_base_url: str,
|
public_base_url: str,
|
||||||
static_dir: str,
|
static_dir: str,
|
||||||
*,
|
|
||||||
rewrite_null_images: bool = False,
|
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""递归遍历响应 JSON,将所有 *_base64 字段改写为 *_url。
|
"""递归遍历响应 JSON,将所有 *_base64 字段改写为 *_url。
|
||||||
|
|
||||||
@@ -97,16 +95,10 @@ def rewrite_base64_to_url(
|
|||||||
- 支持 data: URI 和原始 base64 两种格式
|
- 支持 data: URI 和原始 base64 两种格式
|
||||||
- 解码 base64 → 保存到 static_dir/{uuid}.png
|
- 解码 base64 → 保存到 static_dir/{uuid}.png
|
||||||
- 删除 *_base64 字段,新增 *_url 字段指向公网 URL
|
- 删除 *_base64 字段,新增 *_url 字段指向公网 URL
|
||||||
- 接口52显式开启 rewrite_null_images,空图片也返回 *_url: null;旧接口保持原空字段
|
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
new_dict: Dict[str, Any] = {}
|
new_dict: Dict[str, Any] = {}
|
||||||
for key, value in obj.items():
|
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):
|
if key.endswith("_base64") and isinstance(value, str):
|
||||||
img_bytes = _decode_base64_value(value)
|
img_bytes = _decode_base64_value(value)
|
||||||
if img_bytes is not None:
|
if img_bytes is not None:
|
||||||
@@ -126,12 +118,10 @@ def rewrite_base64_to_url(
|
|||||||
logger.warning("字段 %s 的值无法解码为图片,保留", key)
|
logger.warning("字段 %s 的值无法解码为图片,保留", key)
|
||||||
new_dict[key] = value
|
new_dict[key] = value
|
||||||
else:
|
else:
|
||||||
new_dict[key] = rewrite_base64_to_url(
|
new_dict[key] = rewrite_base64_to_url(value, public_base_url, static_dir)
|
||||||
value, public_base_url, static_dir, rewrite_null_images=rewrite_null_images)
|
|
||||||
return new_dict
|
return new_dict
|
||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
return [rewrite_base64_to_url(item, public_base_url, static_dir,
|
return [rewrite_base64_to_url(item, public_base_url, static_dir) for item in obj]
|
||||||
rewrite_null_images=rewrite_null_images) for item in obj]
|
|
||||||
else:
|
else:
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@@ -258,9 +248,7 @@ async def proxy_request(request: Request, path: str) -> JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 递归改写 base64 图片字段
|
# 递归改写 base64 图片字段
|
||||||
rewritten = rewrite_base64_to_url(
|
rewritten = rewrite_base64_to_url(worker_json, public_base_url, static_dir)
|
||||||
worker_json, public_base_url, static_dir,
|
|
||||||
rewrite_null_images=path == "/api/v1/hairline/generate-52")
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=resp.status_code,
|
status_code=resp.status_code,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
- POST /api/v1/hair/grow-b → mock 响应
|
- POST /api/v1/hair/grow-b → mock 响应
|
||||||
- POST /api/v1/face/features → mock 响应
|
- POST /api/v1/face/features → mock 响应
|
||||||
- POST /api/v1/hairline/generate → mock 响应
|
- POST /api/v1/hairline/generate → mock 响应
|
||||||
- POST /api/v1/hairline/generate-52 → 新规则 mock 响应
|
|
||||||
|
|
||||||
鉴权:校验 X-Internal-Token 头,不匹配返回 401。
|
鉴权:校验 X-Internal-Token 头,不匹配返回 401。
|
||||||
并发模拟:可通过 ?delay=N 让接口 sleep N 秒(默认 1)。
|
并发模拟:可通过 ?delay=N 让接口 sleep N 秒(默认 1)。
|
||||||
@@ -214,7 +213,6 @@ def create_app(port: int, password: str) -> FastAPI:
|
|||||||
|
|
||||||
# --- 接口5 ---
|
# --- 接口5 ---
|
||||||
@app.post("/api/v1/hairline/generate")
|
@app.post("/api/v1/hairline/generate")
|
||||||
@app.post("/api/v1/hairline/generate-52")
|
|
||||||
async def hairline_generate(
|
async def hairline_generate(
|
||||||
request: Request,
|
request: Request,
|
||||||
x_internal_token: Optional[str] = Header(default=None),
|
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},
|
"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 mock_ok(data, delay=delay)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -265,10 +265,10 @@ console.log(features.gene_style); // "自然型"</pre>
|
|||||||
<div class="card" id="if52">
|
<div class="card" id="if52">
|
||||||
<h2>52. 发际线新规则与裁切 <span class="badge post">POST</span> <code>/api/v1/hairline/generate-52</code></h2>
|
<h2>52. 发际线新规则与裁切 <span class="badge post">POST</span> <code>/api/v1/hairline/generate-52</code></h2>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="desc">中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。</p>
|
<p class="desc">仅部署在GPU worker(8187),本次不新增网关路由。中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。</p>
|
||||||
<p>基础图片参数、<code>gender</code>、<code>hair_style</code>、<code>use_mask</code>、<code>prompt</code>、<code>generate_grow_image</code>沿用接口5。新增可选<code>normalize_image</code>,默认false,传true开启扶正与裁切;测试页显式开启。</p>
|
<p>基础图片参数、<code>gender</code>、<code>hair_style</code>、<code>use_mask</code>、<code>prompt</code>、<code>generate_grow_image</code>沿用接口5。新增可选<code>normalize_image</code>,默认false,传true开启扶正与裁切;测试页显式开启。</p>
|
||||||
<p style="margin-top:12px">返回<code>height_rule</code>、<code>framing</code>、<code>guide_landmarks</code>、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。</p>
|
<p style="margin-top:12px">图片返回<code>*_base64</code>,空图片保留字段并返回null。另有<code>height_rule</code>、<code>framing</code>、<code>guide_landmarks</code>、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。</p>
|
||||||
<p style="margin-top:12px"><a href="/static/test_interface52.html" class="link">52号规则测试页</a> · <a href="/static/test_interface52_compare.html" class="link">52号30案例对比页</a></p>
|
<p style="margin-top:12px"><a href="http://localhost:8187/static/test_interface52.html" class="link">52号规则测试页</a> · <a href="http://localhost:8187/static/test_interface52_compare.html" class="link">52号30案例对比页</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,8 +334,8 @@ console.log(features.gene_style); // "自然型"</pre>
|
|||||||
<tr><td>3. B端生发</td><td><a href="/static/test_interface3.html" class="link">/static/test_interface3.html</a></td><td>划线图上传 → 生发效果图</td></tr>
|
<tr><td>3. B端生发</td><td><a href="/static/test_interface3.html" class="link">/static/test_interface3.html</a></td><td>划线图上传 → 生发效果图</td></tr>
|
||||||
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 6项面部特征 + 原始JSON</td></tr>
|
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 6项面部特征 + 原始JSON</td></tr>
|
||||||
<tr><td>5. 发际线PNG</td><td><a href="/static/test_interface5.html" class="link">/static/test_interface5.html</a></td><td>上传+性别 → 发际线方案+中心点坐标</td></tr>
|
<tr><td>5. 发际线PNG</td><td><a href="/static/test_interface5.html" class="link">/static/test_interface5.html</a></td><td>上传+性别 → 发际线方案+中心点坐标</td></tr>
|
||||||
<tr><td>52. 发际线新规则</td><td><a href="/static/test_interface52.html" class="link">/static/test_interface52.html</a></td><td>中下庭定位、扶正裁切、矩形发际线叠加及生发</td></tr>
|
<tr><td>52. 发际线新规则</td><td><a href="http://localhost:8187/static/test_interface52.html" class="link">/static/test_interface52.html</a></td><td>中下庭定位、扶正裁切、矩形发际线叠加及生发</td></tr>
|
||||||
<tr><td>52. 规则对比</td><td><a href="/static/test_interface52_compare.html" class="link">/static/test_interface52_compare.html</a></td><td>30个案例,原图与旧/新规则样式对照</td></tr>
|
<tr><td>52. 规则对比</td><td><a href="http://localhost:8187/static/test_interface52_compare.html" class="link">/static/test_interface52_compare.html</a></td><td>30个案例,原图与旧/新规则样式对照</td></tr>
|
||||||
<tr><td>6. 四庭七眼 v2</td><td><a href="/static/test_interface6.html" class="link">/static/test_interface6.html</a></td><td>同接口1,去顶庭 · 竖线发际线→下巴 · 无头部端线</td></tr>
|
<tr><td>6. 四庭七眼 v2</td><td><a href="/static/test_interface6.html" class="link">/static/test_interface6.html</a></td><td>同接口1,去顶庭 · 竖线发际线→下巴 · 无头部端线</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<p style="font-size:12px;color:#94a3b8;margin-top:12px">
|
<p style="font-size:12px;color:#94a3b8;margin-top:12px">
|
||||||
|
|||||||
@@ -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 asyncio
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
import pytest
|
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name,path", [
|
def test_legacy_hairline_forwarding(monkeypatch):
|
||||||
("hairline_generate", "/api/v1/hairline/generate"),
|
name, path = "hairline_generate", "/api/v1/hairline/generate"
|
||||||
("hairline_generate_52", "/api/v1/hairline/generate-52"),
|
|
||||||
])
|
|
||||||
def test_distinct_hairline_forwarding(monkeypatch, name, path):
|
|
||||||
gateway = importlib.import_module("gateway.app")
|
gateway = importlib.import_module("gateway.app")
|
||||||
request = Request({"type": "http", "method": "POST", "path": path, "headers": []})
|
request = Request({"type": "http", "method": "POST", "path": path, "headers": []})
|
||||||
calls = []
|
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 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 asyncio.run(endpoint(request)) == {"forwarded_to": path}
|
||||||
assert calls == [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)
|
||||||
|
|||||||
@@ -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 asyncio
|
||||||
import json
|
import json
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -11,14 +11,12 @@ from starlette.requests import Request
|
|||||||
from gateway.forward import rewrite_base64_to_url
|
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,
|
value = {"hairline_images": [{"image_high_base64": None, "image_low_base64": None,
|
||||||
"grown_image_base64": None}],
|
"grown_image_base64": None}],
|
||||||
"high_hairline_center_point": None, "face_measure": 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)
|
actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path))
|
||||||
assert actual == {"hairline_images": [{"image_high_url": None, "image_low_url": None,
|
assert actual == value
|
||||||
"grown_image_url": None}],
|
|
||||||
"high_hairline_center_point": None, "face_measure": None}
|
|
||||||
assert not list(tmp_path.iterdir())
|
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")
|
Image.new("RGBA", (2, 2), (255, 255, 255, 0)).save(buf, format="PNG")
|
||||||
value = {"image_middle_base64": base64.b64encode(buf.getvalue()).decode(),
|
value = {"image_middle_base64": base64.b64encode(buf.getvalue()).decode(),
|
||||||
"image_high_base64": None}
|
"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_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
|
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", [
|
@pytest.mark.parametrize("path,expected_key", [
|
||||||
("/api/v1/hairline/generate", "grown_image_base64"),
|
("/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.config as config
|
||||||
import gateway.forward as forward
|
import gateway.forward as forward
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ def test_gateway_does_not_apply_worker_ip_policy(policy, monkeypatch):
|
|||||||
monkeypatch.setattr(gateway, '_proxy', proxy)
|
monkeypatch.setattr(gateway, '_proxy', proxy)
|
||||||
# Deliberately skip lifespan: no workers or configuration are needed.
|
# Deliberately skip lifespan: no workers or configuration are needed.
|
||||||
client = TestClient(gateway.app, client=('203.0.113.9', 50000))
|
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/hairline/generate').json()['code'] == 0
|
||||||
assert client.post('/api/v1/hair/grow').json()['code'] == 0
|
assert client.post('/api/v1/hair/grow').json()['code'] == 0
|
||||||
assert client.get('/static/test_interface52.html').status_code == 200
|
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']
|
||||||
|
|||||||
Reference in New Issue
Block a user