feat(接口4): 用户特征-火山方舟豆包视觉模型(替换Mock)

接口4 改用远程视觉大模型(算法来源 /home/xsl/fuyan),一次返回几十项面部特征。

- face_features.py: 火山方舟 Ark 客户端(单例)+doubao-seed-1-6-vision prompt(移植fuyan)
  +base64 data URI喂图(实测doubao接受)+解析JSON+映射6英文优先字段(face_shape等)
  并保留doubao全部中文字段;has_face 据"图片是否有人脸"判定
- app.py: /face/features 真实实现,三选一图(image_url直传doubao,file/base64转dataURI),
  features 为 JSON 字符串;无人脸→1001;重活线程池
- 配置: API Key 走 worker_config.json.ark_api_key / 环境变量 ARK_API_KEY(不入git);
  worker_config.example 加占位; requirements 加 volcengine-python-sdk[ark]
- 文档: 接口文档/OpenAPI 更新为豆包实现+几十项字段+1001
- 测试: mock doubao 的成功/无人脸/多参,47全绿

⚠️ 唯一调外网云模型的接口,worker 需可达 ark.cn-beijing.volces.com(已实测可达)。
实测 frontal: 42字段, 鹅蛋脸/平眉/18-25岁/静态型/女/少年型。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-06-15 10:50:17 +08:00
co-authored by Claude Opus 4.8
parent 795de68985
commit 0e71830bb6
6 changed files with 233 additions and 37 deletions
+40 -23
View File
@@ -630,20 +630,23 @@ async def hair_grow_b(
---
由**火山方舟 豆包视觉模型**分析,返回一大批面部特征(脸型/眉形/眼型/肤色/三庭五眼/四季色彩季型/
量感/轮廓/基因风格/性别…几十项),**英文优先字段 + doubao 中文字段并存**。
**返回格式**`data.features` 为一个 **JSON 字符串**(不是对象),需要在客户端 `JSON.parse()` 后使用。
当前优先返回字段
英文优先字段(其余中文字段同时返回)
| 字段 | 类型 | 说明 |
|------|------|------|
| face_shape | string | 脸形"鹅蛋脸" |
| eyebrow_shape | string | 眉形"柳叶" |
| facial_age | int | 面部年龄 |
| dynamic_static_type | string | 动静类型,如"静态" |
| gender | string | 性别,如"" |
| gene_style | object | 面部特征对应的基因风格 |
| 字段 | 说明 |
|------|------|
| face_shape | 脸形"鹅蛋脸" |
| eyebrow_shape | 眉形"" |
| facial_age | 面部年龄(区间,如"18-25岁" |
| dynamic_static_type | 动静类型"静态"/"动态型" |
| gender | 性别(""/"" |
| gene_style | 基因风格(如"自然型" |
> 字段后续可随时增删,不固定,客户端按需取用即可
> 字段不固定、可随时增删,客户端按需取用。无人脸返回 `1001`
""",
responses={
200: {
@@ -655,7 +658,7 @@ async def hair_grow_b(
"message": "success",
"request_id": "mock-request-id",
"data": {
"features": '{"face_shape": "鹅蛋脸", "eyebrow_shape": "柳叶", "facial_age": 26, "dynamic_static_type": "静态", "gender": "", "gene_style": {"label": "面部特征标签", "style": "基因风格示例"}}',
"features": '{"图片是否有人脸":"有人","脸型":"鹅蛋脸","眉形":"","面部年龄":"18-25岁","四季色彩季型":"冷夏型","基因风格":"少年型","性别":"","face_shape":"鹅蛋脸","eyebrow_shape":"平眉","facial_age":"18-25岁","dynamic_static_type":"静态型","gender":"","gene_style":"少年型"}',
},
}
}
@@ -676,18 +679,32 @@ async def face_features(
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
):
features = json.dumps(
{
"face_shape": "鹅蛋脸",
"eyebrow_shape": "柳叶眉",
"facial_age": 26,
"dynamic_static_type": "静态",
"gender": "",
"gene_style": {"label": "面部特征标签", "style": "基因风格示例"},
},
ensure_ascii=False,
)
return ok({"features": features})
# 三选一。image_url 直接交给豆包拉取;file/base64 解成字节转 data URI。
provided = [x for x in (image_file, image_url, image_base64) if x]
if len(provided) != 1:
return err(1007, "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个")
img_bytes = None
if not image_url:
raw, e = await resolve_image_bytes(image_file, None, image_base64)
if e is not None:
return e
if len(raw) > MAX_FILE_BYTES:
return err(1006, "文件超出 1 MB 限制")
img_bytes = raw
try:
from fastapi.concurrency import run_in_threadpool
from face_features import analyze_features, has_face
feats = await run_in_threadpool(analyze_features, img_bytes, image_url)
if not has_face(feats):
return err(1001, "无法识别人像")
# 契约:data.features 为 JSON 字符串
return ok({"features": json.dumps(feats, ensure_ascii=False)})
except Exception as ex: # noqa: BLE001
logger.exception("接口4 处理异常")
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------