diff --git a/face_analysis/hairline_grow.py b/face_analysis/hairline_grow.py index 10957cd..269b274 100644 --- a/face_analysis/hairline_grow.py +++ b/face_analysis/hairline_grow.py @@ -598,7 +598,7 @@ _REPAINT_WORKFLOW = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ha def _call_comfyui(image_bgr, mask_bool, prompt=None): - """调本机 ComfyUI 的 Flux-2 inpaint 工作流(hair_repaint.json),返回与输入同分辨率的 BGR。 + """调远端 ComfyUI 的 Flux-2 inpaint 工作流(hair_repaint.json),返回与输入同分辨率的 BGR。 与 swapHair 的区别:ComfyUI 把「原图 VAE 编码作 reference latent + ColorMatch」双重保色, 天生不易染色;提示词自由可调(中文)。mask 经 RGBA alpha 通道传入(透明=重绘区)。 @@ -606,10 +606,10 @@ def _call_comfyui(image_bgr, mask_bool, prompt=None): """ import io from hairline.mask import compose_comfy_rgba - from hairline.comfyui import run as comfyui_run, ping + from hairline.comfyui import COMFYUI_URL, run as comfyui_run, ping if not ping(): - raise SwapError("ComfyUI 不可达(http://127.0.0.1:8188),redraw Flux-2 路跳过") + raise SwapError(f"ComfyUI 不可达({COMFYUI_URL}),redraw Flux-2 路跳过") mask_u8 = (mask_bool.astype(np.uint8)) * 255 rgba_img = compose_comfy_rgba(image_bgr, mask_u8) # alpha=255-mask:透明=重绘区 buf = io.BytesIO() diff --git a/hairline/comfyui.py b/hairline/comfyui.py index 8c1b02a..ace4b0b 100644 --- a/hairline/comfyui.py +++ b/hairline/comfyui.py @@ -1,8 +1,9 @@ """ComfyUI 客户端:用 add_hair.json / add_hair2.json 工作流跑生发图(Flux-2 inpaint)。 -worker 不跑 Flux,只把「划线图 + 遮罩」的 RGBA 上传到本机 ComfyUI(默认 8188), +worker 不跑 Flux,只把「划线图 + 遮罩」的 RGBA 上传到远端 ComfyUI +(默认 http://10.60.74.221:8188,可用环境变量 COMFYUI_URL 覆盖), 替换工作流节点 26 的输入图、随机 seed,提交 /prompt,轮询 /history,取回 /view 输出。 -ComfyUI 开启了 HTTP Basic Auth(user `admin` + 密码),所有请求都带凭据。 +ComfyUI 若开启了 HTTP Basic Auth(user `admin` + 密码),所有请求都带凭据。 支持多工作流:run() 可通过 workflow_path 指定不同工作流 JSON,自动检测 SaveImage 输出节点。 """ @@ -17,7 +18,7 @@ import uuid import httpx -COMFYUI_URL = os.getenv("COMFYUI_URL", "http://127.0.0.1:8188").rstrip("/") +COMFYUI_URL = os.getenv("COMFYUI_URL", "http://10.60.74.221:8188").rstrip("/") _WORKFLOW_DEFAULT = os.getenv( "ADD_HAIR_WORKFLOW", os.path.join(os.path.dirname(os.path.dirname(__file__)), "add_hair.json"), diff --git a/static/img_downscale.js b/static/img_downscale.js new file mode 100644 index 0000000..f01502f --- /dev/null +++ b/static/img_downscale.js @@ -0,0 +1,55 @@ +// 上传图片自动降采样:当总像素 > MAX_PIXELS 时,等比例缩小到 <= TARGET_PIXELS。 +// 用法:在提交前 file = await window.downscaleImageFile(file); +(function () { + var MAX_PIXELS = 1536000; // 触发阈值:超过则缩小 + var TARGET_PIXELS = 786432; // 缩小后总像素上限 + + function loadImage(url) { + return new Promise(function (resolve, reject) { + var img = new Image(); + img.onload = function () { resolve(img); }; + img.onerror = reject; + img.src = url; + }); + } + + async function downscaleImageFile(file) { + // 非图片、GIF、SVG 不处理,原样返回 + if (!file || !file.type || file.type.indexOf('image/') !== 0) return file; + if (file.type === 'image/gif' || file.type === 'image/svg+xml') return file; + + var url = URL.createObjectURL(file); + try { + var img = await loadImage(url); + var w = img.naturalWidth, h = img.naturalHeight; + var total = w * h; + if (!total || total <= MAX_PIXELS) return file; // 未超阈值,原图直接用 + + var scale = Math.sqrt(TARGET_PIXELS / total); + var nw = Math.max(1, Math.round(w * scale)); + var nh = Math.max(1, Math.round(h * scale)); + + var canvas = document.createElement('canvas'); + canvas.width = nw; + canvas.height = nh; + var ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, nw, nh); + + var outType = file.type === 'image/png' ? 'image/png' : 'image/jpeg'; + var quality = outType === 'image/jpeg' ? 0.92 : undefined; + var blob = await new Promise(function (res) { canvas.toBlob(res, outType, quality); }); + if (!blob) return file; + + var base = (file.name || 'image').replace(/\.(jpe?g|png|webp|bmp)$/i, ''); + var name = base + (outType === 'image/png' ? '.png' : '.jpg'); + return new File([blob], name, { type: outType, lastModified: Date.now() }); + } catch (e) { + console.warn('downscaleImageFile 失败,使用原图', e); + return file; + } finally { + URL.revokeObjectURL(url); + } + } + + window.downscaleImageFile = downscaleImageFile; +})(); diff --git a/static/test_interface1.html b/static/test_interface1.html index edfa976..ebd5605 100644 --- a/static/test_interface1.html +++ b/static/test_interface1.html @@ -62,6 +62,7 @@ @media (max-width: 768px) { .results { flex-direction: column; } } .hidden { display: none !important; } +
@@ -127,8 +128,9 @@ function setStatus(text, type) { async function submitTest() { const fileInput = $('imageFile'); - const file = fileInput.files[0]; + let file = fileInput.files[0]; if (!file) { setStatus('请先选择一张图片', 'error'); return; } + file = await window.downscaleImageFile(file); const _reqStart = performance.now(); diff --git a/static/test_interface10.html b/static/test_interface10.html index 72f088a..6550344 100644 --- a/static/test_interface10.html +++ b/static/test_interface10.html @@ -50,6 +50,7 @@ .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%; } +
@@ -197,8 +198,9 @@ function renderMetrics(data) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择一张图片', 'error'); return; } + file = await window.downscaleImageFile(file); const t0 = performance.now(); const btn = $('submitBtn'); diff --git a/static/test_interface11.html b/static/test_interface11.html index fb7cc21..8966b7c 100644 --- a/static/test_interface11.html +++ b/static/test_interface11.html @@ -55,6 +55,7 @@ .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%; } +
@@ -292,8 +293,9 @@ function renderResult(d) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择一张图片', 'error'); return; } + file = await window.downscaleImageFile(file); const t0 = performance.now(); const btn = $('submitBtn'); btn.disabled = true; btn.textContent = '⏳ 请求中...'; diff --git a/static/test_interface11_debug.html b/static/test_interface11_debug.html index 2eda948..68c5179 100644 --- a/static/test_interface11_debug.html +++ b/static/test_interface11_debug.html @@ -53,6 +53,7 @@ .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%; } +
@@ -287,8 +288,9 @@ function renderResult(d) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择图片', 'error'); return; } + file = await window.downscaleImageFile(file); // 页面隐藏但仍提交的固定默认值 const HIDDEN = { seg_model: 'segformer', diff --git a/static/test_interface12.html b/static/test_interface12.html index 5425443..30b132c 100644 --- a/static/test_interface12.html +++ b/static/test_interface12.html @@ -49,6 +49,7 @@ .lightbox img { max-width: 95%; max-height: 95%; } .hidden { display: none; } +
@@ -243,8 +244,9 @@ function renderResult(d) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择图片', 'error'); return; } + file = await window.downscaleImageFile(file); // 页面隐藏但仍提交的固定默认值 const HIDDEN = { seg_model: 'segformer', diff --git a/static/test_interface12_final.html b/static/test_interface12_final.html index 334c5a9..e710574 100644 --- a/static/test_interface12_final.html +++ b/static/test_interface12_final.html @@ -37,6 +37,7 @@ .lightbox img { max-width: 95%; max-height: 95%; } .hidden { display: none; } +
@@ -131,8 +132,9 @@ function dataUriToBlob(dataUri) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择图片', 'error'); return; } + file = await window.downscaleImageFile(file); const btn = $('submitBtn'); btn.disabled = true; btn.textContent = '⏳ 生成中...'; diff --git a/static/test_interface12_final_v2.html b/static/test_interface12_final_v2.html index 450216b..059efd5 100644 --- a/static/test_interface12_final_v2.html +++ b/static/test_interface12_final_v2.html @@ -37,6 +37,7 @@ .lightbox img { max-width: 95%; max-height: 95%; } .hidden { display: none; } +
@@ -100,8 +101,9 @@ function renderResult(d) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择图片', 'error'); return; } + file = await window.downscaleImageFile(file); const btn = $('submitBtn'); btn.disabled = true; btn.textContent = '⏳ 重绘中...'; diff --git a/static/test_interface2.html b/static/test_interface2.html index 3806dbb..35cad4c 100644 --- a/static/test_interface2.html +++ b/static/test_interface2.html @@ -69,6 +69,7 @@ .hidden { display: none !important; } +
@@ -140,9 +141,9 @@ function $(id) { return document.getElementById(id); } function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; } async function submitTest() { - const f = $('imageFile').files[0]; + let f = $('imageFile').files[0]; if (!f) { setStatus('请选择图片', 'error'); return; } - + f = await window.downscaleImageFile(f); _origUrl = URL.createObjectURL(f); $('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ 请求中...'; diff --git a/static/test_interface3.html b/static/test_interface3.html index 7cfc6e4..9e40d5c 100644 --- a/static/test_interface3.html +++ b/static/test_interface3.html @@ -71,6 +71,7 @@ @media (max-width: 800px) { .results-layout, .preview-row { flex-direction: column; } } +
@@ -139,9 +140,9 @@ function $(id) { return document.getElementById(id); } function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; } async function submitTest() { - const mf = $('markedFile').files[0]; + let mf = $('markedFile').files[0]; if (!mf) { setStatus('请选择划线图', 'error'); return; } - + mf = await window.downscaleImageFile(mf); const markedUrl = URL.createObjectURL(mf); // 先显示原图 diff --git a/static/test_interface4.html b/static/test_interface4.html index a5d395d..822d77c 100644 --- a/static/test_interface4.html +++ b/static/test_interface4.html @@ -57,6 +57,7 @@ @media (max-width: 800px) { .results-layout { flex-direction: column; } } +
@@ -121,8 +122,9 @@ function $(id) { return document.getElementById(id); } function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; } async function submitTest() { - const f = $('imageFile').files[0]; + let f = $('imageFile').files[0]; if (!f) { setStatus('请选择图片', 'error'); return; } + f = await window.downscaleImageFile(f); $('imgPreview').innerHTML = 'preview'; $('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ 分析中...'; diff --git a/static/test_interface5.html b/static/test_interface5.html index 6151c0d..acf6dff 100644 --- a/static/test_interface5.html +++ b/static/test_interface5.html @@ -88,6 +88,7 @@ .hidden { display: none !important; } @media (max-width: 800px) { .results-layout { flex-direction: column; } } +
@@ -195,10 +196,11 @@ function selectAllHair(select) { } async function submitTest() { - const f = $('imageFile').files[0]; + let f = $('imageFile').files[0]; if (!f) { setStatus('请选择图片', 'error'); return; } const checked = [...document.querySelectorAll('#hairStyleGroup input:checked')].map(cb => cb.value); if (!checked.length) { setStatus('请至少选择一个发型', 'error'); return; } + f = await window.downscaleImageFile(f); _origUrl = URL.createObjectURL(f); $('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...'; diff --git a/static/test_interface6.html b/static/test_interface6.html index 2d3370b..e679dda 100644 --- a/static/test_interface6.html +++ b/static/test_interface6.html @@ -59,6 +59,7 @@ .diff-list { font-size: 12px; color: #6b7280; margin-top: 6px; line-height: 1.7; } .diff-list li { margin-left: 18px; } +
@@ -129,8 +130,9 @@ function setStatus(text, type) { async function submitTest() { const fileInput = $('imageFile'); - const file = fileInput.files[0]; + let file = fileInput.files[0]; if (!file) { setStatus('请先选择一张图片', 'error'); return; } + file = await window.downscaleImageFile(file); const _reqStart = performance.now(); diff --git a/static/test_interface7.html b/static/test_interface7.html index f916a59..3f25061 100644 --- a/static/test_interface7.html +++ b/static/test_interface7.html @@ -66,6 +66,7 @@ .badge-v2 { background: #7c3aed; color: #fff; font-size: 11px; padding: 2px 8px; border-radius: 10px; margin-left: 6px; vertical-align: middle; } +
@@ -137,9 +138,9 @@ function $(id) { return document.getElementById(id); } function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; } async function submitTest() { - const f = $('imageFile').files[0]; + let f = $('imageFile').files[0]; if (!f) { setStatus('请选择图片', 'error'); return; } - + f = await window.downscaleImageFile(f); _origUrl = URL.createObjectURL(f); $('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ 请求中...'; diff --git a/static/test_interface9.html b/static/test_interface9.html index d14fcb0..70cfdc1 100644 --- a/static/test_interface9.html +++ b/static/test_interface9.html @@ -51,6 +51,7 @@ .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%; } +
@@ -194,8 +195,9 @@ function renderMetrics(data) { } async function submitTest() { - const file = $('imageFile').files[0]; + let file = $('imageFile').files[0]; if (!file) { setStatus('请先选择一张图片', 'error'); return; } + file = await window.downscaleImageFile(file); const t0 = performance.now(); const btn = $('submitBtn');