增加節點Image Queue Processor Pro

This commit is contained in:
aidec
2025-11-12 10:47:50 +08:00
parent 69630377d5
commit 5317d99ea6
4 changed files with 1400 additions and 13 deletions
+5 -1
View File
@@ -28,7 +28,11 @@ Text Queue Processor
將text拉到想要的地方,例如:正向提示詞(Clip)
### 新增功能Image Queue Processor Pro(批量圖生圖推薦使用):
增加了 Image Queue Processor Pro節點,此節點可以直接設定讀取的資料夾,將資料夾圖片檔名分組,指定index才讀取圖片。
跟之前的Image Queue Processor不同,之前的Image Queue Processor是先將所有圖片讀取到記憶體,再依照index讀取圖片,這樣會大幅占用記憶體。新版的pro節點,只讀取資料夾檔案清單,並讀取對應index的圖片,不會占用記憶體。推薦使用這個節點來進行批量圖生圖。
舊版本的大概只支援100~200張左右的圖片就會吃掉好幾GB的記憶體,新版的pro節點目錄中有200張圖大概只吃20MB左右的記憶體。
### 新增功能Image Queue Processor(批量圖生圖):
增加了 Image Queue Processor 節點,可以將圖片每行一組,批量執行(但目前需要搭配Load Image Batch From D(Inspire)這類的使用)
+441
View File
@@ -254,4 +254,445 @@ app.registerExtension({
Object.assign(nodeType.prototype, ImageQueueProcessorNode.prototype);
}
}
});
// ============ 群組控制器節點 ============
// 監聽後端的群組狀態更新事件
api.addEventListener("groupcontroller-update", (event) => {
try {
const data = event.detail;
if (!data || !data.node_id) return;
const node = app.graph.getNodeById(parseInt(data.node_id));
if (!node) return;
// 應用群組狀態
if (node.applyGroupStates) {
node.applyGroupStates(data.group_states, data.control_mode);
}
} catch (error) {
console.error("Error in groupcontroller-update handler:", error);
}
});
// 註冊群組控制器節點
app.registerExtension({
name: "TextBatch.GroupController",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "GroupController") {
console.log("Registering GroupController node");
// 修改 INPUT_TYPES 來動態添加群組 inputs
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
if (originalGetExtraMenuOptions) {
originalGetExtraMenuOptions.apply(this, arguments);
}
options.unshift(
{
content: "🔄 重新掃描群組",
callback: () => {
this.refreshGroupInputs();
}
},
null
);
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const r = onNodeCreated?.apply(this, arguments);
// 初始化屬性
this.groupsData = [];
this.lastGroupSignature = "";
this.lastInputValues = {}; // 存儲上次的 input 值
// 延遲載入群組以確保 graph 已初始化
setTimeout(() => {
this.refreshGroupInputs();
}, 300);
// 啟動輪詢機制,每 100ms 檢查一次輸入值變化
this.updateInterval = setInterval(() => {
this.checkAndApplyInputChanges();
}, 100);
console.log(`GroupController ${this.id} created with polling enabled`);
return r;
};
// 獲取所有群組
nodeType.prototype.getAllGroups = function() {
if (!app.canvas || !app.canvas.graph) {
return [];
}
const groups = [];
// 從 _groups 獲取
if (app.canvas.graph._groups && Array.isArray(app.canvas.graph._groups)) {
groups.push(...app.canvas.graph._groups);
}
return groups;
};
// 獲取群組中的所有節點
nodeType.prototype.getNodesInGroup = function(group) {
if (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes) {
return [];
}
const nodes = [];
for (const node of app.canvas.graph._nodes) {
if (this.isNodeInGroup(node, group)) {
nodes.push(node);
}
}
return nodes;
};
// 判斷節點是否在群組內
nodeType.prototype.isNodeInGroup = function(node, group) {
if (!node || !group) return false;
const nodeX = node.pos[0];
const nodeY = node.pos[1];
const nodeWidth = node.size[0];
const nodeHeight = node.size[1];
const groupX = group._pos[0];
const groupY = group._pos[1];
const groupWidth = group._size[0];
const groupHeight = group._size[1];
const nodeCenterX = nodeX + nodeWidth / 2;
const nodeCenterY = nodeY + nodeHeight / 2;
return nodeCenterX >= groupX && nodeCenterX <= groupX + groupWidth &&
nodeCenterY >= groupY && nodeCenterY <= groupY + groupHeight;
};
// 刷新群組 inputs
nodeType.prototype.refreshGroupInputs = function() {
console.log("Refreshing group inputs...");
const groups = this.getAllGroups();
console.log("Found groups:", groups.length);
if (groups.length === 0) {
console.warn("No groups found");
return;
}
// 生成群組簽名以檢測變化
const groupSignature = groups.map(g => `${g.title}_${g._pos.join(',')}_${g._size.join(',')}`).join('|');
// 如果群組沒有變化,不需要重建
if (this.lastGroupSignature === groupSignature) {
console.log("Groups unchanged, skipping rebuild");
return;
}
this.lastGroupSignature = groupSignature;
this.groupsData = groups;
// 移除舊的群組 inputs 和 widgets
const inputsToRemove = [];
if (this.inputs) {
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
if (input.name.startsWith("enable_group_")) {
inputsToRemove.push(i);
}
}
}
// 從後往前刪除以避免索引問題
for (let i = inputsToRemove.length - 1; i >= 0; i--) {
this.removeInput(inputsToRemove[i]);
}
// 移除舊的群組 widgets(保留 control_mode)
if (this.widgets) {
const widgetsToKeep = this.widgets.filter(w =>
w.name === "control_mode" || !w.name.startsWith("enable_group_")
);
this.widgets = widgetsToKeep;
}
// 為每個群組添加 BOOLEAN input 和對應的 widget
for (const group of groups) {
const groupId = this.sanitizeGroupId(group.title || group.id);
const inputName = `enable_group_${groupId}`;
const displayName = `Enable ${group.title || groupId}`;
// 添加 input(連接點)
this.addInput(inputName, "BOOLEAN", {
label: displayName
});
// 添加對應的 toggle widget
const widget = this.addWidget(
"toggle",
inputName,
true,
(value) => {
console.log(`Widget ${inputName} changed to:`, value);
// widget 變更時立即應用狀態
const modeWidget = this.widgets?.find(w => w.name === "control_mode");
const currentMode = modeWidget ? modeWidget.value : "bypass";
const groupStates = this.getGroupStates();
this.applyGroupStates(groupStates, currentMode);
},
{ on: "yes", off: "no" }
);
// 將 widget 與 input 關聯
widget.linkedInput = inputName;
console.log(`Added input and widget: ${inputName}`);
}
// 更新節點大小
this.setSize(this.computeSize());
console.log(`Loaded ${groups.length} group inputs`);
};
// 清理群組 ID(移除特殊字符)
nodeType.prototype.sanitizeGroupId = function(id) {
return String(id).replace(/[^a-zA-Z0-9_]/g, '_');
};
// 應用群組狀態
nodeType.prototype.applyGroupStates = function(groupStates, controlMode) {
console.log("Applying group states:", groupStates, "Mode:", controlMode);
for (const group of this.groupsData) {
const groupId = this.sanitizeGroupId(group.title || group.id);
const enabled = groupStates[groupId] !== undefined ? groupStates[groupId] : true;
this.setGroupState(group, enabled, controlMode);
}
};
// 設置群組狀態
nodeType.prototype.setGroupState = function(group, enabled, mode) {
const nodes = this.getNodesInGroup(group);
console.log(`Setting group "${group.title}" to ${enabled ? 'enabled' : 'disabled'} (mode: ${mode}), nodes: ${nodes.length}`);
for (const node of nodes) {
if (mode === "bypass") {
node.mode = enabled ? 0 : 4; // 0 = ALWAYS, 4 = BYPASS
} else if (mode === "mute") {
node.mode = enabled ? 0 : 2; // 0 = ALWAYS, 2 = NEVER (muted)
}
}
// 重繪畫布
if (app.canvas) {
app.canvas.setDirty(true, true);
}
};
// 檢查並應用輸入值變化
nodeType.prototype.checkAndApplyInputChanges = function() {
if (!this.inputs || !this.groupsData || this.groupsData.length === 0) {
return;
}
let hasChanged = false;
const currentValues = {};
// 獲取當前所有輸入值
for (const input of this.inputs) {
if (!input.name.startsWith("enable_group_")) continue;
const groupId = input.name.replace("enable_group_", "");
let currentValue = true; // 預設值
// 檢查是否有連接
const link = input.link;
if (link !== null && link !== undefined) {
const linkInfo = app.graph.links[link];
if (linkInfo) {
const originNode = app.graph.getNodeById(linkInfo.origin_id);
if (originNode) {
const outputIndex = linkInfo.origin_slot;
// 嘗試從 widget 獲取值
if (originNode.widgets && originNode.widgets.length > 0) {
const widget = originNode.widgets.find(w =>
w.name === "value" || w.name === "boolean_value" ||
w.name === "BOOLEAN" || w.type === "toggle"
) || originNode.widgets[0];
if (widget && widget.value !== undefined) {
currentValue = widget.value;
}
}
}
}
} else {
// 沒有連接,使用本地 widget 的值
const widget = this.widgets?.find(w => w.name === input.name);
if (widget) {
currentValue = widget.value !== false;
}
}
currentValues[groupId] = currentValue;
// 檢查是否變化
if (this.lastInputValues[groupId] !== currentValue) {
hasChanged = true;
console.log(`GroupController ${this.id}: ${input.name} changed from ${this.lastInputValues[groupId]} to ${currentValue}`);
}
}
// 如果有變化,應用新狀態
if (hasChanged) {
this.lastInputValues = currentValues;
const modeWidget = this.widgets?.find(w => w.name === "control_mode");
const currentMode = modeWidget ? modeWidget.value : "bypass";
this.applyGroupStates(currentValues, currentMode);
}
};
// 獲取群組狀態(從 input 連接或 widget)
nodeType.prototype.getGroupStates = function() {
const groupStates = {};
if (!this.inputs) return groupStates;
for (const input of this.inputs) {
if (!input.name.startsWith("enable_group_")) continue;
const groupId = input.name.replace("enable_group_", "");
// 檢查是否有連接
const link = input.link;
if (link !== null && link !== undefined) {
// 有連接,嘗試從連接獲取最新值
const linkInfo = app.graph.links[link];
if (linkInfo) {
const originNode = app.graph.getNodeById(linkInfo.origin_id);
if (originNode) {
const outputIndex = linkInfo.origin_slot;
// 先嘗試從 outputs[].value 獲取
if (originNode.outputs && originNode.outputs[outputIndex]) {
const output = originNode.outputs[outputIndex];
if (output.value !== undefined) {
groupStates[groupId] = output.value;
continue;
}
}
// 嘗試從 widget 獲取值(對於 bool 節點)
if (originNode.widgets && originNode.widgets.length > 0) {
// 查找 BOOLEAN 類型的 widget
const boolWidget = originNode.widgets.find(w =>
w.name === "value" || w.name === "boolean_value" || w.type === "toggle"
);
if (boolWidget && boolWidget.value !== undefined) {
groupStates[groupId] = boolWidget.value;
continue;
}
// 如果找不到特定的,就使用第一個 widget 的值
if (originNode.widgets[0].value !== undefined) {
groupStates[groupId] = originNode.widgets[0].value;
continue;
}
}
}
}
}
// 沒有連接或無法從連接獲取值,使用 widget 的值
const widget = this.widgets?.find(w => w.name === input.name);
if (widget) {
groupStates[groupId] = widget.value !== false;
} else {
groupStates[groupId] = true; // 預設啟用
}
}
return groupStates;
};
// 監聽執行前事件
const onExecute = nodeType.prototype.onExecute;
nodeType.prototype.onExecute = function() {
// 獲取當前模式
const modeWidget = this.widgets?.find(w => w.name === "control_mode");
const currentMode = modeWidget ? modeWidget.value : "bypass";
// 獲取所有群組狀態
const groupStates = this.getGroupStates();
// 應用狀態到群組
this.applyGroupStates(groupStates, currentMode);
if (onExecute) {
return onExecute.apply(this, arguments);
}
};
// 監聽 widget 變更
const onWidgetChanged = nodeType.prototype.onWidgetChanged;
nodeType.prototype.onWidgetChanged = function(name, value, oldValue, widget) {
if (onWidgetChanged) {
onWidgetChanged.apply(this, arguments);
}
// 如果是群組 widget 或 control_mode 變更,立即應用
if (name.startsWith("enable_group_") || name === "control_mode") {
const modeWidget = this.widgets?.find(w => w.name === "control_mode");
const currentMode = modeWidget ? modeWidget.value : "bypass";
const groupStates = this.getGroupStates();
this.applyGroupStates(groupStates, currentMode);
}
};
// 監聽連接變更
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info) {
if (onConnectionsChange) {
onConnectionsChange.apply(this, arguments);
}
// 當連接變更時,重新應用狀態
if (type === 1) { // 1 = input
console.log(`GroupController ${this.id}: connection changed`);
setTimeout(() => {
const modeWidget = this.widgets?.find(w => w.name === "control_mode");
const currentMode = modeWidget ? modeWidget.value : "bypass";
const groupStates = this.getGroupStates();
this.applyGroupStates(groupStates, currentMode);
}, 100);
}
};
// 清理定時器
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function() {
console.log(`GroupController ${this.id} removed, stopping polling`);
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
if (onRemoved) {
return onRemoved.apply(this, arguments);
}
};
}
}
});
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "../.."
}
],
"settings": {}
}
File diff suppressed because it is too large Load Diff