接口5:改为多选发型 + 每发型返回 middle/high/low 三档叠图(去掉 hairline_level 入参)

- 去掉 hairline_level 入参;middle/high/low 三档都返回
- 入参改为同接口2:gender + hair_style(逗号分隔多选,必填),缺失/越界返回 1007
- 每个选中发型返回 image_{middle,high,low}_base64 三档叠图 + grown_image_base64 生发图,
  按发型分组;order = 发型序号,含 hairline_type
- 生发黑模板仍固定 middle(hairline_texture_black/),每发型 1 张生发图
- best_hairline_center_point 取首个选中发型的 middle 档
- 同步更新测试页(三档并排展示)、integration.html、stub_worker、接口文档/实现说明、test_api

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-07-10 00:19:08 +08:00
co-authored by Claude Opus 4.8
parent 864d7f969a
commit f8e30ad32e
8 changed files with 161 additions and 120 deletions
+35 -42
View File
@@ -58,6 +58,14 @@
.preview-controls { display: flex; gap: 14px; align-items: center; font-size: 12px; color: #9ca3af; }
.preview-controls label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
/* 三档 + 生发图 横向排列 */
.level-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.level-cell { background: #222; border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; }
.level-cell .level-label { font-size: 12px; font-weight: 600; color: #fff; background: rgba(0,0,0,.55); padding: 4px 8px; }
.level-cell img { width: 100%; max-height: 380px; object-fit: contain; display: block; }
.level-cell .level-none { color: #9ca3af; font-size: 13px; padding: 28px 0; text-align: center; }
@media (max-width: 700px) { .level-row { grid-template-columns: repeat(2, 1fr); } }
/* 坐标高亮 */
.coord-box { background: #f9fafb; border-radius: 8px; padding: 12px 16px; margin-top: 10px; font-size: 14px; }
.coord-box .label { font-size: 11px; color: #9ca3af; margin-bottom: 4px; }
@@ -73,7 +81,7 @@
<body>
<div class="container">
<h1>💈 接口5 — 发际线 PNG 生成 测试</h1>
<p class="subtitle">POST /api/v1/hairline/generate &nbsp;|&nbsp; 上传正面照 + 性别(+档位/生发序号)→ N 张发际线叠加图(含生发图+ 最佳中心点坐标</p>
<p class="subtitle">POST /api/v1/hairline/generate &nbsp;|&nbsp; 上传正面照 + 性别 + 多选发型 → 每个发型 middle/high/low 三档发际线叠图 + 生发图 + 最佳中心点坐标</p>
<div class="card">
<div class="card-body">
@@ -84,17 +92,13 @@
<select id="gender"><option value="female" selected>👩 Female5种)</option><option value="male">👨 Male4种)</option></select>
</div>
<div class="form-group">
<label>发际线档位</label>
<select id="hairlineLevel"><option value="middle" selected>middle(默认)</option><option value="high">high</option><option value="low">low</option></select>
</div>
<div class="form-group">
<label title="逗号分隔如 1,2,3;留空则只出发际线不生发">生发序号</label>
<input type="text" id="hairStyle" placeholder="如 1,2 留空不生发">
<label title="必填,逗号分隔多选如 1,2,3">发型序号 *</label>
<input type="text" id="hairStyle" placeholder="必填 如 1,2,3">
</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 正面照 &nbsp;|&nbsp; Female: 椭圆/花瓣/心形/直线/波浪 &nbsp;|&nbsp; Male: 椭圆/M形/直线/倒弧 &nbsp;|&nbsp; 返回中心点坐标(原图像素)</div>
<div class="hint">JPG/PNG 正面照 &nbsp;|&nbsp; 发型序号 Female: 1椭圆/2花瓣/3心形/4直线/5波浪 &nbsp;|&nbsp; Male: 1椭圆/2倒弧/3M形/4直线 &nbsp;|&nbsp; 每个发型返回 middle/high/low 三档 + 生发图</div>
<div id="statusBar" class="status hidden"></div>
</div>
</div>
@@ -103,24 +107,18 @@
<div class="col-main">
<!-- 预览 -->
<div class="card">
<div class="card-header">
<span>📐 原图 + 发际线叠加</span>
<div class="preview-controls">
<label><input type="checkbox" id="showBase" checked onchange="syncOverlay()"> 底图</label>
<label><input type="checkbox" id="showOver" checked onchange="syncOverlay()"> 发际线</label>
</div>
</div>
<div class="card-header"><span>📐 三档发际线叠图 + 生发图</span></div>
<div class="card-body" style="text-align:center">
<div id="previewArea"><span style="color:#9ca3af">← 点击下方方案卡片</span></div>
<div id="previewArea"><span style="color:#9ca3af">← 点击下方发型卡片</span></div>
<div class="coord-box">
<div class="label">📍 最佳发际线中心点(best_hairline_center_point)— 原图像素坐标</div>
<div class="label">📍 最佳发际线中心点(best_hairline_center_point,首个选中发型 middle 档)— 原图像素坐标</div>
<div class="value" id="centerPoint"></div>
</div>
</div>
</div>
<!-- 方案列表 -->
<!-- 发型列表 -->
<div class="card">
<div class="card-header"><span>🎯 发际线方案</span><span style="font-weight:400;font-size:12px;color:#9ca3af">点击切换预览 &nbsp;|&nbsp; order=1 最佳</span></div>
<div class="card-header"><span>🎯 选中发型</span><span style="font-weight:400;font-size:12px;color:#9ca3af">点击切换预览</span></div>
<div class="card-body"><div class="results-grid" id="resultsGrid"></div></div>
</div>
</div>
@@ -143,14 +141,14 @@ function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.classNa
async function submitTest() {
const f = $('imageFile').files[0];
if (!f) { setStatus('请选择图片', 'error'); return; }
const _hs = $('hairStyle').value.trim();
if (!_hs) { setStatus('请填写发型序号(必填,如 1,2,3)', 'error'); return; }
_origUrl = URL.createObjectURL(f);
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
setStatus('请求中...', 'info'); $('resultsArea').classList.add('hidden');
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hairline_level', $('hairlineLevel').value);
const _hs = $('hairStyle').value.trim(); if (_hs) fd.append('hair_style', _hs);
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hair_style', _hs);
const _reqStart = performance.now();
try {
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', body:fd });
@@ -162,7 +160,7 @@ async function submitTest() {
_images = json.data.hairline_images || [];
_center = json.data.best_hairline_center_point;
$('centerPoint').textContent = _center ? '(' + _center.x + ', ' + _center.y + ')' : '—';
setStatus('✅ ' + _images.length + ' 个方案 (' + _elapsed + 's)', 'success');
setStatus('✅ ' + _images.length + ' 个发型 × 三档 (' + _elapsed + 's)', 'success');
renderGrid();
if (_images.length) selectCard(0);
} else {
@@ -178,10 +176,9 @@ function renderGrid() {
let h = '';
_images.forEach((img, i) => {
h += '<div class="result-card'+(i===0?' selected':'')+'" onclick="selectCard('+i+',this)">'+
'<div class="img-wrap"><img src="'+img.image_url+'" alt="#'+img.order+'" loading="lazy"></div>'+
'<div class="info"><span class="badge">#'+(img.order||'—')+'</span>'+
(img.grown_image_url?'<span style="font-size:11px;color:#7c3aed">🌱 生发</span>':'')+
(i===0?'<span style="font-size:11px;color:#059669">⭐ 最佳</span>':'')+'</div></div>';
'<div class="img-wrap"><img src="'+img.image_middle_url+'" alt="#'+img.order+'" loading="lazy"></div>'+
'<div class="info"><span class="badge">#'+(img.order||'—')+' '+(img.hairline_type||'')+'</span>'+
(img.grown_image_url?'<span style="font-size:11px;color:#7c3aed">🌱 生发</span>':'')+'</div></div>';
});
$('resultsGrid').innerHTML = h;
}
@@ -189,28 +186,24 @@ function renderGrid() {
function selectCard(idx, el) {
document.querySelectorAll('.result-card').forEach(c => c.classList.remove('selected'));
if (el) el.classList.add('selected');
const g = _images[idx].grown_image_url;
const it = _images[idx];
const cell = (label, url) => url
? '<div class="level-cell"><div class="level-label">'+label+'</div><img src="'+url+'" alt="'+label+'"></div>'
: '<div class="level-cell"><div class="level-label">'+label+'</div><div class="level-none">无</div></div>';
$('previewArea').innerHTML =
'<div class="preview-stack">'+
'<img class="layer-base5" src="'+_origUrl+'" alt="原图">'+
'<img class="layer-over5" src="'+_images[idx].image_url+'" alt="#'+_images[idx].order+'">'+
'</div>'+
(g?'<div style="margin-top:10px"><div style="font-size:12px;color:#7c3aed;margin-bottom:4px">🌱 生发图</div><img src="'+g+'" alt="生发 #'+_images[idx].order+'" style="max-width:100%;border-radius:8px"></div>':'');
syncOverlay();
}
function syncOverlay() {
const base = document.querySelector('.layer-base5');
const over = document.querySelector('.layer-over5');
if (base) base.style.display = $('showBase').checked ? '' : 'none';
if (over) over.style.display = $('showOver').checked ? '' : 'none';
'<div class="level-row">'+
cell('middle', it.image_middle_url)+
cell('high', it.image_high_url)+
cell('low', it.image_low_url)+
cell('🌱 生发图', it.grown_image_url)+
'</div>';
}
function clearResults() {
_images=[]; _origUrl=''; _center=null;
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
$('imageFile').value=''; $('jsonContent').textContent=''; $('resultsGrid').innerHTML='';
$('previewArea').innerHTML='<span style="color:#9ca3af">← 点击下方方案卡片</span>';
$('previewArea').innerHTML='<span style="color:#9ca3af">← 点击下方发型卡片</span>';
$('centerPoint').textContent='—';
}