Files
hair/static/test_interface12.html
xsl 5bcaeb2594 chore: 所有提示词统一为"填充遮罩区域的头发"
测试结论: 纯生发提示词(不做皮肤处理)效果最佳。
- 替换所有位置的提示词默认值(原"填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"等)
- 涉及25个文件: app.py/hairline/service.py/hairline/redraw.py/工作流json/测试页/benchmark脚本/local_test
- _REDRAW_PROMPT / _DEFAULT_PROMPT / 各接口Form默认值 / 测试页输入框默认值 全部统一
2026-07-26 21:53:39 +08:00

425 lines
23 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>接口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%; }
.hidden { display: none; }
</style>
<script src="/static/img_downscale.js"></script>
</head>
<body>
<div class="container">
<h1>接口12 — 发际线带重绘 <span style="font-size:13px;color:#888">final + 纯红遮罩 → ComfyUI 重绘)</span></h1>
<div class="subtitle">
内部先跑<b>接口11</b>拿到 ④接缝融合最终图(final),再取 <b>⑤-① 发际线重绘带</b>(发际线外推 band_lo_mult×push ~ band_hi_mult×push、经 baseline 截断只留上部)生成
<b>纯红遮罩 PNG</b>(遮罩区=(255,0,0,255),其余全透明)。<br>
前端拿到 final + 遮罩后,调 <b>后端重绘接口(/api/v1/redraw</b>完成重绘。⚠️ 需 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()">🎨 生成 final+遮罩</button>
<button class="btn btn-green" id="redrawBtn" onclick="runLocalRedraw()" disabled>🧪 重绘</button>
</div>
<div class="upload-row" style="margin-top:10px">
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
</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">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;后端只产出 final+纯红遮罩(不做重绘)。隐藏参数(seg_model/hairline_edge/is_hr/edge_erode_px/mb_feather_px/transition_band_px/inpainting_fill/mask_blur/mask_dilate_scale)按默认值随请求提交。beauty_alpha/comfyui_prompt 已不再生效。</div>
</div>
<div class="status hidden" id="statusBar"></div>
<div id="resultsArea" class="hidden">
<div class="card">
<h2 style="margin-top:0">🎯 final + 纯红遮罩 → 重绘结果</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">⑤-① 发际线重绘带遮罩 <small>纯红 alpha PNG(遮罩区=红+不透明,其余全透明)</small></div><img id="maskPng"></div>
<div class="step big"><div class="cap">🧪 重绘结果 <small>final + 遮罩 → ComfyUI(0716add-hair)</small></div><img id="localResult"></div>
</div>
<div style="font-size:12px;color:#888;margin-top:10px">流程:后端产出 final(接缝融合基底)+ 纯红遮罩 PNG,再调后端 /api/v1/redraw 完成发际线补发。</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_band_mask', title: '⑤-② 纯红遮罩 PNG', sub: '遮罩区=(255,0,0,255)、其余全透明;交给后端重绘' },
];
// 缓存最近一次后端返回的 final(JPG data URI)和纯红遮罩(PNG data URI),供重绘使用
let _finalDataUri = '';
let _maskDataUri = '';
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 || '?') + ')', 'info');
const s = d.steps || {};
const finalSrc = pick(s, 'final') || '';
const maskSrc = pick(s, 'redraw_band_mask') || '';
_finalDataUri = finalSrc;
_maskDataUri = maskSrc;
$('finalBase').src = finalSrc;
$('finalBase').onclick = function(){ zoom(this.src); };
$('maskPng').src = maskSrc;
$('maskPng').onclick = function(){ zoom(this.src); };
const grid = $('stepsGrid'); grid.innerHTML = '';
$('stepsInfo').textContent = '(mask=pushed, blend=' + (d.blend_method || '?') + ')';
if (d.redraw && d.redraw.enabled) {
flog('重绘带 band_pixels=' + d.redraw.band_pixels + ' push_px=' + d.redraw.push_px, 'info');
} 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));
});
// final + 遮罩 都有 → 允许调后端重绘
const canRedraw = !!(finalSrc && maskSrc);
$('redrawBtn').disabled = !canRedraw;
flog('renderResult 完成 canRedraw=' + canRedraw, 'info');
}
async function submitTest() {
let file = $('imageFile').files[0];
if (!file) { setStatus('请先选择图片', 'error'); return; }
file = await window.downscaleImageFile(file);
// 页面隐藏但仍提交的固定默认值
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 生成 final + 纯红遮罩,约 10~15s...', '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('后端返回 _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 hasFinal = !!(pick(s, 'final'));
const hasMask = !!(pick(s, 'redraw_band_mask'));
setStatus((hasFinal && hasMask ? '✅ 已生成 final + 纯红遮罩' : '⚠️ 已返回(final/遮罩缺失,见日志)') + ' (' + dt + 's) _rid=' + d._rid, (hasFinal && hasMask) ? '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 = '🎨 生成 final+遮罩';
}
}
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给后端重绘
function dataUriToBlob(dataUri) {
const [meta, b64] = dataUri.split(',');
const mime = (meta.match(/data:([^;]+)/) || [, 'application/octet-stream'])[1];
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return new Blob([arr], { type: mime });
}
async function runLocalRedraw() {
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发';
const btn = $('redrawBtn');
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
flog('===== 调后端重绘 =====', 'info');
flog('提示词=' + prompt, 'info');
const form = new FormData();
form.append('image_file', dataUriToBlob(_finalDataUri), 'final.jpg');
form.append('mask_file', dataUriToBlob(_maskDataUri), 'mask.png');
form.append('prompt', prompt);
const t0 = performance.now();
try {
flog('fetch POST ' + API_BASE + '/api/v1/redraw', 'info');
const resp = await fetch(API_BASE + '/api/v1/redraw', { method: 'POST', body: form });
flog('收到响应 http=' + resp.status, resp.ok ? 'info' : 'error');
const dt = ((performance.now() - t0) / 1000).toFixed(2);
const json = await resp.json();
if (json.code === 0 && json.data && json.data.image_base64) {
$('localResult').src = json.data.image_base64;
$('localResult').onclick = function(){ zoom(this.src); };
setStatus('✅ 重绘完成 (' + dt + 's)', 'success');
flog('重绘完成 耗时=' + dt + 's', 'info');
} else {
const msg = json.message || JSON.stringify(json);
setStatus('❌ 重绘失败:' + msg, 'error');
flog('重绘失败: ' + msg, '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 重绘测试页加载完成(后端产出 final+纯红遮罩 → 后端 /api/v1/redraw 重绘)', 'info');
flog('默认: hairline=chang_bolang, push=0.8, blend=two_stage, cm_strength=0.4;需 ComfyUI(:8188) 在跑', 'info');
</script>
</body>
</html>