Files
hair/local_test/index.html
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

193 lines
8.2 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>发型补全工具</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; padding: 20px; }
h1 { text-align: center; margin-bottom: 20px; color: #e94560; font-size: 28px; }
.container { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.panel { background: #16213e; border-radius: 12px; padding: 20px; }
.panel h2 { margin-bottom: 16px; font-size: 18px; color: #e94560; }
.controls { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; align-items: center; }
.controls label { font-size: 14px; color: #aaa; }
input[type="file"] { color: #ddd; }
input[type="text"] { flex: 1; min-width: 200px; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #0f3460; color: #eee; font-size: 14px; }
button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
.btn-upload { background: #0f3460; color: #eee; border: 1px solid #e94560; }
.btn-upload:hover { background: #1a4a7a; }
.btn-generate { background: #e94560; color: #fff; font-size: 16px; padding: 12px 36px; }
.btn-generate:hover { background: #c73650; }
.btn-generate:disabled { background: #555; cursor: not-allowed; }
.image-wrapper { display: flex; gap: 12px; flex-wrap: wrap; border: 2px dashed #444; border-radius: 8px; padding: 12px; min-height: 300px; background: #0f3460; }
.image-item { flex: 1; min-width: 200px; }
.image-item img { max-width: 100%; border-radius: 6px; }
.image-item h4 { font-size: 12px; color: #aaa; margin-bottom: 6px; }
.placeholder { color: #666; font-size: 16px; text-align: center; padding: 60px 20px; width: 100%; }
.result-area { display: flex; gap: 16px; flex-wrap: wrap; }
.result-area img { max-width: 100%; border-radius: 8px; }
.result-item { flex: 1; min-width: 250px; }
.result-item h3 { font-size: 14px; color: #aaa; margin-bottom: 8px; text-align: center; }
.loading { text-align: center; padding: 40px; color: #e94560; font-size: 18px; }
.loading .spinner { display: inline-block; width: 40px; height: 40px; border: 4px solid #333; border-top-color: #e94560; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 12px; }
@keyframes spin { to { transform: rotate(360deg); } }
.error { color: #ff6b6b; padding: 16px; background: #2a1a1a; border-radius: 8px; margin-top: 12px; }
</style>
</head>
<body>
<h1>💇 发型补全工具</h1>
<div class="container">
<!-- Left: Input -->
<div class="panel">
<h2>1. 上传图片 & 遮罩</h2>
<div class="controls">
<label>人物图片:</label>
<input type="file" id="imageInput" accept="image/*" class="btn-upload">
</div>
<div class="controls">
<label>遮罩图片:</label>
<input type="file" id="maskInput" accept="image/*" class="btn-upload">
</div>
<div class="image-wrapper" id="imageWrapper">
<div class="placeholder" id="placeholder">请上传人物图片和遮罩图片</div>
</div>
<div class="controls" style="margin-top:16px">
<label>提示词:</label>
<input type="text" id="promptInput" value="填充遮罩区域的头发">
</div>
<div style="text-align:center; margin-top:16px">
<button class="btn-generate" id="generateBtn" disabled>🚀 生成</button>
</div>
</div>
<!-- Right: Result -->
<div class="panel">
<h2>2. 对比结果</h2>
<div id="resultArea">
<div class="placeholder">生成结果将显示在这里</div>
</div>
</div>
</div>
<script>
const imageInput = document.getElementById('imageInput');
const maskInput = document.getElementById('maskInput');
const imageWrapper = document.getElementById('imageWrapper');
const placeholder = document.getElementById('placeholder');
const promptInput = document.getElementById('promptInput');
const generateBtn = document.getElementById('generateBtn');
const resultArea = document.getElementById('resultArea');
let originalImage = null;
let maskImage = null;
imageInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const img = new Image();
img.onload = () => {
originalImage = { img: img, file: file };
updatePreview();
checkReady();
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
});
maskInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const img = new Image();
img.onload = () => {
maskImage = { img: img, file: file };
updatePreview();
checkReady();
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
});
function updatePreview() {
placeholder.style.display = 'none';
let html = '';
if (originalImage) {
html += `<div class="image-item"><h4>人物图片</h4><img src="${originalImage.img.src}" alt="原图"></div>`;
}
if (maskImage) {
html += `<div class="image-item"><h4>遮罩图片</h4><img src="${maskImage.img.src}" alt="遮罩"></div>`;
}
imageWrapper.innerHTML = html;
}
function checkReady() {
generateBtn.disabled = !(originalImage && maskImage);
}
generateBtn.addEventListener('click', async () => {
if (!originalImage || !maskImage) return;
generateBtn.disabled = true;
generateBtn.textContent = '⏳ 生成中...';
const startTime = performance.now();
resultArea.innerHTML = '<div class="loading"><div class="spinner"></div><br>正在调用 ComfyUI 生成,请耐心等待...<div id="liveTimer" style="margin-top:8px;font-size:15px;color:#aaa">已用时 0.0s</div></div>';
const liveTimer = document.getElementById('liveTimer');
const timerId = setInterval(() => {
if (liveTimer) liveTimer.textContent = '已用时 ' + ((performance.now() - startTime) / 1000).toFixed(1) + 's';
}, 100);
try {
const formData = new FormData();
formData.append('image', originalImage.file, 'original.' + originalImage.file.name.split('.').pop());
formData.append('mask', maskImage.file, 'mask.' + maskImage.file.name.split('.').pop());
formData.append('prompt', promptInput.value);
const resp = await fetch('/api/generate', { method: 'POST', body: formData });
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Generation failed');
}
const resultBlob = await resp.blob();
const resultUrl = URL.createObjectURL(resultBlob);
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
// 服务端纯推理耗时(若返回该响应头)
const serverTime = resp.headers.get('X-Generate-Time');
const serverInfo = serverTime ? `,服务端推理 ${parseFloat(serverTime).toFixed(1)}s` : '';
resultArea.innerHTML = `
<div style="text-align:center;margin-bottom:12px;color:#4ade80;font-size:16px;font-weight:600">
⏱️ 本次重绘耗时 ${elapsed}s${serverInfo}
</div>
<div class="result-area">
<div class="result-item">
<h3>原图</h3>
<img src="${originalImage.img.src}" alt="原图">
</div>
<div class="result-item">
<h3>生成结果</h3>
<img src="${resultUrl}" alt="生成结果">
</div>
</div>
`;
} catch (err) {
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
resultArea.innerHTML = `<div class="error">❌ ${err.message}<br><span style="color:#aaa;font-size:13px">(耗时 ${elapsed}s</span></div>`;
} finally {
clearInterval(timerId);
generateBtn.disabled = false;
generateBtn.textContent = '🚀 生成';
}
});
</script>
</body>
</html>