Files
hair/static/test_interface11_debug.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

375 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>接口11 调试页(带日志)— 发际线生发</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: #2563eb; color: #fff; }
.btn-primary:disabled { background: #93c5fd; 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; }
.pf .row input[type=range] { flex: 1; }
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; font-family: monospace; }
.status.info { background: #dbeafe; color: #1e40af; 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(260px, 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>接口11 调试页 <span style="font-size:13px;color:#888">(带前后端日志)</span></h1>
<div class="subtitle">
独立调试页,每一步都记录日志。提交后展开"前端日志"和"后端日志",点按钮可下载。<br>
重点排查:遮罩是否走 pushed、①-f/①-g 是否有图、最终遮罩是否正确。遮罩固定 pushed;默认 blend=two_stage、redraw 开、发际线波浪。
</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" style="width:18px;height:18px"><span class="desc" id="cmNote">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>edge_erode_px <span class="desc">融合·贴图前遮罩内缩(px)</span></label>
<div class="row"><input type="number" id="edgeErodePx" min="0" max="40" step="1" value="3"></div>
</div>
<div class="pf">
<label>mb_feather_px <span class="desc">融合·最细层掩码羽化(px)</span></label>
<div class="row"><input type="number" id="mbFeatherPx" min="0" max="5" step="1" value="1"></div>
</div>
<div class="pf">
<label>transition_band_px <span class="desc">融合·过渡带边距(-1=自动)</span></label>
<div class="row"><input type="number" id="transitionBandPx" min="-1" max="128" step="1" value="-1"></div>
</div>
<!-- change_hair 换发型参数 -->
<div class="pf">
<label>inpainting_fill <span class="desc">换发型·重绘填充(治染绿)</span></label>
<select id="inpaintingFill">
<option value="1">1=填充噪声(默认/原始)</option>
<option value="0">0=保留原图(治染绿)</option>
<option value="2">2=纯色填充</option>
<option value="3">3=潜变量噪声</option>
</select>
</div>
<div class="pf">
<label>mask_blur <span class="desc">换发型·重绘遮罩边缘模糊(px)</span></label>
<div class="row"><input type="number" id="maskBlur" min="0" max="64" step="1" value="11"></div>
</div>
<div class="pf">
<label>mask_dilate_scale <span class="desc">换发型·重绘遮罩膨胀缩放</span></label>
<div class="row"><input type="number" id="maskDilateScale" min="0" max="4" step="0.1" value="1.0"></div>
</div>
</div>
<div style="font-size:12px;color:#888;margin-top:8px">遮罩算法固定 pushed。默认 blend=two_stage;多频带融合参数与 change_hair 换发型参数已在上方可调,其余隐藏参数按原默认值随请求提交。<b>本接口不含重绘</b>,发际线带重绘见 <a href="/static/test_interface12.html">接口12 测试页</a></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(320px, 1fr))">
<div class="step big"><div class="cap">输入原图</div><img id="finalInput"></div>
<div class="step big"><div class="cap">最终结果</div><img id="finalOut"></div>
</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';
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: 'baseline_overlay', title: '①-a 发际线分割线', sub: '黄线=baseline' },
{ key: 'upper_overlay', title: '①-b 上半区', sub: '青=baseline以上' },
{ key: 'hair_seg_overlay', title: '①-c 头发分割', sub: '绿=头发像素' },
{ key: 'top_fill_overlay', title: '①-d 填充到基线', sub: '蓝=top_fill(仅eroded/closed' },
{ key: 'closed_overlay', title: '①-e 闭合区域', sub: '紫=closed(仅eroded/closed' },
{ key: 'hairline_overlay', title: '①-f 头发内轮廓线', sub: '绿=头发内轮廓(额头弧+两侧到下颌),黄=baseline折线(仅pushed' },
{ key: 'pushed_overlay', title: '①-g 外推发际线', sub: '青=外推线(进头发push_cm),红=遮罩,绿=内轮廓(仅pushed' },
{ key: 'mask_overlay', title: '① 最终遮罩(叠加)', sub: '红=最终遮罩区' },
{ key: 'mask', title: '① 纯遮罩', sub: '白=贴回区' },
{ key: 'swap_raw', title: '② 生成全帧', sub: '换发型结果' },
{ key: 'hard_paste', title: '③ 严格贴回', sub: '遮罩内=生成,外=原图' },
{ key: 'alpha', title: '④ 融合权重', sub: '白=用生成图' },
{ key: 'final', title: '④ 接缝融合(最终)', sub: '最终输出' },
];
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 开始 (mask=pushed, blend=' + (d.blend_method || '?') + ')', 'info');
const s = d.steps || {};
$('finalInput').src = pick(s, 'input') || '';
$('finalInput').onclick = function(){ zoom(this.src); };
$('finalOut').src = pick(s, 'final') || '';
$('finalOut').onclick = function(){ zoom(this.src); };
const grid = $('stepsGrid'); grid.innerHTML = '';
// 固定 pushed:只展示 pushed 相关步骤(不展示 top_fill/closed;重绘已移到接口12
const showKeys = new Set(['baseline_overlay','upper_overlay','hair_seg_overlay',
'hairline_overlay','pushed_overlay','mask_overlay','mask','swap_raw','hard_paste','alpha','final']);
$('stepsInfo').textContent = '(mask=pushed, blend=' + (d.blend_method || '?') + ')';
STEPS.filter(st => showKeys.has(st.key)).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',
};
flog('===== 提交测试 =====', '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('前端读取 [融合] edge_erode_px=' + ($('edgeErodePx').value || '(默认)')
+ ' mb_feather_px=' + ($('mbFeatherPx').value || '(默认)')
+ ' transition_band_px=' + ($('transitionBandPx').value || '(默认)'), 'info');
flog('前端读取 [换发型] inpainting_fill=' + $('inpaintingFill').value
+ ' mask_blur=' + ($('maskBlur').value || '(默认)')
+ ' mask_dilate_scale=' + ($('maskDilateScale').value || '(默认)'), 'info');
flog('隐藏参数固定: ' + JSON.stringify(HIDDEN), 'info');
const btn = $('submitBtn');
btn.disabled = true; btn.textContent = '⏳ 请求中...';
setStatus('正在请求 (mask=pushed, blend=' + $('blendMethod').value + ')...', '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('swap_mode', 'ext_mask');
form.append('edge_erode_px', $('edgeErodePx').value || '3');
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('mb_feather_px', $('mbFeatherPx').value || '1');
form.append('transition_band_px', $('transitionBandPx').value || '-1');
form.append('inpainting_fill', $('inpaintingFill').value || '1');
form.append('mask_blur', $('maskBlur').value || '11');
form.append('mask_dilate_scale', $('maskDilateScale').value || '1.0');
// 记录发出去的 form 字段
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('后端返回 mask_type=' + d.mask_type + ' blend=' + d.blend_method + ' mask_pixels=' + d.mask_pixels, 'info');
flog('后端返回 hairline_push_cm=' + d.hairline_push_cm + ' hairline_edge=' + d.hairline_edge, 'info');
flog('后端返回 color_match=' + d.color_match + ' cm_strength=' + d.color_match_strength + ' mb_feather_px=' + d.mb_feather_px + ' transition_band_px=' + d.transition_band_px, 'info');
flog('后端返回 inpainting_fill=' + d.inpainting_fill + ' mask_blur=' + d.mask_blur + ' mask_dilate_scale=' + d.mask_dilate_scale, 'info');
flog('后端 _rid=' + d._rid, 'info');
// 详细记录 steps 每个字段长度
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');
});
setStatus('✅ 成功 (' + dt + 's) mask_type=' + d.mask_type + ' _rid=' + d._rid, 'success');
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 = '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 = '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('调试页加载完成(接口11,不含重绘)', 'info');
flog('默认: hairline=chang_bolang, push=0.8, blend=two_stage, cm_strength=0.4;多频带融合(edge_erode/mb_feather/transition)与 change_hair 换发型(inpainting_fill/mask_blur/mask_dilate)参数已在页面可调。重绘见接口12 测试页', 'info');
</script>
</body>
</html>