新增节点
This commit is contained in:
+19
-1
@@ -70,6 +70,16 @@ def print_error(msg):
|
||||
def print_warning(msg):
|
||||
print(f"\033[93m{msg}\033[0m")
|
||||
|
||||
# 启动 banner 上的 JZL 像素字
|
||||
XB_LOGO = "\n".join([
|
||||
" ██╗███████╗██╗",
|
||||
" ██║╚══███╔╝██║",
|
||||
" ██║ ███╔╝ ██║",
|
||||
"██ ██║ ███╔╝ ██║",
|
||||
"╚█████╔╝███████╗███████╗",
|
||||
" ╚════╝ ╚══════╝╚══════╝",
|
||||
])
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
WEB_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "js")
|
||||
@@ -94,6 +104,7 @@ try:
|
||||
from .nodes_digital_human import XB_DigitalHumanParams_Single, XB_DigitalHumanParams_Dual
|
||||
from .nodes_model_loader_gguf import XB_ModelLoaderV1_GGUF, XB_ModelLoaderV2_GGUF, XB_ModelLoaderV3_GGUF
|
||||
from .nodes_status_switch import XB_NodeStatusSwitch
|
||||
from .nodes_fast_toggle import XB_DisableGroups, XB_DisableNodes, XB_AnySwitch
|
||||
from .nodes_flux_encode import XB_FluxMultiImageEncode
|
||||
from .nodes_qwen_encode import XB_TextEncodeQwenImageEdit, XB_TextEncodeQwenImageEditPlus
|
||||
from .nodes_image_scale import XB_ImageScale
|
||||
@@ -285,6 +296,9 @@ try:
|
||||
"XB_WanInfiniteTalk_RelayNode_AllInOne": XB_WanInfiniteTalk_RelayNode_AllInOne,
|
||||
"XB_LTX23_InfiniteStreamer": XB_LTX23_InfiniteStreamer,
|
||||
"XB_NodeStatusSwitch": XB_NodeStatusSwitch,
|
||||
"XB_DisableGroups": XB_DisableGroups,
|
||||
"XB_DisableNodes": XB_DisableNodes,
|
||||
"XB_AnySwitch": XB_AnySwitch,
|
||||
"XB_FluxMultiImageEncode": XB_FluxMultiImageEncode,
|
||||
"XB_TextEncodeQwenImageEdit": XB_TextEncodeQwenImageEdit,
|
||||
"XB_TextEncodeQwenImageEditPlus": XB_TextEncodeQwenImageEditPlus,
|
||||
@@ -447,6 +461,9 @@ try:
|
||||
"XB_WanInfiniteTalk_RelayNode_AllInOne": "XB-BOX - 🆕 InfiniteTalk 无限接力点 (全能)",
|
||||
"XB_LTX23_InfiniteStreamer": "XB-BOX - 🔄 LTX2.3 数字人无限时长对口型",
|
||||
"XB_NodeStatusSwitch": "XB-BOX - ⚡ 节点状态开关",
|
||||
"XB_DisableGroups": "XB-BOX - 🚫 禁用分组",
|
||||
"XB_DisableNodes": "XB-BOX - 🚫 禁用节点",
|
||||
"XB_AnySwitch": "XB-BOX - 🔀 切换任意",
|
||||
"XB_FluxMultiImageEncode": "XB-BOX - 🖼️ FLUX多图编码",
|
||||
"XB_TextEncodeQwenImageEdit": "XB-BOX - 📝 Qwen图像编辑编码",
|
||||
"XB_TextEncodeQwenImageEditPlus": "XB-BOX - 📝 Qwen图像编辑编码Plus",
|
||||
@@ -551,7 +568,8 @@ try:
|
||||
"XB_YuE2Transcription": "XB-BOX - 🎵 YuE2 转谱导入",
|
||||
})
|
||||
|
||||
print_success("\n" + "="*50)
|
||||
print_success("\n" + XB_LOGO)
|
||||
print_success("="*50)
|
||||
print_success("🚀 [XB-BOX] XB_ToolBox Core Modules Loaded Successfully!")
|
||||
print_success("="*50 + "\n")
|
||||
|
||||
|
||||
@@ -51,8 +51,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
|
||||
# ASCII art banner
|
||||
ascii_art = """
|
||||
⡎⠑ ⢀⡀ ⢀⣀ ⡀⢀ ⡇⢸ ⢀⡀ ⠄ ⢀⣀ ⢀⡀ ⢉⡹
|
||||
⠣⠔ ⠣⠜ ⠭⠕ ⣑⡺ ⠸⠃ ⠣⠜ ⠇ ⠣⠤ ⠣⠭ ⠤⠜
|
||||
|
||||
"""
|
||||
print(f"\033[35m{ascii_art}\033[0m")
|
||||
print("XB CosyVoice3 Custom Nodes Loaded - Version 1.2.1")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+221
-176
@@ -2,24 +2,31 @@
|
||||
* XB 引用任意 (Reference Any)
|
||||
* =============================
|
||||
* 融合 rgthree Fast Muter + Any Switch:
|
||||
* - 动态输入槽,连接后自动扩展
|
||||
* - 输入端口显示上游节点名称 (input.name)
|
||||
* - 下拉菜单选择活跃的上游节点,选项 = 上游节点名
|
||||
* - Python 端 "选择" 下拉框 defaultInput: True → 可直接接受上游 STRING 控制
|
||||
* - 选中 → mode=0(活跃),其余 → mode=2(静音),即时生效无延迟
|
||||
* - 动态输入槽:接线自动扩展,端口名显示上游节点名
|
||||
* - 面板上一个下拉框「选择」:选中哪个上游节点就只激活它,其余静音;
|
||||
* 选「全部关闭」= 上游全部静音,本节点输出空
|
||||
* - 「选择」端口还能接 STRING 上游来外部驱动选择(Python 端 defaultInput)
|
||||
*
|
||||
* 参照 rgthree-comfy 的 BaseNodeModeChanger + BaseAnyInputConnectedNode 模式。
|
||||
* ⚠️ 为什么下拉不用 LiteGraph 的 combo widget:
|
||||
* Nodes 2.0(Vue)在节点创建时就把 widget.options 快照进 store,之后 JS 改
|
||||
* options.values 不会同步过去,下拉里会一直显示「未找到结果」,并且一点就把
|
||||
* 值回退成第一项「全部关闭」。所以改为 DOM widget + 原生 <select>,
|
||||
* 两种渲染器下都可靠,也不受上游全静音影响(选项只认连线,不认 mode)。
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { setWidgetHidden } from "./xb_compat.js";
|
||||
|
||||
const NODE_TYPE = "XB_ReferenceAny";
|
||||
const COMBO_WIDGET_NAME = "选择";
|
||||
const WNAME = "选择";
|
||||
const ALL_OFF = "全部关闭";
|
||||
const MODE_ACTIVE = 0;
|
||||
const MODE_MUTE = 2;
|
||||
const PANEL_H = 32;
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
/** 顺着连线(含 Reroute)找真正的上游节点 */
|
||||
function getUpstreamNode(self, inputIndex) {
|
||||
const graph = self.graph;
|
||||
if (!graph) return null;
|
||||
@@ -44,29 +51,38 @@ function getUpstreamNode(self, inputIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Check if an input is the control input (not a dynamic data input). */
|
||||
function isControlInput(inp) {
|
||||
return inp.name === COMBO_WIDGET_NAME;
|
||||
/** 「选择」是控制端口(可以是隐藏 widget 自己占的槽位) */
|
||||
function isValuePort(self, inp) {
|
||||
if (!inp) return true;
|
||||
if (inp.widget) return inp.name === WNAME;
|
||||
return inp.name === WNAME;
|
||||
}
|
||||
|
||||
/** Collect all connected upstream node info (skips control input). */
|
||||
/** 所有已连线的上游节点:[{ index, node, title }] */
|
||||
function getLinkedNodes(self) {
|
||||
const list = [];
|
||||
for (let i = 0; i < (self.inputs?.length || 0); i++) {
|
||||
const inp = self.inputs[i];
|
||||
if (!inp.link || isControlInput(inp)) continue;
|
||||
if (!inp || inp.link == null || isValuePort(self, inp)) continue;
|
||||
const up = getUpstreamNode(self, i);
|
||||
if (up) list.push({ index: i, node: up, title: up.title || `输入${i + 1}` });
|
||||
if (up) list.push({ index: i, node: up, title: up.title || up.type || `输入${i + 1}` });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 数据槽(排除「选择」控制槽与 widget 槽) */
|
||||
function dataSlots(self) {
|
||||
return (self.inputs || [])
|
||||
.map((inp, i) => ({ inp, i }))
|
||||
.filter((x) => x.inp && !isValuePort(self, x.inp));
|
||||
}
|
||||
|
||||
/**
|
||||
* 反向嗅探:顺 "选择" 端口连接找到上游节点,读取其第一个 string 类型 widget 的值。
|
||||
* 反向嗅探:顺「选择」端口找到上游节点,读取它第一个 string 类型 widget 的值。
|
||||
* 支持 String/Text 原语节点在编辑期间实时嗅探。
|
||||
*/
|
||||
function sniffControlString(self) {
|
||||
const input = (self.inputs || []).find(inp => inp.name === COMBO_WIDGET_NAME);
|
||||
const input = (self.inputs || []).find((inp) => inp && inp.name === WNAME && inp.link != null);
|
||||
if (!input?.link) return null;
|
||||
const graph = self.graph;
|
||||
if (!graph) return null;
|
||||
@@ -74,13 +90,26 @@ function sniffControlString(self) {
|
||||
if (!link) return null;
|
||||
const src = graph.getNodeById?.(link.origin_id);
|
||||
if (!src) return null;
|
||||
// Read widget values from source (works for String/Text primitives)
|
||||
for (const w of (src.widgets || [])) {
|
||||
if (w.value != null && typeof w.value === 'string' && w.value !== '') return w.value;
|
||||
if (typeof w.value === "string" && w.value !== "") return w.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 用嗅探到的字符串匹配一个选项(包含匹配,取最长命中) */
|
||||
function matchOption(text, values) {
|
||||
if (typeof text !== "string" || !text) return ALL_OFF;
|
||||
const lower = text.toLowerCase();
|
||||
let best = null;
|
||||
for (const v of values) {
|
||||
if (v === ALL_OFF) continue;
|
||||
if (lower.includes(String(v).toLowerCase())) {
|
||||
if (!best || String(v).length > best.length) best = String(v);
|
||||
}
|
||||
}
|
||||
return best || ALL_OFF;
|
||||
}
|
||||
|
||||
// ── extension ──────────────────────────────────────────────────────────
|
||||
|
||||
app.registerExtension({
|
||||
@@ -97,186 +126,195 @@ app.registerExtension({
|
||||
},
|
||||
|
||||
async loadedGraphNode(node) {
|
||||
if (node.type === NODE_TYPE) setTimeout(() => patchInstance(node), 200);
|
||||
if (node?.type === NODE_TYPE) setTimeout(() => patchInstance(node), 200);
|
||||
},
|
||||
});
|
||||
|
||||
// ── instance patching ──────────────────────────────────────────────────
|
||||
|
||||
function patchInstance(self) {
|
||||
if (self.__xb_patched) return;
|
||||
self.__xb_patched = true;
|
||||
if (self.__xbRefReady) return;
|
||||
self.__xbRefReady = true;
|
||||
|
||||
// Replace the Python-created STRING widget with a combo widget (same name, keeps defaultInput port working)
|
||||
const strIdx = (self.widgets || []).findIndex(w => w.name === COMBO_WIDGET_NAME && w.type !== "combo");
|
||||
if (strIdx >= 0) {
|
||||
const oldW = self.widgets[strIdx];
|
||||
const savedValue = oldW.value || "";
|
||||
self.widgets.splice(strIdx, 1);
|
||||
const combo = self.addWidget("combo", COMBO_WIDGET_NAME, savedValue, () => {}, { values: [] });
|
||||
// Move it to the same position
|
||||
const newIdx = self.widgets.indexOf(combo);
|
||||
if (newIdx > strIdx) {
|
||||
self.widgets.splice(newIdx, 1);
|
||||
self.widgets.splice(strIdx, 0, combo);
|
||||
}
|
||||
const valueWidget = () => (self.widgets || []).find((w) => w.name === WNAME);
|
||||
|
||||
// 「选择」的原生 STRING widget 只作为取值 / 端口载体,界面上藏掉,用下面的下拉代替
|
||||
const hideValueWidget = () => {
|
||||
const w = valueWidget();
|
||||
if (!w || w.__xbHidden) return;
|
||||
w.__xbHidden = true;
|
||||
try { setWidgetHidden(self, w, true); } catch (_) { /* ignore */ }
|
||||
};
|
||||
hideValueWidget();
|
||||
|
||||
// 至少留一个空数据槽
|
||||
if (!dataSlots(self).length) self.addInput("", "*");
|
||||
|
||||
// ── 面板:一个原生 <select> ───────────────────────────────────────
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText = "width:100%;height:100%;box-sizing:border-box;display:flex;align-items:center;gap:6px;"
|
||||
+ "padding:0 6px;background:#2b2b2b;border-radius:6px;";
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = WNAME;
|
||||
label.style.cssText = "flex:0 0 auto;color:#c8c8c8;font-size:12px;";
|
||||
|
||||
const sel = document.createElement("select");
|
||||
sel.style.cssText = "flex:1 1 auto;min-width:0;height:24px;background:#1e1e1e;color:#e6e6e6;"
|
||||
+ "border:1px solid #444;border-radius:5px;font-size:12px;font-family:inherit;padding:0 4px;cursor:pointer;";
|
||||
const stop = (e) => { e.stopPropagation(); };
|
||||
sel.addEventListener("pointerdown", stop);
|
||||
sel.addEventListener("mousedown", stop);
|
||||
sel.addEventListener("click", stop);
|
||||
sel.addEventListener("change", () => {
|
||||
const v = sel.value;
|
||||
const w = valueWidget();
|
||||
if (w && w.value !== v) w.value = v;
|
||||
self.__xbAppliedValue = v;
|
||||
self.__xbUserPick = true;
|
||||
self.applySelection?.(v);
|
||||
self.graph?.setDirtyCanvas?.(true, true);
|
||||
});
|
||||
container.append(label, sel);
|
||||
self.__xbSelectEl = sel;
|
||||
|
||||
const domWidget = self.addDOMWidget?.("xb_ref_select", "XB_REF_SELECT", container, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
});
|
||||
if (domWidget) {
|
||||
domWidget.options = domWidget.options || {};
|
||||
domWidget.options.serialize = false;
|
||||
domWidget.options.getMinHeight = () => PANEL_H;
|
||||
domWidget.computeSize = () => [Math.max(160, Math.round(self.size?.[0] || 200)), PANEL_H];
|
||||
domWidget.computeLayoutSize = () => ({ minHeight: PANEL_H, maxHeight: undefined, minWidth: 160 });
|
||||
}
|
||||
try { self.size = [Math.max(200, self.size?.[0] || 200), Math.max(PANEL_H + 40, self.size?.[1] || 70)]; } catch (_) { /* ignore */ }
|
||||
|
||||
// Ensure at least one dynamic input slot exists (in case FlexibleOptionalInputType starts empty)
|
||||
const hasDynamicInput = (self.inputs || []).some(inp => !isControlInput(inp));
|
||||
if (!hasDynamicInput) self.addInput("", "*");
|
||||
|
||||
// ---- stabilizeInputsOutputs ----
|
||||
// ── 端口维护:接线扩展 / 收空槽 / 端口名跟随上游 ──────────────────
|
||||
self.stabilizeInputsOutputs = function () {
|
||||
let ch = false;
|
||||
const inputs = this.inputs;
|
||||
|
||||
// Find indices of dynamic inputs (skip control inputs)
|
||||
const dynIndices = [];
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
if (!isControlInput(inputs[i])) dynIndices.push(i);
|
||||
}
|
||||
|
||||
// If no dynamic inputs at all, add one
|
||||
if (dynIndices.length === 0) {
|
||||
this.addInput("", "*");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Last dynamic input connected → add new empty slot
|
||||
const lastDyn = inputs[dynIndices[dynIndices.length - 1]];
|
||||
if (lastDyn?.link != null) {
|
||||
this.addInput("", "*");
|
||||
dynIndices.push(inputs.length - 1);
|
||||
ch = true;
|
||||
}
|
||||
|
||||
// Remove empty dynamic inputs (keep exactly one empty at the end)
|
||||
for (let di = dynIndices.length - 2; di >= 0; di--) {
|
||||
const idx = dynIndices[di];
|
||||
if (inputs[idx].link == null) {
|
||||
this.removeInput(idx);
|
||||
ch = true;
|
||||
} else {
|
||||
const up = getUpstreamNode(this, idx);
|
||||
const nm = up?.title || "";
|
||||
if (inputs[idx].name !== nm) { inputs[idx].name = nm; ch = true; }
|
||||
}
|
||||
}
|
||||
return ch;
|
||||
};
|
||||
|
||||
// ---- syncComboOptions (only updates dropdown list, no mode changes) ----
|
||||
self.syncComboOptions = function () {
|
||||
const combo = (this.widgets || []).find(w => w.name === COMBO_WIDGET_NAME);
|
||||
if (!combo) return false;
|
||||
|
||||
const linked = getLinkedNodes(this);
|
||||
const newVals = ["全部关闭", ...linked.map(n => n.title)];
|
||||
const curVals = combo.options?.values || [];
|
||||
|
||||
// Compare (ignore first "全部关闭" if it was already there)
|
||||
const cmp = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
|
||||
if (!cmp(curVals, newVals)) {
|
||||
this.__xb_syncing = true;
|
||||
combo.options.values = newVals;
|
||||
// Preserve current selection; never auto-override "全部关闭"
|
||||
if (!newVals.includes(combo.value)) {
|
||||
combo.value = newVals[0];
|
||||
}
|
||||
this.__xb_syncing = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// ---- applySelection: set modes based on combo value, returns true if changed ----
|
||||
self.applySelection = function (value) {
|
||||
if (value === this.__xb_lastSelection) return false;
|
||||
this.__xb_lastSelection = value;
|
||||
const linked = getLinkedNodes(this);
|
||||
let changed = false;
|
||||
if (value === "全部关闭") {
|
||||
// Mute all upstream nodes, output nothing
|
||||
for (const { node } of linked) {
|
||||
if (node && node.mode !== MODE_MUTE) { node.mode = MODE_MUTE; changed = true; }
|
||||
let slots = dataSlots(this);
|
||||
if (!slots.length) {
|
||||
this.addInput("", "*");
|
||||
return true;
|
||||
}
|
||||
// 末尾已接线 → 追加一个空槽
|
||||
const last = slots[slots.length - 1];
|
||||
if (last.inp.link != null) {
|
||||
this.addInput("", "*");
|
||||
changed = true;
|
||||
slots = dataSlots(this);
|
||||
}
|
||||
// 中间的空槽收掉(只留末尾一个空槽)
|
||||
for (let k = slots.length - 2; k >= 0; k--) {
|
||||
const { inp, i } = slots[k];
|
||||
if (inp && inp.link == null) {
|
||||
this.removeInput(i);
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
for (const { node } of linked) {
|
||||
if (!node) continue;
|
||||
const targetMode = (node.title === value) ? MODE_ACTIVE : MODE_MUTE;
|
||||
if (node.mode !== targetMode) { node.mode = targetMode; changed = true; }
|
||||
}
|
||||
// 端口名 = 上游节点名
|
||||
for (const { inp, i } of dataSlots(this)) {
|
||||
if (inp.link == null) {
|
||||
if (inp.name !== "") { inp.name = ""; inp.label = ""; changed = true; }
|
||||
continue;
|
||||
}
|
||||
const up = getUpstreamNode(this, i);
|
||||
const nm = up?.title || up?.type || "";
|
||||
if (inp.name !== nm) { inp.name = nm; changed = true; }
|
||||
if ((inp.label || "") !== nm) { inp.label = nm; changed = true; }
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
// ── 下拉选项同步(只认连线,不看 mode,所以全静音时也照常显示)──────
|
||||
self.syncSelectOptions = function () {
|
||||
const el = this.__xbSelectEl;
|
||||
if (!el) return false;
|
||||
const linked = getLinkedNodes(this);
|
||||
const titles = [];
|
||||
for (const { title } of linked) {
|
||||
if (title && !titles.includes(title)) titles.push(title);
|
||||
}
|
||||
const values = [ALL_OFF, ...titles];
|
||||
const w = valueWidget();
|
||||
let current = String(w?.value ?? el.value ?? ALL_OFF);
|
||||
if (!values.includes(current)) {
|
||||
// 选中的上游确实不在了(被删 / 断开 / 改名)→ 只有当它连 id 都找不到时才回退
|
||||
const stillLinked = linked.some((x) => x.node?.id === this.__xbSelectedId);
|
||||
const keep = stillLinked ? (linked.find((x) => x.node?.id === this.__xbSelectedId)?.title || ALL_OFF) : ALL_OFF;
|
||||
current = keep;
|
||||
if (w && w.value !== current) w.value = current;
|
||||
}
|
||||
const same = el.options.length === values.length
|
||||
&& values.every((v, i) => el.options[i]?.value === v);
|
||||
if (!same) {
|
||||
el.replaceChildren(...values.map((v) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = v;
|
||||
o.textContent = v;
|
||||
return o;
|
||||
}));
|
||||
}
|
||||
if (el.value !== current) el.value = current;
|
||||
return !same;
|
||||
};
|
||||
|
||||
// ── 应用选择:选中的激活,其余静音;「全部关闭」= 全静音 ────────────
|
||||
self.applySelection = function (value) {
|
||||
const linked = getLinkedNodes(this);
|
||||
let targetId = null;
|
||||
if (value && value !== ALL_OFF) {
|
||||
const hit = linked.find((x) => x.title === value);
|
||||
if (hit) targetId = hit.node.id;
|
||||
}
|
||||
this.__xbSelectedId = targetId;
|
||||
let changed = false;
|
||||
for (const { node } of linked) {
|
||||
if (!node) continue;
|
||||
const want = (node.id === targetId) ? MODE_ACTIVE : MODE_MUTE;
|
||||
if (node.mode !== want) { node.mode = want; changed = true; }
|
||||
}
|
||||
if (changed) this.graph?.setDirtyCanvas?.(true, true);
|
||||
return changed;
|
||||
};
|
||||
|
||||
// ---- setupComboCallback (fires instantly on user click) ----
|
||||
function setupComboCallback() {
|
||||
const combo = (self.widgets || []).find(w => w.name === COMBO_WIDGET_NAME);
|
||||
if (!combo || combo.__xb_cb_set) return;
|
||||
combo.__xb_cb_set = true;
|
||||
|
||||
combo.callback = function (value) {
|
||||
if (self.__xb_syncing) return; // skip if programmatic update
|
||||
self.applySelection(value);
|
||||
};
|
||||
}
|
||||
|
||||
// ---- doStablization (rgthree pattern) ----
|
||||
// ── 周期稳定 ──────────────────────────────────────────────────────
|
||||
self.doStablization = function () {
|
||||
if (!this.graph || this.__xb_removed) return;
|
||||
if (!this.graph || this.__xbRefRemoved) return;
|
||||
let dirty = false;
|
||||
try {
|
||||
dirty = this.stabilizeInputsOutputs() || dirty;
|
||||
dirty = this.syncComboOptions() || dirty;
|
||||
dirty = this.syncSelectOptions() || dirty;
|
||||
|
||||
const combo = (this.widgets || []).find(w => w.name === COMBO_WIDGET_NAME);
|
||||
if (!combo) return;
|
||||
const el = this.__xbSelectEl;
|
||||
const w = valueWidget();
|
||||
const ctrl = (this.inputs || []).find((inp) => inp && inp.name === WNAME);
|
||||
|
||||
// 检查 "选择" 端口是否被接入
|
||||
const selectInput = (this.inputs || []).find(inp => inp.name === COMBO_WIDGET_NAME);
|
||||
const isConnected = selectInput?.link != null;
|
||||
|
||||
if (isConnected) {
|
||||
// 有上游接入 → 反向嗅探,匹配则激活,不匹配则"全部关闭"
|
||||
if (ctrl?.link != null) {
|
||||
// 外部 STRING 驱动选择:嗅探值 → 匹配选项
|
||||
const sniffed = sniffControlString(this);
|
||||
if (sniffed !== this.__xb_lastSniffed) {
|
||||
this.__xb_lastSniffed = sniffed;
|
||||
if (sniffed != null) {
|
||||
const options = combo.options?.values || [];
|
||||
let bestMatch = null;
|
||||
const lowerInput = sniffed.toLowerCase();
|
||||
for (const opt of options) {
|
||||
if (lowerInput.includes(opt.toLowerCase())) {
|
||||
if (!bestMatch || opt.length > bestMatch.length) {
|
||||
bestMatch = opt;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 匹配成功 → 激活对应选项;匹配失败 → 回退到"全部关闭"
|
||||
const target = bestMatch || "全部关闭";
|
||||
this.__xb_syncing = true;
|
||||
combo.value = target;
|
||||
this.__xb_syncing = false;
|
||||
dirty = this.applySelection(target) || dirty;
|
||||
} else {
|
||||
// 嗅探不到值 → 默认"全部关闭"
|
||||
dirty = this.applySelection("全部关闭") || dirty;
|
||||
}
|
||||
if (sniffed !== this.__xbLastSniffed) {
|
||||
this.__xbLastSniffed = sniffed;
|
||||
const target = sniffed == null ? ALL_OFF : matchOption(sniffed, (el ? Array.from(el.options).map((o) => o.value) : []));
|
||||
if (w && w.value !== target) w.value = target;
|
||||
if (el && el.value !== target) el.value = target;
|
||||
this.__xbAppliedValue = target;
|
||||
dirty = this.applySelection(target) || dirty;
|
||||
}
|
||||
} else {
|
||||
// 无上游接入 → 用下拉菜单手动选择的值
|
||||
if (combo.value !== this.__xb_lastSelection) {
|
||||
dirty = this.applySelection(combo.value) || dirty;
|
||||
this.__xbLastSniffed = null;
|
||||
// 手动下拉:值变了才应用(避免每轮都改上游 mode)
|
||||
const cur = String(el?.value ?? w?.value ?? ALL_OFF);
|
||||
if (cur !== this.__xbAppliedValue) {
|
||||
this.__xbAppliedValue = cur;
|
||||
dirty = this.applySelection(cur) || dirty;
|
||||
}
|
||||
// 清除嗅探缓存
|
||||
this.__xb_lastSniffed = null;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (_) { /* ignore */ }
|
||||
if (dirty) this.graph.setDirtyCanvas(true, true);
|
||||
if (!this.__xb_removed) {
|
||||
if (!this.__xbRefRemoved) {
|
||||
this._xbTimer = setTimeout(() => {
|
||||
this._xbTimer = null;
|
||||
this.doStablization?.();
|
||||
@@ -284,7 +322,6 @@ function patchInstance(self) {
|
||||
}
|
||||
};
|
||||
|
||||
// ---- scheduleStabilizeWidgets ----
|
||||
self.scheduleStabilizeWidgets = function (ms = 80) {
|
||||
if (this._xbScheduled) return;
|
||||
this._xbScheduled = true;
|
||||
@@ -296,26 +333,34 @@ function patchInstance(self) {
|
||||
}, ms);
|
||||
};
|
||||
|
||||
// ---- hooks ----
|
||||
// ── hooks ─────────────────────────────────────────────────────────
|
||||
const origConn = self.onConnectionsChange;
|
||||
self.onConnectionsChange = function () {
|
||||
if (origConn) origConn.apply(this, arguments);
|
||||
this.scheduleStabilizeWidgets?.(64);
|
||||
};
|
||||
|
||||
const origConfigure = self.onConfigure;
|
||||
self.onConfigure = function () {
|
||||
const r = origConfigure?.apply(this, arguments);
|
||||
hideValueWidget();
|
||||
this.scheduleStabilizeWidgets?.(64);
|
||||
return r;
|
||||
};
|
||||
|
||||
const origRem = self.onRemoved;
|
||||
self.onRemoved = function () {
|
||||
this.__xb_removed = true;
|
||||
this.__xbRefRemoved = true;
|
||||
if (this._xbTimer) { clearTimeout(this._xbTimer); this._xbTimer = null; }
|
||||
if (origRem) origRem.apply(this, arguments);
|
||||
};
|
||||
|
||||
// ---- kick off ----
|
||||
// ── 启动 ──────────────────────────────────────────────────────────
|
||||
setTimeout(() => {
|
||||
setupComboCallback();
|
||||
// Apply initial selection (defaults to "全部关闭" → all muted)
|
||||
const combo = (self.widgets || []).find(w => w.name === COMBO_WIDGET_NAME);
|
||||
if (combo) self.applySelection(combo.value || "全部关闭");
|
||||
const w = valueWidget();
|
||||
const cur = String(w?.value || ALL_OFF);
|
||||
if (self.__xbSelectEl) self.__xbSelectEl.value = cur;
|
||||
self.__xbAppliedValue = cur;
|
||||
self.doStablization?.();
|
||||
}, 300);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
+208
-136
@@ -7,8 +7,116 @@ import { setWidgetValue } from "./xb_compat.js";
|
||||
|
||||
const isZH = navigator.language.startsWith("zh");
|
||||
|
||||
// 「时长估算」在 Nodes 2.0 里是 PrimeVue 文本框(padding 8px 16px → 31px 高),
|
||||
// 比旁边的数字框高一截;用一段 CSS 压成同样高度、同样的透明背景。
|
||||
const PARAMS_CSS = `
|
||||
div[node-type="XB_VideoParamsMaster"] input.p-inputtext[readonly] {
|
||||
height: 24px !important;
|
||||
min-height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
padding: 0 8px !important;
|
||||
background: var(--color-component-node-widget-background, rgb(49, 50, 53)) !important;
|
||||
border: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: none !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 24px !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const injectParamsCss = () => {
|
||||
try {
|
||||
if (document.getElementById("xb-params-css")) return;
|
||||
const st = document.createElement("style");
|
||||
st.id = "xb-params-css";
|
||||
st.textContent = PARAMS_CSS;
|
||||
document.head.append(st);
|
||||
} catch (_) { /* ignore */ }
|
||||
};
|
||||
injectParamsCss();
|
||||
|
||||
// 共用「画幅比例 + 步长」联动的图像参数节点
|
||||
const IMAGE_PARAMS_NODES = new Set(["XB_ImageParamsMaster", "XB_ImageParamsMasterMini"]);
|
||||
const VIDEO_PARAMS_NODE = "XB_VideoParamsMaster";
|
||||
|
||||
// 画幅比例:自定义 + 官方 Resolution Selector 的比例(旧值自动迁移)
|
||||
const RATIO_ALIAS = {
|
||||
"Free": "自定义",
|
||||
"1:1": "1:1 (Square)",
|
||||
"2:3": "2:3 (Portrait Photo)",
|
||||
"3:2": "3:2 (Photo)",
|
||||
"3:4": "3:4 (Portrait Standard)",
|
||||
"4:3": "4:3 (Standard)",
|
||||
"9:16": "9:16 (Portrait Widescreen)",
|
||||
"16:9": "16:9 (Widescreen)",
|
||||
"21:9": "21:9 (Ultrawide)",
|
||||
};
|
||||
|
||||
const isCustomRatio = (name) => {
|
||||
const v = String(name ?? "");
|
||||
return !v || v.includes("自定义") || v.includes("Free");
|
||||
};
|
||||
|
||||
/** "16:9 (Widescreen)" / "16:9 (LTX)" / "16:9" → 16/9 */
|
||||
const ratioOf = (name) => {
|
||||
if (isCustomRatio(name)) return null;
|
||||
const m = /^(\d+):(\d+)/.exec(String(name));
|
||||
if (!m) return null;
|
||||
const w = Number(m[1]);
|
||||
const h = Number(m[2]);
|
||||
return h ? w / h : null;
|
||||
};
|
||||
|
||||
/** 旧存档里的 "Free" / "16:9" / "16:9 (LTX)" 换成新选项名(值不在选项里就修) */
|
||||
const migrateRatio = (node, wRatio) => {
|
||||
if (!wRatio) return false;
|
||||
const opts = wRatio.options?.values || [];
|
||||
const val = String(wRatio.value ?? "");
|
||||
if (!opts.length || opts.includes(val)) return false;
|
||||
const fixed = RATIO_ALIAS[val] || (val.includes("(LTX)") ? RATIO_ALIAS[val.split(" ")[0]] : null);
|
||||
if (!fixed) return false;
|
||||
wRatio.value = fixed;
|
||||
xb_dispatch(wRatio, fixed);
|
||||
return true;
|
||||
};
|
||||
|
||||
/** 「倍数」当前值(缺省 16) */
|
||||
const multipleOf = (node) => {
|
||||
const w = node.widgets?.find((x) => x.name === "multiple");
|
||||
const v = Math.round(Number(w?.value));
|
||||
return Number.isFinite(v) && v >= 8 ? v : 16;
|
||||
};
|
||||
|
||||
/** 把宽高按比例 + 倍数(步长)规整,保证两边都能整除 */
|
||||
const fitSize = (ratioName, w, h, step) => {
|
||||
const snap = (v) => Math.max(step, Math.round((Number(v) || step) / step) * step);
|
||||
let wid = snap(w);
|
||||
let hei = snap(h);
|
||||
const r = ratioOf(ratioName);
|
||||
if (r) {
|
||||
if (wid >= hei) hei = snap(wid / r);
|
||||
else wid = snap(hei * r);
|
||||
}
|
||||
return { wid: Math.max(step, wid), hei: Math.max(step, hei) };
|
||||
};
|
||||
|
||||
/** 步长控件(数字框的上下箭头步长);min 也设成步长,
|
||||
* 否则 Nodes 2.0 的数字框会按 min + k*step 取整,结果可能不是倍数的整数倍。 */
|
||||
const applyStep = (w, step) => {
|
||||
if (!w) return;
|
||||
w.options = w.options || {};
|
||||
w.options.step = step;
|
||||
w.options.step2 = step;
|
||||
w.options.min = step;
|
||||
const el = w.inputEl || w.element;
|
||||
if (el && typeof el.setAttribute === "function") {
|
||||
el.step = step;
|
||||
el.min = step;
|
||||
el.setAttribute("step", String(step));
|
||||
el.setAttribute("min", String(step));
|
||||
}
|
||||
};
|
||||
|
||||
// 旧版写值函数(仅输入框/回调,Nodes 2.0 下会漏写 widget.value)
|
||||
const __xbLegacyDispatch = (w, val) => {
|
||||
@@ -29,75 +137,87 @@ const xb_dispatch = (w, val) => setWidgetValue(w, val);
|
||||
app.registerExtension({
|
||||
name: "xiaobai.mediaparams.split",
|
||||
|
||||
// —— nodeCreated:子工作流画幅联动(步长由 Python step=16 负责)——
|
||||
// —— nodeCreated:画幅比例 + 倍数(步长)联动 ——
|
||||
async nodeCreated(node) {
|
||||
if (IMAGE_PARAMS_NODES.has(node.comfyClass)) {
|
||||
const wR = node.widgets?.find(w => w.name === "aspect_ratio");
|
||||
const wW = node.widgets?.find(w => w.name === "width");
|
||||
const wH = node.widgets?.find(w => w.name === "height");
|
||||
const wM = node.widgets?.find(w => w.name === "multiple");
|
||||
if (!wR || !wW || !wH) return;
|
||||
node._xb_syncing = false;
|
||||
const rm = {"1:1":1,"16:9":16/9,"9:16":9/16,"4:3":4/3,"3:4":3/4,"21:9":21/9,"2:3":2/3,"3:2":3/2};
|
||||
migrateRatio(node, wR);
|
||||
applyStep(wW, multipleOf(node));
|
||||
applyStep(wH, multipleOf(node));
|
||||
|
||||
const sW = () => { if(node._xb_syncing||node._xb_from_polling) return;
|
||||
const r=wR.value; if(r.includes("Free")) return; const cr=rm[r]; if(!cr) return;
|
||||
let w=Math.round((parseInt(wW.value,10)||16)/16)*16; w=Math.max(16,w);
|
||||
let h=Math.round((w/cr)/16)*16; h=Math.max(16,h);
|
||||
if(h!==parseInt(wH.value,10)){node._xb_syncing=true;wH.value=h;xb_dispatch(wH,h);node._xb_syncing=false;} };
|
||||
const sH = () => { if(node._xb_syncing||node._xb_from_polling) return;
|
||||
const r=wR.value; if(r.includes("Free")) return; const cr=rm[r]; if(!cr) return;
|
||||
let h=Math.round((parseInt(wH.value,10)||16)/16)*16; h=Math.max(16,h);
|
||||
let w=Math.round((h*cr)/16)*16; w=Math.max(16,w);
|
||||
if(w!==parseInt(wW.value,10)){node._xb_syncing=true;wW.value=w;xb_dispatch(wW,w);node._xb_syncing=false;} };
|
||||
const sync = () => {
|
||||
if (node._xb_syncing || node._xb_from_polling) return;
|
||||
const step = multipleOf(node);
|
||||
const { wid, hei } = fitSize(wR.value, wW.value, wH.value, step);
|
||||
node._xb_syncing = true;
|
||||
if (parseInt(wW.value, 10) !== wid) { wW.value = wid; xb_dispatch(wW, wid); }
|
||||
if (parseInt(wH.value, 10) !== hei) { wH.value = hei; xb_dispatch(wH, hei); }
|
||||
node._xb_syncing = false;
|
||||
};
|
||||
|
||||
const oc=wR.callback; wR.callback=function(v){if(oc)oc.apply(this,arguments);if(node._xb_from_polling)return;sW();};
|
||||
const ow=wW.callback; wW.callback=function(v){if(ow)ow.apply(this,arguments);sW();};
|
||||
const oh=wH.callback; wH.callback=function(v){if(oh)oh.apply(this,arguments);sH();};
|
||||
const oc = wR.callback; wR.callback = function (v) { if (oc) oc.apply(this, arguments); if (node._xb_from_polling) return; sync(); };
|
||||
const ow = wW.callback; wW.callback = function (v) { if (ow) ow.apply(this, arguments); sync(); };
|
||||
const oh = wH.callback; wH.callback = function (v) { if (oh) oh.apply(this, arguments); sync(); };
|
||||
const om = wM?.callback; if (wM) wM.callback = function (v) {
|
||||
if (om) om.apply(this, arguments);
|
||||
const step = multipleOf(node);
|
||||
applyStep(wW, step); applyStep(wH, step);
|
||||
sync();
|
||||
};
|
||||
}
|
||||
|
||||
if (node.comfyClass === "XB_VideoParamsMaster") {
|
||||
if (node.comfyClass === VIDEO_PARAMS_NODE) {
|
||||
const wR = node.widgets?.find(w => w.name === "aspect_ratio");
|
||||
const wW = node.widgets?.find(w => w.name === "width");
|
||||
const wH = node.widgets?.find(w => w.name === "height");
|
||||
const wF = node.widgets?.find(w => w.name === "fps");
|
||||
const wFF = node.widgets?.find(w => w.name === "fps_float");
|
||||
const wL = node.widgets?.find(w => w.name === "length");
|
||||
const wM = node.widgets?.find(w => w.name === "multiple");
|
||||
if (!wR || !wW || !wH) return;
|
||||
node._xb_syncing = false;
|
||||
const rS={"1:1":1,"16:9":16/9,"9:16":9/16,"4:3":4/3,"3:4":3/4,"21:9":21/9,"2:3":2/3,"3:2":3/2};
|
||||
const rL={"16:9 (LTX)":16/9,"9:16 (LTX)":9/16,"4:3 (LTX)":4/3,"3:4 (LTX)":3/4};
|
||||
migrateRatio(node, wR);
|
||||
applyStep(wW, multipleOf(node));
|
||||
applyStep(wH, multipleOf(node));
|
||||
|
||||
const sW = () => { if(node._xb_syncing||node._xb_from_polling) return;
|
||||
const r=wR.value; if(r.includes("Free")) return;
|
||||
const isL=r.includes("(LTX)"); const rm=isL?rL:rS; const cr=rm[r]; if(!cr) return;
|
||||
const s=isL?32:16;
|
||||
let w=Math.round((parseInt(wW.value,10)||s)/s)*s; w=Math.max(s,w);
|
||||
let h=Math.round((w/cr)/s)*s; h=Math.max(s,h);
|
||||
if(h!==parseInt(wH.value,10)){node._xb_syncing=true;wH.value=h;xb_dispatch(wH,h);node._xb_syncing=false;} };
|
||||
const sH = () => { if(node._xb_syncing||node._xb_from_polling) return;
|
||||
const r=wR.value; if(r.includes("Free")) return;
|
||||
const isL=r.includes("(LTX)"); const rm=isL?rL:rS; const cr=rm[r]; if(!cr) return;
|
||||
const s=isL?32:16;
|
||||
let h=Math.round((parseInt(wH.value,10)||s)/s)*s; h=Math.max(s,h);
|
||||
let w=Math.round((h*cr)/s)*s; w=Math.max(s,w);
|
||||
if(w!==parseInt(wW.value,10)){node._xb_syncing=true;wW.value=w;xb_dispatch(wW,w);node._xb_syncing=false;} };
|
||||
const sync = (driveBy) => {
|
||||
if (node._xb_syncing || node._xb_from_polling) return;
|
||||
const step = multipleOf(node);
|
||||
const { wid, hei } = fitSize(wR.value, wW.value, wH.value, step);
|
||||
node._xb_syncing = true;
|
||||
if (parseInt(wW.value, 10) !== wid) { wW.value = wid; xb_dispatch(wW, wid); }
|
||||
if (parseInt(wH.value, 10) !== hei) { wH.value = hei; xb_dispatch(wH, hei); }
|
||||
node._xb_syncing = false;
|
||||
};
|
||||
|
||||
const oc=wR.callback; wR.callback=function(v){if(oc)oc.apply(this,arguments);if(node._xb_from_polling)return;sW();};
|
||||
const ow=wW.callback; wW.callback=function(v){if(ow)ow.apply(this,arguments);sW();};
|
||||
const oh=wH.callback; wH.callback=function(v){if(oh)oh.apply(this,arguments);sH();};
|
||||
const oc = wR.callback; wR.callback = function (v) { if (oc) oc.apply(this, arguments); if (node._xb_from_polling) return; sync("w"); };
|
||||
const ow = wW.callback; wW.callback = function (v) { if (ow) ow.apply(this, arguments); sync("w"); };
|
||||
const oh = wH.callback; wH.callback = function (v) { if (oh) oh.apply(this, arguments); sync("h"); };
|
||||
const om = wM?.callback; if (wM) wM.callback = function (v) {
|
||||
if (om) om.apply(this, arguments);
|
||||
const step = multipleOf(node);
|
||||
applyStep(wW, step); applyStep(wH, step);
|
||||
sync("w");
|
||||
};
|
||||
|
||||
if(wF&&wFF){const of=wF.callback;wF.callback=function(v){if(of)of.apply(this,arguments);if(node._xb_syncing||node._xb_from_polling)return;
|
||||
const val=Math.round(Number(v));if(wFF.value!==val){node._xb_syncing=true;wFF.value=val;xb_dispatch(wFF,val);node._xb_syncing=false;}};
|
||||
const off=wFF.callback;wFF.callback=function(v){if(off)off.apply(this,arguments);if(node._xb_syncing||node._xb_from_polling)return;
|
||||
const val=Math.round(Number(v));if(wF.value!==val){node._xb_syncing=true;wF.value=val;xb_dispatch(wF,val);node._xb_syncing=false;}};}
|
||||
if(wL){node._xb_last_len=parseInt(wL.value,10)||1;const ol=wL.callback;wL.callback=function(v){if(ol)ol.apply(this,arguments);if(node._xb_syncing||node._xb_from_polling)return;
|
||||
const isF=(wR.value||"").includes("Free");const as=isF?1:8;
|
||||
const isF=isCustomRatio(wR.value);const as=isF?1:8;
|
||||
let len=parseInt(v,10)||1;if(!isF&&(len-1)%as!==0){len=len>node._xb_last_len?Math.ceil((len-1)/as)*as+1:Math.floor((len-1)/as)*as+1;len=Math.max(1,len);
|
||||
if(wL.value!==len){node._xb_syncing=true;wL.value=len;xb_dispatch(wL,len);node._xb_syncing=false;}}node._xb_last_len=len;};}
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
injectParamsCss();
|
||||
setInterval(() => {
|
||||
if (!app.graph || !app.graph._nodes) return;
|
||||
|
||||
@@ -133,87 +253,49 @@ app.registerExtension({
|
||||
node._xb_last_height = parseInt(wHeight.value, 10) || 1024;
|
||||
node._xb_last_ratio = wRatio.value;
|
||||
node._xb_last_dur = parseFloat(wDisp.value) || 0;
|
||||
node._xb_need_normalize = true; // 载入后先规整一次
|
||||
}
|
||||
if (migrateRatio(node, wRatio)) node._xb_need_normalize = true;
|
||||
|
||||
let needsUpdate = false;
|
||||
let valRatio = wRatio.value;
|
||||
let isFree = valRatio.includes("Free");
|
||||
let isFree = isCustomRatio(valRatio);
|
||||
let rChanged = valRatio !== node._xb_last_ratio;
|
||||
let isLTX = valRatio.includes("(LTX)");
|
||||
|
||||
let currentStep = isFree ? 1 : (isLTX ? 32 : 16);
|
||||
wWidth.options.step = wWidth.options.step2 = currentStep; wHeight.options.step = wHeight.options.step2 = currentStep;
|
||||
if (wWidth.inputEl) { wWidth.inputEl.step = currentStep; wWidth.inputEl.setAttribute("step", String(currentStep)); }
|
||||
else if (wWidth.element) { wWidth.element.step = currentStep; wWidth.element.setAttribute("step", String(currentStep)); }
|
||||
if (wHeight.inputEl) { wHeight.inputEl.step = currentStep; wHeight.inputEl.setAttribute("step", String(currentStep)); }
|
||||
else if (wHeight.element) { wHeight.element.step = currentStep; wHeight.element.setAttribute("step", String(currentStep)); }
|
||||
// 步长 = 倍数(宽高必须能被它整除)
|
||||
let currentStep = multipleOf(node);
|
||||
applyStep(wWidth, currentStep);
|
||||
applyStep(wHeight, currentStep);
|
||||
|
||||
let wid = parseInt(wWidth.value, 10) || currentStep;
|
||||
let hei = parseInt(wHeight.value, 10) || currentStep;
|
||||
let wChanged = wid !== node._xb_last_width;
|
||||
let hChanged = hei !== node._xb_last_height;
|
||||
|
||||
if (wChanged || hChanged || rChanged) {
|
||||
if (!isFree && !isLTX) {
|
||||
const ratioMap = {
|
||||
"1:1": 1.0, "16:9": 16.0 / 9.0, "9:16": 9.0 / 16.0,
|
||||
"4:3": 4.0 / 3.0, "3:4": 3.0 / 4.0, "21:9": 21.0 / 9.0,
|
||||
"2:3": 2.0 / 3.0, "3:2": 3.0 / 2.0
|
||||
};
|
||||
let currentRatio = ratioMap[valRatio];
|
||||
|
||||
if (wChanged && wid % currentStep !== 0) {
|
||||
wid = wid > node._xb_last_width ? Math.ceil(wid / currentStep) * currentStep : Math.floor(wid / currentStep) * currentStep;
|
||||
} else { wid = Math.round(wid / currentStep) * currentStep; }
|
||||
wid = Math.max(currentStep, wid);
|
||||
|
||||
if (hChanged && hei % currentStep !== 0) {
|
||||
hei = hei > node._xb_last_height ? Math.ceil(hei / currentStep) * currentStep : Math.floor(hei / currentStep) * currentStep;
|
||||
} else { hei = Math.round(hei / currentStep) * currentStep; }
|
||||
hei = Math.max(currentStep, hei);
|
||||
|
||||
if (currentRatio) {
|
||||
if (rChanged || wChanged) {
|
||||
hei = Math.round((wid / currentRatio) / currentStep) * currentStep;
|
||||
hei = Math.max(currentStep, hei);
|
||||
} else if (hChanged) {
|
||||
wid = Math.round((hei * currentRatio) / currentStep) * currentStep;
|
||||
wid = Math.max(currentStep, wid);
|
||||
}
|
||||
}
|
||||
} else if (isLTX) {
|
||||
const ratioMap = {
|
||||
"16:9 (LTX)": 16.0 / 9.0, "9:16 (LTX)": 9.0 / 16.0,
|
||||
"4:3 (LTX)": 4.0 / 3.0, "3:4 (LTX)": 3.0 / 4.0
|
||||
};
|
||||
let currentRatio = ratioMap[valRatio];
|
||||
|
||||
if (wChanged && wid % currentStep !== 0) {
|
||||
wid = wid > node._xb_last_width ? Math.ceil(wid / currentStep) * currentStep : Math.floor(wid / currentStep) * currentStep;
|
||||
} else { wid = Math.round(wid / currentStep) * currentStep; }
|
||||
wid = Math.max(currentStep, wid);
|
||||
|
||||
if (hChanged && hei % currentStep !== 0) {
|
||||
hei = hei > node._xb_last_height ? Math.ceil(hei / currentStep) * currentStep : Math.floor(hei / currentStep) * currentStep;
|
||||
} else { hei = Math.round(hei / currentStep) * currentStep; }
|
||||
hei = Math.max(currentStep, hei);
|
||||
|
||||
if (currentRatio) {
|
||||
if (rChanged || wChanged) {
|
||||
hei = Math.round((wid / currentRatio) / currentStep) * currentStep;
|
||||
hei = Math.max(currentStep, hei);
|
||||
} else if (hChanged) {
|
||||
wid = Math.round((hei * currentRatio) / currentStep) * currentStep;
|
||||
wid = Math.max(currentStep, wid);
|
||||
}
|
||||
if (wChanged || hChanged || rChanged || node._xb_need_normalize) {
|
||||
node._xb_need_normalize = false;
|
||||
const snap = (v) => Math.max(currentStep, Math.round(v / currentStep) * currentStep);
|
||||
let nw = snap(wid);
|
||||
let nh = snap(hei);
|
||||
const currentRatio = ratioOf(valRatio);
|
||||
if (currentRatio) {
|
||||
const ratioOnly = rChanged && !wChanged && !hChanged;
|
||||
if (ratioOnly) {
|
||||
// ????????????????? Python ????
|
||||
if (nw >= nh) nh = snap(nw / currentRatio);
|
||||
else nw = snap(nh * currentRatio);
|
||||
} else if (wChanged || !hChanged) {
|
||||
nh = snap(nw / currentRatio);
|
||||
} else {
|
||||
nw = snap(nh * currentRatio);
|
||||
}
|
||||
}
|
||||
|
||||
wWidth.value = wid; wHeight.value = hei;
|
||||
node._xb_last_width = wid; node._xb_last_height = hei;
|
||||
wWidth.value = nw; wHeight.value = nh;
|
||||
node._xb_last_width = nw; node._xb_last_height = nh;
|
||||
node._xb_last_ratio = valRatio;
|
||||
xb_dispatch(wWidth, wid);
|
||||
xb_dispatch(wHeight, hei);
|
||||
xb_dispatch(wWidth, nw);
|
||||
xb_dispatch(wHeight, nh);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
@@ -279,58 +361,48 @@ app.registerExtension({
|
||||
node._xb_last_width = parseInt(wWidth.value, 10) || 1024;
|
||||
node._xb_last_height = parseInt(wHeight.value, 10) || 1024;
|
||||
node._xb_last_ratio = wRatio.value;
|
||||
node._xb_need_normalize = true; // ????????
|
||||
}
|
||||
if (migrateRatio(node, wRatio)) node._xb_need_normalize = true;
|
||||
|
||||
let needsUpdate = false;
|
||||
let valRatio = wRatio.value;
|
||||
let isFree = valRatio.includes("Free");
|
||||
let rChanged = valRatio !== node._xb_last_ratio;
|
||||
|
||||
let currentStep = isFree ? 1 : 16;
|
||||
wWidth.options.step = wWidth.options.step2 = currentStep; wHeight.options.step = wHeight.options.step2 = currentStep;
|
||||
if (wWidth.inputEl) { wWidth.inputEl.step = currentStep; wWidth.inputEl.setAttribute("step", String(currentStep)); }
|
||||
else if (wWidth.element) { wWidth.element.step = currentStep; wWidth.element.setAttribute("step", String(currentStep)); }
|
||||
if (wHeight.inputEl) { wHeight.inputEl.step = currentStep; wHeight.inputEl.setAttribute("step", String(currentStep)); }
|
||||
else if (wHeight.element) { wHeight.element.step = currentStep; wHeight.element.setAttribute("step", String(currentStep)); }
|
||||
// 步长 = 倍数(宽高必须能被它整除)
|
||||
let currentStep = multipleOf(node);
|
||||
applyStep(wWidth, currentStep);
|
||||
applyStep(wHeight, currentStep);
|
||||
|
||||
let wid = parseInt(wWidth.value, 10) || currentStep;
|
||||
let hei = parseInt(wHeight.value, 10) || currentStep;
|
||||
let wChanged = wid !== node._xb_last_width;
|
||||
let hChanged = hei !== node._xb_last_height;
|
||||
|
||||
if (wChanged || hChanged || rChanged) {
|
||||
const ratioMap = {
|
||||
"1:1": 1.0, "16:9": 16.0 / 9.0, "9:16": 9.0 / 16.0,
|
||||
"4:3": 4.0 / 3.0, "3:4": 3.0 / 4.0, "21:9": 21.0 / 9.0,
|
||||
"2:3": 2.0 / 3.0, "3:2": 3.0 / 2.0
|
||||
};
|
||||
let currentRatio = ratioMap[valRatio];
|
||||
|
||||
if (wChanged && wid % currentStep !== 0) {
|
||||
wid = wid > node._xb_last_width ? Math.ceil(wid / currentStep) * currentStep : Math.floor(wid / currentStep) * currentStep;
|
||||
} else { wid = Math.round(wid / currentStep) * currentStep; }
|
||||
wid = Math.max(currentStep, wid);
|
||||
|
||||
if (hChanged && hei % currentStep !== 0) {
|
||||
hei = hei > node._xb_last_height ? Math.ceil(hei / currentStep) * currentStep : Math.floor(hei / currentStep) * currentStep;
|
||||
} else { hei = Math.round(hei / currentStep) * currentStep; }
|
||||
hei = Math.max(currentStep, hei);
|
||||
|
||||
if (wChanged || hChanged || rChanged || node._xb_need_normalize) {
|
||||
node._xb_need_normalize = false;
|
||||
const snap = (v) => Math.max(currentStep, Math.round(v / currentStep) * currentStep);
|
||||
let nw = snap(wid);
|
||||
let nh = snap(hei);
|
||||
const currentRatio = ratioOf(valRatio);
|
||||
if (currentRatio) {
|
||||
if (rChanged || wChanged) {
|
||||
hei = Math.round((wid / currentRatio) / currentStep) * currentStep;
|
||||
hei = Math.max(currentStep, hei);
|
||||
} else if (hChanged) {
|
||||
wid = Math.round((hei * currentRatio) / currentStep) * currentStep;
|
||||
wid = Math.max(currentStep, wid);
|
||||
const ratioOnly = rChanged && !wChanged && !hChanged;
|
||||
if (ratioOnly) {
|
||||
// ????????????????? Python ????
|
||||
if (nw >= nh) nh = snap(nw / currentRatio);
|
||||
else nw = snap(nh * currentRatio);
|
||||
} else if (wChanged || !hChanged) {
|
||||
nh = snap(nw / currentRatio);
|
||||
} else {
|
||||
nw = snap(nh * currentRatio);
|
||||
}
|
||||
}
|
||||
|
||||
wWidth.value = wid; wHeight.value = hei;
|
||||
node._xb_last_width = wid; node._xb_last_height = hei;
|
||||
wWidth.value = nw; wHeight.value = nh;
|
||||
node._xb_last_width = nw; node._xb_last_height = nh;
|
||||
node._xb_last_ratio = valRatio;
|
||||
xb_dispatch(wWidth, wid);
|
||||
xb_dispatch(wHeight, hei);
|
||||
xb_dispatch(wWidth, nw);
|
||||
xb_dispatch(wHeight, nh);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
+61
-13
@@ -191,21 +191,47 @@
|
||||
"XB_LoadVideo": {
|
||||
"display_name": "XB-BOX - 🎬 Load Video",
|
||||
"inputs": {
|
||||
"file": { "name": "Video File" },
|
||||
"force_rate": { "name": "Force FPS" },
|
||||
"custom_width": { "name": "Output Width" },
|
||||
"custom_height": { "name": "Output Height" },
|
||||
"frame_load_cap": { "name": "Frame Cap" },
|
||||
"skip_first_frames": { "name": "Skip First Frames" },
|
||||
"select_every_nth": { "name": "Select Every Nth" },
|
||||
"autoplay": { "name": "Autoplay" }
|
||||
"file": {
|
||||
"name": "Video File"
|
||||
},
|
||||
"force_rate": {
|
||||
"name": "Force FPS"
|
||||
},
|
||||
"custom_width": {
|
||||
"name": "Output Width"
|
||||
},
|
||||
"custom_height": {
|
||||
"name": "Output Height"
|
||||
},
|
||||
"frame_load_cap": {
|
||||
"name": "Frame Cap"
|
||||
},
|
||||
"skip_first_frames": {
|
||||
"name": "Skip First Frames"
|
||||
},
|
||||
"select_every_nth": {
|
||||
"name": "Select Every Nth"
|
||||
},
|
||||
"autoplay": {
|
||||
"name": "Autoplay"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": { "name": "images" },
|
||||
"1": { "name": "audio" },
|
||||
"2": { "name": "fps" },
|
||||
"3": { "name": "bit_depth" },
|
||||
"4": { "name": "color_space" }
|
||||
"0": {
|
||||
"name": "images"
|
||||
},
|
||||
"1": {
|
||||
"name": "audio"
|
||||
},
|
||||
"2": {
|
||||
"name": "fps"
|
||||
},
|
||||
"3": {
|
||||
"name": "bit_depth"
|
||||
},
|
||||
"4": {
|
||||
"name": "color_space"
|
||||
}
|
||||
}
|
||||
},
|
||||
"XB_StringMerge": {
|
||||
@@ -707,5 +733,27 @@
|
||||
"name": "📋 Report"
|
||||
}
|
||||
}
|
||||
},
|
||||
"XB_DisableGroups": {
|
||||
"display_name": "XB-BOX - 🚫 Disable Groups",
|
||||
"description": "Inspired by rgthree Fast Groups Muter: no wiring. Right-click the node to pick the groups to control; the node surface only shows a switch list — flip a switch to mute / bypass / restore every node in that group. The right-click menu also holds the disable mode and toggle mode (always keep one on / multi with all off or all on).",
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"XB_DisableNodes": {
|
||||
"display_name": "XB-BOX - 🚫 Disable Nodes",
|
||||
"description": "Inspired by rgthree Fast Muter: no wiring. Right-click the node to pick the nodes to control (whole groups can be added at once); the node surface only shows a switch list — flip a switch to mute / bypass / restore that node. The right-click menu also holds the disable mode and toggle mode.",
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"XB_AnySwitch": {
|
||||
"display_name": "XB-BOX - 🔀 Any Switch",
|
||||
"description": "Inspired by rgthree Any Switch: dynamic any-type inputs (2 by default, more added as you connect) with a single any-type output. There is no selector control; it always forwards the first non-empty input.",
|
||||
"inputs": {},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "Output"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +225,9 @@
|
||||
},
|
||||
"fps_float": {
|
||||
"name": "帧率(小数)"
|
||||
},
|
||||
"multiple": {
|
||||
"name": "倍数"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -268,6 +271,9 @@
|
||||
},
|
||||
"strength_int": {
|
||||
"name": "强度(整数)"
|
||||
},
|
||||
"multiple": {
|
||||
"name": "倍数"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -333,6 +339,9 @@
|
||||
},
|
||||
"batch_size": {
|
||||
"name": "生成数量"
|
||||
},
|
||||
"multiple": {
|
||||
"name": "倍数"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5723,5 +5732,27 @@
|
||||
"name": "📋 报告"
|
||||
}
|
||||
}
|
||||
},
|
||||
"XB_DisableGroups": {
|
||||
"display_name": "XB-BOX - 🚫 禁用分组",
|
||||
"description": "参考 rgthree 禁用框:不接线,右击节点勾选要控制的分组(可随时增删);节点表面只有开关列表,点开关即可把该分组内所有节点静音 / 绕过 / 恢复。右击还能改禁用方式、切换模式(多选可全关全开 / 永远保持一个开启)。",
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"XB_DisableNodes": {
|
||||
"display_name": "XB-BOX - 🚫 禁用节点",
|
||||
"description": "参考 rgthree 禁用节点:不接线,右击节点勾选要控制的节点(可按分组整组加入);节点表面只有开关列表,点开关即可静音 / 绕过 / 恢复该节点。右击还能改禁用方式、切换模式。",
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"XB_AnySwitch": {
|
||||
"display_name": "XB-BOX - 🔀 切换任意",
|
||||
"description": "参考 rgthree 任意切换:动态任意类型输入(默认 2 个,连满自动增加)/ 任意类型输出;没有选择控件,永远输出第一个非空输入。",
|
||||
"inputs": {},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "任意类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
XB-BOX 快速开关(参考 rgthree-comfy 的 Fast Muter / Fast Groups Muter / Any Switch)
|
||||
=================================================================================
|
||||
三个节点:
|
||||
|
||||
① XB_DisableGroups —— 禁用分组
|
||||
· **不接线**:在节点面板/右键菜单里勾选要控制的分组(可以随时增删);
|
||||
· 点一行 = 把该分组里的所有节点一次性「静音 / 恢复」;
|
||||
· 与 rgthree「禁用框」一致:状态直接读写画布上节点自己的 mode(随工作流保存)。
|
||||
|
||||
② XB_DisableNodes —— 禁用节点
|
||||
· **不接线**:右击勾选要控制的节点(支持按分组整组加入);
|
||||
· 点一行 = 把该节点「静音 / 绕过 / 恢复」;
|
||||
· 与 rgthree「禁用节点」一致,但不占用输入端口、不需要拖线绑定。
|
||||
|
||||
③ XB_AnySwitch —— 切换任意
|
||||
· 动态任意类型输入(默认 any_01 / any_02,连满自动追加);
|
||||
· 与 rgthree「任意切换」完全一致:没有选择控件,永远输出第一个非空输入。
|
||||
|
||||
⚠️ 前两个节点**只在前端工作**(控制别的节点的 mode),Python 端不参与执行:
|
||||
它们没有任何输入输出,ComfyUI 不会把它们放进执行队列。
|
||||
"""
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
"""一个永远“等于”任何类型的占位类型(credit: pythongosssss / rgthree)"""
|
||||
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
|
||||
def __eq__(self, other):
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash("*")
|
||||
|
||||
|
||||
class FlexibleOptionalInputType(dict):
|
||||
"""让 optional 输入可以无限扩展(任何名字都视为存在,类型由构造时指定)。"""
|
||||
|
||||
def __init__(self, type_, data=None):
|
||||
self.type_ = type_
|
||||
self.data = data
|
||||
if self.data is not None:
|
||||
for k, v in self.data.items():
|
||||
self[k] = v
|
||||
|
||||
def __getitem__(self, key):
|
||||
if self.data is not None and key in self.data:
|
||||
return self.data[key]
|
||||
return (self.type_,)
|
||||
|
||||
def __contains__(self, key):
|
||||
return True
|
||||
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
|
||||
class XB_DisableGroups:
|
||||
"""禁用分组:不接线,勾选分组后一键静音 / 恢复整组节点。"""
|
||||
|
||||
DESCRIPTION = (
|
||||
"禁用分组(参考 rgthree 禁用框):不接线,在面板或右键菜单里勾选要控制的分组;"
|
||||
"点一行即可把该分组内所有节点静音 / 绕过 / 恢复。状态直接写在画布的节点上,随工作流保存。"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {}}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "XB_ToolBox/Utils"
|
||||
OUTPUT_NODE = False
|
||||
|
||||
def noop(self):
|
||||
"""纯前端控制节点,执行时什么都不做。"""
|
||||
return {}
|
||||
|
||||
|
||||
class XB_DisableNodes:
|
||||
"""禁用节点:不接线,勾选节点(可按分组整组加入)后一键静音 / 绕过 / 恢复。"""
|
||||
|
||||
DESCRIPTION = (
|
||||
"禁用节点(参考 rgthree 禁用节点):不接线,在面板或右键菜单里勾选要控制的节点(支持按分组整组加入);"
|
||||
"点一行即可静音 / 绕过 / 恢复该节点。状态直接写在画布的节点上,随工作流保存。"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {}}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "XB_ToolBox/Utils"
|
||||
OUTPUT_NODE = False
|
||||
|
||||
def noop(self):
|
||||
return {}
|
||||
|
||||
|
||||
class XB_AnySwitch:
|
||||
"""切换任意:动态任意类型输入,直接输出第一个非空输入(无选择控件)。"""
|
||||
|
||||
DESCRIPTION = (
|
||||
"切换任意(参考 rgthree 任意切换):把不同来源接到任意类型的输入端口,端口会随连线自动增加;"
|
||||
"节点没有选择控件,永远输出从上往下第一个非空的输入。"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": FlexibleOptionalInputType(any_type),
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
RETURN_NAMES = ("*",)
|
||||
FUNCTION = "switch"
|
||||
CATEGORY = "XB_ToolBox/Utils"
|
||||
OUTPUT_NODE = False
|
||||
|
||||
@staticmethod
|
||||
def _items(kwargs):
|
||||
items = [(k, v) for k, v in kwargs.items() if isinstance(k, str) and k.startswith("any_")]
|
||||
items.sort(key=lambda kv: kv[0])
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _is_empty(value):
|
||||
if value is None:
|
||||
return True
|
||||
# 上下文为空时的兜底(与 rgthree 一致)
|
||||
if isinstance(value, dict) and "model" in value and "clip" in value:
|
||||
return all(v is None for v in value.values())
|
||||
return False
|
||||
|
||||
def switch(self, **kwargs):
|
||||
"""取第一个非空输入(与 rgthree Any Switch 同行为)。"""
|
||||
for _k, value in self._items(kwargs):
|
||||
if not self._is_empty(value):
|
||||
return (value,)
|
||||
return (None,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"XB_DisableGroups": XB_DisableGroups,
|
||||
"XB_DisableNodes": XB_DisableNodes,
|
||||
"XB_AnySwitch": XB_AnySwitch,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"XB_DisableGroups": "XB-BOX - 🚫 禁用分组",
|
||||
"XB_DisableNodes": "XB-BOX - 🚫 禁用节点",
|
||||
"XB_AnySwitch": "XB-BOX - 🔀 切换任意",
|
||||
}
|
||||
+110
-62
@@ -15,6 +15,96 @@ import nodes
|
||||
|
||||
from .xb_av import open_av
|
||||
|
||||
# ============================================================
|
||||
# 画幅比例 / 倍数 公共逻辑
|
||||
# 画幅比例 = 自定义 + 官方 Resolution Selector 的比例;
|
||||
# 倍数(multiple)约束宽高:既作为调节步长,也保证宽高能整除。
|
||||
# ============================================================
|
||||
XB_ASPECT_RATIOS = [
|
||||
("自定义", None),
|
||||
("1:1 (Square)", (1, 1)),
|
||||
("2:3 (Portrait Photo)", (2, 3)),
|
||||
("3:2 (Photo)", (3, 2)),
|
||||
("3:4 (Portrait Standard)", (3, 4)),
|
||||
("4:3 (Standard)", (4, 3)),
|
||||
("9:16 (Portrait Widescreen)", (9, 16)),
|
||||
("16:9 (Widescreen)", (16, 9)),
|
||||
("21:9 (Ultrawide)", (21, 9)),
|
||||
]
|
||||
XB_ASPECT_VALUES = [name for name, _ in XB_ASPECT_RATIOS]
|
||||
XB_ASPECT_MAP = {name: r for name, r in XB_ASPECT_RATIOS if r}
|
||||
|
||||
# 旧工作流里的值 → 新值(保证老图照常开)
|
||||
XB_ASPECT_LEGACY = {
|
||||
"Free": "自定义",
|
||||
"1:1": "1:1 (Square)",
|
||||
"2:3": "2:3 (Portrait Photo)",
|
||||
"3:2": "3:2 (Photo)",
|
||||
"3:4": "3:4 (Portrait Standard)",
|
||||
"4:3": "4:3 (Standard)",
|
||||
"9:16": "9:16 (Portrait Widescreen)",
|
||||
"16:9": "16:9 (Widescreen)",
|
||||
"21:9": "21:9 (Ultrawide)",
|
||||
}
|
||||
|
||||
XB_MULTIPLE_DEFAULT = 16
|
||||
|
||||
|
||||
def xb_multiple(value, default=XB_MULTIPLE_DEFAULT):
|
||||
"""把「倍数」规整成 >= 8 的整数。"""
|
||||
try:
|
||||
m = int(round(float(value)))
|
||||
except Exception:
|
||||
m = default
|
||||
return max(8, m)
|
||||
|
||||
|
||||
def xb_snap(value, m):
|
||||
"""吸附到 m 的整数倍(并保证 >= m)。"""
|
||||
try:
|
||||
v = int(round(float(value)))
|
||||
except Exception:
|
||||
v = m
|
||||
return max(m, int(round(v / m)) * m)
|
||||
|
||||
|
||||
def xb_aspect_ratio(name):
|
||||
"""画幅比例 → 宽高比(宽/高);自定义 / 未知返回 None。"""
|
||||
if name in XB_ASPECT_MAP:
|
||||
w, h = XB_ASPECT_MAP[name]
|
||||
return w / h
|
||||
name = XB_ASPECT_LEGACY.get(name, name)
|
||||
if name in XB_ASPECT_MAP:
|
||||
w, h = XB_ASPECT_MAP[name]
|
||||
return w / h
|
||||
# 兼容 "16:9 (LTX)" 这类旧写法
|
||||
for key, ratio in XB_ASPECT_MAP.items():
|
||||
if name and name.startswith(key.split(" ")[0]):
|
||||
w, h = ratio
|
||||
return w / h
|
||||
return None
|
||||
|
||||
|
||||
def xb_fit_size(name, width, height, m):
|
||||
"""按比例 + 倍数算出最终宽高(两边都能被 m 整除)。"""
|
||||
w = xb_snap(width, m)
|
||||
h = xb_snap(height, m)
|
||||
ratio = xb_aspect_ratio(name)
|
||||
if ratio:
|
||||
if w >= h:
|
||||
h = xb_snap(w / ratio, m)
|
||||
else:
|
||||
w = xb_snap(h * ratio, m)
|
||||
return max(m, w), max(m, h)
|
||||
|
||||
|
||||
def xb_multiple_input():
|
||||
"""「倍数」控件定义(放在最下面)。"""
|
||||
return ("INT", {
|
||||
"default": XB_MULTIPLE_DEFAULT, "min": 8, "max": 128, "step": 4,
|
||||
"tooltip": "宽高必须是它的整数倍;调节分辨率时也按这个步长走(默认 16)。",
|
||||
})
|
||||
|
||||
# ============================================================
|
||||
# XB_ImageParamsMaster — 图像参数主控
|
||||
# ============================================================
|
||||
@@ -23,12 +113,13 @@ class XB_ImageParamsMaster:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"aspect_ratio": (["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "2:3", "3:2"], {"default": "Free"}),
|
||||
"aspect_ratio": (XB_ASPECT_VALUES, {"default": "自定义"}),
|
||||
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16}),
|
||||
"height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16}),
|
||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1}),
|
||||
"strength_float": ("FLOAT", {"default": 1.00, "min": 0.00, "max": 10.00, "step": 0.01}),
|
||||
"strength_int": ("INT", {"default": 1, "min": 0, "max": 1000, "step": 1}),
|
||||
"multiple": xb_multiple_input(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,22 +128,9 @@ class XB_ImageParamsMaster:
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "XB_ToolBox/Image_Params"
|
||||
|
||||
def process(self, aspect_ratio, width, height, batch_size, strength_float, strength_int):
|
||||
if "Free" in aspect_ratio:
|
||||
return (width, height, batch_size, float(strength_float), int(strength_int), max(width, height))
|
||||
|
||||
# 解析宽高比约束,使用 round() 而非 // 避免累积误差
|
||||
ratio_map = {"1:1": 1.0, "16:9": 16/9, "9:16": 9/16, "4:3": 4/3, "3:4": 3/4, "21:9": 21/9, "2:3": 2/3, "3:2": 3/2}
|
||||
target_ratio = ratio_map.get(aspect_ratio, 1.0)
|
||||
step = 16
|
||||
|
||||
if width >= height:
|
||||
safe_w = max(step, round(width / step) * step)
|
||||
safe_h = max(step, round((safe_w / target_ratio) / step) * step)
|
||||
else:
|
||||
safe_h = max(step, round(height / step) * step)
|
||||
safe_w = max(step, round((safe_h * target_ratio) / step) * step)
|
||||
|
||||
def process(self, aspect_ratio, width, height, batch_size, strength_float, strength_int, multiple=XB_MULTIPLE_DEFAULT):
|
||||
m = xb_multiple(multiple)
|
||||
safe_w, safe_h = xb_fit_size(aspect_ratio, width, height, m)
|
||||
return (safe_w, safe_h, batch_size, float(strength_float), int(strength_int), max(safe_w, safe_h))
|
||||
|
||||
# ============================================================
|
||||
@@ -65,10 +143,11 @@ class XB_ImageParamsMasterMini:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"aspect_ratio": (["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "2:3", "3:2"], {"default": "Free"}),
|
||||
"aspect_ratio": (XB_ASPECT_VALUES, {"default": "自定义"}),
|
||||
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16}),
|
||||
"height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16}),
|
||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1}),
|
||||
"multiple": xb_multiple_input(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,22 +156,9 @@ class XB_ImageParamsMasterMini:
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "XB_ToolBox/Image_Params"
|
||||
|
||||
def process(self, aspect_ratio, width, height, batch_size):
|
||||
if "Free" in aspect_ratio:
|
||||
return (width, height, batch_size, max(width, height))
|
||||
|
||||
# 解析宽高比约束,使用 round() 而非 // 避免累积误差
|
||||
ratio_map = {"1:1": 1.0, "16:9": 16/9, "9:16": 9/16, "4:3": 4/3, "3:4": 3/4, "21:9": 21/9, "2:3": 2/3, "3:2": 3/2}
|
||||
target_ratio = ratio_map.get(aspect_ratio, 1.0)
|
||||
step = 16
|
||||
|
||||
if width >= height:
|
||||
safe_w = max(step, round(width / step) * step)
|
||||
safe_h = max(step, round((safe_w / target_ratio) / step) * step)
|
||||
else:
|
||||
safe_h = max(step, round(height / step) * step)
|
||||
safe_w = max(step, round((safe_h * target_ratio) / step) * step)
|
||||
|
||||
def process(self, aspect_ratio, width, height, batch_size, multiple=XB_MULTIPLE_DEFAULT):
|
||||
m = xb_multiple(multiple)
|
||||
safe_w, safe_h = xb_fit_size(aspect_ratio, width, height, m)
|
||||
return (safe_w, safe_h, batch_size, max(safe_w, safe_h))
|
||||
|
||||
# ============================================================
|
||||
@@ -103,13 +169,14 @@ class XB_VideoParamsMaster:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"aspect_ratio": (["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "2:3", "3:2", "16:9 (LTX)", "9:16 (LTX)", "4:3 (LTX)", "3:4 (LTX)"], {"default": "Free"}),
|
||||
"aspect_ratio": (XB_ASPECT_VALUES, {"default": "自定义"}),
|
||||
"duration_display": ("STRING", {"default": "Video Duration: 0.00 s", "multiline": False}),
|
||||
"width": ("INT", {"default": 480, "min": 64, "max": 8192, "step": 16}),
|
||||
"height": ("INT", {"default": 832, "min": 64, "max": 8192, "step": 16}),
|
||||
"length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 1}),
|
||||
"fps": ("INT", {"default": 16, "min": 1, "max": 120, "step": 1}),
|
||||
"fps_float": ("FLOAT", {"default": 16.0, "min": 1.0, "max": 120.0, "step": 0.01}),
|
||||
"multiple": xb_multiple_input(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,36 +185,17 @@ class XB_VideoParamsMaster:
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "XB_ToolBox/Image_Params"
|
||||
|
||||
def process(self, aspect_ratio, duration_display, width, height, length, fps, fps_float):
|
||||
def process(self, aspect_ratio, duration_display, width, height, length, fps, fps_float,
|
||||
multiple=XB_MULTIPLE_DEFAULT):
|
||||
final_fps = int(round(fps))
|
||||
m = xb_multiple(multiple)
|
||||
safe_w, safe_h = xb_fit_size(aspect_ratio, width, height, m)
|
||||
|
||||
if "Free" in aspect_ratio:
|
||||
return (width, height, length, final_fps, float(final_fps), max(width, height))
|
||||
|
||||
if "LTX" in aspect_ratio:
|
||||
# LTX 模式使用 step=32
|
||||
step = 32
|
||||
ratio_map = {"16:9 (LTX)": 16/9, "9:16 (LTX)": 9/16, "4:3 (LTX)": 4/3, "3:4 (LTX)": 3/4}
|
||||
target_ratio = ratio_map.get(aspect_ratio, 16/9)
|
||||
if width >= height:
|
||||
safe_w = max(step, round(width / step) * step)
|
||||
safe_h = max(step, round((safe_w / target_ratio) / step) * step)
|
||||
else:
|
||||
safe_h = max(step, round(height / step) * step)
|
||||
safe_w = max(step, round((safe_h * target_ratio) / step) * step)
|
||||
is_free = xb_aspect_ratio(aspect_ratio) is None
|
||||
if is_free:
|
||||
safe_len = max(1, int(length))
|
||||
else:
|
||||
# 解析宽高比并强制约束,使用 round() 避免累积误差
|
||||
step = 16
|
||||
ratio_map = {"16:9": 16/9, "9:16": 9/16, "1:1": 1.0, "4:3": 4/3, "3:4": 3/4, "21:9": 21/9, "2:3": 2/3, "3:2": 3/2}
|
||||
target_ratio = ratio_map.get(aspect_ratio, 1.0)
|
||||
if width >= height:
|
||||
safe_w = max(step, round(width / step) * step)
|
||||
safe_h = max(step, round((safe_w / target_ratio) / step) * step)
|
||||
else:
|
||||
safe_h = max(step, round(height / step) * step)
|
||||
safe_w = max(step, round((safe_h * target_ratio) / step) * step)
|
||||
|
||||
safe_len = max(1, round((length - 1) / 8) * 8 + 1)
|
||||
safe_len = max(1, round((length - 1) / 8) * 8 + 1)
|
||||
|
||||
return (safe_w, safe_h, safe_len, final_fps, float(final_fps), max(safe_w, safe_h))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user