Compare commits
2
Commits
76f7c06905
...
a3f4c822cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3f4c822cb | ||
|
|
bee4bdae10 |
+9
-2
@@ -185,11 +185,18 @@ async def health():
|
|||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
async def index():
|
async def index():
|
||||||
cfg = load_config()
|
|
||||||
return {
|
return {
|
||||||
"service": "旷视五接口 — 网关",
|
"service": "旷视五接口 — 网关",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"docs": f"{cfg['public_base_url']}/docs",
|
"docs": "/docs",
|
||||||
|
"integration_guide": "/static/integration.html",
|
||||||
|
"test_pages": {
|
||||||
|
"if1_measure": "/static/test_interface1.html",
|
||||||
|
"if2_hair_grow": "/static/test_interface2.html",
|
||||||
|
"if3_hair_grow_b": "/static/test_interface3.html",
|
||||||
|
"if4_features": "/static/test_interface4.html",
|
||||||
|
"if5_hairline": "/static/test_interface5.html",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>旷视五接口 — 前端接入说明</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f8f9fb; color: #1e293b; line-height: 1.6; }
|
||||||
|
.wrap { max-width: 960px; margin: 0 auto; padding: 32px 24px; }
|
||||||
|
|
||||||
|
h1 { font-size: 26px; margin-bottom: 8px; }
|
||||||
|
.subtitle { color: #64748b; font-size: 14px; margin-bottom: 32px; }
|
||||||
|
|
||||||
|
.card { background: #fff; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,.05); margin-bottom: 24px; overflow: hidden; }
|
||||||
|
.card h2 { font-size: 16px; padding: 16px 20px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }
|
||||||
|
.card-body { padding: 18px 20px; }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #f1f5f9; }
|
||||||
|
th { background: #f8fafc; font-weight: 700; color: #475569; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
code { font-family: "SF Mono", "Fira Code", monospace; font-size: 12.5px; background: #f1f5f9; padding: 2px 6px; border-radius: 4px; color: #0f172a; }
|
||||||
|
pre { background: #1e293b; color: #e2e8f0; padding: 16px 20px; border-radius: 8px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12.5px; line-height: 1.5; overflow-x: auto; }
|
||||||
|
|
||||||
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 11px; font-weight: 700; }
|
||||||
|
.badge.post { background: #dbeafe; color: #1e40af; }
|
||||||
|
.badge.get { background: #d1fae5; color: #065f46; }
|
||||||
|
.badge.warn { background: #fef3c7; color: #92400e; }
|
||||||
|
|
||||||
|
.endpoint { margin-bottom: 20px; }
|
||||||
|
.endpoint .url { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
|
||||||
|
.endpoint .desc { color: #64748b; font-size: 13px; margin-bottom: 10px; }
|
||||||
|
.endpoint .params { font-size: 12px; color: #94a3b8; margin-bottom: 8px; }
|
||||||
|
|
||||||
|
.link { color: #2563eb; text-decoration: none; font-size: 13px; }
|
||||||
|
.link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.toc { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; }
|
||||||
|
.toc a { font-size: 13px; color: #2563eb; text-decoration: none; padding: 4px 12px; background: #eff6ff; border-radius: 6px; }
|
||||||
|
.toc a:hover { background: #dbeafe; }
|
||||||
|
|
||||||
|
hr { border: none; border-top: 1px solid #e2e8f0; margin: 20px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>📡 旷视五接口 — 前端接入说明</h1>
|
||||||
|
<p class="subtitle">Base URL: <code>https://hair.xiangsilian.com</code> | 在线文档: <a href="/docs" class="link">/docs</a> | 在线测试页见底部</p>
|
||||||
|
|
||||||
|
<div class="toc">
|
||||||
|
<a href="#overview">通用约定</a>
|
||||||
|
<a href="#if1">接口1</a>
|
||||||
|
<a href="#if2">接口2</a>
|
||||||
|
<a href="#if3">接口3</a>
|
||||||
|
<a href="#if4">接口4</a>
|
||||||
|
<a href="#if5">接口5</a>
|
||||||
|
<a href="#errors">错误码</a>
|
||||||
|
<a href="#test">在线测试</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 通用约定 ======== -->
|
||||||
|
<div class="card" id="overview">
|
||||||
|
<h2>🔧 通用约定</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<table>
|
||||||
|
<tr><td>协议</td><td>HTTPS</td></tr>
|
||||||
|
<tr><td>请求方式</td><td>全部 <span class="badge post">POST</span></td></tr>
|
||||||
|
<tr><td>编码</td><td>UTF-8</td></tr>
|
||||||
|
<tr><td>Content-Type</td><td><code>multipart/form-data</code></td></tr>
|
||||||
|
<tr><td>图片参数</td><td>三选一:<code>image_file</code>(文件上传)/ <code>image_url</code>(URL)/ <code>image_base64</code>(base64 + 前缀)</td></tr>
|
||||||
|
<tr><td>图片格式</td><td>JPG / PNG,≤ 1 MB,单人正面照</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:14px;font-size:13px"><strong>统一响应结构</strong>:</p>
|
||||||
|
<pre>{
|
||||||
|
"code": 0, // 0=成功,非0=错误
|
||||||
|
"message": "success",
|
||||||
|
"request_id": "...", // 请求追踪 ID
|
||||||
|
"data": { ... } // 业务数据,各接口不同
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 接口1 ======== -->
|
||||||
|
<div class="card" id="if1">
|
||||||
|
<h2>1. 四庭七眼测量 <span class="badge post">POST</span> <code>/api/v1/face/measure</code></h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="desc">上传正面照 → 返回标注好的 PNG(仅标注图层,透明底)+ 四庭七眼厘米数值 + 关键点像素坐标。</p>
|
||||||
|
|
||||||
|
<p><strong>入参</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
|
<tr><td>image_file</td><td>file</td><td>三选一</td><td>上传图片文件</td></tr>
|
||||||
|
<tr><td>image_url</td><td>string</td><td>三选一</td><td>图片 URL</td></tr>
|
||||||
|
<tr><td>image_base64</td><td>string</td><td>三选一</td><td>base64 字符串,需带 data:image/...;base64, 前缀</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>annotated_image_url</code></td><td>string</td><td>标注 PNG URL(仅标注图层,透明底,叠加到原图上显示)</td></tr>
|
||||||
|
<tr><td><code>face_total_height_cm</code></td><td>number</td><td>全脸高度(cm)</td></tr>
|
||||||
|
<tr><td><code>four_courts</code></td><td>object</td><td>四庭:top/upper/middle/lower,各含 _cm 和 ratios</td></tr>
|
||||||
|
<tr><td><code>seven_eyes</code></td><td>object</td><td>七眼:eye_width/face_width/inter_eye_distance,各含 _cm 和 ratios</td></tr>
|
||||||
|
<tr><td><code>landmarks</code></td><td>object</td><td>5 个关键点像素坐标:hair_top/hairline/brow_center/nose_bottom/chin_tip</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:12px;font-size:12px;color:#64748b">💡 前端把标注图叠加到原图上即可呈现测量效果(标注图白色线条 #FFFFFF,透明底)。</p>
|
||||||
|
|
||||||
|
<p style="margin-top:8px"><strong>请求示例</strong>(fetch):</p>
|
||||||
|
<pre>const fd = new FormData();
|
||||||
|
fd.append('image_file', file); // 或 image_url / image_base64
|
||||||
|
|
||||||
|
const res = await fetch('https://hair.xiangsilian.com/api/v1/face/measure', {
|
||||||
|
method: 'POST',
|
||||||
|
body: fd
|
||||||
|
});
|
||||||
|
const { code, data } = await res.json();
|
||||||
|
// data.annotated_image_url → 标注 PNG
|
||||||
|
// data.face_total_height_cm → 全脸高度
|
||||||
|
// data.four_courts.ratios → { top_court: 0.25, ... }
|
||||||
|
// 前端叠加显示:原图 + annotated_image_url(absolute 叠加)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 接口2 ======== -->
|
||||||
|
<div class="card" id="if2">
|
||||||
|
<h2>2. C端生发 <span class="badge post">POST</span> <code>/api/v1/hair/grow</code></h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="desc">上传正面照 + 性别 → 多组发际线方案。每组含发际线叠加预览图 + ComfyUI 生发效果图。</p>
|
||||||
|
|
||||||
|
<p><strong>入参</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
|
<tr><td>image_file / image_url / image_base64</td><td>—</td><td>三选一</td><td>用户正面照</td></tr>
|
||||||
|
<tr><td>gender</td><td>string</td><td>✅ 必填</td><td><code>"male"</code> / <code>"female"</code></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:12px"><strong>data.results[] 元素</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>image_url</code></td><td>string</td><td>发际线叠加预览图(曲线叠在原图上)</td></tr>
|
||||||
|
<tr><td><code>grown_image_url</code></td><td>string</td><td>生发后效果图(ComfyUI/Flux「植发3个月」)⚠ 可空</td></tr>
|
||||||
|
<tr><td><code>hairline_type</code></td><td>string</td><td>发际线类型 key</td></tr>
|
||||||
|
<tr><td><code>order</code></td><td>int</td><td>排序(1=最佳,当前按贴图顺序)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:8px;font-size:12px;color:#64748b">
|
||||||
|
Female 5 种:ellipse/flower/heart/straight/wave |
|
||||||
|
Male 4 种:ellipse/m/straight/inverse_arc<br>
|
||||||
|
⚠ 生发图由本机 ComfyUI 生成,耗时可达数分钟,fetch 超时需放大(≥5min)。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 接口3 ======== -->
|
||||||
|
<div class="card" id="if3">
|
||||||
|
<h2>3. B端生发(医生端) <span class="badge post">POST</span> <code>/api/v1/hair/grow-b</code></h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="desc">医生用马克笔在用户照片额头画线 → 拍照上传 → 系统生成生发效果图。</p>
|
||||||
|
|
||||||
|
<p><strong>入参</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
|
<tr><td>marked_image_file / marked_image_url / marked_image_base64</td><td>—</td><td>✅ 三选一</td><td>已用马克笔标注发际线的图片</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>hair_growth_image_url</code></td><td>string</td><td>生发后效果图 URL ⚠ 可空</td></tr>
|
||||||
|
<tr><td><code>hairline_type</code></td><td>string</td><td>固定 <code>"custom"</code>(手绘定制)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="font-size:12px;color:#64748b">⚠ 划线图未检测到划线(或无人脸)→ code=1001</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 接口4 ======== -->
|
||||||
|
<div class="card" id="if4">
|
||||||
|
<h2>4. 用户特征分析 <span class="badge post">POST</span> <code>/api/v1/face/features</code></h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="desc">上传照片 → 火山方舟豆包视觉模型分析 → 返回几十项面部特征(脸型/眉形/肤色/四季色彩…)。</p>
|
||||||
|
|
||||||
|
<p><strong>入参</strong>:image_file / image_url / image_base64 三选一。无其他参数。</p>
|
||||||
|
|
||||||
|
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>features</code></td><td>string</td><td><strong>JSON 字符串</strong>(不是对象!客户端需 <code>JSON.parse()</code>)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:8px"><strong>features 英文优先字段</strong>(其余中文字段同时返回,共~42个):</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>字段</th><th>说明</th><th>字段</th><th>说明</th></tr>
|
||||||
|
<tr><td>face_shape</td><td>脸型</td><td>eyebrow_shape</td><td>眉形</td></tr>
|
||||||
|
<tr><td>facial_age</td><td>面部年龄区间</td><td>gender</td><td>性别</td></tr>
|
||||||
|
<tr><td>dynamic_static_type</td><td>动静类型</td><td>gene_style</td><td>基因风格</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<pre>const res = await fetch('https://hair.xiangsilian.com/api/v1/face/features', {
|
||||||
|
method: 'POST',
|
||||||
|
body: fd
|
||||||
|
});
|
||||||
|
const { code, data } = await res.json();
|
||||||
|
const features = JSON.parse(data.features); // ← 注意:data.features 是字符串!
|
||||||
|
console.log(features.face_shape); // "鹅蛋脸"
|
||||||
|
console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保留)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 接口5 ======== -->
|
||||||
|
<div class="card" id="if5">
|
||||||
|
<h2>5. 发际线 PNG 生成 <span class="badge post">POST</span> <code>/api/v1/hairline/generate</code></h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="desc">上传正面照 + 性别 → N 张发际线叠加图 + 最佳发际线中心点坐标。</p>
|
||||||
|
|
||||||
|
<p><strong>入参</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
|
<tr><td>image_file / image_url / image_base64</td><td>—</td><td>三选一</td><td>用户正面照</td></tr>
|
||||||
|
<tr><td>gender</td><td>string</td><td>✅ 必填</td><td><code>"male"</code> / <code>"female"</code></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||||
|
<table>
|
||||||
|
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>hairline_images[]</code></td><td>object[]</td><td>发际线叠加图列表,每项含 image_url + order</td></tr>
|
||||||
|
<tr><td><code>best_hairline_center_point</code></td><td>object</td><td>最佳发际线中心点像素坐标 <code>{ x: number, y: number }</code></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 错误码 ======== -->
|
||||||
|
<div class="card" id="errors">
|
||||||
|
<h2>⚠ 错误码</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<table>
|
||||||
|
<tr><th>code</th><th>message</th><th>说明</th></tr>
|
||||||
|
<tr><td>1001</td><td>无法识别人像</td><td>未检测到人脸</td></tr>
|
||||||
|
<tr><td>1002</td><td>人像分辨率过低</td><td>低于最低分辨率</td></tr>
|
||||||
|
<tr><td>1003</td><td>角度问题,非正面照</td><td>非正面 / 角度过大</td></tr>
|
||||||
|
<tr><td>1005</td><td>检测到多张人脸</td><td>仅支持单人</td></tr>
|
||||||
|
<tr><td>1006</td><td>文件超出大小限制</td><td>单文件 > 1 MB</td></tr>
|
||||||
|
<tr><td>1007</td><td>图片参数错误 / 后端不可用</td><td>参数传错 / 服务繁忙请稍后重试</td></tr>
|
||||||
|
<tr><td>1008</td><td>图片格式不支持</td><td>非 JPG/PNG / base64 解码失败</td></tr>
|
||||||
|
</table>
|
||||||
|
<p style="font-size:12px;color:#94a3b8;margin-top:8px">1004 已废弃(接口2 不再自动判性别,改由客户端传 gender 参数)。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======== 在线测试页 ======== -->
|
||||||
|
<div class="card" id="test">
|
||||||
|
<h2>🧪 在线测试页面</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<table>
|
||||||
|
<tr><th>接口</th><th>测试页</th><th>功能</th></tr>
|
||||||
|
<tr><td>1. 四庭七眼</td><td><a href="/static/test_interface1.html" class="link">/static/test_interface1.html</a></td><td>上传照片 → 原图+标注叠加,底图/标注开关,指标卡片</td></tr>
|
||||||
|
<tr><td>2. C端生发</td><td><a href="/static/test_interface2.html" class="link">/static/test_interface2.html</a></td><td>上传+性别 → 方案一覧(原图/叠加/生发),双图对比</td></tr>
|
||||||
|
<tr><td>3. B端生发</td><td><a href="/static/test_interface3.html" class="link">/static/test_interface3.html</a></td><td>划线图上传 → 生发效果图</td></tr>
|
||||||
|
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 42项面部特征表格 + 原始JSON</td></tr>
|
||||||
|
<tr><td>5. 发际线PNG</td><td><a href="/static/test_interface5.html" class="link">/static/test_interface5.html</a></td><td>上传+性别 → 发际线方案+中心点坐标</td></tr>
|
||||||
|
</table>
|
||||||
|
<p style="font-size:12px;color:#94a3b8;margin-top:12px">
|
||||||
|
完整 API 文档:<a href="/docs" class="link">/docs</a>(Swagger UI)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="text-align:center;font-size:12px;color:#cbd5e1;margin-top:32px">
|
||||||
|
Base: https://hair.xiangsilian.com | Swagger: /docs | 接入说明: /static/integration.html
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>接口4 — 用户特征分析 测试页</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
||||||
|
h1 { font-size: 22px; margin-bottom: 6px; }
|
||||||
|
.subtitle { color: #888; font-size: 13px; margin-bottom: 24px; }
|
||||||
|
|
||||||
|
.card { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
|
||||||
|
.card-header { font-weight: 700; font-size: 14px; padding: 14px 18px; border-bottom: 1px solid #f0f0f0; background: #fafafa; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.card-body { padding: 18px; }
|
||||||
|
|
||||||
|
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.file-input { flex: 1; min-width: 200px; }
|
||||||
|
.file-input input[type=file] { width: 100%; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
|
||||||
|
.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: .2s; }
|
||||||
|
.btn-primary { background: #2563eb; color: #fff; }
|
||||||
|
.btn-primary:hover { background: #1d4ed8; }
|
||||||
|
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
|
||||||
|
.btn-sm { padding: 6px 14px; font-size: 13px; }
|
||||||
|
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
|
||||||
|
.btn-outline:hover { background: #f9fafb; }
|
||||||
|
.hint { font-size: 12px; color: #9ca3af; margin-top: 8px; }
|
||||||
|
|
||||||
|
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; }
|
||||||
|
.status.info { background: #dbeafe; color: #1e40af; display: block; }
|
||||||
|
.status.error { background: #fee2e2; color: #991b1b; display: block; }
|
||||||
|
.status.success { background: #d1fae5; color: #065f46; display: block; }
|
||||||
|
|
||||||
|
.results-layout { display: flex; gap: 24px; }
|
||||||
|
.col-main { flex: 1.5; min-width: 0; }
|
||||||
|
.col-side { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
|
/* 特征表格 */
|
||||||
|
.feat-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
.feat-table th, .feat-table td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #f1f5f9; }
|
||||||
|
.feat-table th { background: #f8fafc; font-weight: 700; color: #475569; font-size: 11px; text-transform: uppercase; letter-spacing: .3px; }
|
||||||
|
.feat-table td:first-child { font-weight: 600; color: #1e293b; width: 200px; }
|
||||||
|
.feat-table tr:hover td { background: #f8fafc; }
|
||||||
|
.feat-table .highlight td { background: #eff6ff; }
|
||||||
|
|
||||||
|
/* 图片预览 */
|
||||||
|
.img-preview { text-align: center; }
|
||||||
|
.img-preview .thumb { background: #222; border-radius: 8px; overflow: hidden; display: inline-block; line-height: 0; }
|
||||||
|
.img-preview .thumb img { max-width: 100%; max-height: 280px; object-fit: contain; }
|
||||||
|
|
||||||
|
.json-panel { max-height: 550px; overflow: auto; }
|
||||||
|
.json-content { padding: 14px 16px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
|
||||||
|
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
.count-badge { background: #dbeafe; color: #1e40af; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 700; }
|
||||||
|
|
||||||
|
@media (max-width: 800px) { .results-layout { flex-direction: column; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔬 接口4 — 用户特征分析 测试</h1>
|
||||||
|
<p class="subtitle">POST /api/v1/face/features | 上传照片 → 火山方舟豆包视觉模型 → 42项面部特征 | data.features 为 JSON 字符串</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="upload-row">
|
||||||
|
<div class="file-input"><input type="file" id="imageFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png"></div>
|
||||||
|
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 分析特征</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">JPG/PNG ≤1MB 正面照 | 调用火山方舟豆包视觉模型,耗时约数秒</div>
|
||||||
|
<div id="statusBar" class="status hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results-layout hidden" id="resultsArea">
|
||||||
|
<div class="col-main">
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><span>📸 上传的照片</span></div>
|
||||||
|
<div class="card-body img-preview">
|
||||||
|
<div class="thumb" id="imgPreview"><span style="color:#9ca3af;padding:40px">—</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 英文优先字段 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><span>⭐ 英文优先字段</span><span id="countBadge" class="count-badge hidden"></span></div>
|
||||||
|
<div class="card-body" style="padding:0">
|
||||||
|
<table class="feat-table" id="enTable"></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 全部字段 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><span>📋 全部特征字段</span></div>
|
||||||
|
<div class="card-body" style="padding:0;max-height:500px;overflow:auto">
|
||||||
|
<table class="feat-table" id="allTable"></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-side">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><span>📄 原始 JSON 响应</span><button class="btn btn-outline btn-sm" onclick="copyJson()">📋 复制</button></div>
|
||||||
|
<div class="json-panel"><pre class="json-content" id="jsonContent"></pre></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = window.location.origin;
|
||||||
|
|
||||||
|
const EN_KEYS = ['face_shape','eyebrow_shape','facial_age','dynamic_static_type','gender','gene_style'];
|
||||||
|
const EN_LABELS = {
|
||||||
|
face_shape:'脸型', eyebrow_shape:'眉形', facial_age:'面部年龄',
|
||||||
|
dynamic_static_type:'动静类型', gender:'性别', gene_style:'基因风格'
|
||||||
|
};
|
||||||
|
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; }
|
||||||
|
|
||||||
|
async function submitTest() {
|
||||||
|
const f = $('imageFile').files[0];
|
||||||
|
if (!f) { setStatus('请选择图片', 'error'); return; }
|
||||||
|
if (f.size > 1_000_000) { setStatus('文件超过 1 MB', 'error'); return; }
|
||||||
|
|
||||||
|
$('imgPreview').innerHTML = '<img src="'+URL.createObjectURL(f)+'" alt="preview">';
|
||||||
|
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ 分析中...';
|
||||||
|
setStatus('⏳ 调用豆包视觉模型...', 'info'); $('resultsArea').classList.add('hidden');
|
||||||
|
|
||||||
|
const fd = new FormData(); fd.append('image_file', f);
|
||||||
|
try {
|
||||||
|
const r = await fetch(API_BASE + '/api/v1/face/features', { method:'POST', body:fd });
|
||||||
|
const json = await r.json();
|
||||||
|
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||||
|
$('resultsArea').classList.remove('hidden');
|
||||||
|
|
||||||
|
if (json.code === 0) {
|
||||||
|
const feats = JSON.parse(json.data.features);
|
||||||
|
setStatus('✅ 分析完成 — ' + Object.keys(feats).length + ' 项特征', 'success');
|
||||||
|
renderFeatures(feats);
|
||||||
|
} else {
|
||||||
|
setStatus('❌ code=' + json.code + ' ' + json.message, 'error');
|
||||||
|
}
|
||||||
|
} catch(e) { setStatus('❌ ' + e.message, 'error'); $('resultsArea').classList.remove('hidden'); }
|
||||||
|
finally { $('submitBtn').disabled = false; $('submitBtn').textContent = '🚀 分析特征'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFeatures(feats) {
|
||||||
|
// 英文优先字段
|
||||||
|
let enHtml = '';
|
||||||
|
EN_KEYS.forEach(k => {
|
||||||
|
if (feats[k] !== undefined) {
|
||||||
|
enHtml += '<tr class="highlight"><td>'+k+'</td><td>'+EN_LABELS[k]+'</td><td>'+feats[k]+'</td></tr>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('enTable').innerHTML = enHtml;
|
||||||
|
|
||||||
|
// 全部字段
|
||||||
|
const allKeys = Object.keys(feats);
|
||||||
|
let allHtml = '';
|
||||||
|
allKeys.forEach(k => {
|
||||||
|
const isEn = EN_KEYS.includes(k);
|
||||||
|
allHtml += '<tr'+(isEn?' class="highlight"':'')+'><td>'+k+'</td><td>'+(isEn?EN_LABELS[k]||'':'')+'</td><td>'+feats[k]+'</td></tr>';
|
||||||
|
});
|
||||||
|
$('allTable').innerHTML = allHtml;
|
||||||
|
|
||||||
|
$('countBadge').textContent = allKeys.length + ' 项';
|
||||||
|
$('countBadge').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearResults() {
|
||||||
|
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
|
||||||
|
$('imageFile').value=''; $('jsonContent').textContent='';
|
||||||
|
$('imgPreview').innerHTML='<span style="color:#9ca3af;padding:40px">—</span>';
|
||||||
|
$('enTable').innerHTML=''; $('allTable').innerHTML='';
|
||||||
|
$('countBadge').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyJson() {
|
||||||
|
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
|
||||||
|
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const dz = $('imageFile');
|
||||||
|
dz.addEventListener('dragover', e => { e.preventDefault(); dz.style.borderColor='#2563eb'; });
|
||||||
|
dz.addEventListener('dragleave', () => dz.style.borderColor='#ddd');
|
||||||
|
dz.addEventListener('drop', e => { e.preventDefault(); dz.style.borderColor='#ddd'; if(e.dataTransfer.files.length){ dz.files=e.dataTransfer.files; setStatus('已选择: '+e.dataTransfer.files[0].name, 'info'); } });
|
||||||
|
dz.addEventListener('change', () => { if(dz.files.length) setStatus('已选择: '+dz.files[0].name, 'info'); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user