Files
hair/static/test_interface3.html
UbuntuandCursor 87ff2c15d0 chore: 精简生发提示词,去掉磨皮/美颜要求
统一改为「填充遮罩区域的头发」,涉及后端默认值、ComfyUI 工作流 JSON、
测试页、benchmark 脚本、local_test。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 15:57:57 +08:00

227 lines
12 KiB
HTML
Raw Permalink 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>接口3 — B端生发 测试页</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-group { margin-bottom: 14px; }
.upload-group .label { font-weight: 700; font-size: 13px; color: #374151; margin-bottom: 6px; }
.upload-group .label .tag { font-size: 11px; padding: 2px 8px; border-radius: 10px; margin-left: 6px; }
.upload-group .label .tag.required { background: #fee2e2; color: #991b1b; }
.upload-group input[type=file] { width: 100%; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
.upload-row { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.upload-row .upload-group { flex: 1; min-width: 250px; }
.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; }
.preview-row { display: flex; gap: 16px; }
.preview-row > div { flex: 1; text-align: center; }
.preview-row .thumb { background: #222; border-radius: 8px; overflow: hidden; display: inline-block; line-height: 0; }
.preview-row .thumb img { max-width: 100%; max-height: 350px; object-fit: contain; }
.preview-row .caption { font-size: 11px; color: #9ca3af; margin-top: 6px; }
.na { color: #d1d5db; font-size: 12px; padding: 40px 20px; background: #f9fafb; border-radius: 6px; }
.result-field { margin-bottom: 14px; }
.result-field .name { font-size: 12px; font-weight: 600; color: #6b7280; margin-bottom: 4px; text-transform: uppercase; letter-spacing: .5px; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; background: #dbeafe; color: #1e40af; }
/* blend */
.blend-container { position: relative; width: 100%; background: #222; border-radius: 8px; overflow: hidden; line-height: 0; }
.blend-container img { display: block; width: 100%; max-height: 500px; object-fit: contain; }
.blend-bottom { position: relative; z-index: 1; }
.blend-top { position: absolute; top: 0; left: 0; z-index: 2; transition: opacity 0.06s; }
.blend-placeholder { color: #9ca3af; font-size: 12px; padding: 80px 20px; background: #f9fafb; line-height: 1.5; }
.blend-controls { display: flex; align-items: center; gap: 10px; margin-top: 14px; justify-content: center; }
.blend-controls input[type=range] { width: 220px; accent-color: #2563eb; }
.blend-label { font-size: 12px; color: #6b7280; font-weight: 600; }
.blend-value { font-size: 13px; font-weight: 600; color: #374151; min-width: 42px; text-align: center; background: #f3f4f6; border-radius: 6px; padding: 2px 8px; }
.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; }
.tag-new { background: #059669; color: #fff; font-size: 10px; padding: 1px 6px; border-radius: 8px; margin-left: 4px; }
@media (max-width: 800px) { .results-layout, .preview-row { flex-direction: column; } }
</style>
<script src="/static/img_downscale.js"></script>
</head>
<body>
<div class="container">
<h1>🏥 接口3 — B端生发 测试</h1>
<p class="subtitle">POST /api/v1/hair/grow-b &nbsp;|&nbsp; 上传马克笔划线图 → 生发效果图</p>
<div class="card">
<div class="card-body">
<div class="upload-group">
<div class="label">✏️ 划线图(marked_image<span class="tag required">必填</span></div>
<input type="file" id="markedFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png">
</div>
<div class="upload-group" style="margin-top:14px">
<div class="label">🪄 画发际线(use_mask<span class="tag-new">NEW</span></div>
<select id="useMask" style="padding:8px 12px;border:2px solid #ddd;border-radius:8px;font-size:14px;background:#fff;min-width:280px">
<option value="true" selected>✅ 画发际线(默认)— 检测划线并生成遮罩</option>
<option value="false">⬜ 不画发际线 — 跳过划线检测,直接送划线图(测试对比用)</option>
</select>
<div class="hint">false 时跳过划线检测、直接送划线图,用于对比测试</div>
</div>
<div class="upload-group" style="margin-top:14px">
<div class="label">💬 提示词(prompt</div>
<input type="text" id="promptInput" value="填充遮罩区域的头发" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;max-width:500px">
</div>
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
<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; 用马克笔在用户照片额头画发际线后拍照上传 &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>🔄 原图 / 生发图 叠加对比 &nbsp;</span><span class="badge" id="hairlineType" style="display:none"></span></div>
<div class="card-body" style="text-align:center">
<div class="blend-container" id="blendContainer">
<img id="blendBottom" class="blend-bottom" alt="原图(划线图)" style="display:none">
<img id="blendTop" class="blend-top" alt="生发效果图" style="display:none;opacity:0.5">
<div class="blend-placeholder" id="blendPlaceholder"></div>
</div>
<div class="blend-controls hidden" id="blendControls">
<span class="blend-label">原图</span>
<input type="range" id="blendSlider" min="0" max="1" step="0.01" value="0.5">
<span class="blend-label">生发图</span>
<span class="blend-value" id="blendValue">0.50</span>
</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;
function $(id) { return document.getElementById(id); }
function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; }
async function submitTest() {
let mf = $('markedFile').files[0];
if (!mf) { setStatus('请选择划线图', 'error'); return; }
mf = await window.downscaleImageFile(mf);
const markedUrl = URL.createObjectURL(mf);
// 先显示原图
$('blendPlaceholder').style.display = 'none';
$('blendBottom').src = markedUrl;
$('blendBottom').style.display = 'block';
$('blendTop').style.display = 'none';
$('blendControls').classList.add('hidden');
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
setStatus('请求中(生发需要数十秒)...', 'info'); $('resultsArea').classList.remove('hidden');
const fd = new FormData();
fd.append('marked_image_file', mf);
fd.append('use_mask', $('useMask').value);
fd.append('prompt', $('promptInput').value);
const _reqStart = performance.now();
try {
const r = await fetch(API_BASE + '/api/v1/hair/grow-b', { method:'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, 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) {
const d = json.data;
if (d.hairline_type) {
$('hairlineType').textContent = '发际线: ' + d.hairline_type;
$('hairlineType').style.display = 'inline-block';
}
const grownSrc = d.hair_growth_image_url || (d.hair_growth_image_base64 ? 'data:image/jpeg;base64,' + d.hair_growth_image_base64 : '');
if (grownSrc) {
$('blendTop').src = grownSrc;
$('blendTop').style.display = 'block';
$('blendTop').style.opacity = $('blendSlider').value;
$('blendControls').classList.remove('hidden');
$('blendValue').textContent = parseFloat($('blendSlider').value).toFixed(2);
} else {
$('blendTop').style.display = 'none';
$('blendControls').classList.add('hidden');
setStatus('⚠ 生发图未返回 — ComfyUI 未就绪或生成失败', 'info');
return;
}
setStatus('✅ 成功 (' + _elapsed + 's) &nbsp;|&nbsp; 发际线类型: ' + (d.hairline_type || '—'), 'success');
} else {
setStatus('❌ (' + _elapsed + 's) code=' + json.code + ' ' + json.message, 'error');
$('blendTop').style.display = 'none';
}
} 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 clearResults() {
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
$('markedFile').value=''; $('jsonContent').textContent='';
$('blendBottom').src = ''; $('blendBottom').style.display = 'none';
$('blendTop').src = ''; $('blendTop').style.display = 'none';
$('blendPlaceholder').style.display = 'block';
$('blendControls').classList.add('hidden');
$('blendSlider').value = 0.5; $('blendValue').textContent = '0.50';
$('hairlineType').textContent = ''; $('hairlineType').style.display = 'none';
}
$('blendSlider').addEventListener('input', function() {
$('blendTop').style.opacity = this.value;
$('blendValue').textContent = parseFloat(this.value).toFixed(2);
});
function copyJson() {
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
});
}
</script>
</body>
</html>