接口5:改为多选发型 + 每发型返回 middle/high/low 三档叠图(去掉 hairline_level 入参)

- 去掉 hairline_level 入参;middle/high/low 三档都返回
- 入参改为同接口2:gender + hair_style(逗号分隔多选,必填),缺失/越界返回 1007
- 每个选中发型返回 image_{middle,high,low}_base64 三档叠图 + grown_image_base64 生发图,
  按发型分组;order = 发型序号,含 hairline_type
- 生发黑模板仍固定 middle(hairline_texture_black/),每发型 1 张生发图
- best_hairline_center_point 取首个选中发型的 middle 档
- 同步更新测试页(三档并排展示)、integration.html、stub_worker、接口文档/实现说明、test_api

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-07-10 00:19:08 +08:00
co-authored by Claude Opus 4.8
parent 864d7f969a
commit f8e30ad32e
8 changed files with 161 additions and 120 deletions
+27 -28
View File
@@ -971,7 +971,8 @@ async def face_features(
summary="接口5 发际线PNG生成",
tags=["人脸分析"],
description=f"""
输入用户照片,返回 N 张用户发际线的 PNG 图片,并标注最合适发际线的面部中间点坐标。
输入用户照片 + 性别 + 多选发型,对每个选中发型返回 middle/high/low 三档发际线叠图与生发图,
并标注最合适发际线的面部中间点坐标。
{_image_fields_desc}
@@ -979,22 +980,21 @@ async def face_features(
---
**入参**
- 必填 `gender``male`/`female`),决定返回的发际线集合(female 5 / male 4)。
- 可选 `hairline_level``middle`(默认) / `high` / `low`),选用不同档位的发际线贴图
- 可选 `hair_style`(发型序号,逗号分隔如 `1,2,3`):对选中的发际线类型额外**生发**(同接口2)。
留空则只返回发际线叠图、不生发。越界/非法返回 `1007`。
**入参**(同接口2:先选性别,再多选发型)
- 必填 `gender``male`/`female`),决定发型集合(female 5 / male 4)。
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`
`female`1=ellipse,2=flower,3=heart,4=straight,5=wave`male`1=ellipse,2=inverse_arc,3=m,4=straight。
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
注:生发黑模板固定取 `hairline_texture_black/`middle),与 `hairline_level` 无关
注:生发黑模板固定取 `hairline_texture_black/`middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle
**返回说明**
- `hairline_images`发际线叠加图列表(发际线曲线叠加在用户照片上,同接口2预览),
数量 = 该性别的发际线类型数,本期按贴图顺序 `order=1..N`(暂不计算合适度)。
worker 返回 `image_base64`,网关落盘后改写为 `image_url`。
每项还含 `grown_image_base64`:当 `order` 在 `hair_style` 中时为该类型的**生发图**,否则为 `null`
- `best_hairline_center_point`:最佳(`order=1`)发际线曲线的**面部中间点**坐标,
- `hairline_images`**选中发型**的列表,数量 = 所选发型数,`order` = 发型序号。每项含:
- `image_middle_url` / `image_high_url` / `image_low_url`:该发型 middle/high/low 三档发际线叠图(同接口2预览)。
- `grown_image_url`:该发型的**生发图**(生发失败时为 `null`
- `hairline_type`:发际线类型 key
worker 返回 `*_base64`,网关落盘后改写为 `*_url`。
- `best_hairline_center_point`**首个选中发型**的 middle 档发际线曲线**面部中间点**坐标,
以**原图像素**为基准(左上角为原点,x 向右,y 向下)。
""",
responses={
@@ -1008,8 +1008,8 @@ async def face_features(
"request_id": "mock-request-id",
"data": {
"hairline_images": [
{"image_base64": "iVBORw0KGgo...", "grown_image_base64": "iVBORw0KGgo...", "order": 1},
{"image_base64": "iVBORw0KGgo...", "grown_image_base64": None, "order": 2},
{"hairline_type": "ellipse", "image_middle_base64": "iVBORw0KGgo...", "image_high_base64": "iVBORw0KGgo...", "image_low_base64": "iVBORw0KGgo...", "grown_image_base64": "iVBORw0KGgo...", "order": 1},
{"hairline_type": "flower", "image_middle_base64": "iVBORw0KGgo...", "image_high_base64": "iVBORw0KGgo...", "image_low_base64": "iVBORw0KGgo...", "grown_image_base64": None, "order": 2},
],
"best_hairline_center_point": {"x": 540, "y": 430},
},
@@ -1032,23 +1032,18 @@ async def hairline_generate(
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
hairline_level: str = Form(default="middle", description="际线贴图档位 middle(默认)/high/low"),
hair_style: Optional[str] = Form(default=None, description="生发发型序号逗号分隔(可选,如 1,2,3)。留空则只返回发际线叠图不生发。female:1-5 male:1-4"),
hair_style: Optional[str] = Form(default=None, description="型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
prompt: str = Form(default="补充遮罩区域的头发", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
):
if gender not in ("male", "female"):
return err(1004, "gender 必填且只能为 male / female")
if hairline_level not in ("middle", "high", "low"):
return err(1004, "hairline_level 只能为 middle / high / low")
# hair_style 可选:留空 → 不生发;非空但非法 → 1007
hair_styles = None
if hair_style and hair_style.strip():
max_styles = {"female": 5, "male": 4}[gender]
hair_styles = _parse_hair_styles(hair_style, max_styles)
if hair_styles is None:
return err(1007, f"hair_style 需为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
# hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007
max_styles = {"female": 5, "male": 4}[gender]
hair_styles = _parse_hair_styles(hair_style, max_styles)
if hair_styles is None:
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e is not None:
@@ -1063,15 +1058,19 @@ async def hairline_generate(
from hairline.service import generate_hairline_pngs
res = await run_in_threadpool(
generate_hairline_pngs, image, gender, hairline_level, hair_styles, use_mask, prompt)
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt)
if res is None:
return err(1001, "无法识别人像")
hairline_images = []
for it in res["images"]:
ov = it["overlays"]
hairline_images.append({
"image_base64": _jpg_b64(it["image_bgr"]), # 发际线叠图 JPG
"grown_image_base64": (_png_to_jpg_b64(it["grown_png"]) # 生发图 JPG(未生发为 null)
"hairline_type": it["hairline_type"],
"image_middle_base64": _jpg_b64(ov["middle"]), # 发际线叠图 middle 档 JPG
"image_high_base64": _jpg_b64(ov["high"]), # 发际线叠图 high 档 JPG
"image_low_base64": _jpg_b64(ov["low"]), # 发际线叠图 low 档 JPG
"grown_image_base64": (_png_to_jpg_b64(it["grown_png"]) # 生发图 JPG(失败为 null
if it.get("grown_png") else None),
"order": it["order"],
})