优化节点

This commit is contained in:
wjluoxiao
2026-11-07 19:16:34 +08:00
parent 3ce3a030d6
commit e22bc8d7e9
3 changed files with 86 additions and 1 deletions
+71 -1
View File
@@ -38,6 +38,51 @@ const OUTPUT_ALIASES = {
const normKey = (s) => String(s || "").toLowerCase().replace(/[\s_\-]/g, "");
// ── 分辨率参数节点(输出 Width/Height,但节点上**没有**名叫 width/height 的部件)──
// 这类节点必须用与后端完全相同的公式现场算。
// 否则会掉进下面的兜底逻辑,把 megapixels 当成宽高:0.9MP → int(0.9)=0(碰巧当没填),
// 1.0MP → 1 → 2×2 的预览(畸形)。这就是「超过 0.9MP 预览就坏」的原因。
const RESOLUTION_NODE_TYPES = new Set([
"JZL_HailuoH3VideoParams",
"JZL_HailuoH3VideoParamsPro",
"XB_HailuoH3VideoParams",
]);
/** Python round() 是「四舍六入五取偶」,JS Math.round 是五入 —— 必须一致,否则预览与实际输出差一档 */
function roundHalfEven(x) {
const f = Math.floor(x);
const diff = x - f;
if (diff > 0.5) return f + 1;
if (diff < 0.5) return f;
return f % 2 === 0 ? f : f + 1;
}
function aspectRatioText(node) {
const w = (node.widgets || []).find((x) => x && x.name === "aspect_ratio");
return String((w && w.value) || "16:9 (Widescreen)");
}
function ratioOf(text) {
const m = String(text || "").match(/(\d+(?:\.\d+)?)\s*[::]\s*(\d+(?:\.\d+)?)/);
if (!m) return [16, 9];
const a = Number(m[1]);
const b = Number(m[2]);
return a > 0 && b > 0 ? [a, b] : [16, 9];
}
/** 与节点 process() 完全一致:MP×1024² → 按比例开方 → 对齐 multiple */
function computeResolution(node) {
const [wr, hr] = ratioOf(aspectRatioText(node));
const mp = widgetNum(node, "megapixels", 1.0) || 1.0;
const mult = Math.max(1, Math.round(widgetNum(node, "multiple", 32) || 32));
const total = mp * 1024 * 1024;
const scale = Math.sqrt(total / (wr * hr));
return {
width: Math.max(mult, roundHalfEven((wr * scale) / mult) * mult),
height: Math.max(mult, roundHalfEven((hr * scale) / mult) * mult),
};
}
/** 按「输出名 / 别名」去找上游对应的部件(名字或中文标签都算) */
function matchWidgetByOutput(ws, outName, ok, raw) {
if (!outName) return null;
@@ -57,16 +102,41 @@ function matchWidgetByOutput(ws, outName, ok, raw) {
/** 从上游节点里挑出「能代表该输出的值」(兼容各种整数/Primitive/参数面板节点) */
function pickUpstreamValue(src, slot, outName, numeric) {
const ws = src.widgets || [];
const key = normKey(outName);
const isDim = key === "width" || key === "height" || key === "宽度" || key === "高度";
const raw = (w) => (w && w.value != null && w.value !== "" ? w.value : null);
const ok = (v) => {
if (v == null || v === "") return false;
if (!numeric) return true;
return Number.isFinite(Number(v));
const n = Number(v);
if (!Number.isFinite(n)) return false;
// 分辨率不可能小于 16:拦住 megapixels / scale_factor 这类小数被误当成宽高
if (isDim && n < 16) return false;
return true;
};
// 0) 分辨率参数节点:节点上没有 width/height 部件,只能按公式现场算
if (isDim && RESOLUTION_NODE_TYPES.has(src.type)) {
const r = computeResolution(src);
return key === "width" || key === "宽度" ? r.width : r.height;
}
// 1) 输出名 / 别名 对应的部件(名称或中文标签)
const byName = matchWidgetByOutput(ws, outName, ok, raw);
if (byName) return raw(byName);
// 宽高:到这里还认不出「哪个部件是宽 / 高」就不能再往下猜了。
// 按槽位/存档值猜会把 megapixels(1.0)、multiple(32) 之类的参数当成分辨率 →
// 预览变成 2×2 / 32×32 的畸形条。返回 null 只会退回「保持源尺寸」,永远不会错得离谱。
// 只有「槽位与部件一一对应」时才敢按槽位取值:单部件节点(Primitive / rgthree 数值)
// 或输出数与部件数相同的节点。
if (isDim) {
const aligned = ws.length === 1
|| (Array.isArray(src.outputs) && src.outputs.length === ws.length);
if (!aligned) return null;
const dimW = ws[slot] || ws[0];
return ok(raw(dimW)) ? raw(dimW) : null;
}
// 2) 名字像数值的部件(rgthree「数量多功能工具」的 value#1 / 中文「数值」等)
const numericName = /value|val$|数值|值|number|num|int|float|count/i;
for (const w of ws) {
+8
View File
@@ -22,6 +22,7 @@ from comfy_extras.nodes_video import preview_input_video
DIMMAX = 8192
BIGMAX = (2**53) - 1
MIN_DIM = 16 # 宽高低于它就当「没填」(拦截小数/脏值被当成分辨率)
def _video_files():
@@ -69,6 +70,13 @@ def _check_peak_memory(frames, width, height, hint):
def _resize(images, width, height):
"""0 表示按原始比例;两个都给则强制拉伸到指定尺寸。"""
width = int(width or 0)
height = int(height or 0)
# 小于 MIN_DIM 的宽高当「没填」:上游若误连了小数(如 megapixels 0.9)会变成 1px 高的畸形输出
if width < MIN_DIM:
width = 0
if height < MIN_DIM:
height = 0
if images.shape[0] == 0 or not width and not height:
return images
src_h, src_w = int(images.shape[1]), int(images.shape[2])
+7
View File
@@ -30,6 +30,7 @@ else:
ENCODE_ARGS = ("utf-8", "backslashreplace")
PREVIEW_MAX = 960 # 预览转码的长边上限(只影响预览,不影响真实输出)
MIN_DIM = 16 # 宽高低于它就当「没填」(拦截小数/脏值被当成分辨率)
async def _stream_media(request, args, filename, content_type="video/webm"):
@@ -110,6 +111,12 @@ async def xb_video_preview(request):
cap = max(0, int(_num("frame_load_cap")))
width = max(0, int(_num("custom_width")))
height = max(0, int(_num("custom_height")))
# 小于 MIN_DIM 的宽高一律当「没填」:上游若是把小数(如 megapixels 0.9)误连进来,
# 会在下面的 _even() 里变成 2px,预览就成了拉伸的畸形长条
if width < MIN_DIM:
width = 0
if height < MIN_DIM:
height = 0
# 与后端 XB_LoadVideo 完全一致:跳过帧 → 强制帧率重采样 → 每隔 nth 帧取一帧
# 输出帧率 = (强制帧率 or 源帧率) / nth