- 接口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>
375 lines
20 KiB
HTML
375 lines
20 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; }
|
||
.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,'<') + '</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>
|