feat: 接口12/接口2 发际线重绘改走 local_test 外部 ComfyUI 服务

后端 generate_hairline_redraw 跳过内置 Flux-2 重绘,改为产出 final(接缝融合基底)
+ 纯红遮罩 PNG(redraw_band_mask_base64,遮罩区=(255,0,0,255)、其余全透明)。

接口2 female(generate_grow_results_swap) 取 final+遮罩后在后端调 local_test
(0716add-hair.json 工作流) 完成重绘,结果作为生发图返回;male 分支不变。

测试页 test_interface12.html / test_interface12_final.html 改为两阶段:
先生成 final+纯红遮罩,再前端调 local_test 重绘并展示;color_match 默认不勾选。

local_test/app.py 加 CORS 头(OPTIONS 预检),支持浏览器跨域直连。
This commit is contained in:
xsl
2026-07-17 00:00:15 +08:00
parent 0bbb15d668
commit 5fbc03a6df
9 changed files with 291 additions and 101 deletions
+103 -33
View File
@@ -47,22 +47,32 @@
.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>
</head>
<body>
<div class="container">
<h1>接口12 — 发际线带重绘 <span style="font-size:13px;color:#888">Flux-2 保色重绘)</span></h1>
<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>Flux-2ComfyUI</b> 保色重绘,重绘结果与 final 融合<br>
接口11 参数用于内部生成 final 与重绘带;<b>comfyui_prompt</b> 控制 Flux-2 提示词。⚠️ 需 ComfyUI(:8188) 在跑。
内部先跑<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>外部重绘服务(local_test,默认 :8899</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()">🎨 提交重绘</button>
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🎨 生成 final+遮罩</button>
<button class="btn btn-green" id="redrawBtn" onclick="runLocalRedraw()" disabled>🧪 调 local_test 重绘</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="localTestUrl" value="http://127.0.0.1:8899" 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">重绘提示词</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>
@@ -99,7 +109,7 @@
</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 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>
@@ -119,23 +129,23 @@
</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>
<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 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">🎯 两版重绘对比</h2>
<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">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 class="step big"><div class="cap">⑤-① 发际线重绘带遮罩 <small>纯红 alpha PNG(遮罩区=红+不透明,其余全透明</small></div><img id="maskPng"></div>
<div class="step big"><div class="cap">🧪 local_test 重绘结果 <small>final + 遮罩 → 外部 ComfyUI(0716add-hair.json)</small></div><img id="localResult"></div>
</div>
<div style="font-size:12px;color:#888;margin-top:10px">对比要点:A 会重绘整张脸(五官/发型都可能变);B 只在发际线带加发、其余区域保留 final 结构并叠加全脸美颜(强度由 beauty_alpha 控制)</div>
<div style="font-size:12px;color:#888;margin-top:10px">流程:后端产出 final(接缝融合基底)+ 纯红遮罩 PNG,前端把两者 POST 到 local_test 重绘服务完成发际线补发</div>
</div>
<div class="card">
@@ -180,10 +190,13 @@ 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美颜' },
{ key: 'redraw_band_mask', title: '⑤-② 纯红遮罩 PNG', sub: '遮罩区=(255,0,0,255)、其余全透明;交给 local_test' },
];
// 缓存最近一次后端返回的 final(JPG data URI)和纯红遮罩(PNG data URI),供 local_test 重绘使用
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; }
@@ -200,19 +213,20 @@ function stepCard(title, sub, src) {
}
function renderResult(d) {
flog('renderResult 开始 (blend=' + (d.blend_method || '?') + ', beauty_alpha=' + d.beauty_alpha + ')', 'info');
flog('renderResult 开始 (blend=' + (d.blend_method || '?') + ')', 'info');
const s = d.steps || {};
$('finalBase').src = pick(s, 'final') || '';
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); };
$('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); };
$('maskPng').src = maskSrc;
$('maskPng').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 + ')';
$('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');
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');
}
@@ -222,7 +236,10 @@ function renderResult(d) {
flog(' 渲染 ' + st.key + ': ' + (hasImg ? '有图(' + src.length + '字符)' : '无图'), hasImg ? 'info' : 'warn');
grid.appendChild(stepCard(st.title, st.sub, src));
});
flog('renderResult 完成', 'info');
// final + 遮罩 都有 → 允许调 local_test 重绘
const canRedraw = !!(finalSrc && maskSrc);
$('redrawBtn').disabled = !canRedraw;
flog('renderResult 完成 canRedraw=' + canRedraw, 'info');
}
async function submitTest() {
@@ -254,8 +271,8 @@ async function submitTest() {
flog('隐藏参数固定: ' + JSON.stringify(HIDDEN), 'info');
const btn = $('submitBtn');
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
setStatus('正在请求(内部跑接口11,再 Flux-2 重绘,约 15~30s...', 'info');
btn.disabled = true; btn.textContent = '⏳ 生成中...';
setStatus('正在请求(内部跑接口11 生成 final + 纯红遮罩,约 10~15s...', 'info');
$('resultsArea').classList.remove('hidden');
const form = new FormData();
@@ -305,15 +322,15 @@ async function submitTest() {
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');
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');
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');
@@ -323,7 +340,60 @@ async function submitTest() {
setStatus('❌ 网络错误: ' + err.message, 'error');
flog('网络错误: ' + err.message, 'error');
} finally {
btn.disabled = false; btn.textContent = '🎨 提交重绘';
btn.disabled = false; btn.textContent = '🎨 生成 final+遮罩';
}
}
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给 local_test
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 base = $('localTestUrl').value.trim().replace(/\/+$/, '');
if (!base) { setStatus('请填写重绘服务地址', 'error'); return; }
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮';
const btn = $('redrawBtn');
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
flog('===== 调 local_test 重绘 =====', 'info');
flog('服务地址=' + base + ' 提示词=' + prompt, 'info');
const form = new FormData();
form.append('image', dataUriToBlob(_finalDataUri), 'final.jpg');
form.append('mask', dataUriToBlob(_maskDataUri), 'mask.png');
form.append('prompt', prompt);
const t0 = performance.now();
try {
flog('fetch POST ' + base + '/api/generate', 'info');
const resp = await fetch(base + '/api/generate', { method: 'POST', body: form });
flog('收到响应 http=' + resp.status + ' type=' + resp.headers.get('content-type'), resp.ok ? 'info' : 'error');
const dt = ((performance.now() - t0) / 1000).toFixed(2);
if (resp.ok && resp.headers.get('content-type', '').includes('image/')) {
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
$('localResult').src = url;
$('localResult').onclick = function(){ zoom(this.src); };
setStatus('✅ local_test 重绘完成 (' + dt + 's)', 'success');
flog('local_test 重绘完成 耗时=' + dt + 's 大小=' + blob.size + 'B', 'info');
} else {
// 非图片响应 → 多半是 JSON 错误
let msg = resp.status + '';
try { const j = await resp.json(); msg = j.error || JSON.stringify(j); } catch (_) {}
setStatus('❌ local_test 重绘失败:' + msg, 'error');
flog('local_test 重绘失败: ' + msg, 'error');
}
} catch (err) {
setStatus('❌ local_test 网络错误:' + err.message + '(确认服务在跑且已加 CORS', 'error');
flog('local_test 网络错误: ' + err.message, 'error');
} finally {
btn.disabled = false; btn.textContent = '🧪 调 local_test 重绘';
}
}
@@ -354,8 +424,8 @@ async function downloadBackendLog() {
$('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');
flog('接口12 重绘测试页加载完成(后端产出 final+纯红遮罩 → 前端调 local_test 重绘', 'info');
flog('默认: hairline=chang_bolang, push=0.8, blend=two_stage, cm_strength=0.4;需 ComfyUI(:8188) 在跑;重绘需 local_test(:8899) 在跑', 'info');
</script>
</body>
</html>
+80 -13
View File
@@ -40,17 +40,26 @@
</head>
<body>
<div class="container">
<h1>接口12 final — 发际线带重绘 <span style="font-size:13px;color:#888">(精简版)</span></h1>
<h1>接口12 final — 发际线带重绘 <span style="font-size:13px;color:#888">(精简版:final + 纯红遮罩 → 外部 ComfyUI 重绘</span></h1>
<div class="subtitle">
只需上传图片 + 选择发型,其余参数全部用当前调优默认值(<code>/api/v1/hairline/grow_v2_final</code>)。<br>
重绘输出为<b>整帧重绘</b>(全脸美颜 + 全脸重绘,=手动 ComfyUI)。⚠️ 需 ComfyUI(:8188) 在跑。
后端产出 <b>④ final(接缝融合基底)</b> + <b>⑤-② 纯红遮罩 PNG</b>,前端调 <b>外部重绘服务(local_test,默认 :8899</b>完成发际线带重绘。⚠️ 需 ComfyUI(:8188) 在跑。
局部加发版见 <a href="/static/test_interface12_final_v2.html">接口12 final v2</a>;完整参数调试见 <a href="/static/test_interface12.html">接口12 调试页</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>
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🎨 生成 final+遮罩</button>
<button class="btn btn-green" id="redrawBtn" onclick="runLocalRedraw()" disabled>🧪 调 local_test 重绘</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="localTestUrl" value="http://127.0.0.1:8899" 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">重绘提示词</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="params">
<div class="pf">
@@ -70,11 +79,13 @@
<div id="resultsArea" class="hidden">
<div class="card">
<h2 style="margin-top:0">🎯 整帧重绘结果</h2>
<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>全脸美颜 + 全脸重绘(=手动 ComfyUI</small></div><img id="outFull"></div>
<div class="step big"><div class="cap">⑤-① 发际线重绘带遮罩 <small>纯红 alpha PNG(遮罩区=红+不透明,其余全透明</small></div><img id="maskPng"></div>
<div class="step big"><div class="cap">🧪 local_test 重绘结果 <small>final + 遮罩 → 外部 ComfyUI(0716add-hair.json)</small></div><img id="localResult"></div>
</div>
<div style="font-size:12px;color:#888;margin-top:10px">流程:后端产出 final(接缝融合基底)+ 纯红遮罩 PNG,前端把两者 POST 到 local_test 重绘服务完成发际线补发。</div>
</div>
</div>
</div>
@@ -86,6 +97,10 @@ const API_BASE = window.location.origin;
const ENDPOINT = '/api/v1/hairline/grow_v2_final';
const TOKEN = 'dev-shared-secret-2026';
// 缓存最近一次后端返回的 final(data URI)和纯红遮罩(data URI),供 local_test 重绘使用
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; }
@@ -93,10 +108,26 @@ function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = '
function renderResult(d) {
const s = d.steps || {};
$('finalBase').src = pick(s, 'final') || '';
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); };
$('outFull').src = pick(s, 'redraw_full') || pick(s, 'redraw_c') || '';
$('outFull').onclick = function(){ zoom(this.src); };
$('maskPng').src = maskSrc;
$('maskPng').onclick = function(){ zoom(this.src); };
const canRedraw = !!(finalSrc && maskSrc);
$('redrawBtn').disabled = !canRedraw;
}
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给 local_test
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 submitTest() {
@@ -104,8 +135,8 @@ async function submitTest() {
if (!file) { setStatus('请先选择图片', 'error'); return; }
const btn = $('submitBtn');
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
setStatus('正在请求(内部跑接口11,再 Flux-2 重绘,约 15~30s...', 'info');
btn.disabled = true; btn.textContent = '⏳ 生成中...';
setStatus('正在请求(内部跑接口11 生成 final + 纯红遮罩,约 10~15s...', 'info');
$('resultsArea').classList.remove('hidden');
const form = new FormData();
@@ -124,8 +155,9 @@ async function submitTest() {
if (json.code === 0) {
const d = json.data;
const okRedraw = d.redraw && d.redraw.enabled && !d.redraw.c_error && pick(d.steps, 'redraw_full');
setStatus((okRedraw ? '✅ 整帧重绘成功' : '⚠️ 已返回(重绘可能未生效)') + ' (' + dt + 's)', okRedraw ? 'success' : 'error');
const hasFinal = !!(pick(d.steps, 'final'));
const hasMask = !!(pick(d.steps, 'redraw_band_mask'));
setStatus((hasFinal && hasMask ? '✅ 已生成 final + 纯红遮罩' : '⚠️ 已返回(final/遮罩缺失,见日志)') + ' (' + dt + 's)', (hasFinal && hasMask) ? 'success' : 'error');
renderResult(d);
} else {
setStatus('❌ 业务错误 code=' + json.code + '' + json.message, 'error');
@@ -133,7 +165,42 @@ async function submitTest() {
} catch (err) {
setStatus('❌ 网络错误: ' + err.message, 'error');
} finally {
btn.disabled = false; btn.textContent = '🎨 提交重绘';
btn.disabled = false; btn.textContent = '🎨 生成 final+遮罩';
}
}
async function runLocalRedraw() {
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
const base = $('localTestUrl').value.trim().replace(/\/+$/, '');
if (!base) { setStatus('请填写重绘服务地址', 'error'); return; }
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮';
const btn = $('redrawBtn');
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
const form = new FormData();
form.append('image', dataUriToBlob(_finalDataUri), 'final.jpg');
form.append('mask', dataUriToBlob(_maskDataUri), 'mask.png');
form.append('prompt', prompt);
const t0 = performance.now();
try {
const resp = await fetch(base + '/api/generate', { method: 'POST', body: form });
const dt = ((performance.now() - t0) / 1000).toFixed(2);
if (resp.ok && resp.headers.get('content-type', '').includes('image/')) {
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
$('localResult').src = url;
$('localResult').onclick = function(){ zoom(this.src); };
setStatus('✅ local_test 重绘完成 (' + dt + 's)', 'success');
} else {
let msg = resp.status + '';
try { const j = await resp.json(); msg = j.error || JSON.stringify(j); } catch (_) {}
setStatus('❌ local_test 重绘失败:' + msg, 'error');
}
} catch (err) {
setStatus('❌ local_test 网络错误:' + err.message + '(确认服务在跑且已加 CORS', 'error');
} finally {
btn.disabled = false; btn.textContent = '🧪 调 local_test 重绘';
}
}
</script>