Files
hair/static/test_interface3.html
T
xsl b61ea6f33b feat(接口5): 生发机制对齐接口2 + 统一ComfyUI重绘提示词
接口5改造(生发机制与接口2一致,按性别分流):
- generate_hairline_pngs: 生发图来源从 _grow_from_texture(局部inpaint)
  改为按性别分流——female走 generate_grow_results_swap(swapHair+Flux整帧重绘),
  male走 generate_grow_results(ComfyUI add_hair)
- 新增参数 redraw_max_side/unet_name/v2_defaults(female路径透传)
- 接口5 handler 加 flux_model/redraw_max_side Form参数
- 保留接口5独有输出: 3档叠图(middle/high/low)/中心点/face_measure
- 已验证: female日志出现"接口2女 管线降分辨率max_side=640"+swap+Flux;
  male走add_hair.json; generate_grow_image=false正确跳过生发

统一ComfyUI重绘提示词:
- "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" → "填充遮罩区域的头发"
- 覆盖: _REDRAW_PROMPT/_DEFAULT_PROMPT常量 + app.py各接口Form默认 +
  工作流JSON节点60(add_hair/0716add-hair-api/hair_repaint) + 全部测试页输入框
2026-07-27 23:36:15 +08:00

227 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>接口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?v=2"></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 = resolveImgSrc(d.hair_growth_image_url, d.hair_growth_image_base64, 'image/jpeg');
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>