feat(接口4): 精简输出为固定6个英文字段,缩短提示词
- face_features.py:_PROMPT 只问6项特征(+有无人脸),analyze_features 只返回6个英文字段;无人脸返回 None(不再依赖 has_face 在外部判定) - gateway/app.py:无脸判定改为 feats is None,去掉 has_face import - app.py / docs / test_interface4.html:Swagger/文档/示例/测试页同步 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,3 +24,6 @@ hairline/models/face-parsing/model.safetensors
|
|||||||
static/annotations/*
|
static/annotations/*
|
||||||
!static/annotations/.gitkeep
|
!static/annotations/.gitkeep
|
||||||
tests/output/
|
tests/output/
|
||||||
|
|
||||||
|
# 本地临时遮罩测试页(不入 git)
|
||||||
|
test_local.py
|
||||||
|
|||||||
@@ -653,13 +653,10 @@ async def hair_grow_b(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
由**火山方舟 豆包视觉模型**分析,返回一大批面部特征(脸型/眉形/眼型/肤色/三庭五眼/四季色彩季型/
|
由**火山方舟 豆包视觉模型**分析,返回**固定 6 个英文字段**。
|
||||||
量感/轮廓/基因风格/性别…几十项),**英文优先字段 + doubao 中文字段并存**。
|
|
||||||
|
|
||||||
**返回格式**:`data.features` 为一个 **JSON 字符串**(不是对象),需要在客户端 `JSON.parse()` 后使用。
|
**返回格式**:`data.features` 为一个 **JSON 字符串**(不是对象),需要在客户端 `JSON.parse()` 后使用。
|
||||||
|
|
||||||
英文优先字段(其余中文字段同时返回):
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| face_shape | 脸形(如"鹅蛋脸") |
|
| face_shape | 脸形(如"鹅蛋脸") |
|
||||||
@@ -669,7 +666,7 @@ async def hair_grow_b(
|
|||||||
| gender | 性别("男"/"女") |
|
| gender | 性别("男"/"女") |
|
||||||
| gene_style | 基因风格(如"自然型") |
|
| gene_style | 基因风格(如"自然型") |
|
||||||
|
|
||||||
> 字段不固定、可随时增删,客户端按需取用。无人脸返回 `1001`。
|
> 无人脸返回 `1001`。
|
||||||
""",
|
""",
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
@@ -681,7 +678,7 @@ async def hair_grow_b(
|
|||||||
"message": "success",
|
"message": "success",
|
||||||
"request_id": "mock-request-id",
|
"request_id": "mock-request-id",
|
||||||
"data": {
|
"data": {
|
||||||
"features": '{"图片是否有人脸":"有人","脸型":"鹅蛋脸","眉形":"平眉","面部年龄":"18-25岁","四季色彩季型":"冷夏型","基因风格":"少年型","性别":"女","face_shape":"鹅蛋脸","eyebrow_shape":"平眉","facial_age":"18-25岁","dynamic_static_type":"静态型","gender":"女","gene_style":"少年型"}',
|
"features": '{"face_shape":"鹅蛋脸","eyebrow_shape":"平眉","facial_age":"18-25岁","dynamic_static_type":"静态型","gender":"女","gene_style":"少年型"}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +703,6 @@ async def face_features(
|
|||||||
# 网关不会把本接口转发到 worker,故此处仅留 Mock 占位、保持 worker 无外网依赖。
|
# 网关不会把本接口转发到 worker,故此处仅留 Mock 占位、保持 worker 无外网依赖。
|
||||||
features = json.dumps(
|
features = json.dumps(
|
||||||
{
|
{
|
||||||
"_note": "接口4 由网关实现,worker 此响应为占位 Mock",
|
|
||||||
"face_shape": "鹅蛋脸", "eyebrow_shape": "平眉", "facial_age": "18-25岁",
|
"face_shape": "鹅蛋脸", "eyebrow_shape": "平眉", "facial_age": "18-25岁",
|
||||||
"dynamic_static_type": "静态型", "gender": "女", "gene_style": "少年型",
|
"dynamic_static_type": "静态型", "gender": "女", "gene_style": "少年型",
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-8
@@ -283,11 +283,7 @@
|
|||||||
|
|
||||||
### 输出(data)
|
### 输出(data)
|
||||||
|
|
||||||
`data.features` 是一个 **JSON 字符串**(不是对象,客户端 `JSON.parse()` 后用)。内含**几十项**特征:
|
`data.features` 是一个 **JSON 字符串**(不是对象,客户端 `JSON.parse()` 后用),**仅含以下 6 个英文字段**:
|
||||||
脸型/眉形/眼型/鼻型/眼袋/法令纹/人中/瞳色/脖长/肤色、三庭五眼、四季色彩季型、量感/轮廓类型、
|
|
||||||
直曲量感得分、面部立体度、瞳距、对比度、基因风格、动静类型、性别……字段不固定、可随时增删。
|
|
||||||
|
|
||||||
其中**英文优先字段**(与 doubao 中文字段并存,方便客户端直接取):
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -309,12 +305,12 @@
|
|||||||
"message": "success",
|
"message": "success",
|
||||||
"request_id": "mock-request-id",
|
"request_id": "mock-request-id",
|
||||||
"data": {
|
"data": {
|
||||||
"features": "{\"图片是否有人脸\":\"有人\",\"脸型\":\"鹅蛋脸\",\"眉形\":\"平眉\",\"面部年龄\":\"18-25岁\",\"四季色彩季型\":\"冷夏型\",\"基因风格\":\"少年型\",\"性别\":\"女\",\"face_shape\":\"鹅蛋脸\",\"eyebrow_shape\":\"平眉\",\"facial_age\":\"18-25岁\",\"dynamic_static_type\":\"静态型\",\"gender\":\"女\",\"gene_style\":\"少年型\"}"
|
"features": "{\"face_shape\":\"鹅蛋脸\",\"eyebrow_shape\":\"平眉\",\"facial_age\":\"18-25岁\",\"dynamic_static_type\":\"静态型\",\"gender\":\"女\",\"gene_style\":\"少年型\"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> `features` 为字符串形式的 JSON,字段后续可随时增删,不固定。
|
> `features` 为字符串形式的 JSON,固定上述 6 个字段。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -372,7 +368,7 @@
|
|||||||
| 1 四庭七眼测量 | 用户照片 | 标注 PNG(无人物)+ 四庭/七眼厘米数值与坐标 |
|
| 1 四庭七眼测量 | 用户照片 | 标注 PNG(无人物)+ 四庭/七眼厘米数值与坐标 |
|
||||||
| 2 C 端生发 | 用户照片 | 生发后图片 + 多张发际线(带类型与排序) |
|
| 2 C 端生发 | 用户照片 | 生发后图片 + 多张发际线(带类型与排序) |
|
||||||
| 3 B 端生发 | 划线图片 | 最合适发际线图片 + 生发后图片 |
|
| 3 B 端生发 | 划线图片 | 最合适发际线图片 + 生发后图片 |
|
||||||
| 4 用户特征 | 用户照片 | N 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格…) |
|
| 4 用户特征 | 用户照片 | 6 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格) |
|
||||||
| 5 发际线 PNG | 用户照片 | N 张发际线 PNG + 最合适发际线面部中间点坐标 |
|
| 5 发际线 PNG | 用户照片 | N 张发际线 PNG + 最合适发际线面部中间点坐标 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+17
-30
@@ -18,7 +18,7 @@ logger = logging.getLogger("hair.worker")
|
|||||||
ARK_BASE_URL = os.getenv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
ARK_BASE_URL = os.getenv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
||||||
ARK_MODEL = os.getenv("ARK_MODEL", "doubao-seed-1-6-vision-250815")
|
ARK_MODEL = os.getenv("ARK_MODEL", "doubao-seed-1-6-vision-250815")
|
||||||
|
|
||||||
# doubao 中文键 → 接口4 英文优先字段
|
# doubao 中文键 → 接口4 英文优先字段(仅保留这 6 项)
|
||||||
_KEY_MAP = {
|
_KEY_MAP = {
|
||||||
"脸型": "face_shape",
|
"脸型": "face_shape",
|
||||||
"眉形": "eyebrow_shape",
|
"眉形": "eyebrow_shape",
|
||||||
@@ -28,27 +28,13 @@ _KEY_MAP = {
|
|||||||
"基因风格": "gene_style",
|
"基因风格": "gene_style",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 移植自 fuyan FaceArk.GetPicDesc 的特征枚举(去掉身高体重前缀)
|
# 仅请求接口4 需要的 6 个字段(+「图片是否有人脸」用于 1001 判定,不进最终输出)
|
||||||
_PROMPT = (
|
_PROMPT = (
|
||||||
"分析一下图片告诉我以下特征,只要答案,格式为json字符串,"
|
"分析一下图片告诉我以下特征,只要答案,格式为json字符串,"
|
||||||
"图片是否有人脸(有人/没人) 三庭五眼特征(答案要有三庭五眼四个字,9个字以内) "
|
"图片是否有人脸(有人/没人) "
|
||||||
"面部年龄(给出区间年龄)鼻长(鼻长适中/长鼻/短鼻) "
|
"脸型(圆形脸/心形脸/菱形脸/鹅蛋脸/方形脸/长形脸/瓜子脸) 眉形 "
|
||||||
"脸型(圆形脸/心形脸/菱形脸/鹅蛋脸/方形脸/长形脸/瓜子脸) 嘴型 "
|
"面部年龄(给出区间年龄) 动静类型(静态型/动态型) 性别(男/女) "
|
||||||
"眼袋(答案要有眼袋两个个字) 眼型 鼻型 眼皮(双眼皮/单眼皮) "
|
|
||||||
"法令纹(有法令纹/无法令纹) 人中(人中适中/人中长/人中短) 眉形 "
|
|
||||||
"瞳色(答案要有瞳色两个字) 脖长(脖长适中/脖子短/脖子长) "
|
|
||||||
"肤色(粉一白/粉二白/粉三白/黄一白/黄二白/黄黑皮)"
|
|
||||||
"直得分 曲得分 直曲总分(直得分-曲得分) 大量感得分 小量感得分 量感总分(大量感得分-小量感得) "
|
|
||||||
"面部立体度(总分十分)瞳距(毫米)对比度(对比度较强/对比度适中/对比度较弱)"
|
|
||||||
"鼻子立体度(立体度高/立体度适中/立体度低)"
|
|
||||||
"色相(中间表示0,最大值分别是-5和5,负数表示偏冷,正数表示偏暖)"
|
|
||||||
"亮度(中间表示0,最大值分别是-5和5;负数表示暗沉,正数表示白皙)"
|
|
||||||
"色度(中间表示0,最大值分别是-5和5;负数表示饱和度低,正数表示鲜艳)"
|
|
||||||
"面部颜色对比度(10分制)"
|
|
||||||
"四季色彩季型(净春型/暖春型/浅春型/浅夏型/冷夏型/柔夏型/柔秋型/暖秋型/深秋型/净冬型/冷冬型/深冬型)"
|
|
||||||
"基因风格(戏剧型/睿智型/自然型/古典型/优雅型/浪漫型/前卫型/少女型/少年型)"
|
"基因风格(戏剧型/睿智型/自然型/古典型/优雅型/浪漫型/前卫型/少女型/少年型)"
|
||||||
"量感类型(大量感/中量感/小量感)轮廓类型(轮廓偏曲/轮廓适中/轮廓偏直)"
|
|
||||||
"动静类型(静态型/动态型)性别(男/女)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_client = None
|
_client = None
|
||||||
@@ -102,11 +88,11 @@ def _image_to_url(image_bytes: bytes = None, image_url: str = None) -> str:
|
|||||||
return f"data:image/{fmt};base64," + base64.b64encode(image_bytes).decode()
|
return f"data:image/{fmt};base64," + base64.b64encode(image_bytes).decode()
|
||||||
|
|
||||||
|
|
||||||
def analyze_features(image_bytes: bytes = None, image_url: str = None) -> dict:
|
def analyze_features(image_bytes: bytes = None, image_url: str = None):
|
||||||
"""调 doubao 视觉模型分析人脸特征。
|
"""调 doubao 视觉模型分析人脸特征。
|
||||||
|
|
||||||
Returns: dict —— 含接口4 英文优先字段(face_shape 等) + doubao 全部中文字段。
|
Returns: dict —— 仅含接口4 的 6 个英文字段(face_shape/eyebrow_shape/facial_age/
|
||||||
无人脸时 doubao 的「图片是否有人脸」= 没人,调用方据此可判 1001。
|
dynamic_static_type/gender/gene_style);**无人脸返回 None**(调用方据此判 1001)。
|
||||||
"""
|
"""
|
||||||
url = _image_to_url(image_bytes, image_url)
|
url = _image_to_url(image_bytes, image_url)
|
||||||
resp = get_client().chat.completions.create(
|
resp = get_client().chat.completions.create(
|
||||||
@@ -120,12 +106,11 @@ def analyze_features(image_bytes: bytes = None, image_url: str = None) -> dict:
|
|||||||
}],
|
}],
|
||||||
)
|
)
|
||||||
text = resp.choices[0].message.content
|
text = resp.choices[0].message.content
|
||||||
data = _parse_json(text) # doubao 中文字段
|
raw = _parse_json(text) # doubao 原始中文字段
|
||||||
# 英文优先字段映射(doubao 缺某字段则跳过)
|
if not has_face(raw):
|
||||||
for zh, en in _KEY_MAP.items():
|
return None
|
||||||
if zh in data and en not in data:
|
# 只保留 6 个英文字段(doubao 缺某字段则跳过)
|
||||||
data[en] = data[zh]
|
return {en: raw[zh] for zh, en in _KEY_MAP.items() if zh in raw}
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def has_face(features: dict) -> bool:
|
def has_face(features: dict) -> bool:
|
||||||
@@ -139,5 +124,7 @@ if __name__ == "__main__":
|
|||||||
path = sys.argv[1] if len(sys.argv) > 1 else "tests/fixtures/frontal.jpg"
|
path = sys.argv[1] if len(sys.argv) > 1 else "tests/fixtures/frontal.jpg"
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
feats = analyze_features(image_bytes=f.read())
|
feats = analyze_features(image_bytes=f.read())
|
||||||
print("has_face:", has_face(feats))
|
if feats is None:
|
||||||
print(json.dumps(feats, ensure_ascii=False, indent=2))
|
print("无人脸(1001)")
|
||||||
|
else:
|
||||||
|
print(json.dumps(feats, ensure_ascii=False, indent=2))
|
||||||
|
|||||||
+2
-2
@@ -291,7 +291,7 @@ async def face_features(
|
|||||||
})
|
})
|
||||||
|
|
||||||
from fastapi.concurrency import run_in_threadpool
|
from fastapi.concurrency import run_in_threadpool
|
||||||
from face_features import analyze_features, has_face
|
from face_features import analyze_features
|
||||||
|
|
||||||
try:
|
try:
|
||||||
feats = await run_in_threadpool(analyze_features, img_bytes, image_url)
|
feats = await run_in_threadpool(analyze_features, img_bytes, image_url)
|
||||||
@@ -302,7 +302,7 @@ async def face_features(
|
|||||||
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
|
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
|
||||||
})
|
})
|
||||||
|
|
||||||
if not has_face(feats):
|
if feats is None:
|
||||||
return JSONResponse(status_code=200, content={
|
return JSONResponse(status_code=200, content={
|
||||||
"code": 1001, "message": "无法识别人像",
|
"code": 1001, "message": "无法识别人像",
|
||||||
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
|
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🔬 接口4 — 用户特征分析 测试</h1>
|
<h1>🔬 接口4 — 用户特征分析 测试</h1>
|
||||||
<p class="subtitle">POST /api/v1/face/features | 上传照片 → AI视觉模型 → 42项面部特征 | data.features 为 JSON 字符串</p>
|
<p class="subtitle">POST /api/v1/face/features | 上传照片 → AI视觉模型 → 6项面部特征 | data.features 为 JSON 字符串</p>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|||||||
Reference in New Issue
Block a user