Files
hair/static/test_interface12.html
T
xslandCursor a208fe88ec feat: 拆分接口11/12,新增接口12 final / final v2 精简重绘端点
- 接口11 移除重绘,仅生成 final;接口12 (grow_v2) 负责发际线带 Flux-2 重绘
- 接口12 重绘带改为发际线外推 band_lo_mult~band_hi_mult 倍 push(默认 0.5~1.5),页面可调
- 接口12 同时产出 A 整帧重绘 与 B 局部加发+全脸美颜(beauty_alpha 可调)
- 新增 grow_v2_final(整帧重绘)/ grow_v2_final_v2(B 局部+美颜)端点:仅需图片+发型 ID,其余用固化默认值(color_match 关闭)
- 配套精简测试页 test_interface12_final.html / test_interface12_final_v2.html / test_interface12.html
- 恢复接口11 调试页多频段与换发型可调参数、color_match 默认不勾选
- 删除旧脚本 batch_grow_v2.py / gen_report_hairline_v2.py / test_simple.html

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 00:04:36 +08:00

362 lines
20 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>接口12 — 发际线带重绘(Flux-2)测试页</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 1240px; margin: 0 auto; padding: 24px; }
h1 { font-size: 22px; margin-bottom: 4px; }
h2 { font-size: 16px; margin: 20px 0 12px; }
.subtitle { color: #888; font-size: 13px; margin-bottom: 16px; line-height: 1.6; }
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
input[type=file] { flex: 1; min-width: 200px; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; font-weight: 600; }
.btn-primary { background: #7c3aed; color: #fff; }
.btn-primary:disabled { background: #c4b5fd; cursor: not-allowed; }
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
.btn-green { background: #059669; color: #fff; }
.btn-orange { background: #ea580c; color: #fff; }
.params { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; margin-top: 16px; }
.pf { display: flex; flex-direction: column; gap: 4px; }
.pf label { font-size: 13px; font-weight: 600; }
.pf .desc { font-size: 11px; color: #9ca3af; font-weight: 400; }
.pf select, .pf input[type=number] { padding: 8px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
.pf .row { display: flex; gap: 8px; align-items: center; }
.pf .row input[type=number] { width: 80px; }
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; font-family: monospace; }
.status.info { background: #ede9fe; color: #5b21b6; display: block; }
.status.error { background: #fee2e2; color: #991b1b; display: block; }
.status.success { background: #d1fae5; color: #065f46; display: block; }
.steps { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.step { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.step .cap { font-size: 13px; font-weight: 600; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #f0f0f0; }
.step .cap small { color: #999; font-weight: 400; display:block; margin-top:2px; }
.step img { width: 100%; display: block; background: #eee; cursor: zoom-in; }
.step .noimg { padding: 30px; text-align: center; color: #ccc; font-size: 13px; }
.big img { max-height: 520px; object-fit: contain; }
.log-panel { background: #1e1e1e; color: #d4d4d4; padding: 14px; border-radius: 8px;
font: 12px/1.6 Consolas, Monaco, monospace; white-space: pre-wrap; word-break: break-all;
max-height: 400px; overflow: auto; }
.log-panel .ts { color: #569cd6; }
.log-panel .lvl-info { color: #4ec9b0; }
.log-panel .lvl-warn { color: #dcdcaa; }
.log-panel .lvl-err { color: #f48771; }
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.85); display: none; align-items: center; justify-content: center; z-index: 50; cursor: zoom-out; }
.lightbox img { max-width: 95%; max-height: 95%; }
</style>
</head>
<body>
<div class="container">
<h1>接口12 — 发际线带重绘 <span style="font-size:13px;color:#888">Flux-2 保色重绘)</span></h1>
<div class="subtitle">
内部先跑<b>接口11</b>拿到 ④接缝融合最终图(final),再取 <b>⑤-① 发际线重绘带</b>(发际线外推 band_lo_mult×push ~ band_hi_mult×push、经 baseline 截断只留上部)作遮罩,
<b>Flux-2ComfyUI</b> 保色重绘,重绘结果与 final 融合。<br>
接口11 参数用于内部生成 final 与重绘带;<b>comfyui_prompt</b> 控制 Flux-2 提示词。⚠️ 需 ComfyUI(:8188) 在跑。
对照仅生成不重绘:<a href="/static/test_interface11_debug.html">接口11 调试页</a>
</div>
<div class="card">
<div class="upload-row">
<input type="file" id="imageFile" accept="image/*">
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🎨 提交重绘</button>
</div>
<div class="upload-row" style="margin-top:10px">
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
<input type="text" id="token" value="dev-shared-secret-2026" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
</div>
<div class="params">
<div class="pf">
<label>发际线类型 ID</label>
<select id="hairlineId">
<option value="chang_bolang" selected>chang_bolang(波浪)</option>
<option value="chang_zhixian">chang_zhixian(直线)</option>
<option value="chang_tuoyuan">chang_tuoyuan(椭圆)</option>
<option value="chang_xinxing">chang_xinxing(心形)</option>
<option value="chang_huaban">chang_huaban(花瓣)</option>
</select>
</div>
<div class="pf">
<label>hairline_push_cm <span class="desc">发际线外推(cm),也决定重绘带宽度</span></label>
<div class="row"><input type="number" id="hairlinePushCm" min="0" max="3" step="0.1" value="0.8"></div>
</div>
<div class="pf">
<label>mb_levels <span class="desc">多频段金字塔层数</span></label>
<div class="row"><input type="number" id="mbLevels" min="2" max="6" step="1" value="5"></div>
</div>
<div class="pf">
<label>blend_method <span class="desc">接缝融合方法</span></label>
<select id="blendMethod">
<option value="two_stage" selected>two_stage(泊松→多频段,大色差)</option>
<option value="multiband">multiband(多频段金字塔)</option>
<option value="seamless">seamless(泊松无缝克隆)</option>
<option value="feather">feather(高斯羽化)</option>
<option value="alpha_gradient">alpha_gradient(距离变换)</option>
</select>
</div>
<div class="pf">
<label>color_match <span class="desc">融合前颜色迁移(消除色差)</span></label>
<div class="row"><input type="checkbox" id="colorMatch" checked style="width:18px;height:18px"><span class="desc">multiband/feather 生效;seamless/two_stage 自带调色</span></div>
</div>
<div class="pf">
<label>color_match_strength <span class="desc">颜色迁移强度(0~1)</span></label>
<div class="row"><input type="number" id="cmStrength" min="0" max="1" step="0.05" value="0.4"></div>
</div>
<div class="pf">
<label>beauty_alpha <span class="desc">B版 band外美颜强度(0~1)</span></label>
<div class="row"><input type="number" id="beautyAlpha" min="0" max="1" step="0.05" value="0.6"></div>
</div>
<div class="pf">
<label>band_lo_mult <span class="desc">重绘带外推倍率下限(×push)</span></label>
<div class="row"><input type="number" id="bandLoMult" min="0" max="3" step="0.1" value="0.5"></div>
</div>
<div class="pf">
<label>band_hi_mult <span class="desc">重绘带外推倍率上限(×push)</span></label>
<div class="row"><input type="number" id="bandHiMult" min="0" max="3" step="0.1" value="1.5"></div>
</div>
</div>
<div class="pf" style="margin-top:14px">
<label>comfyui_prompt <span class="desc">Flux-2 提示词,留空=默认「补充遮罩区域的头发,加一点美颜」</span></label>
<textarea id="comfyuiPrompt" rows="2" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical" placeholder="留空用默认提示词"></textarea>
</div>
<div style="font-size:12px;color:#888;margin-top:8px">遮罩固定 pushed,重绘固定 Flux-2;隐藏参数(seg_model/hairline_edge/is_hr/edge_erode_px/mb_feather_px/transition_band_px/inpainting_fill/mask_blur/mask_dilate_scale)按原默认值随请求提交。换发型走 GPU + Flux-2 重绘,单次约 15~30s。</div>
</div>
<div class="status hidden" id="statusBar"></div>
<div id="resultsArea" class="hidden">
<div class="card">
<h2 style="margin-top:0">🎯 两版重绘对比</h2>
<div class="steps" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
<div class="step big"><div class="cap">接口11 final <small>④ 接缝融合(重绘输入基底,无美颜)</small></div><img id="finalBase"></div>
<div class="step big"><div class="cap">A · 整帧重绘 <small>全脸美颜 + 全脸重绘(=手动 ComfyUI)</small></div><img id="outFull"></div>
<div class="step big"><div class="cap">B · 局部加发+全脸美颜 <small>加发只在发际线带、美颜保留全脸</small></div><img id="outBand"></div>
</div>
<div style="font-size:12px;color:#888;margin-top:10px">对比要点:A 会重绘整张脸(五官/发型都可能变);B 只在发际线带加发、其余区域保留 final 结构并叠加全脸美颜(强度由 beauty_alpha 控制)。</div>
</div>
<div class="card">
<h2 style="margin-top:0">🪜 分步可视化 <span style="font-size:12px;color:#888" id="stepsInfo"></span></h2>
<div class="steps" id="stepsGrid"></div>
</div>
</div>
<div class="card">
<h2 style="margin-top:0; display:flex; justify-content:space-between; align-items:center">
<span>📋 前端日志</span>
<div>
<button class="btn btn-green" style="padding:6px 14px;font-size:13px" onclick="downloadFrontendLog()">下载前端日志</button>
<button class="btn btn-orange" style="padding:6px 14px;font-size:13px" onclick="downloadBackendLog()">下载后端日志</button>
<button class="btn btn-outline" style="padding:6px 14px;font-size:13px" onclick="clearLog()">清空</button>
</div>
</h2>
<div class="log-panel" id="logPanel"></div>
</div>
</div>
<div class="lightbox" id="lightbox" onclick="this.style.display='none'"><img id="lightboxImg" alt=""></div>
<script>
const API_BASE = window.location.origin;
const ENDPOINT = '/api/v1/hairline/grow_v2';
const LOG_API = '/api/v1/debug/hairline_log';
const FE_LOGS = [];
function flog(msg, level) {
const ts = new Date().toLocaleTimeString('zh-CN', {hour12:false}) + '.' + String(Date.now()%1000).padStart(3,'0');
const line = { ts, level: level||'info', msg };
FE_LOGS.push(line);
const panel = document.getElementById('logPanel');
const cls = level === 'warn' ? 'lvl-warn' : level === 'error' ? 'lvl-err' : 'lvl-info';
panel.innerHTML += '<span class="ts">[' + ts + ']</span> <span class="' + cls + '">' + line.msg.replace(/</g,'&lt;') + '</span>\n';
panel.scrollTop = panel.scrollHeight;
}
function clearLog() { FE_LOGS.length = 0; document.getElementById('logPanel').innerHTML = ''; }
const STEPS = [
{ key: 'input', title: '原图', sub: '接口输入' },
{ key: 'final', title: '接口11 ④ final', sub: '接缝融合最终图(重绘输入基底,无美颜)' },
{ key: 'redraw_band_overlay', title: '⑤-① 发际线重绘带', sub: '紫=lo×push↔hi×push之间、经 baseline 截断只留上部' },
{ key: 'redraw_full', title: 'A · 整帧重绘(Flux-2)', sub: '全脸美颜+全脸重绘(=手动 ComfyUI' },
{ key: 'redraw_band', title: 'B · 局部加发+全脸美颜', sub: 'band内=ComfyUI加发;band外=final+beauty_alpha美颜' },
];
function $(id) { return document.getElementById(id); }
function setStatus(text, type) { const b = $('statusBar'); b.textContent = text; b.className = 'status ' + type; }
function pick(obj, name) { if (!obj) return null; return obj[name + '_url'] || obj[name + '_base64'] || null; }
function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = 'flex'; }
function stepCard(title, sub, src) {
const div = document.createElement('div');
div.className = 'step';
const hasImg = src && src.length > 50;
const img = hasImg ? '<img src="' + src + '" onclick="zoom(this.src)">'
: '<div class="noimg">无图(后端返回空)</div>';
div.innerHTML = '<div class="cap">' + title + '<small>' + (sub||'') + (hasImg ? ' ('+src.length+'字符)' : '') + '</small></div>' + img;
return div;
}
function renderResult(d) {
flog('renderResult 开始 (blend=' + (d.blend_method || '?') + ', beauty_alpha=' + d.beauty_alpha + ')', 'info');
const s = d.steps || {};
$('finalBase').src = pick(s, 'final') || '';
$('finalBase').onclick = function(){ zoom(this.src); };
$('outFull').src = pick(s, 'redraw_full') || pick(s, 'redraw_c') || '';
$('outFull').onclick = function(){ zoom(this.src); };
$('outBand').src = pick(s, 'redraw_band') || '';
$('outBand').onclick = function(){ zoom(this.src); };
const grid = $('stepsGrid'); grid.innerHTML = '';
$('stepsInfo').textContent = '(mask=pushed, blend=' + (d.blend_method || '?') + ', redraw=Flux-2, beauty_alpha=' + d.beauty_alpha + ')';
if (d.redraw && d.redraw.enabled) {
flog('重绘带 band_pixels=' + d.redraw.band_pixels + ' push_px=' + d.redraw.push_px, 'info');
if (d.redraw.c_error) flog('Flux-2 重绘失败: ' + d.redraw.c_error, 'warn');
} else if (d.redraw && d.redraw.error) {
flog('重绘带计算失败: ' + d.redraw.error, 'warn');
}
STEPS.forEach(st => {
const src = pick(s, st.key);
const hasImg = src && src.length > 50;
flog(' 渲染 ' + st.key + ': ' + (hasImg ? '有图(' + src.length + '字符)' : '无图'), hasImg ? 'info' : 'warn');
grid.appendChild(stepCard(st.title, st.sub, src));
});
flog('renderResult 完成', 'info');
}
async function submitTest() {
const file = $('imageFile').files[0];
if (!file) { setStatus('请先选择图片', 'error'); return; }
// 页面隐藏但仍提交的固定默认值
const HIDDEN = {
seg_model: 'segformer',
hairline_edge: 'column',
is_hr: 'false',
edge_erode_px: '3',
mb_feather_px: '1',
transition_band_px: '-1',
inpainting_fill: '1',
mask_blur: '11',
mask_dilate_scale: '1.0',
};
flog('===== 提交重绘(接口12 =====', 'info');
flog('前端读取 hairline_id=' + $('hairlineId').value, 'info');
flog('前端读取 hairline_push_cm=' + ($('hairlinePushCm').value || '(默认)'), 'info');
flog('前端读取 mb_levels=' + ($('mbLevels').value || '(默认)'), 'info');
flog('前端读取 blend_method=' + $('blendMethod').value, 'info');
flog('前端读取 color_match=' + $('colorMatch').checked, 'info');
flog('前端读取 color_match_strength=' + ($('cmStrength').value || '(默认)'), 'info');
flog('前端读取 beauty_alpha=' + ($('beautyAlpha').value || '(默认)'), 'info');
flog('前端读取 重绘带倍率 lo=' + ($('bandLoMult').value || '0.5') + ' hi=' + ($('bandHiMult').value || '1.5'), 'info');
flog('前端读取 comfyui_prompt=' + ($('comfyuiPrompt').value.trim() || '(默认)'), 'info');
flog('隐藏参数固定: ' + JSON.stringify(HIDDEN), 'info');
const btn = $('submitBtn');
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
setStatus('正在请求(内部先跑接口11,再 Flux-2 重绘,约 15~30s...', 'info');
$('resultsArea').classList.remove('hidden');
const form = new FormData();
form.append('image_file', file);
form.append('hairline_id', $('hairlineId').value);
form.append('gen_backend', 'swaphair');
form.append('hairgrow_strength', '0.75');
form.append('is_hr', HIDDEN.is_hr);
form.append('seg_model', HIDDEN.seg_model);
form.append('erode_cm', '0.6');
form.append('swap_mode', 'ext_mask');
form.append('edge_erode_px', HIDDEN.edge_erode_px);
form.append('denoising_strength', '0.6');
form.append('mb_levels', $('mbLevels').value || '5');
form.append('hairline_push_cm', $('hairlinePushCm').value || '0.8');
form.append('hairline_edge', HIDDEN.hairline_edge);
form.append('blend_method', $('blendMethod').value);
form.append('color_match', $('colorMatch').checked ? 'true' : 'false');
form.append('color_match_strength', $('cmStrength').value || '0.4');
form.append('beauty_alpha', $('beautyAlpha').value || '0.6');
form.append('band_lo_mult', $('bandLoMult').value || '0.5');
form.append('band_hi_mult', $('bandHiMult').value || '1.5');
form.append('mb_feather_px', HIDDEN.mb_feather_px);
form.append('transition_band_px', HIDDEN.transition_band_px);
form.append('inpainting_fill', HIDDEN.inpainting_fill);
form.append('mask_blur', HIDDEN.mask_blur);
form.append('mask_dilate_scale', HIDDEN.mask_dilate_scale);
const cp = $('comfyuiPrompt').value.trim();
if (cp) form.append('comfyui_prompt', cp);
const sentFields = {};
form.forEach((v, k) => { sentFields[k] = (k === 'image_file') ? '[文件]' : v; });
flog('实际发送的 FormData: ' + JSON.stringify(sentFields), 'info');
const t0 = performance.now();
try {
const headers = {};
const tok = $('token').value.trim();
if (tok) headers['X-Internal-Token'] = tok;
flog('fetch POST ' + ENDPOINT, 'info');
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', headers, body: form });
flog('收到响应 http=' + resp.status, resp.ok ? 'info' : 'error');
const json = await resp.json();
const dt = ((performance.now() - t0) / 1000).toFixed(2);
flog('JSON 解析完成 code=' + json.code + ' 耗时=' + dt + 's', json.code === 0 ? 'info' : 'error');
if (json.code === 0) {
const d = json.data;
flog('后端返回 blend=' + d.blend_method + ' hairline_push_cm=' + d.hairline_push_cm + ' mask_pixels=' + d.mask_pixels, 'info');
flog('后端返回 comfyui_prompt=' + d.comfyui_prompt, 'info');
flog('后端 _rid=' + d._rid, 'info');
const s = d.steps || {};
Object.keys(s).filter(k => k.endsWith('_base64')).forEach(k => {
const len = s[k] ? s[k].length : 0;
flog(' steps.' + k + ' = ' + (len > 0 ? len + '字符' : '空'), len > 0 ? 'info' : 'warn');
});
const okRedraw = d.redraw && d.redraw.enabled && !d.redraw.c_error && (pick(s,'redraw_full'));
setStatus((okRedraw ? '✅ 重绘成功(A整帧 / B局部+美颜)' : '⚠️ 已返回(重绘可能未生效,见日志)') + ' (' + dt + 's) _rid=' + d._rid, okRedraw ? 'success' : 'error');
renderResult(d);
} else {
setStatus('❌ 业务错误 code=' + json.code + '' + json.message, 'error');
flog('业务错误: ' + json.message, 'error');
}
} catch (err) {
setStatus('❌ 网络错误: ' + err.message, 'error');
flog('网络错误: ' + err.message, 'error');
} finally {
btn.disabled = false; btn.textContent = '🎨 提交重绘';
}
}
function downloadFrontendLog() {
const text = FE_LOGS.map(l => '[' + l.ts + '] [' + l.level.toUpperCase() + '] ' + l.msg).join('\n');
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'iface12_frontend_log_' + Date.now() + '.txt';
a.click();
}
async function downloadBackendLog() {
flog('下载后端日志...', 'info');
try {
const resp = await fetch(API_BASE + LOG_API + '?tail=1000');
const text = await resp.text();
flog('后端日志获取成功 ' + text.length + ' 字符', 'info');
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'iface12_backend_log_' + Date.now() + '.txt';
a.click();
} catch (err) {
flog('下载后端日志失败: ' + err.message, 'error');
}
}
$('imageFile').addEventListener('change', function(){ if(this.files.length) flog('选择图片: ' + this.files[0].name, 'info'); });
flog('接口12 重绘测试页加载完成(两版对比:A整帧 / B局部加发+全脸美颜)', 'info');
flog('默认: hairline=chang_bolang, push=0.8, blend=two_stage, cm_strength=0.4, beauty_alpha=0.6;需 ComfyUI(:8188) 在跑', 'info');
</script>
</body>
</html>