Files
hair/static/test_interface5.html
T
xslandClaude Opus 4.8 9675e147a0 接口5:新增 hairline_level 档位(middle/high/low)贴图 + 集成接口2生发能力
- hairline_level: 可选 middle(默认)/high/low,选用不同高度档位发际线贴图;
  新增 hairline_texture_high/low 两套贴图,get_texture_map 改为按档位缓存
- hair_style: 可选逗号分隔序号,对选中发际线类型同步生发(ComfyUI),
  结果合并进 hairline_images[].grown_image_base64(未选中为 null);
  生发黑模板固定取 middle(hairline_texture_black/),与档位无关
- 新增 use_mask/prompt 生发控制参数(同接口2)
- 测试页/接口文档同步更新

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:30:21 +08:00

233 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>接口5 — 发际线PNG生成 测试页</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; }
.form-group { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.form-group label { font-size: 13px; font-weight: 600; color: #374151; }
.form-group select { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
.form-group input[type=text] { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; width: 150px; }
.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; }
/* 方案网格 */
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; margin-top: 12px; }
.result-card { background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; cursor: pointer; transition: .15s; border: 3px solid transparent; }
.result-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,.12); }
.result-card.selected { border-color: #2563eb; }
.result-card .img-wrap { background: #222; min-height: 150px; max-height: 200px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.result-card .img-wrap img { max-width: 100%; max-height: 200px; object-fit: contain; }
.result-card .info { padding: 10px 12px; font-size: 12px; display: flex; justify-content: space-between; align-items: center; }
.result-card .info .badge { background: #2563eb; color: #fff; padding: 1px 8px; border-radius: 10px; font-size: 11px; }
/* 预览叠加 */
.preview-stack { position: relative; display: inline-block; line-height: 0; background: #222; border-radius: 8px; overflow: hidden; }
.preview-stack img { display: block; max-width: 100%; max-height: 450px; }
.preview-stack .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
.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; }
/* 坐标高亮 */
.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; }
.coord-box .value { font-weight: 700; font-size: 18px; color: #111827; }
.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; }
@media (max-width: 800px) { .results-layout { flex-direction: column; } }
</style>
</head>
<body>
<div class="container">
<h1>💈 接口5 — 发际线 PNG 生成 测试</h1>
<p class="subtitle">POST /api/v1/hairline/generate &nbsp;|&nbsp; 上传正面照 + 性别(+档位/生发序号)→ N 张发际线叠加图(含生发图)+ 最佳中心点坐标</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>
<div class="form-group">
<label>性别</label>
<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 留空不生发">
</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 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 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-body" style="text-align:center">
<div id="previewArea"><span style="color:#9ca3af">← 点击下方方案卡片</span></div>
<div class="coord-box">
<div class="label">📍 最佳发际线中心点(best_hairline_center_point)— 原图像素坐标</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-body"><div class="results-grid" id="resultsGrid"></div></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;
let _images = [], _origUrl = '', _center = null;
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; }
_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 _reqStart = performance.now();
try {
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', body:fd });
const json = await r.json();
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
$('jsonContent').textContent = JSON.stringify(json, null, 2);
$('resultsArea').classList.remove('hidden');
if (json.code === 0) {
_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');
renderGrid();
if (_images.length) selectCard(0);
} else {
setStatus('❌ (' + _elapsed + 's) code=' + json.code + ' ' + json.message, 'error');
}
} catch(e) {
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
setStatus('❌ (' + _elapsed + 's) ' + e.message, 'error'); $('resultsArea').classList.remove('hidden'); }
finally { $('submitBtn').disabled = false; $('submitBtn').textContent = '🚀 提交'; }
}
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>';
});
$('resultsGrid').innerHTML = h;
}
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;
$('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';
}
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>';
$('centerPoint').textContent='—';
}
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>