422 lines
26 KiB
HTML
422 lines
26 KiB
HTML
<!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; display:flex; align-items:center; gap:8px; }
|
||
.step .cap .badge { flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; min-width:24px; height:24px; padding:0 6px; border-radius:6px; background:#2563eb; color:#fff; font-size:13px; font-weight:700; }
|
||
.step .cap .ttext { flex:1; }
|
||
.step .cap small { color: #999; font-weight: 400; display:block; margin-top:2px; }
|
||
.step .desc { font-size: 12px; line-height: 1.7; color: #4b5563; padding: 10px 12px; background: #f9fafb; border-bottom: 1px solid #f0f0f0; }
|
||
.step .desc b { color:#1f2937; }
|
||
.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"><span class="badge" style="background:#9ca3af">输入</span><span class="ttext">输入原图</span></div><img id="finalInput"></div>
|
||
<div class="step big"><div class="cap"><span class="badge">⑪</span><span class="ttext">最终结果<small>接口11 最终输出 = ⑩接缝融合结果</small></span></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,'<') + '</span>\n';
|
||
panel.scrollTop = panel.scrollHeight;
|
||
}
|
||
function clearLog() { FE_LOGS.length = 0; document.getElementById('logPanel').innerHTML = ''; }
|
||
|
||
const STEPS = [
|
||
{ no: '①', key: 'baseline_overlay', title: '发际线分割线 baseline', sub: '黄线=baseline(眉峰水平连线)',
|
||
desc: '用 468 点人脸关键点定位两侧眉峰,连成一条水平线作为「上半区」的底界。'
|
||
+ '这条线把画面分成上下两半:以上是额头+头发(要处理),以下是五官(保持不动)。'
|
||
+ '它是后续所有遮罩、外推、贴回的坐标基准线。图中的黄线就是 baseline,红点为 151 号中心点(径向外推的圆心)。' },
|
||
{ no: '②', key: 'upper_overlay', title: '上半区 upper', sub: '青色=baseline 以上区域',
|
||
desc: '把 baseline 以上到画面顶部的整片区域标为「上半区」。后续的头发分割、发际线外推、'
|
||
+ '最终遮罩都只会在这个上半区内计算,确保下半张脸(眉、眼、鼻、嘴)永远不被改动。'
|
||
+ '青色覆盖的就是上半区范围。' },
|
||
{ no: '③', key: 'hair_seg_overlay', title: '头发分割', sub: '绿色=头发像素',
|
||
desc: '用语义分割模型(默认 SegFormer,可选 BiSeNet)逐像素判断哪些是「头发」。'
|
||
+ '绿色覆盖的就是被识别为头发的像素。这一步的目的是找到现有头发的边界,'
|
||
+ '为下一步「发际线内轮廓」提供输入——发际线就长在头发区域的内边缘上。' },
|
||
{ no: '④', key: 'hairline_overlay', title: '头发内轮廓线', sub: '绿=内轮廓(额头弧+两侧),黄=baseline',
|
||
desc: '从头发分割结果里提取出「头发的内轮廓」:即头发与皮肤交界的那条线。'
|
||
+ '它包含额头弧线(发际线本体)和两侧向下的鬓角轮廓。'
|
||
+ '提取方式 hairline_edge=column 时按逐列(竖向)找头发最低点连成线;=contour 时用形态学轮廓。'
|
||
+ '绿色折线就是提取出的内轮廓,黄色折线是 ① 的 baseline。这条内轮廓是外推发际线的起点。' },
|
||
{ no: '⑤', key: 'pushed_overlay', title: '外推发际线 + 遮罩', sub: '青=外推线,红=遮罩,绿=内轮廓,红点=圆心',
|
||
desc: '把 ④ 的发际线内轮廓「往头发方向(向头顶)推进 hairline_push_cm 厘米」得到一条新的外推线(青色)。'
|
||
+ '外推方式:以眉心(151 点)为圆心做径向外推,推过的这段就是「要新长出头发的区域」。'
|
||
+ '然后用【外推线(上界)到 baseline(下界)】之间的闭合区域作为最终遮罩(红色半透明)。'
|
||
+ 'push_cm 越大,新发际线越靠上、生发区越大(默认 0.8cm)。' },
|
||
{ no: '⑥', key: 'mask_overlay', title: '最终遮罩(叠加图)', sub: '红色=要重绘/贴回的区域',
|
||
desc: '把 ⑤ 算出的遮罩叠回原图看效果。红色区域 = 需要被新生成的头发覆盖的位置(遮罩内),'
|
||
+ '红色以外 = 保持原样不动(遮罩外)。这张图用来直观确认遮罩范围是否合理——'
|
||
+ '理想情况是红色正好覆盖额头该生发的区域,不越界到眉毛或脸颊。' },
|
||
{ no: '⑦', key: 'mask', title: '纯遮罩', sub: '白色=贴回区,黑色=保留区',
|
||
desc: '同一张遮罩的纯黑白版本(无原图背景)。白色=贴回区,黑色=保留区。'
|
||
+ '这张纯遮罩会作为 ext_mask 传给换发型服务,让 webui 精确地只在这个区域内重绘画头发。'
|
||
+ '它的好处是不受背景图干扰,便于检查遮罩形状是否干净(无噪点、无破洞)。' },
|
||
{ no: '⑧', key: 'swap_raw', title: '换发型生成', sub: 'change_hair 换该发际线类型后的整帧图',
|
||
desc: '调用 change_hair 换发型服务(POST :8801/api/swapHair/v1),传入原图 + 发际线类型 ID(hairline_id)。'
|
||
+ '服务用对应发型的 LoRA 模型(webui img2img,denoising_strength 控制生发强度)生成一张'
|
||
+ '「同一个人、换成该发际线类型发型」的完整图。注意:这张图是整帧都变了,'
|
||
+ '下一步会严格按遮罩只取额头那块,其余丢掉,保证五官不动。inpainting_fill/mask_blur 等参数控制这里的重绘方式。' },
|
||
{ no: '⑨', key: 'hard_paste', title: '严格贴回(无融合)', sub: '遮罩内=生成图,遮罩外=原图',
|
||
desc: '把 ⑧ 的生成图按 ⑥/⑦ 的遮罩「硬贴」回原图:遮罩内用生成图,遮罩外完全保留原图。'
|
||
+ '这是没有做任何接缝处理的版本,因此遮罩边缘通常能看到明显的接缝/色差。'
|
||
+ '它的作用是让你对比看出「融合前后的差别」——边缘接缝要靠下一步的融合来消除。' },
|
||
{ no: '⑩', key: 'alpha', title: '融合权重 alpha', sub: '白=用生成图,黑=保留原图,灰=过渡',
|
||
desc: '决定每个像素最终取多少比例的生成图。纯白(=1)完全用生成图,纯黑(=0)完全保留原图,'
|
||
+ '灰色是两者按比例过渡。blend_method=multiband/two_stage 时是多层金字塔权重(过渡带较宽、自然);'
|
||
+ '=feather/alpha_gradient 时是单层羽化(硬边缘软过渡)。这张图用来理解融合是怎么"渐变"地把新头发融进去的。' },
|
||
{ no: '⑪', key: 'final', title: '接缝融合(最终输出)', sub: '接口11 最终结果',
|
||
desc: '按 ⑩ 的权重,把生成图和原图加权融合,得到无接缝的最终图。这就是接口11 的最终返回结果。'
|
||
+ 'blend_method 选择融合算法:multiband=多频段金字塔(分频段融合,大色差场景用 two_stage 先泊松调色再多频段);'
|
||
+ 'seamless=泊松无缝克隆(梯度域自动调色);feather/alpha_gradient=简单羽化。'
|
||
+ 'color_match=true 时融合前还做一次 Reinhard 颜色迁移消除整体色差(seamless/two_stage 自带调色故跳过)。'
|
||
+ '本接口不含重绘,需要重绘(美颜/补发丝)见接口12。' },
|
||
];
|
||
|
||
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(st, 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>';
|
||
const badge = st.no ? '<span class="badge">' + st.no + '</span>' : '';
|
||
const ttext = '<span class="ttext">' + (st.title||'') + '<small>' + (st.sub||'') + (hasImg ? ' ('+src.length+'字符)' : '') + '</small></span>';
|
||
const desc = st.desc ? '<div class="desc">' + st.desc + '</div>' : '';
|
||
div.innerHTML = '<div class="cap">' + badge + ttext + '</div>' + desc + 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, 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');
|
||
}
|
||
}
|
||
|
||
// 步骤序号 → 对应英文 key 的映射,供按序号定位步骤
|
||
const STEP_BY_NO = {};
|
||
STEPS.forEach(s => { if (s.no) STEP_BY_NO[s.no] = s.key; });
|
||
|
||
// 参数联动(遮罩/融合已固定,无下拉联动)
|
||
$('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>
|