接口4 不碰本地GPU/模型,仅调外网豆包→放网关本地实现更合理。文档含: 路由改本地处理(不转发)、httpx调方舟(OpenAI兼容,无需SDK)、prompt原文、 base64 data URI喂图、字段映射(6英文+全部中文)、无人脸→1001、配置(ark密钥入网关config)、 错误码、worker侧回收步骤、自测。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
接口 4:用户特征 — 网关侧实现方案
结论先行:接口 4 不碰任何本地 GPU/模型,只是「调一次外网豆包视觉模型 + 解析 JSON」。 因此在网关本地实现(不转发给 worker)最合理:网关本就是对外那台、天然有公网出口; worker 由此保持纯内网/离线。本文档供网关开发照做。
算法来源
/home/xsl/fuyan(FaceArk.py);worker 侧已有一版可跑通的实现face_features.py(用 volcengine SDK),可直接作为「逻辑参考」。
0. 与现状的差异
- 现网关
gateway/app.py里/api/v1/face/features是盲转发给 worker:@app.post("/api/v1/face/features", tags=["人脸分析"]) async def face_features(request: Request): return await _proxy(request, "/api/v1/face/features") - 改为网关本地处理(解析图片 → 调豆包 → 返回),不再转发。
- 其余 4 个接口(1/2/3/5)仍然盲转发给 worker,不变。
1. 对外契约(不变,接口文档.md 接口4)
POST /api/v1/face/features- 输入:图片三选一(
image_file/image_url/image_base64),无其它参数。 - 输出:标准信封
{code, message, request_id, data},其中data.features是一个 JSON 字符串(不是对象),内含几十项面部特征。无人脸返回 1001。
2. 依赖与配置
2.1 依赖
不需要 volcengine SDK。豆包/方舟是 OpenAI 兼容 接口,网关用现成的 httpx 直接调即可,保持轻量。
(若想省事,也可 pip install "volcengine-python-sdk[ark]" 直接照搬 worker 的 face_features.py,二选一。)
2.2 配置(加到 gateway/config.json,含密钥不入 git)
{
"ark": {
"api_key": "14fc0280-fc65-462d-ac2d-50178c0212e3",
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-vision-250815",
"timeout_seconds": 60
}
}
config.example.json里放占位("api_key": "your-volcengine-ark-api-key")。- 也可用环境变量覆盖(
ARK_API_KEY等),按网关现有风格来。 - ⚠️ 网关机需能访问
ark.cn-beijing.volces.com(已实测可达,401=要鉴权即连通)。
3. 豆包调用(httpx 版)
3.1 请求
POST {base_url}/chat/completions,头 Authorization: Bearer {api_key},体:
{
"model": "doubao-seed-1-6-vision-250815",
"messages": [{
"role": "user",
"content": [
{ "type": "image_url", "image_url": { "url": "<图片URL 或 base64 data URI>" } },
{ "type": "text", "text": "<下面 §3.3 的 PROMPT>" }
]
}]
}
返回里取 resp["choices"][0]["message"]["content"](是个带 ```json 包裹的字符串)。
3.2 图片怎么喂
- 传了
image_url→ 直接把这个 URL 塞进去(豆包自己去拉,最省)。 - 传了
image_file/image_base64→ 转成 base64 data URI:data:image/jpeg;base64,XXXX(PNG 头\x89PNG用image/png,否则image/jpeg)。 (已实测豆包接受 data URI,无需先把图落到公网。)
3.3 PROMPT(移植自 fuyan,去掉身高体重前缀,原样使用)
分析一下图片告诉我以下特征,只要答案,格式为json字符串,图片是否有人脸(有人/没人) 三庭五眼特征(答案要有三庭五眼四个字,9个字以内) 面部年龄(给出区间年龄)鼻长(鼻长适中/长鼻/短鼻) 脸型(圆形脸/心形脸/菱形脸/鹅蛋脸/方形脸/长形脸/瓜子脸) 嘴型 眼袋(答案要有眼袋两个个字) 眼型 鼻型 眼皮(双眼皮/单眼皮) 法令纹(有法令纹/无法令纹) 人中(人中适中/人中长/人中短) 眉形 瞳色(答案要有瞳色两个字) 脖长(脖长适中/脖子短/脖子长) 肤色(粉一白/粉二白/粉三白/黄一白/黄二白/黄黑皮)直得分 曲得分 直曲总分(直得分-曲得分) 大量感得分 小量感得分 量感总分(大量感得分-小量感得) 面部立体度(总分十分)瞳距(毫米)对比度(对比度较强/对比度适中/对比度较弱)鼻子立体度(立体度高/立体度适中/立体度低)色相(中间表示0,最大值分别是-5和5,负数表示偏冷,正数表示偏暖)亮度(中间表示0,最大值分别是-5和5;负数表示暗沉,正数表示白皙)色度(中间表示0,最大值分别是-5和5;负数表示饱和度低,正数表示鲜艳)面部颜色对比度(10分制)四季色彩季型(净春型/暖春型/浅春型/浅夏型/冷夏型/柔夏型/柔秋型/暖秋型/深秋型/净冬型/冷冬型/深冬型)基因风格(戏剧型/睿智型/自然型/古典型/优雅型/浪漫型/前卫型/少女型/少年型)量感类型(大量感/中量感/小量感)轮廓类型(轮廓偏曲/轮廓适中/轮廓偏直)动静类型(静态型/动态型)性别(男/女)
4. 解析 + 字段映射
- 去包裹解析:
content去掉首尾json后json.loads,得到 doubao 的中文字段 dict。 - 英文优先字段(与中文并存,方便客户端直接取):
_KEY_MAP = { "脸型": "face_shape", "眉形": "eyebrow_shape", "面部年龄": "facial_age", "动静类型": "dynamic_static_type", "性别": "gender", "基因风格": "gene_style", } for zh, en in _KEY_MAP.items(): if zh in data and en not in data: data[en] = data[zh] - 无人脸判定:
data["图片是否有人脸"]含「没人」/「没有」→ 视为无人脸 → 返回 1001。 data.features=json.dumps(data, ensure_ascii=False)(字符串)。
字段几十项:脸型/眉形/眼型/鼻型/眼袋/法令纹/人中/瞳色/脖长/肤色、三庭五眼、四季色彩季型、 量感/轮廓类型、各项得分、瞳距、对比度、基因风格、动静类型、性别…… 字段不固定、可增删。
5. 路由实现(替换盲转发)
gateway/app.py:
@app.post("/api/v1/face/features", tags=["人脸分析"])
async def face_features(request: Request):
from gateway.face_features import handle_features # 新增模块
return await handle_features(request) # 网关本地处理,不再 _proxy
gateway/face_features.py(新增,伪代码):
import base64, json, uuid, httpx
from fastapi import Request
from fastapi.responses import JSONResponse
from gateway.config import get_config
PROMPT = "...(§3.3 原样)..."
_KEY_MAP = {...} # §4
def _env(code, msg):
return JSONResponse(status_code=200, content={
"code": code, "message": msg, "request_id": f"gw-{uuid.uuid4().hex[:8]}", "data": None})
async def handle_features(request: Request) -> JSONResponse:
form = await request.form()
f = form.get("image_file"); url = form.get("image_url"); b64 = form.get("image_base64")
provided = [x for x in (f, url, b64) if x]
if len(provided) != 1:
return _env(1007, "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个")
# 图片 → URL 或 data URI
if url:
image_ref = url
else:
raw = (await f.read()) if f is not None else _decode_b64(b64) # base64 去 data:前缀再 decode
if len(raw) > 1_000_000:
return _env(1006, "文件超出 1 MB 限制")
fmt = "png" if raw[:8] == b"\x89PNG\r\n\x1a\n" else "jpeg"
image_ref = f"data:image/{fmt};base64," + base64.b64encode(raw).decode()
cfg = get_config()["ark"]
try:
async with httpx.AsyncClient(timeout=cfg.get("timeout_seconds", 60)) as cli:
r = await cli.post(
f'{cfg["base_url"]}/chat/completions',
headers={"Authorization": f'Bearer {cfg["api_key"]}'},
json={"model": cfg["model"], "messages": [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": image_ref}},
{"type": "text", "text": PROMPT}]}]})
r.raise_for_status()
text = r.json()["choices"][0]["message"]["content"]
data = _parse_json(text) # 去 ```json 包裹
for zh, en in _KEY_MAP.items():
if zh in data and en not in data: data[en] = data[zh]
if _no_face(data):
return _env(1001, "无法识别人像")
return JSONResponse(status_code=200, content={
"code": 0, "message": "success", "request_id": f"gw-{uuid.uuid4().hex[:8]}",
"data": {"features": json.dumps(data, ensure_ascii=False)}})
except Exception as ex:
return _env(1007, f"处理失败:{ex}")
_parse_json:s.strip(),若以 ``` 开头去掉反引号和开头的json,再json.loads。_no_face:"没人" in str(data.get("图片是否有人脸",""))。_decode_b64:有data:前缀就split(",",1)[1]再base64.b64decode。
⚠️ 网关其它接口是盲转发(不读 body);接口4 这里读了 form,没问题(不再转发,body 自己消费)。
6. 错误码
| 码 | 触发 |
|---|---|
| 1007 | 图片参数 0 个或多个;或调用/解析异常兜底 |
| 1006 | 文件 > 1MB(file/base64 路径) |
| 1001 | 豆包判定「没人」 |
| 0 | 正常,data.features 为 JSON 字符串 |
7. worker 侧回收(迁移后做)
接口4 改由网关处理、不再转发后,worker 的 /api/v1/face/features 就成了死代码。建议:
- worker
app.py把接口4 回退为 Mock(或保留真实实现做直连兜底,二选一); - worker
requirements.txt去掉volcengine-python-sdk[ark](worker 保持无外网依赖); worker_config.json去掉ark_api_key(密钥只放网关配置)。- worker 仓库现有
face_features.py可作为网关实现的逻辑参考,迁移完成后 worker 侧可删。
这步不阻塞网关实现;先把网关跑通,worker 回收随后做。
8. 自测
# 经网关(对外,客户端不带 token)
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/features \
-F image_file=@<一张人像> | python -m json.tool
# 期望:code 0;data.features 是 JSON 字符串,parse 后含 face_shape/gender/基因风格 等几十项
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/features \
-F image_file=@<风景图> ; # 期望 code 1001(豆包判无人脸;注意合成图偶有波动)
实测参考(worker 直跑 frontal):返回 42 字段,
鹅蛋脸 / 平眉 / 18-25岁 / 静态型 / 女 / 少年型。
文档版本: v1.0 | 创建日期: 2026-06-15 | 模型: 火山方舟 doubao-seed-1-6-vision | 运行位置: 网关(本地 httpx 调用,不经 worker)| 逻辑参考: worker
face_features.py//home/xsl/fuyan