feat: 适配 RTX 3090 (24GB) 环境优化

硬件迁移:从 RTX 5090 (32GB) 迁移到 RTX 3090 (24GB)

主要改动:
1. hairline/comfyui.py: 轮询间隔从 0.2s 降到 0.05s
2. hairline/service.py: PNG 编码 compress_level=1,节省 ~240ms
3. add_hair.json: 工作流使用 4B FP8 模型 + steps=4
4. static/test_interface3.html: 修复图片显示(添加 data:image/jpeg;base64, 前缀)

性能优化后接口3响应时间:6.6-7.3s(之前 8.66s)
This commit is contained in:
xsl
2026-07-18 18:58:40 +08:00
parent 659c037270
commit 74ccab0ff8
9 changed files with 311 additions and 563 deletions
+29 -38
View File
@@ -53,11 +53,11 @@
</head>
<body>
<div class="container">
<h1>接口12 — 发际线带重绘 <span style="font-size:13px;color:#888">final + 纯红遮罩 → 外部 ComfyUI 重绘)</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>纯红遮罩 PNG</b>(遮罩区=(255,0,0,255),其余全透明)。<br>
前端拿到 final + 遮罩后,调 <b>外部重绘服务(local_test,默认 :8899</b>完成重绘。⚠️ 需 ComfyUI(:8188) 在跑。
前端拿到 final + 遮罩后,调 <b>后端重绘接口(/api/v1/redraw</b>完成重绘。⚠️ 需 ComfyUI(:8188) 在跑。
对照仅生成不重绘:<a href="/static/test_interface11_debug.html">接口11 调试页</a>
</div>
@@ -65,11 +65,7 @@
<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>🧪 调 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://10.60.74.221:8899" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
<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>
@@ -144,9 +140,9 @@
<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">🧪 local_test 重绘结果 <small>final + 遮罩 → 外部 ComfyUI(0716add-hair.json)</small></div><img id="localResult"></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,前端把两者 POST 到 local_test 重绘服务完成发际线补发。</div>
<div style="font-size:12px;color:#888;margin-top:10px">流程:后端产出 final(接缝融合基底)+ 纯红遮罩 PNG,再调后端 /api/v1/redraw 完成发际线补发。</div>
</div>
<div class="card">
@@ -191,10 +187,10 @@ 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)、其余全透明;交给 local_test' },
{ key: 'redraw_band_mask', title: '⑤-② 纯红遮罩 PNG', sub: '遮罩区=(255,0,0,255)、其余全透明;交给后端重绘' },
];
// 缓存最近一次后端返回的 final(JPG data URI)和纯红遮罩(PNG data URI),供 local_test 重绘使用
// 缓存最近一次后端返回的 final(JPG data URI)和纯红遮罩(PNG data URI),供重绘使用
let _finalDataUri = '';
let _maskDataUri = '';
@@ -237,7 +233,7 @@ function renderResult(d) {
flog(' 渲染 ' + st.key + ': ' + (hasImg ? '有图(' + src.length + '字符)' : '无图'), hasImg ? 'info' : 'warn');
grid.appendChild(stepCard(st.title, st.sub, src));
});
// final + 遮罩 都有 → 允许调 local_test 重绘
// final + 遮罩 都有 → 允许调后端重绘
const canRedraw = !!(finalSrc && maskSrc);
$('redrawBtn').disabled = !canRedraw;
flog('renderResult 完成 canRedraw=' + canRedraw, 'info');
@@ -346,7 +342,7 @@ async function submitTest() {
}
}
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给 local_test
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给后端重绘
function dataUriToBlob(dataUri) {
const [meta, b64] = dataUri.split(',');
const mime = (meta.match(/data:([^;]+)/) || [, 'application/octet-stream'])[1];
@@ -358,44 +354,39 @@ function dataUriToBlob(dataUri) {
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');
flog('===== 调后端重绘 =====', 'info');
flog('提示词=' + prompt, 'info');
const form = new FormData();
form.append('image', dataUriToBlob(_finalDataUri), 'final.jpg');
form.append('mask', dataUriToBlob(_maskDataUri), 'mask.png');
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 ' + 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');
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);
if (resp.ok && resp.headers.get('content-type', '').includes('image/')) {
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
$('localResult').src = url;
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('✅ local_test 重绘完成 (' + dt + 's)', 'success');
flog('local_test 重绘完成 耗时=' + dt + 's 大小=' + blob.size + 'B', 'info');
setStatus('✅ 重绘完成 (' + dt + 's)', 'success');
flog('重绘完成 耗时=' + dt + 's', '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');
const msg = json.message || JSON.stringify(json);
setStatus('❌ 重绘失败:' + msg, 'error');
flog('重绘失败: ' + msg, 'error');
}
} catch (err) {
setStatus('❌ local_test 网络错误:' + err.message + '(确认服务在跑且已加 CORS', 'error');
flog('local_test 网络错误: ' + err.message, 'error');
setStatus('❌ 重绘网络错误:' + err.message, 'error');
flog('重绘网络错误: ' + err.message, 'error');
} finally {
btn.disabled = false; btn.textContent = '🧪 调 local_test 重绘';
btn.disabled = false; btn.textContent = '🧪 重绘';
}
}
@@ -426,8 +417,8 @@ async function downloadBackendLog() {
$('imageFile').addEventListener('change', function(){ if(this.files.length) flog('选择图片: ' + this.files[0].name, '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');
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>
+20 -26
View File
@@ -41,10 +41,10 @@
</head>
<body>
<div class="container">
<h1>接口12 final — 发际线带重绘 <span style="font-size:13px;color:#888">(精简版:final + 纯红遮罩 → 外部 ComfyUI 重绘)</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>④ final(接缝融合基底)</b> + <b>⑤-② 纯红遮罩 PNG</b>前端<b>外部重绘服务(local_test,默认 :8899</b>完成发际线带重绘。⚠️ 需 ComfyUI(:8188) 在跑。
后端产出 <b>④ final(接缝融合基底)</b> + <b>⑤-② 纯红遮罩 PNG</b><b>后端重绘接口(/api/v1/redraw</b>完成发际线带重绘。⚠️ 需 ComfyUI(:8188) 在跑。
局部加发版见 <a href="/static/test_interface12_final_v2.html">接口12 final v2</a>;完整参数调试见 <a href="/static/test_interface12.html">接口12 调试页</a>
</div>
@@ -52,11 +52,7 @@
<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>🧪 调 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://10.60.74.221:8899" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
<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>
@@ -84,9 +80,9 @@
<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">🧪 local_test 重绘结果 <small>final + 遮罩 → 外部 ComfyUI(0716add-hair.json)</small></div><img id="localResult"></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,前端把两者 POST 到 local_test 重绘服务完成发际线补发。</div>
<div style="font-size:12px;color:#888;margin-top:10px">流程:后端产出 final(接缝融合基底)+ 纯红遮罩 PNG,再调后端 /api/v1/redraw 完成发际线补发。</div>
</div>
</div>
</div>
@@ -98,7 +94,7 @@ 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 重绘使用
// 缓存最近一次后端返回的 final(data URI)和纯红遮罩(data URI),供重绘使用
let _finalDataUri = '';
let _maskDataUri = '';
@@ -121,7 +117,7 @@ function renderResult(d) {
$('redrawBtn').disabled = !canRedraw;
}
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给 local_test
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给后端重绘
function dataUriToBlob(dataUri) {
const [meta, b64] = dataUri.split(',');
const mime = (meta.match(/data:([^;]+)/) || [, 'application/octet-stream'])[1];
@@ -173,36 +169,34 @@ async function submitTest() {
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('image_file', dataUriToBlob(_finalDataUri), 'final.jpg');
form.append('mask_file', 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 resp = await fetch(API_BASE + '/api/v1/redraw', {
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;
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('✅ local_test 重绘完成 (' + dt + 's)', 'success');
setStatus('✅ 重绘完成 (' + 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');
setStatus('❌ 重绘失败:' + (json.message || JSON.stringify(json)), 'error');
}
} catch (err) {
setStatus('❌ local_test 网络错误:' + err.message + '(确认服务在跑且已加 CORS', 'error');
setStatus('❌ 重绘网络错误:' + err.message, 'error');
} finally {
btn.disabled = false; btn.textContent = '🧪 调 local_test 重绘';
btn.disabled = false; btn.textContent = '🧪 重绘';
}
}
</script>
+1 -1
View File
@@ -175,7 +175,7 @@ async function submitTest() {
$('hairlineType').style.display = 'inline-block';
}
const grownSrc = d.hair_growth_image_url || d.hair_growth_image_base64;
const grownSrc = d.hair_growth_image_url || (d.hair_growth_image_base64 ? 'data:image/jpeg;base64,' + d.hair_growth_image_base64 : '');
if (grownSrc) {
$('blendTop').src = grownSrc;
$('blendTop').style.display = 'block';