feat(接口4): 新增轮廓类型 contour_type + 提示词防漏键加固

- 豆包提示词追加「轮廓类型(轮廓偏曲/轮廓适中/轮廓偏直)」,_KEY_MAP 映射为 contour_type
- 提示词显式要求包含全部键:实测豆包随机漏键(面部年龄历史 ~26% 缺失),加固后 9/9 次全字段返回
- 同步接口文档/测试页至 7 字段;测试页顺带移除硬编码内部密码
This commit is contained in:
xsl
2026-09-07 00:35:24 +08:00
parent d242e0fc48
commit ebcaff1295
4 changed files with 21 additions and 15 deletions
+5 -4
View File
@@ -365,7 +365,7 @@
### 输出(data)
`data.features` 是一个 **JSON 字符串**(不是对象,客户端 `JSON.parse()` 后用),**仅含以下 6 个英文字段**:
`data.features` 是一个 **JSON 字符串**(不是对象,客户端 `JSON.parse()` 后用),**仅含以下 7 个英文字段**:
| 字段 | 说明 |
|------|------|
@@ -375,6 +375,7 @@
| dynamic_static_type | 动静类型(静态型/动态型)|
| gender | 性别(男/女)|
| gene_style | 基因风格(如"自然型")|
| contour_type | 轮廓类型(轮廓偏曲/轮廓适中/轮廓偏直)|
> 无人脸返回 `1001`(据 doubao「图片是否有人脸」判定)。⚠️ 本接口是唯一调**外网云模型**的接口,
> worker 需可访问 `ark.cn-beijing.volces.com`;API Key 走 worker 配置/环境变量。
@@ -387,12 +388,12 @@
"message": "success",
"request_id": "mock-request-id",
"data": {
"features": "{\"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\":\"少年型\",\"contour_type\":\"轮廓适中\"}"
}
}
```
> `features` 为字符串形式的 JSON,固定上述 6 个字段。
> `features` 为字符串形式的 JSON,固定上述 7 个字段。
---
@@ -700,7 +701,7 @@
| 2 C 端生发 | `/api/v1/hair/grow` | 用户照片 + gender + hair_style(多选) | 每发型预览 PNG + 生发图(female 1..5=换发型+重绘,其余=原生生发) |
| 7 C 端生发 v2 | `/api/v1/hair/grow-v2` | 同接口2 | 同接口2(add_hair2 工作流,已弃用隐藏) |
| 3 B 端生发 | `/api/v1/hair/grow-b` | 划线图片 | 检测到的发际线 + 生发后图片 |
| 4 用户特征 | `/api/v1/face/features` | 用户照片 | 6 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格),网关本机实现 |
| 4 用户特征 | `/api/v1/face/features` | 用户照片 | 7 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格/轮廓类型),网关本机实现 |
| 5 发际线 PNG | `/api/v1/hairline/generate` | 用户照片 + gender + hair_style(多选) | 每个选中发型 middle/high/low 三档发际线叠图 + 生发图 + 最合适发际线面部中间点坐标 |
| 9 头发遮罩 | `/api/v1/head/mask` | 用户照片 | 头发遮罩 + 分步可视化(BiSeNet/SegFormer 双路) |
| 10 外缘膨胀带 | `/api/v1/head/band` | 用户照片 | 头部外缘带状遮罩 + 分步可视化 |
+11 -6
View File
@@ -1,6 +1,6 @@
"""接口4:用户面部特征分析。
- 眉形 / 面部年龄 / 动静类型 / 性别 / 基因风格:火山方舟豆包视觉模型
- 眉形 / 面部年龄 / 动静类型 / 性别 / 基因风格 / 轮廓类型:火山方舟豆包视觉模型
- face_shape(脸型):本机 MediaPipe 分类(face/face_shape_classifier.py)覆盖,
不用豆包结果
@@ -26,15 +26,20 @@ _KEY_MAP = {
"动静类型": "dynamic_static_type",
"性别": "gender",
"基因风格": "gene_style",
"轮廓类型": "contour_type",
}
# 豆包只问 5 项 + 是否有人脸;脸型由本地分类器给出
# 豆包只问 6 项 + 是否有人脸;脸型由本地分类器给出。
# 「必须包含全部键」:实测豆包会随机漏键(尤其面部年龄,历史 ~26% 缺失),
# 显式要求可显著降低漏键率。
_PROMPT = (
"分析一下图片告诉我以下特征,只要答案,格式为json字符串,"
"必须包含以下全部键且每键都要有值、缺一不可:"
"图片是否有人脸(有人/没人) "
"眉形 "
"面部年龄(给出区间年龄) 动静类型(静态型/动态型) 性别(男/女) "
"面部年龄(给出区间年龄,如18-25岁) 动静类型(静态型/动态型) 性别(男/女) "
"基因风格(戏剧型/睿智型/自然型/古典型/优雅型/浪漫型/前卫型/少女型/少年型) "
"轮廓类型(轮廓偏曲/轮廓适中/轮廓偏直)"
)
_client = None # 缓存的 Ark client(api_key 变更时自动重建)
@@ -137,10 +142,10 @@ def _local_face_shape(image_bytes: bytes = None, image_url: str = None) -> str:
def analyze_features(image_bytes: bytes = None, image_url: str = None):
"""豆包分析 5 项特征 + 本机 MediaPipe 覆盖 face_shape。
"""豆包分析 6 项特征 + 本机 MediaPipe 覆盖 face_shape。
Returns: dict —— 6 个英文字段(face_shape/eyebrow_shape/facial_age/
dynamic_static_type/gender/gene_style);**无人脸返回 None**(调用方据此判 1001)。
Returns: dict —— 7 个英文字段(face_shape/eyebrow_shape/facial_age/
dynamic_static_type/gender/gene_style/contour_type);**无人脸返回 None**(调用方据此判 1001)。
"""
url = _image_to_url(image_bytes, image_url)
resp = get_client().chat.completions.create(
+1 -1
View File
@@ -493,7 +493,7 @@ async def face_features(
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带前缀)"),
):
"""接口4:用户特征分析 — 豆包 5 项 + 本机 MediaPipe 脸型(face/),不经过 worker。"""
"""接口4:用户特征分析 — 豆包 6 项 + 本机 MediaPipe 脸型(face/),不经过 worker。"""
import uuid as _uuid
# 三选一校验
+3 -3
View File
@@ -112,10 +112,10 @@
<script>
const API_BASE = window.location.origin;
const EN_KEYS = ['face_shape','eyebrow_shape','facial_age','dynamic_static_type','gender','gene_style'];
const EN_KEYS = ['face_shape','eyebrow_shape','facial_age','dynamic_static_type','gender','gene_style','contour_type'];
const EN_LABELS = {
face_shape:'脸型', eyebrow_shape:'眉形', facial_age:'面部年龄',
dynamic_static_type:'动静类型', gender:'性别', gene_style:'基因风格'
dynamic_static_type:'动静类型', gender:'性别', gene_style:'基因风格', contour_type:'轮廓类型'
};
function $(id) { return document.getElementById(id); }
@@ -133,7 +133,7 @@ async function submitTest() {
const fd = new FormData(); fd.append('image_file', f);
const _reqStart = performance.now();
try {
const r = await fetch(API_BASE + '/api/v1/face/features', { method:'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
const r = await fetch(API_BASE + '/api/v1/face/features', { method:'POST', body:fd });
const json = await r.json();
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
$('jsonContent').textContent = JSON.stringify(json, null, 2);