feat(接口2/7): hair_style 支持逗号分隔多选,如 1,2,3

- 参数类型从 int 改为 string(逗号分隔),自动解析去重排序
- 越界/非法值返回 1007
- _parse_hair_styles() 辅助函数:解析 + 去重 + 范围校验
- service 层 hair_style 参数改为 hair_styles: list[int]
- 接口2 和 接口7 同步更新
- 服务已重启,smoke test 通过

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
xsl
2026-06-22 23:36:13 +08:00
co-authored by Claude
parent 1d23414d21
commit 9678b54267
5 changed files with 57 additions and 29 deletions
+43 -12
View File
@@ -247,6 +247,33 @@ def _png_to_jpg_b64(png_bytes) -> str:
return _jpg_b64(img)
def _parse_hair_styles(raw: Optional[str], max_styles: int) -> Optional[list[int]]:
"""解析逗号分隔的发型序号字符串 → 去重排序列表。非法返回 None。
"1,2,3" → [1, 2, 3] "3,1" → [1, 3] "1" → [1]
"""
if raw is None or not isinstance(raw, str) or not raw.strip():
return None
try:
styles = []
for part in raw.split(","):
part = part.strip()
if not part:
continue
v = int(part)
if v < 1 or v > max_styles:
return None
styles.append(v)
if not styles:
return None
# 去重保持顺序
seen = set()
unique = [s for s in styles if not (s in seen or seen.add(s))] # type: ignore[func-returns-value]
return unique
except (ValueError, TypeError):
return None
# ---------------------------------------------------------------------------
# 通用图片请求 BodyJSON 方式,用于 url / base64
# ---------------------------------------------------------------------------
@@ -459,8 +486,9 @@ async def face_measure(
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。
非法或缺失返回 `1004`。
- **hair_style**(必填):`int`,发型序号。`female`1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave
`male`1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界返回 `1007`。
- **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。
`female`1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave
`male`1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007`。
- **beauty_enabled**:本期保留但不生效。
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`female),
@@ -478,6 +506,7 @@ async def face_measure(
"data": {
"results": [
{"image_base64": "iVBORw0KGgo...", "hairline_type": "ellipse", "order": 1},
{"image_base64": "iVBORw0KGgo...", "hairline_type": "flower", "order": 2},
]
},
}
@@ -502,7 +531,7 @@ async def hair_grow(
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(必填)"),
hair_style: Optional[int] = Form(default=None, description="发型序号(必填)。female:1-5 male:1-4"),
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
prompt: str = Form(default="补充遮罩区域的头发", description="ComfyUI 提示词,会替换工作流节点60的文本"),
@@ -511,10 +540,11 @@ async def hair_grow(
if gender not in ("male", "female"):
return err(1004, "gender 必填且只能为 male / female")
# 2. hair_style 必填校验(越界 → 1007)
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007
max_styles = {"female": 5, "male": 4}[gender]
if hair_style is None or not isinstance(hair_style, int) or hair_style < 1 or hair_style > max_styles:
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数,收到 {hair_style!r}")
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}")
# 3. 三选一取图
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
@@ -537,7 +567,7 @@ async def hair_grow(
from hairline.service import generate_grow_results
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_style)
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_styles)
if items is None:
return err(1001, "无法识别人像")
@@ -622,7 +652,7 @@ async def hair_grow_v2(
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(必填)"),
hair_style: Optional[int] = Form(default=None, description="发型序号(必填)。female:1-5 male:1-4"),
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
prompt: str = Form(default="补充遮罩区域的头发", description="ComfyUI 提示词,会替换工作流节点60的文本"),
@@ -631,10 +661,11 @@ async def hair_grow_v2(
if gender not in ("male", "female"):
return err(1004, "gender 必填且只能为 male / female")
# 2. hair_style 必填校验(越界 → 1007)
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007
max_styles = {"female": 5, "male": 4}[gender]
if hair_style is None or not isinstance(hair_style, int) or hair_style < 1 or hair_style > max_styles:
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数,收到 {hair_style!r}")
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}")
# 3. 三选一取图
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
@@ -657,7 +688,7 @@ async def hair_grow_v2(
from hairline.service import generate_grow_results
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_style, _WORKFLOW2_PATH)
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_styles, _WORKFLOW2_PATH)
if items is None:
return err(1001, "无法识别人像")