fix(gateway): 将接口11鉴权测试页移到标准地址
This commit is contained in:
@@ -197,7 +197,7 @@ const { code, data } = await res.json();
|
||||
<pre>curl -sS -X POST "$BASE_URL/api/v1/face/measure-11" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-F 'image_file=@photo.jpg'</pre>
|
||||
<p><a href="/static/test_interface11_auth.html" class="link">打开接口 11 测试页</a></p>
|
||||
<p><a href="/static/test_interface11.html" class="link">打开接口 11 测试页</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -526,7 +526,7 @@ console.log(features.gene_style); // "自然型"</pre>
|
||||
<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>11. 四庭七眼(鉴权)</td><td><a href="/static/test_interface11_auth.html" class="link">/static/test_interface11_auth.html</a></td><td>登录、测试鉴权、刷新令牌、四庭七眼测量</td></tr>
|
||||
<tr><td>11. 四庭七眼(鉴权)</td><td><a href="/static/test_interface11.html" class="link">/static/test_interface11.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>22. 生发密度与发际线高度</td><td><a href="/static/test_interface22.html" class="link">/static/test_interface22.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>
|
||||
|
||||
+53
-390
@@ -3,413 +3,76 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>接口11 — 发际线生发 测试页</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: 1240px; margin: 0 auto; padding: 24px; }
|
||||
h1 { font-size: 22px; margin-bottom: 8px; }
|
||||
.subtitle { color: #888; font-size: 13px; margin-bottom: 20px; line-height: 1.6; }
|
||||
h2 { font-size: 16px; margin: 24px 0 12px; }
|
||||
|
||||
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
|
||||
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
input[type=file] { flex: 1; min-width: 200px; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
|
||||
.btn { padding: 10px 24px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
|
||||
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
|
||||
.hint { font-size: 12px; color: #9ca3af; margin-top: 8px; line-height: 1.6; }
|
||||
|
||||
.params { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 14px; margin-top: 16px; }
|
||||
.pf { display: flex; flex-direction: column; gap: 4px; }
|
||||
.pf label { font-size: 13px; font-weight: 600; }
|
||||
.pf .desc { font-size: 11px; color: #9ca3af; font-weight: 400; }
|
||||
.pf select, .pf input[type=number], .pf input[type=text] { padding: 8px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
|
||||
.pf .row { display: flex; gap: 8px; align-items: center; }
|
||||
.pf .row input[type=number] { width: 80px; }
|
||||
.pf .row input[type=range] { flex: 1; }
|
||||
.chk { display: flex; align-items: center; gap: 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; }
|
||||
|
||||
.metrics { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.metric { background: #f9fafb; border: 1px solid #eee; border-radius: 8px; padding: 10px 16px; min-width: 110px; }
|
||||
.metric .label { font-size: 12px; color: #888; }
|
||||
.metric .value { font-size: 17px; font-weight: 700; color: #111; }
|
||||
|
||||
.steps { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
|
||||
.step { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||||
.step .cap { font-size: 13px; font-weight: 600; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #f0f0f0; }
|
||||
.step .cap small { color: #999; font-weight: 400; display:block; margin-top:2px; }
|
||||
.step img { width: 100%; display: block; background: #eee; cursor: zoom-in; }
|
||||
|
||||
.big img { max-height: 520px; object-fit: contain; }
|
||||
|
||||
.json-panel { max-height: 320px; overflow: auto; background: #1e1e1e; color: #d4d4d4; padding: 14px; border-radius: 8px; font: 12px/1.5 Consolas, Monaco, monospace; white-space: pre-wrap; word-break: break-all; }
|
||||
.hidden { display: none; }
|
||||
|
||||
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.85); display: none; align-items: center; justify-content: center; z-index: 50; cursor: zoom-out; }
|
||||
.lightbox img { max-width: 95%; max-height: 95%; }
|
||||
</style>
|
||||
<script src="/static/img_downscale.js"></script>
|
||||
<title>接口11 — 四庭七眼测量 测试页</title>
|
||||
<link rel="stylesheet" href="/static/face_measure_auth_test.css">
|
||||
<script src="/static/client_auth.js" defer></script>
|
||||
<script src="/static/img_downscale.js" defer></script>
|
||||
<script src="/static/face_measure_auth_test.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>接口11 — 发际线生发</h1>
|
||||
<div class="subtitle">
|
||||
接口9 头发遮罩 → change_hair 换发型(发际线类型 LoRA)→ 严格按遮罩贴回原图 → 接缝羽化融合。<br>
|
||||
输入:发际线较高 / 头发稀少的正脸图 + 发际线类型 ID。输出:同一个人同一发型、压低发际线后的图。每一步可视化,所有可调参数都在下面。
|
||||
</div>
|
||||
<h1>📐 四庭七眼测量 — 接口测试</h1>
|
||||
<p class="subtitle">POST /api/v1/face/measure-11 | 上传正面照 → 原图+标注叠加显示</p>
|
||||
|
||||
<div class="card">
|
||||
<form id="client-auth" class="upload-card" method="post" action="/api/v1/auth/login" aria-label="账号登录">
|
||||
<div class="auth-fields">
|
||||
<label>用户名 <input name="username" autocomplete="username" required></label>
|
||||
<label>密码 <input name="password" type="password" autocomplete="current-password" required></label>
|
||||
<button class="btn btn-primary btn-sm" type="submit">登录</button>
|
||||
<button class="btn btn-outline btn-sm" id="client-auth-check" type="button">测试鉴权</button>
|
||||
<button class="btn btn-outline btn-sm" id="client-auth-refresh" type="button" disabled>刷新令牌</button>
|
||||
</div>
|
||||
<p id="client-auth-status" class="upload-hint" role="status">请使用对接账号登录后提交图片。</p>
|
||||
</form>
|
||||
|
||||
<!-- 上传区 -->
|
||||
<form class="upload-card" id="measure-test" data-endpoint="/api/v1/face/measure-11"
|
||||
data-total-height-label="全脸总高" data-include-top-court>
|
||||
<div class="upload-row">
|
||||
<input type="file" id="imageFile" accept="image/*">
|
||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交测试</button>
|
||||
<button class="btn btn-outline" onclick="clearResults()">清空</button>
|
||||
<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" type="submit">🚀 提交测试</button>
|
||||
<button class="btn btn-outline btn-sm" id="clearBtn" type="button">清除结果</button>
|
||||
</div>
|
||||
<div class="upload-row" style="margin-top:10px">
|
||||
<label style="font-size:13px; font-weight:600; white-space:nowrap">X-Internal-Token</label>
|
||||
<input type="text" id="token" placeholder="直连 worker 需填;走网关可留空" value=""
|
||||
style="flex:1; min-width:200px; padding:8px; border:1px solid #ddd; border-radius:8px">
|
||||
<div class="upload-hint">
|
||||
支持 JPG / PNG 正面照 | 也可拖拽图片到文件选择框
|
||||
</div>
|
||||
<div id="statusBar" class="status" role="status"></div>
|
||||
</form>
|
||||
|
||||
<div class="params">
|
||||
<div class="pf">
|
||||
<label>gen_backend <span class="desc">生成后端</span></label>
|
||||
<select id="genBackend">
|
||||
<option value="swaphair">swaphair(换发型LoRA,应用发际线类型)</option>
|
||||
<option value="hairgrow">hairgrow(区域生发inpaint,压低发际线)</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 指标速览 -->
|
||||
<div id="metricsBar" class="metrics hidden"></div>
|
||||
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>发际线类型 ID <span class="desc">= change_hair hair_id(仅swaphair)</span></label>
|
||||
<select id="hairlineId">
|
||||
<option value="chang_zhixian">chang_zhixian(直线)</option>
|
||||
<option value="chang_tuoyuan">chang_tuoyuan(椭圆)</option>
|
||||
<option value="chang_bolang">chang_bolang(波浪)</option>
|
||||
<option value="chang_xinxing">chang_xinxing(心形)</option>
|
||||
<option value="chang_huaban">chang_huaban(花瓣)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>swap_mode <span class="desc">换发型取图模式</span></label>
|
||||
<select id="swapMode">
|
||||
<option value="ext_mask">ext_mask(改造换发型,用接口9遮罩重绘)</option>
|
||||
<option value="as_is">as_is(不改换发型,贴回再裁)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>seg_model <span class="desc">头发分割模型</span></label>
|
||||
<select id="segModel">
|
||||
<option value="segformer">segformer(默认)</option>
|
||||
<option value="bisenet">bisenet</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>erode_cm <span class="desc">baseline参考内缩(cm)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="erodeCm" min="0" max="5" step="0.1" value="0.6">
|
||||
<input type="range" id="erodeRange" min="0" max="3" step="0.1" value="0.6">
|
||||
<!-- 结果双栏 -->
|
||||
<div class="results hidden" id="resultsArea">
|
||||
<!-- 左:图片叠加 -->
|
||||
<div>
|
||||
<div class="panel-header">
|
||||
<span>🖼️ 原图 + 标注叠加</span>
|
||||
<div class="anno-controls">
|
||||
<label><input type="checkbox" id="showBase" checked> 底图</label>
|
||||
<label><input type="checkbox" id="showAnno" checked> 标注层</label>
|
||||
<label>透明度 <input type="range" id="annoOpacity" min="0" max="100" value="100"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>hairline_push_cm <span class="desc">发际线外推距离(cm)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="hairlinePushCm" min="0" max="3" step="0.1" value="1.0">
|
||||
<input type="range" id="hairlinePushRange" min="0" max="3" step="0.1" value="1.0">
|
||||
<div class="img-panel" id="imgPanel">
|
||||
<span id="imagePlaceholder" class="placeholder">提交后显示叠加效果</span>
|
||||
<div class="img-stack" id="imageStack" hidden>
|
||||
<img class="layer-base" id="baseImage" alt="原图">
|
||||
<img class="layer-anno" id="annotationImage" alt="标注层">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>hairline_edge <span class="desc">发际线提取方式</span></label>
|
||||
<select id="hairlineEdge">
|
||||
<option value="column">column(逐列最低点,稳定)</option>
|
||||
<option value="contour">contour(形态学轮廓,精确)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>mb_levels <span class="desc">多频段金字塔层数</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="mbLevels" min="2" max="6" step="1" value="5">
|
||||
<input type="range" id="mbLevelsRange" min="2" max="6" step="1" value="5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>edge_erode_px <span class="desc">贴图前遮罩内缩(px)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="edgeErodePx" min="0" max="40" step="1" value="3">
|
||||
<input type="range" id="edgeErodeRange" min="0" max="30" step="1" value="3">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>denoising_strength <span class="desc">换发型重绘强度(仅swaphair)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="denoise" min="0.2" max="1.0" step="0.05" value="0.6">
|
||||
<input type="range" id="denoiseRange" min="0.2" max="1.0" step="0.05" value="0.6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="gen:hairgrow">
|
||||
<label>hairgrow_strength <span class="desc">区域生发强度(仅hairgrow)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="hgStrength" min="0.2" max="1.0" step="0.05" value="0.75">
|
||||
<input type="range" id="hgStrengthRange" min="0.2" max="1.0" step="0.05" value="0.75">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>is_hr <span class="desc">高清模式</span></label>
|
||||
<div class="chk"><input type="checkbox" id="isHr"><span style="font-size:13px">1152×1536(否则 576×768)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">
|
||||
发际线类型 ID 必须是 change_hair 已训练的 hair_id(下拉为已有的 5 个)。参数改动后点「提交测试」重新计算,设置自动记住。<br>
|
||||
换发型走 GPU + SD img2img,单次约 10~15s;is_hr 更慢。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status hidden" id="statusBar"></div>
|
||||
|
||||
<div id="resultsArea" class="hidden">
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0">🎯 最终结果对比</h2>
|
||||
<div class="steps" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr))">
|
||||
<div class="step big"><div class="cap">输入原图</div><img id="finalInput"></div>
|
||||
<div class="step big"><div class="cap">最终结果 <small>按遮罩贴回 + 接缝融合</small></div><img id="finalOut"></div>
|
||||
<!-- 右:JSON -->
|
||||
<div>
|
||||
<div class="panel-header">
|
||||
<span>📋 JSON 响应</span>
|
||||
<button class="btn btn-outline btn-sm" id="copyBtn" type="button">📋 复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0">📏 参数与指标</h2>
|
||||
<div class="metrics" id="metricsBar"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0">🪜 分步可视化</h2>
|
||||
<div class="steps" id="stepsGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0; display:flex; justify-content:space-between; align-items:center">
|
||||
<span>原始 JSON</span>
|
||||
<button class="btn btn-outline" style="padding:6px 14px; font-size:13px" onclick="copyJson()">复制</button>
|
||||
</h2>
|
||||
<div class="json-panel" id="jsonContent"></div>
|
||||
<div class="json-panel"><pre class="json-content" id="jsonContent"></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lightbox" id="lightbox" onclick="this.style.display='none'"><img id="lightboxImg" alt=""></div>
|
||||
|
||||
<script>
|
||||
const API_BASE = window.location.origin;
|
||||
const ENDPOINT = '/api/v1/hairline/grow';
|
||||
|
||||
// 分步展示的图(按算法文档 4 步;key 前缀、标题、副标题)
|
||||
const STEPS = [
|
||||
// 遮罩计算全过程(接口9 子步骤,蓝→红)
|
||||
{ key: 'baseline_overlay', title: '①-a 发际线分割线', sub: '黄线=baseline(151中心点标红、其余标绿)' },
|
||||
{ key: 'upper_overlay', title: '①-b 分割线上半区', sub: '青=baseline 以上区域(裁剪范围)' },
|
||||
{ key: 'hair_seg_overlay', title: '①-c 头发分割', sub: '绿=分割模型原始头发像素(segformer/bisenet)' },
|
||||
{ key: 'top_fill_overlay', title: '①-d 填充到基线', sub: '蓝=头发向下填充含额头(延伸到图底,未裁剪)' },
|
||||
{ key: 'closed_overlay', title: '①-e 闭合区域', sub: '紫=top_fill ∩ 上半区(头发+额头,底=基线)' },
|
||||
// pushed 模式专有(非 pushed 模式无图)
|
||||
{ key: 'hairline_overlay', title: '①-f 头发内轮廓线', sub: '绿=头发内轮廓(额头弧+两侧到下颌) + 黄=baseline(仅pushed)' },
|
||||
{ key: 'pushed_overlay', title: '①-g 外推发际线', sub: '青=外推线(进头发push_cm) + 红=外推遮罩(仅pushed)' },
|
||||
// 最终遮罩
|
||||
{ key: 'mask_overlay', title: '① 最终遮罩(叠加)', sub: '红=遮罩区(含额头,外缘内缩)' },
|
||||
{ key: 'mask', title: '① 纯遮罩', sub: '白=生成/贴回区(传给换发型作遮罩 & 贴回)' },
|
||||
{ key: 'swap_raw', title: '② 生成全帧', sub: 'change_hair 生成,已对齐原图' },
|
||||
{ key: 'hard_paste', title: '③ 严格按遮罩贴回', sub: '遮罩内=生成,遮罩外=原图,无融合' },
|
||||
{ key: 'alpha', title: '④ 融合权重 alpha', sub: '羽化/渐变,白=用生成图' },
|
||||
{ key: 'final', title: '④ 接缝融合(最终)', sub: '遮罩边缘自然过渡' },
|
||||
];
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function setStatus(text, type) { const b = $('statusBar'); b.textContent = text; b.className = 'status ' + type; }
|
||||
|
||||
// 网关把 *_base64 改写为 *_url;直连 worker 保留 *_base64。两者都兼容。
|
||||
function pick(obj, name) { if (!obj) return null; return obj[name + '_url'] || obj[name + '_base64'] || null; }
|
||||
|
||||
function stepCard(title, sub, src) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'step';
|
||||
const img = src ? '<img src="' + src + '" alt="' + title + '" onclick="zoom(this.src)">'
|
||||
: '<div style="padding:30px;text-align:center;color:#bbb;font-size:13px">无图</div>';
|
||||
div.innerHTML = '<div class="cap">' + title + '<small>' + (sub || '') + '</small></div>' + img;
|
||||
return div;
|
||||
}
|
||||
function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = 'flex'; }
|
||||
|
||||
function renderMetrics(d) {
|
||||
const sz = d.image_size || {}, t = d.timings_ms || {};
|
||||
const items = [
|
||||
{ label: 'gen_backend', value: d.gen_backend },
|
||||
{ label: 'hairline_id', value: d.hairline_id },
|
||||
{ label: 'swap_mode', value: d.swap_mode },
|
||||
{ label: 'seg_model', value: d.seg_model },
|
||||
{ label: 'mask', value: d.mask_type },
|
||||
{ label: 'blend', value: d.blend_method },
|
||||
{ label: 'mb_levels', value: d.mb_levels },
|
||||
{ label: 'hairline_push', value: d.hairline_push_cm + 'cm' },
|
||||
{ label: 'hairline_edge', value: d.hairline_edge },
|
||||
{ label: 'denoise', value: d.denoising_strength },
|
||||
{ label: 'px_per_cm', value: d.px_per_cm },
|
||||
{ label: '内缩', value: d.erode_cm + 'cm/' + d.erode_px + 'px' },
|
||||
{ label: '遮罩像素', value: d.mask_pixels },
|
||||
{ label: '尺寸', value: (sz.width||'?') + '×' + (sz.height||'?') },
|
||||
{ label: '耗时(遮罩/换发/融合)', value: (t.mask||0) + '/' + (t.swap||0) + '/' + (t.blend||0) + 'ms' },
|
||||
];
|
||||
$('metricsBar').innerHTML = items.map(m =>
|
||||
'<div class="metric"><div class="label">' + m.label + '</div><div class="value">' + (m.value ?? '—') + '</div></div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderResult(d) {
|
||||
const s = d.steps || {};
|
||||
$('finalInput').src = pick(s, 'input') || '';
|
||||
$('finalInput').onclick = function(){ zoom(this.src); };
|
||||
$('finalOut').src = pick(s, 'final') || '';
|
||||
$('finalOut').onclick = function(){ zoom(this.src); };
|
||||
renderMetrics(d);
|
||||
const grid = $('stepsGrid'); grid.innerHTML = '';
|
||||
// 固定 pushed:只展示 pushed 相关步骤(不展示 top_fill/closed)
|
||||
const showKeys = new Set(['baseline_overlay','upper_overlay','hair_seg_overlay',
|
||||
'hairline_overlay','pushed_overlay','mask_overlay','mask','swap_raw','hard_paste','alpha','final']);
|
||||
STEPS.filter(st => showKeys.has(st.key)).forEach(st =>
|
||||
grid.appendChild(stepCard(st.title, st.sub, pick(s, st.key))));
|
||||
}
|
||||
|
||||
async function submitTest() {
|
||||
let file = $('imageFile').files[0];
|
||||
if (!file) { setStatus('请先选择一张图片', 'error'); return; }
|
||||
file = await window.downscaleImageFile(file);
|
||||
const t0 = performance.now();
|
||||
const btn = $('submitBtn');
|
||||
btn.disabled = true; btn.textContent = '⏳ 请求中...';
|
||||
setStatus('正在请求(换发型走 GPU,约 10~15s)...', 'info');
|
||||
$('resultsArea').classList.add('hidden');
|
||||
|
||||
const form = new FormData();
|
||||
form.append('image_file', file);
|
||||
form.append('hairline_id', $('hairlineId').value.trim());
|
||||
form.append('gen_backend', $('genBackend').value);
|
||||
form.append('hairgrow_strength', $('hgStrength').value || '0.75');
|
||||
form.append('is_hr', $('isHr').checked ? 'true' : 'false');
|
||||
form.append('seg_model', $('segModel').value);
|
||||
form.append('erode_cm', $('erodeCm').value || '0.6');
|
||||
setStatus('正在请求(固定 pushed + multiband) hairline_push_cm=' + ($('hairlinePushCm').value||'1.0')
|
||||
+ ' hairline_edge=' + $('hairlineEdge').value
|
||||
+ '(换发型走 GPU,约 10~15s)...', 'info');
|
||||
form.append('swap_mode', $('swapMode').value);
|
||||
form.append('edge_erode_px', $('edgeErodePx').value || '3');
|
||||
form.append('denoising_strength', $('denoise').value || '0.6');
|
||||
form.append('mb_levels', $('mbLevels').value || '5');
|
||||
form.append('hairline_push_cm', $('hairlinePushCm').value || '1.0');
|
||||
form.append('hairline_edge', $('hairlineEdge').value);
|
||||
|
||||
try {
|
||||
const headers = {};
|
||||
const tok = $('token').value.trim();
|
||||
if (tok) headers['X-Internal-Token'] = tok;
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', headers, body: form });
|
||||
const json = await resp.json();
|
||||
const dt = ((performance.now() - t0) / 1000).toFixed(2);
|
||||
$('jsonContent').textContent = JSON.stringify(json, (k, v) =>
|
||||
(typeof v === 'string' && v.length > 120) ? v.slice(0, 60) + '…(' + v.length + ')' : v, 2);
|
||||
$('resultsArea').classList.remove('hidden');
|
||||
if (json.code === 0) {
|
||||
setStatus('✅ 成功 (' + dt + 's)', 'success');
|
||||
renderResult(json.data);
|
||||
} else {
|
||||
setStatus('❌ 业务错误 (' + dt + 's) — code ' + json.code + ':' + json.message, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('❌ 网络错误: ' + err.message, 'error');
|
||||
$('jsonContent').textContent = 'Error: ' + err.message;
|
||||
$('resultsArea').classList.remove('hidden');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '🚀 提交测试';
|
||||
}
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
$('resultsArea').classList.add('hidden');
|
||||
$('statusBar').className = 'status hidden';
|
||||
$('imageFile').value = '';
|
||||
$('jsonContent').textContent = '';
|
||||
}
|
||||
function copyJson() {
|
||||
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
|
||||
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 参数持久化 + 联动 ----
|
||||
const FIELDS = ['token','genBackend','hairlineId','swapMode','segModel','erodeCm','edgeErodePx','denoise','hgStrength','mbLevels','hairlinePushCm','hairlineEdge'];
|
||||
function saveField(id) { try { localStorage.setItem('if11_' + id, $(id).type === 'checkbox' ? ($(id).checked?'1':'0') : $(id).value); } catch(e){} }
|
||||
function linkNumRange(numId, rngId) {
|
||||
const num = $(numId), rng = $(rngId);
|
||||
function sync(from) {
|
||||
let v = parseFloat(from.value); if (isNaN(v)) v = 0;
|
||||
num.value = v; rng.value = Math.min(parseFloat(rng.max), Math.max(parseFloat(rng.min), v));
|
||||
saveField(numId);
|
||||
}
|
||||
num.addEventListener('input', () => sync(num));
|
||||
rng.addEventListener('input', () => sync(rng));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const dz = $('imageFile');
|
||||
dz.addEventListener('change', () => { if (dz.files.length) setStatus('已选择: ' + dz.files[0].name, 'info'); });
|
||||
FIELDS.forEach(id => {
|
||||
try { const s = localStorage.getItem('if11_' + id); if (s !== null) $(id).value = s; } catch(e){}
|
||||
$(id).addEventListener('change', () => saveField(id));
|
||||
});
|
||||
try { $('isHr').checked = localStorage.getItem('if11_isHr') === '1'; } catch(e){}
|
||||
$('isHr').addEventListener('change', () => saveField('isHr'));
|
||||
linkNumRange('erodeCm','erodeRange');
|
||||
linkNumRange('edgeErodePx','edgeErodeRange');
|
||||
linkNumRange('denoise','denoiseRange');
|
||||
linkNumRange('hgStrength','hgStrengthRange');
|
||||
linkNumRange('mbLevels','mbLevelsRange');
|
||||
linkNumRange('hairlinePushCm','hairlinePushRange');
|
||||
// 初始化滑块值
|
||||
$('erodeRange').value = Math.min(3, parseFloat($('erodeCm').value)||0.6);
|
||||
$('edgeErodeRange').value = Math.min(30, parseFloat($('edgeErodePx').value)||3);
|
||||
$('denoiseRange').value = Math.min(1.0, parseFloat($('denoise').value)||0.6);
|
||||
$('hgStrengthRange').value = Math.min(1.0, parseFloat($('hgStrength').value)||0.75);
|
||||
$('mbLevelsRange').value = Math.min(6, Math.max(2, parseFloat($('mbLevels').value)||5));
|
||||
$('hairlinePushRange').value = Math.min(3, parseFloat($('hairlinePushCm').value)||1.0);
|
||||
|
||||
// 参数联动:按 gen_backend 显隐无用参数(data-show 声明,遮罩/融合已固定无选项)
|
||||
function applyVisibility() {
|
||||
const gen = $('genBackend').value;
|
||||
document.querySelectorAll('.pf[data-show]').forEach(el => {
|
||||
const conds = el.dataset.show.split(',').map(s => s.trim());
|
||||
let vis = true;
|
||||
conds.forEach(c => {
|
||||
const [k, v] = c.split(':');
|
||||
if (k === 'gen' && v !== gen) vis = false;
|
||||
});
|
||||
el.style.display = vis ? '' : 'none';
|
||||
});
|
||||
}
|
||||
$('genBackend').addEventListener('change', applyVisibility);
|
||||
applyVisibility();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,77 +2,10 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>接口 11 — 四庭七眼测量鉴权测试</title>
|
||||
<link rel="stylesheet" href="/static/face_measure_auth_test.css">
|
||||
<script src="/static/client_auth.js" defer></script>
|
||||
<script src="/static/img_downscale.js" defer></script>
|
||||
<script src="/static/face_measure_auth_test.js" defer></script>
|
||||
<meta http-equiv="refresh" content="0; url=/static/test_interface11.html">
|
||||
<title>接口 11 测试页</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>接口 11 — 四庭七眼测量鉴权测试</h1>
|
||||
<p class="subtitle">POST /api/v1/face/measure-11 | 上传正面照 → 原图+标注叠加显示</p>
|
||||
|
||||
<form id="client-auth" class="upload-card" method="post" action="/api/v1/auth/login" aria-label="账号登录">
|
||||
<div class="auth-fields">
|
||||
<label>用户名 <input name="username" autocomplete="username" required></label>
|
||||
<label>密码 <input name="password" type="password" autocomplete="current-password" required></label>
|
||||
<button class="btn btn-primary btn-sm" type="submit">登录</button>
|
||||
<button class="btn btn-outline btn-sm" id="client-auth-check" type="button">测试鉴权</button>
|
||||
<button class="btn btn-outline btn-sm" id="client-auth-refresh" type="button" disabled>刷新令牌</button>
|
||||
</div>
|
||||
<p id="client-auth-status" class="upload-hint" role="status">请使用对接账号登录后提交图片。</p>
|
||||
</form>
|
||||
|
||||
<!-- 上传区 -->
|
||||
<form class="upload-card" id="measure-test" data-endpoint="/api/v1/face/measure-11"
|
||||
data-total-height-label="全脸总高" data-include-top-court>
|
||||
<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" type="submit">🚀 提交测试</button>
|
||||
<button class="btn btn-outline btn-sm" id="clearBtn" type="button">清除结果</button>
|
||||
</div>
|
||||
<div class="upload-hint">
|
||||
支持 JPG / PNG 正面照 | 也可拖拽图片到文件选择框
|
||||
</div>
|
||||
<div id="statusBar" class="status" role="status"></div>
|
||||
</form>
|
||||
|
||||
<!-- 指标速览 -->
|
||||
<div id="metricsBar" class="metrics hidden"></div>
|
||||
|
||||
<!-- 结果双栏 -->
|
||||
<div class="results hidden" id="resultsArea">
|
||||
<!-- 左:图片叠加 -->
|
||||
<div>
|
||||
<div class="panel-header">
|
||||
<span>🖼️ 原图 + 标注叠加</span>
|
||||
<div class="anno-controls">
|
||||
<label><input type="checkbox" id="showBase" checked> 底图</label>
|
||||
<label><input type="checkbox" id="showAnno" checked> 标注层</label>
|
||||
<label>透明度 <input type="range" id="annoOpacity" min="0" max="100" value="100"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img-panel" id="imgPanel">
|
||||
<span id="imagePlaceholder" class="placeholder">提交后显示叠加效果</span>
|
||||
<div class="img-stack" id="imageStack" hidden>
|
||||
<img class="layer-base" id="baseImage" alt="原图">
|
||||
<img class="layer-anno" id="annotationImage" alt="标注层">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右:JSON -->
|
||||
<div>
|
||||
<div class="panel-header">
|
||||
<span>📋 JSON 响应</span>
|
||||
<button class="btn btn-outline btn-sm" id="copyBtn" type="button">📋 复制</button>
|
||||
</div>
|
||||
<div class="json-panel"><pre class="json-content" id="jsonContent"></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><a href="/static/test_interface11.html">打开接口 11 测试页</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user