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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user