mirror of
https://github.com/jags111/efficiency-nodes-comfyui.git
synced 2026-03-21 21:22:13 -03:00
Efficiency Nodes V2.0
This commit is contained in:
@@ -44,8 +44,10 @@ const NODE_COLORS = {
|
||||
"Manual XY Entry Info": "cyan",
|
||||
"Join XY Inputs of Same Type": "cyan",
|
||||
"Image Overlay": "random",
|
||||
"Noise Control Script": "none",
|
||||
"HighRes-Fix Script": "yellow",
|
||||
"Tiled Sampling Script": "none",
|
||||
"Tiled Upscaler Script": "red",
|
||||
"AnimateDiff Script": "random",
|
||||
"Evaluate Integers": "pale_blue",
|
||||
"Evaluate Floats": "pale_blue",
|
||||
"Evaluate Strings": "pale_blue",
|
||||
|
||||
144
js/gif_preview.js
Normal file
144
js/gif_preview.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import { app } from '../../scripts/app.js'
|
||||
import { api } from '../../scripts/api.js'
|
||||
|
||||
function offsetDOMWidget(
|
||||
widget,
|
||||
ctx,
|
||||
node,
|
||||
widgetWidth,
|
||||
widgetY,
|
||||
height
|
||||
) {
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(0, widgetY + margin)
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(widget.inputEl.style, {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth}px`,
|
||||
height: `${(height || widget.parent?.inputHeight || 32) - margin}px`,
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
zIndex: 5, //app.graph._nodes.indexOf(node),
|
||||
})
|
||||
}
|
||||
|
||||
export const hasWidgets = (node) => {
|
||||
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const cleanupNode = (node) => {
|
||||
if (!hasWidgets(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const w of node.widgets) {
|
||||
if (w.canvas) {
|
||||
w.canvas.remove()
|
||||
}
|
||||
if (w.inputEl) {
|
||||
w.inputEl.remove()
|
||||
}
|
||||
// calls the widget remove callback
|
||||
w.onRemoved?.()
|
||||
}
|
||||
}
|
||||
|
||||
const CreatePreviewElement = (name, val, format) => {
|
||||
const [type] = format.split('/')
|
||||
const w = {
|
||||
name,
|
||||
type,
|
||||
value: val,
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
},
|
||||
computeSize: function (_) {
|
||||
const ratio = this.inputRatio || 1
|
||||
const width = Math.max(220, this.parent.size[0])
|
||||
return [width, (width / ratio + 10)]
|
||||
},
|
||||
onRemoved: function () {
|
||||
if (this.inputEl) {
|
||||
this.inputEl.remove()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
|
||||
w.inputEl.src = w.value
|
||||
if (type === 'video') {
|
||||
w.inputEl.setAttribute('type', 'video/webm');
|
||||
w.inputEl.autoplay = true
|
||||
w.inputEl.loop = true
|
||||
w.inputEl.controls = false;
|
||||
}
|
||||
w.inputEl.onload = function () {
|
||||
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
|
||||
}
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
}
|
||||
|
||||
const gif_preview = {
|
||||
name: 'efficiency.gif_preview',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
switch (nodeData.name) {
|
||||
case 'KSampler (Efficient)':{
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
const prefix = 'ad_gif_preview_'
|
||||
const r = onExecuted ? onExecuted.apply(this, message) : undefined
|
||||
|
||||
if (this.widgets) {
|
||||
const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`)
|
||||
if (pos !== -1) {
|
||||
for (let i = pos; i < this.widgets.length; i++) {
|
||||
this.widgets[i].onRemoved?.()
|
||||
}
|
||||
this.widgets.length = pos
|
||||
}
|
||||
if (message?.gifs) {
|
||||
message.gifs.forEach((params, i) => {
|
||||
const previewUrl = api.apiURL(
|
||||
'/view?' + new URLSearchParams(params).toString()
|
||||
)
|
||||
const w = this.addCustomWidget(
|
||||
CreatePreviewElement(`${prefix}_${i}`, previewUrl, params.format || 'image/gif')
|
||||
)
|
||||
w.parent = this
|
||||
})
|
||||
}
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = () => {
|
||||
cleanupNode(this)
|
||||
return onRemoved?.()
|
||||
}
|
||||
}
|
||||
if (message?.gifs && message.gifs.length > 0) {
|
||||
this.setSize([this.size[0], this.computeSize([this.size[0], this.size[1]])[1]]);
|
||||
}
|
||||
return r
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension(gif_preview)
|
||||
180
js/node_options/addLinks.js
Normal file
180
js/node_options/addLinks.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
import { addNode } from "./common/utils.js";
|
||||
|
||||
function createKSamplerEntry(node, samplerType, subNodeType = null, isSDXL = false) {
|
||||
const samplerLabelMap = {
|
||||
"Eff": "KSampler (Efficient)",
|
||||
"Adv": "KSampler Adv. (Efficient)",
|
||||
"SDXL": "KSampler SDXL (Eff.)"
|
||||
};
|
||||
|
||||
const subNodeLabelMap = {
|
||||
"XYPlot": "XY Plot",
|
||||
"NoiseControl": "Noise Control Script",
|
||||
"HiResFix": "HighRes-Fix Script",
|
||||
"TiledUpscale": "Tiled Upscaler Script",
|
||||
"AnimateDiff": "AnimateDiff Script"
|
||||
};
|
||||
|
||||
const nicknameMap = {
|
||||
"KSampler (Efficient)": "KSampler",
|
||||
"KSampler Adv. (Efficient)": "KSampler(Adv)",
|
||||
"KSampler SDXL (Eff.)": "KSampler",
|
||||
"XY Plot": "XY Plot",
|
||||
"Noise Control Script": "NoiseControl",
|
||||
"HighRes-Fix Script": "HiResFix",
|
||||
"Tiled Upscaler Script": "TiledUpscale",
|
||||
"AnimateDiff Script": "AnimateDiff"
|
||||
};
|
||||
|
||||
const kSamplerLabel = samplerLabelMap[samplerType];
|
||||
const subNodeLabel = subNodeLabelMap[subNodeType];
|
||||
|
||||
const kSamplerNickname = nicknameMap[kSamplerLabel];
|
||||
const subNodeNickname = nicknameMap[subNodeLabel];
|
||||
|
||||
const contentLabel = subNodeNickname ? `${kSamplerNickname} + ${subNodeNickname}` : kSamplerNickname;
|
||||
|
||||
return {
|
||||
content: contentLabel,
|
||||
callback: function() {
|
||||
const kSamplerNode = addNode(kSamplerLabel, node, { shiftX: node.size[0] + 50 });
|
||||
|
||||
// Standard connections for all samplers
|
||||
node.connect(0, kSamplerNode, 0); // MODEL
|
||||
node.connect(1, kSamplerNode, 1); // CONDITIONING+
|
||||
node.connect(2, kSamplerNode, 2); // CONDITIONING-
|
||||
|
||||
// Additional connections for non-SDXL
|
||||
if (!isSDXL) {
|
||||
node.connect(3, kSamplerNode, 3); // LATENT
|
||||
node.connect(4, kSamplerNode, 4); // VAE
|
||||
}
|
||||
|
||||
if (subNodeLabel) {
|
||||
const subNode = addNode(subNodeLabel, node, { shiftX: 50, shiftY: node.size[1] + 50 });
|
||||
const dependencyIndex = isSDXL ? 3 : 5;
|
||||
node.connect(dependencyIndex, subNode, 0);
|
||||
subNode.connect(0, kSamplerNode, dependencyIndex);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createStackerNode(node, type) {
|
||||
const stackerLabelMap = {
|
||||
"LoRA": "LoRA Stacker",
|
||||
"ControlNet": "Control Net Stacker"
|
||||
};
|
||||
|
||||
const contentLabel = stackerLabelMap[type];
|
||||
|
||||
return {
|
||||
content: contentLabel,
|
||||
callback: function() {
|
||||
const stackerNode = addNode(contentLabel, node);
|
||||
|
||||
// Calculate the left shift based on the width of the new node
|
||||
const shiftX = -(stackerNode.size[0] + 25);
|
||||
|
||||
stackerNode.pos[0] += shiftX; // Adjust the x position of the new node
|
||||
|
||||
// Introduce a Y offset of 200 for ControlNet Stacker node
|
||||
if (type === "ControlNet") {
|
||||
stackerNode.pos[1] += 300;
|
||||
}
|
||||
|
||||
// Connect outputs to the Efficient Loader based on type
|
||||
if (type === "LoRA") {
|
||||
stackerNode.connect(0, node, 0);
|
||||
} else if (type === "ControlNet") {
|
||||
stackerNode.connect(0, node, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createXYPlotNode(node, type) {
|
||||
const contentLabel = "XY Plot";
|
||||
|
||||
return {
|
||||
content: contentLabel,
|
||||
callback: function() {
|
||||
const xyPlotNode = addNode(contentLabel, node);
|
||||
|
||||
// Center the X coordinate of the XY Plot node
|
||||
const centerXShift = (node.size[0] - xyPlotNode.size[0]) / 2;
|
||||
xyPlotNode.pos[0] += centerXShift;
|
||||
|
||||
// Adjust the Y position to place it below the loader node
|
||||
xyPlotNode.pos[1] += node.size[1] + 60;
|
||||
|
||||
// Depending on the node type, connect the appropriate output to the XY Plot node
|
||||
if (type === "Efficient") {
|
||||
node.connect(6, xyPlotNode, 0);
|
||||
} else if (type === "SDXL") {
|
||||
node.connect(3, xyPlotNode, 0);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getMenuValues(type, node) {
|
||||
const subNodeTypes = [null, "XYPlot", "NoiseControl", "HiResFix", "TiledUpscale", "AnimateDiff"];
|
||||
const excludedSubNodeTypes = ["NoiseControl", "HiResFix", "TiledUpscale", "AnimateDiff"]; // Nodes to exclude from the menu
|
||||
|
||||
const menuValues = [];
|
||||
|
||||
// Add the new node types to the menu first for the correct order
|
||||
menuValues.push(createStackerNode(node, "LoRA"));
|
||||
menuValues.push(createStackerNode(node, "ControlNet"));
|
||||
|
||||
for (const subNodeType of subNodeTypes) {
|
||||
// Skip adding submenu items that are in the excludedSubNodeTypes array
|
||||
if (!excludedSubNodeTypes.includes(subNodeType)) {
|
||||
const menuEntry = createKSamplerEntry(node, type === "Efficient" ? "Eff" : "SDXL", subNodeType, type === "SDXL");
|
||||
menuValues.push(menuEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the standalone XY Plot option after the KSampler without any subNodeTypes and before any other KSamplers with subNodeTypes
|
||||
menuValues.splice(3, 0, createXYPlotNode(node, type));
|
||||
|
||||
return menuValues;
|
||||
}
|
||||
|
||||
function showAddLinkMenuCommon(value, options, e, menu, node, type) {
|
||||
const values = getMenuValues(type, node);
|
||||
new LiteGraph.ContextMenu(values, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.addLinks",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
const linkTypes = {
|
||||
"Efficient Loader": "Efficient",
|
||||
"Eff. Loader SDXL": "SDXL"
|
||||
};
|
||||
|
||||
const linkType = linkTypes[nodeData.name];
|
||||
|
||||
if (linkType) {
|
||||
addMenuHandler(nodeType, function(insertOption) {
|
||||
insertOption({
|
||||
content: "⛓ Add link...",
|
||||
has_submenu: true,
|
||||
callback: (value, options, e, menu, node) => showAddLinkMenuCommon(value, options, e, menu, node, linkType)
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
152
js/node_options/addScripts.js
Normal file
152
js/node_options/addScripts.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
import { addNode } from "./common/utils.js";
|
||||
|
||||
const connectionMap = {
|
||||
"KSampler (Efficient)": ["input", 5],
|
||||
"KSampler Adv. (Efficient)": ["input", 5],
|
||||
"KSampler SDXL (Eff.)": ["input", 3],
|
||||
"XY Plot": ["output", 0],
|
||||
"Noise Control Script": ["input & output", 0],
|
||||
"HighRes-Fix Script": ["input & output", 0],
|
||||
"Tiled Upscaler Script": ["input & output", 0],
|
||||
"AnimateDiff Script": ["output", 0]
|
||||
};
|
||||
|
||||
/**
|
||||
* connect this node output to the input of another node
|
||||
* @method connect
|
||||
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
|
||||
* @param {LGraphNode} node the target node
|
||||
* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
|
||||
* @return {Object} the link_info is created, otherwise null
|
||||
LGraphNode.prototype.connect = function(output_slot, target_node, input_slot)
|
||||
**/
|
||||
|
||||
function addAndConnectScriptNode(scriptType, selectedNode) {
|
||||
const selectedNodeType = connectionMap[selectedNode.type];
|
||||
const newNodeType = connectionMap[scriptType];
|
||||
|
||||
// 1. Create the new node without position adjustments
|
||||
const newNode = addNode(scriptType, selectedNode, { shiftX: 0, shiftY: 0 });
|
||||
|
||||
// 2. Adjust position of the new node based on conditions
|
||||
if (newNodeType[0].includes("input") && selectedNodeType[0].includes("output")) {
|
||||
newNode.pos[0] += selectedNode.size[0] + 50;
|
||||
} else if (newNodeType[0].includes("output") && selectedNodeType[0].includes("input")) {
|
||||
newNode.pos[0] -= (newNode.size[0] + 50);
|
||||
}
|
||||
|
||||
// 3. Logic for connecting the nodes
|
||||
switch (selectedNodeType[0]) {
|
||||
case "output":
|
||||
if (newNodeType[0] === "input & output") {
|
||||
// For every node that was previously connected to the selectedNode's output
|
||||
const connectedNodes = selectedNode.getOutputNodes(selectedNodeType[1]);
|
||||
if (connectedNodes && connectedNodes.length) {
|
||||
for (let connectedNode of connectedNodes) {
|
||||
// Disconnect the node from selectedNode's output
|
||||
selectedNode.disconnectOutput(selectedNodeType[1]);
|
||||
// Connect the newNode's output to the previously connected node,
|
||||
// using the appropriate slot based on the type of the connectedNode
|
||||
const targetSlot = (connectedNode.type in connectionMap) ? connectionMap[connectedNode.type][1] : 0;
|
||||
newNode.connect(0, connectedNode, targetSlot);
|
||||
}
|
||||
}
|
||||
// Connect selectedNode's output to newNode's input
|
||||
selectedNode.connect(selectedNodeType[1], newNode, newNodeType[1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case "input":
|
||||
if (newNodeType[0] === "output") {
|
||||
newNode.connect(0, selectedNode, selectedNodeType[1]);
|
||||
} else if (newNodeType[0] === "input & output") {
|
||||
const ogInputNode = selectedNode.getInputNode(selectedNodeType[1]);
|
||||
if (ogInputNode) {
|
||||
ogInputNode.connect(0, newNode, 0);
|
||||
}
|
||||
newNode.connect(0, selectedNode, selectedNodeType[1]);
|
||||
}
|
||||
break;
|
||||
case "input & output":
|
||||
if (newNodeType[0] === "output") {
|
||||
newNode.connect(0, selectedNode, 0);
|
||||
} else if (newNodeType[0] === "input & output") {
|
||||
|
||||
const connectedNodes = selectedNode.getOutputNodes(0);
|
||||
if (connectedNodes && connectedNodes.length) {
|
||||
for (let connectedNode of connectedNodes) {
|
||||
selectedNode.disconnectOutput(0);
|
||||
newNode.connect(0, connectedNode, connectedNode.type in connectionMap ? connectionMap[connectedNode.type][1] : 0);
|
||||
}
|
||||
}
|
||||
// Connect selectedNode's output to newNode's input
|
||||
selectedNode.connect(selectedNodeType[1], newNode, newNodeType[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
function createScriptEntry(node, scriptType) {
|
||||
return {
|
||||
content: scriptType,
|
||||
callback: function() {
|
||||
addAndConnectScriptNode(scriptType, node);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getScriptOptions(nodeType, node) {
|
||||
const allScriptTypes = [
|
||||
"XY Plot",
|
||||
"Noise Control Script",
|
||||
"HighRes-Fix Script",
|
||||
"Tiled Upscaler Script",
|
||||
"AnimateDiff Script"
|
||||
];
|
||||
|
||||
// Filter script types based on node type
|
||||
const scriptTypes = allScriptTypes.filter(scriptType => {
|
||||
const scriptBehavior = connectionMap[scriptType][0];
|
||||
|
||||
if (connectionMap[nodeType][0] === "output") {
|
||||
return scriptBehavior.includes("input"); // Includes nodes that are "input" or "input & output"
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return scriptTypes.map(script => createScriptEntry(node, script));
|
||||
}
|
||||
|
||||
|
||||
function showAddScriptMenu(_, options, e, menu, node) {
|
||||
const scriptOptions = getScriptOptions(node.type, node);
|
||||
new LiteGraph.ContextMenu(scriptOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.addScripts",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (connectionMap[nodeData.name]) {
|
||||
addMenuHandler(nodeType, function(insertOption) {
|
||||
insertOption({
|
||||
content: "📜 Add script...",
|
||||
has_submenu: true,
|
||||
callback: showAddScriptMenu
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
89
js/node_options/addXYinputs.js
Normal file
89
js/node_options/addXYinputs.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler, addNode } from "./common/utils.js";
|
||||
|
||||
const nodePxOffsets = 80;
|
||||
|
||||
function getXYInputNodes() {
|
||||
return [
|
||||
"XY Input: Seeds++ Batch",
|
||||
"XY Input: Add/Return Noise",
|
||||
"XY Input: Steps",
|
||||
"XY Input: CFG Scale",
|
||||
"XY Input: Sampler/Scheduler",
|
||||
"XY Input: Denoise",
|
||||
"XY Input: VAE",
|
||||
"XY Input: Prompt S/R",
|
||||
"XY Input: Aesthetic Score",
|
||||
"XY Input: Refiner On/Off",
|
||||
"XY Input: Checkpoint",
|
||||
"XY Input: Clip Skip",
|
||||
"XY Input: LoRA",
|
||||
"XY Input: LoRA Plot",
|
||||
"XY Input: LoRA Stacks",
|
||||
"XY Input: Control Net",
|
||||
"XY Input: Control Net Plot",
|
||||
"XY Input: Manual XY Entry"
|
||||
];
|
||||
}
|
||||
|
||||
function showAddXYInputMenu(type, e, menu, node) {
|
||||
const specialNodes = [
|
||||
"XY Input: LoRA Plot",
|
||||
"XY Input: Control Net Plot",
|
||||
"XY Input: Manual XY Entry"
|
||||
];
|
||||
|
||||
const values = getXYInputNodes().map(nodeType => {
|
||||
return {
|
||||
content: nodeType,
|
||||
callback: function() {
|
||||
const newNode = addNode(nodeType, node);
|
||||
|
||||
// Calculate the left shift based on the width of the new node
|
||||
const shiftX = -(newNode.size[0] + 35);
|
||||
newNode.pos[0] += shiftX;
|
||||
|
||||
if (specialNodes.includes(nodeType)) {
|
||||
newNode.pos[1] += 20;
|
||||
// Connect both outputs to the XY Plot's 2nd and 3rd input.
|
||||
newNode.connect(0, node, 1);
|
||||
newNode.connect(1, node, 2);
|
||||
} else if (type === 'X') {
|
||||
newNode.pos[1] += 20;
|
||||
newNode.connect(0, node, 1); // Connect to 2nd input
|
||||
} else {
|
||||
newNode.pos[1] += node.size[1] + 45;
|
||||
newNode.connect(0, node, 2); // Connect to 3rd input
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
new LiteGraph.ContextMenu(values, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "efficiency.addXYinputs",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "XY Plot") {
|
||||
addMenuHandler(nodeType, function(insertOption) {
|
||||
insertOption({
|
||||
content: "✏️ Add 𝚇 input...",
|
||||
has_submenu: true,
|
||||
callback: (value, options, e, menu, node) => showAddXYInputMenu('X', e, menu, node)
|
||||
});
|
||||
insertOption({
|
||||
content: "✏️ Add 𝚈 input...",
|
||||
has_submenu: true,
|
||||
callback: (value, options, e, menu, node) => showAddXYInputMenu('Y', e, menu, node)
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
104
js/node_options/common/modelInfoDialog.css
Normal file
104
js/node_options/common/modelInfoDialog.css
Normal file
@@ -0,0 +1,104 @@
|
||||
.pysssss-model-info {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
max-width: 90vw;
|
||||
}
|
||||
.pysssss-model-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pysssss-model-info h2 {
|
||||
text-align: center;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.pysssss-model-info p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.pysssss-model-info a {
|
||||
color: dodgerblue;
|
||||
}
|
||||
.pysssss-model-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.pysssss-model-tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
gap: 10px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
.pysssss-model-tag {
|
||||
background-color: rgb(128, 213, 247);
|
||||
color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border-radius: 5px;
|
||||
padding: 2px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pysssss-model-tag--selected span::before {
|
||||
content: "✅";
|
||||
position: absolute;
|
||||
background-color: dodgerblue;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.pysssss-model-tag:hover {
|
||||
outline: 2px solid dodgerblue;
|
||||
}
|
||||
.pysssss-model-tag p {
|
||||
margin: 0;
|
||||
}
|
||||
.pysssss-model-tag span {
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
background-color: dodgerblue;
|
||||
color: #fff;
|
||||
padding: 2px;
|
||||
position: relative;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pysssss-model-metadata .comfy-modal-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
.pysssss-model-metadata label {
|
||||
margin-right: 1ch;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.pysssss-model-metadata span {
|
||||
color: dodgerblue;
|
||||
}
|
||||
|
||||
.pysssss-preview {
|
||||
max-width: 50%;
|
||||
margin-left: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.pysssss-preview img {
|
||||
max-height: 300px;
|
||||
}
|
||||
.pysssss-preview button {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.pysssss-model-notes {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.pysssss-model-notes:empty {
|
||||
display: none;
|
||||
}
|
||||
303
js/node_options/common/modelInfoDialog.js
Normal file
303
js/node_options/common/modelInfoDialog.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { addStylesheet } from "./utils.js";
|
||||
|
||||
addStylesheet(import.meta.url);
|
||||
|
||||
class MetadataDialog extends ComfyDialog {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.element.classList.add("pysssss-model-metadata");
|
||||
}
|
||||
show(metadata) {
|
||||
super.show(
|
||||
$el(
|
||||
"div",
|
||||
Object.keys(metadata).map((k) =>
|
||||
$el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })])
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelInfoDialog extends ComfyDialog {
|
||||
constructor(name) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.element.classList.add("pysssss-model-info");
|
||||
}
|
||||
|
||||
get customNotes() {
|
||||
return this.metadata["pysssss.notes"];
|
||||
}
|
||||
|
||||
set customNotes(v) {
|
||||
this.metadata["pysssss.notes"] = v;
|
||||
}
|
||||
|
||||
get hash() {
|
||||
return this.metadata["pysssss.sha256"];
|
||||
}
|
||||
|
||||
async show(type, value) {
|
||||
this.type = type;
|
||||
|
||||
const req = api.fetchApi("/pysssss/metadata/" + encodeURIComponent(`${type}/${value}`));
|
||||
this.info = $el("div", { style: { flex: "auto" } });
|
||||
this.img = $el("img", { style: { display: "none" } });
|
||||
this.imgWrapper = $el("div.pysssss-preview", [this.img]);
|
||||
this.main = $el("main", { style: { display: "flex" } }, [this.info, this.imgWrapper]);
|
||||
this.content = $el("div.pysssss-model-content", [$el("h2", { textContent: this.name }), this.main]);
|
||||
|
||||
const loading = $el("div", { textContent: "ℹ️ Loading...", parent: this.content });
|
||||
|
||||
super.show(this.content);
|
||||
|
||||
this.metadata = await (await req).json();
|
||||
this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = "";
|
||||
this.viewMetadata.removeAttribute("disabled");
|
||||
|
||||
loading.remove();
|
||||
this.addInfo();
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
const btns = super.createButtons();
|
||||
this.viewMetadata = $el("button", {
|
||||
type: "button",
|
||||
textContent: "View raw metadata",
|
||||
disabled: "disabled",
|
||||
style: {
|
||||
opacity: 0.5,
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
onclick: (e) => {
|
||||
if (this.metadata) {
|
||||
new MetadataDialog().show(this.metadata);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
btns.unshift(this.viewMetadata);
|
||||
return btns;
|
||||
}
|
||||
|
||||
getNoteInfo() {
|
||||
function parseNote() {
|
||||
if (!this.customNotes) return [];
|
||||
|
||||
let notes = [];
|
||||
// Extract links from notes
|
||||
const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
|
||||
let end = 0;
|
||||
let m;
|
||||
do {
|
||||
m = r.exec(this.customNotes);
|
||||
let pos;
|
||||
let fin = 0;
|
||||
if (m) {
|
||||
pos = m.index;
|
||||
fin = m.index + m[0].length;
|
||||
} else {
|
||||
pos = this.customNotes.length;
|
||||
}
|
||||
|
||||
let pre = this.customNotes.substring(end, pos);
|
||||
if (pre) {
|
||||
pre = pre.replaceAll("\n", "<br>");
|
||||
notes.push(
|
||||
$el("span", {
|
||||
innerHTML: pre,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (m) {
|
||||
notes.push(
|
||||
$el("a", {
|
||||
href: m[0],
|
||||
textContent: m[0],
|
||||
target: "_blank",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
end = fin;
|
||||
} while (m);
|
||||
return notes;
|
||||
}
|
||||
|
||||
let textarea;
|
||||
let notesContainer;
|
||||
const editText = "✏️ Edit";
|
||||
const edit = $el("a", {
|
||||
textContent: editText,
|
||||
href: "#",
|
||||
style: {
|
||||
float: "right",
|
||||
color: "greenyellow",
|
||||
textDecoration: "none",
|
||||
},
|
||||
onclick: async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (textarea) {
|
||||
this.customNotes = textarea.value;
|
||||
|
||||
const resp = await api.fetchApi(
|
||||
"/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
|
||||
{
|
||||
method: "POST",
|
||||
body: this.customNotes,
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.status !== 200) {
|
||||
console.error(resp);
|
||||
alert(`Error saving notes (${req.status}) ${req.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.textContent = editText;
|
||||
textarea.remove();
|
||||
textarea = null;
|
||||
|
||||
notesContainer.replaceChildren(...parseNote.call(this));
|
||||
} else {
|
||||
e.target.textContent = "💾 Save";
|
||||
textarea = $el("textarea", {
|
||||
style: {
|
||||
width: "100%",
|
||||
minWidth: "200px",
|
||||
minHeight: "50px",
|
||||
},
|
||||
textContent: this.customNotes,
|
||||
});
|
||||
e.target.after(textarea);
|
||||
notesContainer.replaceChildren();
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
notesContainer = $el("div.pysssss-model-notes", parseNote.call(this));
|
||||
return $el(
|
||||
"div",
|
||||
{
|
||||
style: { display: "contents" },
|
||||
},
|
||||
[edit, notesContainer]
|
||||
);
|
||||
}
|
||||
|
||||
addInfo() {
|
||||
this.addInfoEntry("Notes", this.getNoteInfo());
|
||||
}
|
||||
|
||||
addInfoEntry(name, value) {
|
||||
return $el(
|
||||
"p",
|
||||
{
|
||||
parent: this.info,
|
||||
},
|
||||
[
|
||||
typeof name === "string" ? $el("label", { textContent: name + ": " }) : name,
|
||||
typeof value === "string" ? $el("span", { textContent: value }) : value,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async getCivitaiDetails() {
|
||||
const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash);
|
||||
if (req.status === 200) {
|
||||
return await req.json();
|
||||
} else if (req.status === 404) {
|
||||
throw new Error("Model not found");
|
||||
} else {
|
||||
throw new Error(`Error loading info (${req.status}) ${req.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
addCivitaiInfo() {
|
||||
const promise = this.getCivitaiDetails();
|
||||
const content = $el("span", { textContent: "ℹ️ Loading..." });
|
||||
|
||||
this.addInfoEntry(
|
||||
$el("label", [
|
||||
$el("img", {
|
||||
style: {
|
||||
width: "18px",
|
||||
position: "relative",
|
||||
top: "3px",
|
||||
margin: "0 5px 0 0",
|
||||
},
|
||||
src: "https://civitai.com/favicon.ico",
|
||||
}),
|
||||
$el("span", { textContent: "Civitai: " }),
|
||||
]),
|
||||
content
|
||||
);
|
||||
|
||||
return promise
|
||||
.then((info) => {
|
||||
content.replaceChildren(
|
||||
$el("a", {
|
||||
href: "https://civitai.com/models/" + info.modelId,
|
||||
textContent: "View " + info.model.name,
|
||||
target: "_blank",
|
||||
})
|
||||
);
|
||||
|
||||
if (info.images?.length) {
|
||||
this.img.src = info.images[0].url;
|
||||
this.img.style.display = "";
|
||||
|
||||
this.imgSave = $el("button", {
|
||||
textContent: "Use as preview",
|
||||
parent: this.imgWrapper,
|
||||
onclick: async () => {
|
||||
// Convert the preview to a blob
|
||||
const blob = await (await fetch(this.img.src)).blob();
|
||||
|
||||
// Store it in temp
|
||||
const name = "temp_preview." + new URL(this.img.src).pathname.split(".")[1];
|
||||
const body = new FormData();
|
||||
body.append("image", new File([blob], name));
|
||||
body.append("overwrite", "true");
|
||||
body.append("type", "temp");
|
||||
|
||||
const resp = await api.fetchApi("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (resp.status !== 200) {
|
||||
console.error(resp);
|
||||
alert(`Error saving preview (${req.status}) ${req.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use as preview
|
||||
await api.fetchApi("/pysssss/save/" + encodeURIComponent(`${this.type}/${this.name}`), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
filename: name,
|
||||
type: "temp",
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
app.refreshComboInNodes();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return info;
|
||||
})
|
||||
.catch((err) => {
|
||||
content.textContent = "⚠️ " + err.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
94
js/node_options/common/utils.js
Normal file
94
js/node_options/common/utils.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { app } from '../../../../scripts/app.js'
|
||||
import { $el } from "../../../../scripts/ui.js";
|
||||
|
||||
export function addStylesheet(url) {
|
||||
if (url.endsWith(".js")) {
|
||||
url = url.substr(0, url.length - 2) + "css";
|
||||
}
|
||||
$el("link", {
|
||||
parent: document.head,
|
||||
rel: "stylesheet",
|
||||
type: "text/css",
|
||||
href: url.startsWith("http") ? url : getUrl(url),
|
||||
});
|
||||
}
|
||||
|
||||
export function getUrl(path, baseUrl) {
|
||||
if (baseUrl) {
|
||||
return new URL(path, baseUrl).toString();
|
||||
} else {
|
||||
return new URL("../" + path, import.meta.url).toString();
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadImage(url) {
|
||||
return new Promise((res, rej) => {
|
||||
const img = new Image();
|
||||
img.onload = res;
|
||||
img.onerror = rej;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
export function addMenuHandler(nodeType, cb) {
|
||||
|
||||
const GROUPED_MENU_ORDER = {
|
||||
"🔄 Swap with...": 0,
|
||||
"⛓ Add link...": 1,
|
||||
"📜 Add script...": 2,
|
||||
"🔍 View model info...": 3,
|
||||
"🌱 Seed behavior...": 4,
|
||||
"📐 Set Resolution...": 5,
|
||||
"✏️ Add 𝚇 input...": 6,
|
||||
"✏️ Add 𝚈 input...": 7
|
||||
};
|
||||
|
||||
const originalGetOpts = nodeType.prototype.getExtraMenuOptions;
|
||||
|
||||
nodeType.prototype.getExtraMenuOptions = function () {
|
||||
let r = originalGetOpts ? originalGetOpts.apply(this, arguments) || [] : [];
|
||||
|
||||
const insertOption = (option) => {
|
||||
if (GROUPED_MENU_ORDER.hasOwnProperty(option.content)) {
|
||||
// Find the right position for the option
|
||||
let targetPos = r.length; // default to the end
|
||||
|
||||
for (let i = 0; i < r.length; i++) {
|
||||
if (GROUPED_MENU_ORDER.hasOwnProperty(r[i].content) &&
|
||||
GROUPED_MENU_ORDER[option.content] < GROUPED_MENU_ORDER[r[i].content]) {
|
||||
targetPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Insert the option at the determined position
|
||||
r.splice(targetPos, 0, option);
|
||||
} else {
|
||||
// If the option is not in the GROUPED_MENU_ORDER, simply add it to the end
|
||||
r.push(option);
|
||||
}
|
||||
};
|
||||
|
||||
cb.call(this, insertOption);
|
||||
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
export function findWidgetByName(node, widgetName) {
|
||||
return node.widgets.find(widget => widget.name === widgetName);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
export function addNode(name, nextTo, options) {
|
||||
options = { select: true, shiftX: 0, shiftY: 0, before: false, ...(options || {}) };
|
||||
const node = LiteGraph.createNode(name);
|
||||
app.graph.add(node);
|
||||
node.pos = [
|
||||
nextTo.pos[0] + options.shiftX,
|
||||
nextTo.pos[1] + options.shiftY,
|
||||
];
|
||||
if (options.select) {
|
||||
app.canvas.selectNode(node, false);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
336
js/node_options/modelInfo.js
Normal file
336
js/node_options/modelInfo.js
Normal file
@@ -0,0 +1,336 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { $el } from "../../../scripts/ui.js";
|
||||
import { ModelInfoDialog } from "./common/modelInfoDialog.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
|
||||
const MAX_TAGS = 500;
|
||||
|
||||
class LoraInfoDialog extends ModelInfoDialog {
|
||||
getTagFrequency() {
|
||||
if (!this.metadata.ss_tag_frequency) return [];
|
||||
|
||||
const datasets = JSON.parse(this.metadata.ss_tag_frequency);
|
||||
const tags = {};
|
||||
for (const setName in datasets) {
|
||||
const set = datasets[setName];
|
||||
for (const t in set) {
|
||||
if (t in tags) {
|
||||
tags[t] += set[t];
|
||||
} else {
|
||||
tags[t] = set[t];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(tags).sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
getResolutions() {
|
||||
let res = [];
|
||||
if (this.metadata.ss_bucket_info) {
|
||||
const parsed = JSON.parse(this.metadata.ss_bucket_info);
|
||||
if (parsed?.buckets) {
|
||||
for (const { resolution, count } of Object.values(parsed.buckets)) {
|
||||
res.push([count, `${resolution.join("x")} * ${count}`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]);
|
||||
let r = this.metadata.ss_resolution;
|
||||
if (r) {
|
||||
const s = r.split(",");
|
||||
const w = s[0].replace("(", "");
|
||||
const h = s[1].replace(")", "");
|
||||
res.push(`${w.trim()}x${h.trim()} (Base res)`);
|
||||
} else if ((r = this.metadata["modelspec.resolution"])) {
|
||||
res.push(r + " (Base res");
|
||||
}
|
||||
if (!res.length) {
|
||||
res.push("⚠️ Unknown");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
getTagList(tags) {
|
||||
return tags.map((t) =>
|
||||
$el(
|
||||
"li.pysssss-model-tag",
|
||||
{
|
||||
dataset: {
|
||||
tag: t[0],
|
||||
},
|
||||
$: (el) => {
|
||||
el.onclick = () => {
|
||||
el.classList.toggle("pysssss-model-tag--selected");
|
||||
};
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("p", {
|
||||
textContent: t[0],
|
||||
}),
|
||||
$el("span", {
|
||||
textContent: t[1],
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addTags() {
|
||||
let tags = this.getTagFrequency();
|
||||
let hasMore;
|
||||
if (tags?.length) {
|
||||
const c = tags.length;
|
||||
let list;
|
||||
if (c > MAX_TAGS) {
|
||||
tags = tags.slice(0, MAX_TAGS);
|
||||
hasMore = $el("p", [
|
||||
$el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }),
|
||||
$el("a", {
|
||||
href: "#",
|
||||
textContent: `Show all ${c}`,
|
||||
onclick: () => {
|
||||
list.replaceChildren(...this.getTagList(this.getTagFrequency()));
|
||||
hasMore.remove();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
list = $el("ol.pysssss-model-tags-list", this.getTagList(tags));
|
||||
this.tags = $el("div", [list]);
|
||||
} else {
|
||||
this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" });
|
||||
}
|
||||
|
||||
this.content.append(this.tags);
|
||||
|
||||
if (hasMore) {
|
||||
this.content.append(hasMore);
|
||||
}
|
||||
}
|
||||
|
||||
async addInfo() {
|
||||
this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
|
||||
this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
|
||||
this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
|
||||
|
||||
this.addInfoEntry(
|
||||
"Resolution",
|
||||
$el(
|
||||
"select",
|
||||
this.getResolutions().map((r) => $el("option", { textContent: r }))
|
||||
)
|
||||
);
|
||||
|
||||
super.addInfo();
|
||||
const p = this.addCivitaiInfo();
|
||||
this.addTags();
|
||||
|
||||
const info = await p;
|
||||
if (info) {
|
||||
$el(
|
||||
"p",
|
||||
{
|
||||
parent: this.content,
|
||||
textContent: "Trained Words: ",
|
||||
},
|
||||
[
|
||||
$el("pre", {
|
||||
textContent: info.trainedWords.join(", "),
|
||||
style: {
|
||||
whiteSpace: "pre-wrap",
|
||||
margin: "10px 0",
|
||||
background: "#222",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
},
|
||||
}),
|
||||
]
|
||||
);
|
||||
$el("div", {
|
||||
parent: this.content,
|
||||
innerHTML: info.description,
|
||||
style: {
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
const btns = super.createButtons();
|
||||
|
||||
function copyTags(e, tags) {
|
||||
const textarea = $el("textarea", {
|
||||
parent: document.body,
|
||||
style: {
|
||||
position: "fixed",
|
||||
},
|
||||
textContent: tags.map((el) => el.dataset.tag).join(", "),
|
||||
});
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
if (!e.target.dataset.text) {
|
||||
e.target.dataset.text = e.target.textContent;
|
||||
}
|
||||
e.target.textContent = "Copied " + tags.length + " tags";
|
||||
setTimeout(() => {
|
||||
e.target.textContent = e.target.dataset.text;
|
||||
}, 1000);
|
||||
} catch (ex) {
|
||||
prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
btns.unshift(
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Copy Selected",
|
||||
onclick: (e) => {
|
||||
copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag--selected")]);
|
||||
},
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Copy All",
|
||||
onclick: (e) => {
|
||||
copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag")]);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return btns;
|
||||
}
|
||||
}
|
||||
|
||||
class CheckpointInfoDialog extends ModelInfoDialog {
|
||||
async addInfo() {
|
||||
super.addInfo();
|
||||
const info = await this.addCivitaiInfo();
|
||||
if (info) {
|
||||
this.addInfoEntry("Base Model", info.baseModel || "⚠️ Unknown");
|
||||
|
||||
$el("div", {
|
||||
parent: this.content,
|
||||
innerHTML: info.description,
|
||||
style: {
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateNames = (prefix, start, end) => {
|
||||
const result = [];
|
||||
if (start < end) {
|
||||
for (let i = start; i <= end; i++) {
|
||||
result.push(`${prefix}${i}`);
|
||||
}
|
||||
} else {
|
||||
for (let i = start; i >= end; i--) {
|
||||
result.push(`${prefix}${i}`);
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NOTE: Orders reversed so they appear in ascending order
|
||||
const infoHandler = {
|
||||
"Efficient Loader": {
|
||||
"loras": ["lora_name"],
|
||||
"checkpoints": ["ckpt_name"]
|
||||
},
|
||||
"Eff. Loader SDXL": {
|
||||
"checkpoints": ["refiner_ckpt_name", "base_ckpt_name"]
|
||||
},
|
||||
"LoRA Stacker": {
|
||||
"loras": generateNames("lora_name_", 50, 1)
|
||||
},
|
||||
"XY Input: LoRA": {
|
||||
"loras": generateNames("lora_name_", 50, 1)
|
||||
},
|
||||
"HighRes-Fix Script": {
|
||||
"checkpoints": ["hires_ckpt_name"]
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions and other parts of your code remain unchanged
|
||||
|
||||
app.registerExtension({
|
||||
name: "efficiency.ModelInfo",
|
||||
beforeRegisterNodeDef(nodeType) {
|
||||
const types = infoHandler[nodeType.comfyClass];
|
||||
|
||||
if (types) {
|
||||
addMenuHandler(nodeType, function (insertOption) { // Here, we are calling addMenuHandler
|
||||
let submenuItems = []; // to store submenu items
|
||||
|
||||
const addSubMenuOption = (type, widgetNames) => {
|
||||
widgetNames.forEach(widgetName => {
|
||||
const widgetValue = this.widgets.find(w => w.name === widgetName)?.value;
|
||||
|
||||
// Check if widgetValue is "None"
|
||||
if (!widgetValue || widgetValue === "None") {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = widgetValue;
|
||||
if (value.content) {
|
||||
value = value.content;
|
||||
}
|
||||
const cls = type === "loras" ? LoraInfoDialog : CheckpointInfoDialog;
|
||||
|
||||
const label = widgetName;
|
||||
|
||||
// Push to submenuItems
|
||||
submenuItems.push({
|
||||
content: label,
|
||||
callback: async () => {
|
||||
new cls(value).show(type, value);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof types === 'object') {
|
||||
Object.keys(types).forEach(type => {
|
||||
addSubMenuOption(type, types[type]);
|
||||
});
|
||||
}
|
||||
|
||||
// If we have submenu items, use insertOption
|
||||
if (submenuItems.length) {
|
||||
insertOption({ // Using insertOption here
|
||||
content: "🔍 View model info...",
|
||||
has_submenu: true,
|
||||
callback: (value, options, e, menu, node) => {
|
||||
new LiteGraph.ContextMenu(submenuItems, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false; // This ensures the original context menu doesn't proceed
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
88
js/node_options/setResolution.js
Normal file
88
js/node_options/setResolution.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// Additional functions and imports
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler, findWidgetByName } from "./common/utils.js";
|
||||
|
||||
// A mapping for resolutions based on the type of the loader
|
||||
const RESOLUTIONS = {
|
||||
"Efficient Loader": [
|
||||
{width: 512, height: 512},
|
||||
{width: 512, height: 768},
|
||||
{width: 512, height: 640},
|
||||
{width: 640, height: 512},
|
||||
{width: 640, height: 768},
|
||||
{width: 640, height: 640},
|
||||
{width: 768, height: 512},
|
||||
{width: 768, height: 768},
|
||||
{width: 768, height: 640},
|
||||
],
|
||||
"Eff. Loader SDXL": [
|
||||
{width: 1024, height: 1024},
|
||||
{width: 1152, height: 896},
|
||||
{width: 896, height: 1152},
|
||||
{width: 1216, height: 832},
|
||||
{width: 832, height: 1216},
|
||||
{width: 1344, height: 768},
|
||||
{width: 768, height: 1344},
|
||||
{width: 1536, height: 640},
|
||||
{width: 640, height: 1536}
|
||||
]
|
||||
};
|
||||
|
||||
// Function to set the resolution of a node
|
||||
function setNodeResolution(node, width, height) {
|
||||
let widthWidget = findWidgetByName(node, "empty_latent_width");
|
||||
let heightWidget = findWidgetByName(node, "empty_latent_height");
|
||||
|
||||
if (widthWidget) {
|
||||
widthWidget.value = width;
|
||||
}
|
||||
|
||||
if (heightWidget) {
|
||||
heightWidget.value = height;
|
||||
}
|
||||
}
|
||||
|
||||
// The callback for the resolution submenu
|
||||
function resolutionMenuCallback(node, width, height) {
|
||||
return function() {
|
||||
setNodeResolution(node, width, height);
|
||||
};
|
||||
}
|
||||
|
||||
// Show the set resolution submenu
|
||||
function showResolutionMenu(value, options, e, menu, node) {
|
||||
const resolutions = RESOLUTIONS[node.type];
|
||||
if (!resolutions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolutionOptions = resolutions.map(res => ({
|
||||
content: `${res.width} x ${res.height}`,
|
||||
callback: resolutionMenuCallback(node, res.width, res.height)
|
||||
}));
|
||||
|
||||
new LiteGraph.ContextMenu(resolutionOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false; // This ensures the original context menu doesn't proceed
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.SetResolution",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (["Efficient Loader", "Eff. Loader SDXL"].includes(nodeData.name)) {
|
||||
addMenuHandler(nodeType, function (insertOption) {
|
||||
insertOption({
|
||||
content: "📐 Set Resolution...",
|
||||
has_submenu: true,
|
||||
callback: showResolutionMenu
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
135
js/node_options/swapLoaders.js
Normal file
135
js/node_options/swapLoaders.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
import { findWidgetByName } from "./common/utils.js";
|
||||
|
||||
function replaceNode(oldNode, newNodeName) {
|
||||
const newNode = LiteGraph.createNode(newNodeName);
|
||||
if (!newNode) {
|
||||
return;
|
||||
}
|
||||
app.graph.add(newNode);
|
||||
|
||||
newNode.pos = oldNode.pos.slice();
|
||||
newNode.size = oldNode.size.slice();
|
||||
|
||||
// Transfer widget values
|
||||
const widgetMapping = {
|
||||
"ckpt_name": "base_ckpt_name",
|
||||
"vae_name": "vae_name",
|
||||
"clip_skip": "base_clip_skip",
|
||||
"positive": "positive",
|
||||
"negative": "negative",
|
||||
"prompt_style": "prompt_style",
|
||||
"empty_latent_width": "empty_latent_width",
|
||||
"empty_latent_height": "empty_latent_height",
|
||||
"batch_size": "batch_size"
|
||||
};
|
||||
|
||||
let effectiveWidgetMapping = widgetMapping;
|
||||
|
||||
// Invert the mapping when going from "Eff. Loader SDXL" to "Efficient Loader"
|
||||
if (oldNode.type === "Eff. Loader SDXL" && newNodeName === "Efficient Loader") {
|
||||
effectiveWidgetMapping = {};
|
||||
for (const [key, value] of Object.entries(widgetMapping)) {
|
||||
effectiveWidgetMapping[value] = key;
|
||||
}
|
||||
}
|
||||
|
||||
oldNode.widgets.forEach(widget => {
|
||||
const newName = effectiveWidgetMapping[widget.name];
|
||||
if (newName) {
|
||||
const newWidget = findWidgetByName(newNode, newName);
|
||||
if (newWidget) {
|
||||
newWidget.value = widget.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hardcoded transfer for specific outputs based on the output names from the nodes in the image
|
||||
const outputMapping = {
|
||||
"MODEL": null, // Not present in "Eff. Loader SDXL"
|
||||
"CONDITIONING+": null, // Not present in "Eff. Loader SDXL"
|
||||
"CONDITIONING-": null, // Not present in "Eff. Loader SDXL"
|
||||
"LATENT": "LATENT",
|
||||
"VAE": "VAE",
|
||||
"CLIP": null, // Not present in "Eff. Loader SDXL"
|
||||
"DEPENDENCIES": "DEPENDENCIES"
|
||||
};
|
||||
|
||||
// Transfer connections from old node outputs to new node outputs based on the outputMapping
|
||||
oldNode.outputs.forEach((output, index) => {
|
||||
if (output && output.links && outputMapping[output.name]) {
|
||||
const newOutputName = outputMapping[output.name];
|
||||
|
||||
// If the new node does not have this output, skip
|
||||
if (newOutputName === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOutputIndex = newNode.findOutputSlot(newOutputName);
|
||||
if (newOutputIndex !== -1) {
|
||||
output.links.forEach(link => {
|
||||
const targetLinkInfo = oldNode.graph.links[link];
|
||||
if (targetLinkInfo) {
|
||||
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
|
||||
if (targetNode) {
|
||||
newNode.connect(newOutputIndex, targetNode, targetLinkInfo.target_slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old node
|
||||
app.graph.remove(oldNode);
|
||||
}
|
||||
|
||||
function replaceNodeMenuCallback(currentNode, targetNodeName) {
|
||||
return function() {
|
||||
replaceNode(currentNode, targetNodeName);
|
||||
};
|
||||
}
|
||||
|
||||
function showSwapMenu(value, options, e, menu, node) {
|
||||
const swapOptions = [];
|
||||
|
||||
if (node.type !== "Efficient Loader") {
|
||||
swapOptions.push({
|
||||
content: "Efficient Loader",
|
||||
callback: replaceNodeMenuCallback(node, "Efficient Loader")
|
||||
});
|
||||
}
|
||||
|
||||
if (node.type !== "Eff. Loader SDXL") {
|
||||
swapOptions.push({
|
||||
content: "Eff. Loader SDXL",
|
||||
callback: replaceNodeMenuCallback(node, "Eff. Loader SDXL")
|
||||
});
|
||||
}
|
||||
|
||||
new LiteGraph.ContextMenu(swapOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false; // This ensures the original context menu doesn't proceed
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.SwapLoaders",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (["Efficient Loader", "Eff. Loader SDXL"].includes(nodeData.name)) {
|
||||
addMenuHandler(nodeType, function (insertOption) {
|
||||
insertOption({
|
||||
content: "🔄 Swap with...",
|
||||
has_submenu: true,
|
||||
callback: showSwapMenu
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
191
js/node_options/swapSamplers.js
Normal file
191
js/node_options/swapSamplers.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
import { findWidgetByName } from "./common/utils.js";
|
||||
|
||||
function replaceNode(oldNode, newNodeName) {
|
||||
// Create new node
|
||||
const newNode = LiteGraph.createNode(newNodeName);
|
||||
if (!newNode) {
|
||||
return;
|
||||
}
|
||||
app.graph.add(newNode);
|
||||
|
||||
// Position new node at the same position as the old node
|
||||
newNode.pos = oldNode.pos.slice();
|
||||
|
||||
// Define widget mappings
|
||||
const mappings = {
|
||||
"KSampler (Efficient) <-> KSampler Adv. (Efficient)": {
|
||||
seed: "noise_seed",
|
||||
cfg: "cfg",
|
||||
sampler_name: "sampler_name",
|
||||
scheduler: "scheduler",
|
||||
preview_method: "preview_method",
|
||||
vae_decode: "vae_decode"
|
||||
},
|
||||
"KSampler (Efficient) <-> KSampler SDXL (Eff.)": {
|
||||
seed: "noise_seed",
|
||||
cfg: "cfg",
|
||||
sampler_name: "sampler_name",
|
||||
scheduler: "scheduler",
|
||||
preview_method: "preview_method",
|
||||
vae_decode: "vae_decode"
|
||||
},
|
||||
"KSampler Adv. (Efficient) <-> KSampler SDXL (Eff.)": {
|
||||
noise_seed: "noise_seed",
|
||||
steps: "steps",
|
||||
cfg: "cfg",
|
||||
sampler_name: "sampler_name",
|
||||
scheduler: "scheduler",
|
||||
start_at_step: "start_at_step",
|
||||
preview_method: "preview_method",
|
||||
vae_decode: "vae_decode"}
|
||||
};
|
||||
|
||||
const swapKey = `${oldNode.type} <-> ${newNodeName}`;
|
||||
|
||||
let widgetMapping = {};
|
||||
|
||||
// Check if a reverse mapping is needed
|
||||
if (!mappings[swapKey]) {
|
||||
const reverseKey = `${newNodeName} <-> ${oldNode.type}`;
|
||||
const reverseMapping = mappings[reverseKey];
|
||||
if (reverseMapping) {
|
||||
widgetMapping = Object.entries(reverseMapping).reduce((acc, [key, value]) => {
|
||||
acc[value] = key;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
} else {
|
||||
widgetMapping = mappings[swapKey];
|
||||
}
|
||||
|
||||
if (oldNode.type === "KSampler (Efficient)" && (newNodeName === "KSampler Adv. (Efficient)" || newNodeName === "KSampler SDXL (Eff.)")) {
|
||||
const denoise = Math.min(Math.max(findWidgetByName(oldNode, "denoise").value, 0), 1); // Ensure denoise is between 0 and 1
|
||||
const steps = Math.min(Math.max(findWidgetByName(oldNode, "steps").value, 0), 10000); // Ensure steps is between 0 and 10000
|
||||
|
||||
const total_steps = Math.floor(steps / denoise);
|
||||
const start_at_step = total_steps - steps;
|
||||
|
||||
findWidgetByName(newNode, "steps").value = Math.min(Math.max(total_steps, 0), 10000); // Ensure total_steps is between 0 and 10000
|
||||
findWidgetByName(newNode, "start_at_step").value = Math.min(Math.max(start_at_step, 0), 10000); // Ensure start_at_step is between 0 and 10000
|
||||
}
|
||||
else if ((oldNode.type === "KSampler Adv. (Efficient)" || oldNode.type === "KSampler SDXL (Eff.)") && newNodeName === "KSampler (Efficient)") {
|
||||
const stepsAdv = Math.min(Math.max(findWidgetByName(oldNode, "steps").value, 0), 10000); // Ensure stepsAdv is between 0 and 10000
|
||||
const start_at_step = Math.min(Math.max(findWidgetByName(oldNode, "start_at_step").value, 0), 10000); // Ensure start_at_step is between 0 and 10000
|
||||
|
||||
const denoise = Math.min(Math.max((stepsAdv - start_at_step) / stepsAdv, 0), 1); // Ensure denoise is between 0 and 1
|
||||
const stepsTotal = stepsAdv - start_at_step;
|
||||
|
||||
findWidgetByName(newNode, "denoise").value = denoise;
|
||||
findWidgetByName(newNode, "steps").value = Math.min(Math.max(stepsTotal, 0), 10000); // Ensure stepsTotal is between 0 and 10000
|
||||
}
|
||||
|
||||
// Transfer widget values from old node to new node
|
||||
oldNode.widgets.forEach(widget => {
|
||||
const newName = widgetMapping[widget.name];
|
||||
if (newName) {
|
||||
const newWidget = findWidgetByName(newNode, newName);
|
||||
if (newWidget) {
|
||||
newWidget.value = widget.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Determine the starting indices based on the node types
|
||||
let oldNodeInputStartIndex = 0;
|
||||
let newNodeInputStartIndex = 0;
|
||||
let oldNodeOutputStartIndex = 0;
|
||||
let newNodeOutputStartIndex = 0;
|
||||
|
||||
if (oldNode.type === "KSampler SDXL (Eff.)" || newNodeName === "KSampler SDXL (Eff.)") {
|
||||
oldNodeInputStartIndex = (oldNode.type === "KSampler SDXL (Eff.)") ? 1 : 3;
|
||||
newNodeInputStartIndex = (newNodeName === "KSampler SDXL (Eff.)") ? 1 : 3;
|
||||
oldNodeOutputStartIndex = (oldNode.type === "KSampler SDXL (Eff.)") ? 1 : 3;
|
||||
newNodeOutputStartIndex = (newNodeName === "KSampler SDXL (Eff.)") ? 1 : 3;
|
||||
}
|
||||
|
||||
// Transfer connections from old node to new node
|
||||
oldNode.inputs.slice(oldNodeInputStartIndex).forEach((input, index) => {
|
||||
if (input && input.link !== null) {
|
||||
const originLinkInfo = oldNode.graph.links[input.link];
|
||||
if (originLinkInfo) {
|
||||
const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
|
||||
if (originNode) {
|
||||
originNode.connect(originLinkInfo.origin_slot, newNode, index + newNodeInputStartIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
oldNode.outputs.slice(oldNodeOutputStartIndex).forEach((output, index) => {
|
||||
if (output && output.links) {
|
||||
output.links.forEach(link => {
|
||||
const targetLinkInfo = oldNode.graph.links[link];
|
||||
if (targetLinkInfo) {
|
||||
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
|
||||
if (targetNode) {
|
||||
newNode.connect(index + newNodeOutputStartIndex, targetNode, targetLinkInfo.target_slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old node
|
||||
app.graph.remove(oldNode);
|
||||
}
|
||||
|
||||
function replaceNodeMenuCallback(currentNode, targetNodeName) {
|
||||
return function() {
|
||||
replaceNode(currentNode, targetNodeName);
|
||||
};
|
||||
}
|
||||
|
||||
function showSwapMenu(value, options, e, menu, node) {
|
||||
const swapOptions = [];
|
||||
|
||||
if (node.type !== "KSampler (Efficient)") {
|
||||
swapOptions.push({
|
||||
content: "KSampler (Efficient)",
|
||||
callback: replaceNodeMenuCallback(node, "KSampler (Efficient)")
|
||||
});
|
||||
}
|
||||
if (node.type !== "KSampler Adv. (Efficient)") {
|
||||
swapOptions.push({
|
||||
content: "KSampler Adv. (Efficient)",
|
||||
callback: replaceNodeMenuCallback(node, "KSampler Adv. (Efficient)")
|
||||
});
|
||||
}
|
||||
if (node.type !== "KSampler SDXL (Eff.)") {
|
||||
swapOptions.push({
|
||||
content: "KSampler SDXL (Eff.)",
|
||||
callback: replaceNodeMenuCallback(node, "KSampler SDXL (Eff.)")
|
||||
});
|
||||
}
|
||||
|
||||
new LiteGraph.ContextMenu(swapOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false; // This ensures the original context menu doesn't proceed
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.SwapSamplers",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (["KSampler (Efficient)", "KSampler Adv. (Efficient)", "KSampler SDXL (Eff.)"].includes(nodeData.name)) {
|
||||
addMenuHandler(nodeType, function (insertOption) {
|
||||
insertOption({
|
||||
content: "🔄 Swap with...",
|
||||
has_submenu: true,
|
||||
callback: showSwapMenu
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
100
js/node_options/swapScripts.js
Normal file
100
js/node_options/swapScripts.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
|
||||
function replaceNode(oldNode, newNodeName) {
|
||||
const newNode = LiteGraph.createNode(newNodeName);
|
||||
if (!newNode) {
|
||||
return;
|
||||
}
|
||||
app.graph.add(newNode);
|
||||
|
||||
newNode.pos = oldNode.pos.slice();
|
||||
|
||||
// Transfer connections from old node to new node
|
||||
// XY Plot and AnimateDiff have only one output
|
||||
if(["XY Plot", "AnimateDiff Script"].includes(oldNode.type)) {
|
||||
if (oldNode.outputs[0] && oldNode.outputs[0].links) {
|
||||
oldNode.outputs[0].links.forEach(link => {
|
||||
const targetLinkInfo = oldNode.graph.links[link];
|
||||
if (targetLinkInfo) {
|
||||
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
|
||||
if (targetNode) {
|
||||
newNode.connect(0, targetNode, targetLinkInfo.target_slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Noise Control Script, HighRes-Fix Script, and Tiled Upscaler Script have 1 input and 1 output at index 0
|
||||
if (oldNode.inputs[0] && oldNode.inputs[0].link !== null) {
|
||||
const originLinkInfo = oldNode.graph.links[oldNode.inputs[0].link];
|
||||
if (originLinkInfo) {
|
||||
const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
|
||||
if (originNode) {
|
||||
originNode.connect(originLinkInfo.origin_slot, newNode, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNode.outputs[0] && oldNode.outputs[0].links) {
|
||||
oldNode.outputs[0].links.forEach(link => {
|
||||
const targetLinkInfo = oldNode.graph.links[link];
|
||||
if (targetLinkInfo) {
|
||||
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
|
||||
if (targetNode) {
|
||||
newNode.connect(0, targetNode, targetLinkInfo.target_slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old node
|
||||
app.graph.remove(oldNode);
|
||||
}
|
||||
|
||||
function replaceNodeMenuCallback(currentNode, targetNodeName) {
|
||||
return function() {
|
||||
replaceNode(currentNode, targetNodeName);
|
||||
};
|
||||
}
|
||||
|
||||
function showSwapMenu(value, options, e, menu, node) {
|
||||
const scriptNodes = [
|
||||
"XY Plot",
|
||||
"Noise Control Script",
|
||||
"HighRes-Fix Script",
|
||||
"Tiled Upscaler Script",
|
||||
"AnimateDiff Script"
|
||||
];
|
||||
|
||||
const swapOptions = scriptNodes.filter(n => n !== node.type).map(n => ({
|
||||
content: n,
|
||||
callback: replaceNodeMenuCallback(node, n)
|
||||
}));
|
||||
|
||||
new LiteGraph.ContextMenu(swapOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.SwapScripts",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (["XY Plot", "Noise Control Script", "HighRes-Fix Script", "Tiled Upscaler Script", "AnimateDiff Script"].includes(nodeData.name)) {
|
||||
addMenuHandler(nodeType, function (insertOption) {
|
||||
insertOption({
|
||||
content: "🔄 Swap with...",
|
||||
has_submenu: true,
|
||||
callback: showSwapMenu
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
98
js/node_options/swapXYinputs.js
Normal file
98
js/node_options/swapXYinputs.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { addMenuHandler } from "./common/utils.js";
|
||||
|
||||
function replaceNode(oldNode, newNodeName) {
|
||||
const newNode = LiteGraph.createNode(newNodeName);
|
||||
if (!newNode) {
|
||||
return;
|
||||
}
|
||||
app.graph.add(newNode);
|
||||
|
||||
newNode.pos = oldNode.pos.slice();
|
||||
|
||||
// Handle the special nodes with two outputs
|
||||
const nodesWithTwoOutputs = ["XY Input: LoRA Plot", "XY Input: Control Net Plot", "XY Input: Manual XY Entry"];
|
||||
let outputCount = nodesWithTwoOutputs.includes(oldNode.type) ? 2 : 1;
|
||||
|
||||
// Transfer output connections from old node to new node
|
||||
oldNode.outputs.slice(0, outputCount).forEach((output, index) => {
|
||||
if (output && output.links) {
|
||||
output.links.forEach(link => {
|
||||
const targetLinkInfo = oldNode.graph.links[link];
|
||||
if (targetLinkInfo) {
|
||||
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
|
||||
if (targetNode) {
|
||||
newNode.connect(index, targetNode, targetLinkInfo.target_slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old node
|
||||
app.graph.remove(oldNode);
|
||||
}
|
||||
|
||||
function replaceNodeMenuCallback(currentNode, targetNodeName) {
|
||||
return function() {
|
||||
replaceNode(currentNode, targetNodeName);
|
||||
};
|
||||
}
|
||||
|
||||
function showSwapMenu(value, options, e, menu, node) {
|
||||
const swapOptions = [];
|
||||
const xyInputNodes = [
|
||||
"XY Input: Seeds++ Batch",
|
||||
"XY Input: Add/Return Noise",
|
||||
"XY Input: Steps",
|
||||
"XY Input: CFG Scale",
|
||||
"XY Input: Sampler/Scheduler",
|
||||
"XY Input: Denoise",
|
||||
"XY Input: VAE",
|
||||
"XY Input: Prompt S/R",
|
||||
"XY Input: Aesthetic Score",
|
||||
"XY Input: Refiner On/Off",
|
||||
"XY Input: Checkpoint",
|
||||
"XY Input: Clip Skip",
|
||||
"XY Input: LoRA",
|
||||
"XY Input: LoRA Plot",
|
||||
"XY Input: LoRA Stacks",
|
||||
"XY Input: Control Net",
|
||||
"XY Input: Control Net Plot",
|
||||
"XY Input: Manual XY Entry"
|
||||
];
|
||||
|
||||
for (const nodeType of xyInputNodes) {
|
||||
if (node.type !== nodeType) {
|
||||
swapOptions.push({
|
||||
content: nodeType,
|
||||
callback: replaceNodeMenuCallback(node, nodeType)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new LiteGraph.ContextMenu(swapOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false; // This ensures the original context menu doesn't proceed
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.swapXYinputs",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name.startsWith("XY Input:")) {
|
||||
addMenuHandler(nodeType, function (insertOption) {
|
||||
insertOption({
|
||||
content: "🔄 Swap with...",
|
||||
has_submenu: true,
|
||||
callback: showSwapMenu
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
121
js/previewfix.js
121
js/previewfix.js
@@ -1,13 +1,10 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const ext = {
|
||||
app.registerExtension({
|
||||
name: "efficiency.previewfix",
|
||||
ws: null,
|
||||
maxCount: 0,
|
||||
currentCount: 0,
|
||||
sendBlob: false,
|
||||
startProcessing: false,
|
||||
lastBlobURL: null,
|
||||
lastExecutedNodeId: null,
|
||||
blobsToRevoke: [], // Array to accumulate blob URLs for revocation
|
||||
debug: false,
|
||||
|
||||
log(...args) {
|
||||
@@ -18,89 +15,53 @@ const ext = {
|
||||
if (this.debug) console.error(...args);
|
||||
},
|
||||
|
||||
async sendBlobDataAsDataURL(blobURL) {
|
||||
const blob = await fetch(blobURL).then(res => res.blob());
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = () => this.ws.send(reader.result);
|
||||
},
|
||||
shouldRevokeBlobForNode(nodeId) {
|
||||
const node = app.graph.getNodeById(nodeId);
|
||||
|
||||
const validTitles = [
|
||||
"KSampler (Efficient)",
|
||||
"KSampler Adv. (Efficient)",
|
||||
"KSampler SDXL (Eff.)"
|
||||
];
|
||||
|
||||
handleCommandMessage(data) {
|
||||
Object.assign(this, {
|
||||
maxCount: data.maxCount,
|
||||
sendBlob: data.sendBlob,
|
||||
startProcessing: data.startProcessing,
|
||||
currentCount: 0
|
||||
});
|
||||
|
||||
if (!this.startProcessing && this.lastBlobURL) {
|
||||
this.log("[BlobURLLogger] Revoking last Blob URL:", this.lastBlobURL);
|
||||
URL.revokeObjectURL(this.lastBlobURL);
|
||||
this.lastBlobURL = null;
|
||||
if (!node || !validTitles.includes(node.title)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const getValue = name => ((node.widgets || []).find(w => w.name === name) || {}).value;
|
||||
return getValue("preview_method") !== "none" && getValue("vae_decode").includes("true");
|
||||
},
|
||||
|
||||
init() {
|
||||
this.log("[BlobURLLogger] Initializing...");
|
||||
|
||||
this.ws = new WebSocket('ws://127.0.0.1:8288');
|
||||
|
||||
this.ws.addEventListener('open', () => this.log('[BlobURLLogger] WebSocket connection opened.'));
|
||||
this.ws.addEventListener('error', err => this.error('[BlobURLLogger] WebSocket Error:', err));
|
||||
this.ws.addEventListener('message', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.maxCount !== undefined && data.sendBlob !== undefined && data.startProcessing !== undefined) {
|
||||
this.handleCommandMessage(data);
|
||||
}
|
||||
} catch (err) {
|
||||
this.error('[BlobURLLogger] Error parsing JSON:', err);
|
||||
}
|
||||
});
|
||||
|
||||
setup() {
|
||||
// Intercepting blob creation to store and immediately revoke the last blob URL
|
||||
const originalCreateObjectURL = URL.createObjectURL;
|
||||
URL.createObjectURL = (object) => {
|
||||
const blobURL = originalCreateObjectURL.call(this, object);
|
||||
if (blobURL.startsWith('blob:') && this.startProcessing) {
|
||||
const blobURL = originalCreateObjectURL(object);
|
||||
if (blobURL.startsWith('blob:')) {
|
||||
this.log("[BlobURLLogger] Blob URL created:", blobURL);
|
||||
this.lastBlobURL = blobURL;
|
||||
if (this.sendBlob && this.currentCount < this.maxCount) {
|
||||
this.sendBlobDataAsDataURL(blobURL);
|
||||
|
||||
// If the current node meets the criteria, add the blob URL to the revocation list
|
||||
if (this.shouldRevokeBlobForNode(this.lastExecutedNodeId)) {
|
||||
this.blobsToRevoke.push(blobURL);
|
||||
}
|
||||
this.currentCount++;
|
||||
}
|
||||
return blobURL;
|
||||
};
|
||||
|
||||
this.log("[BlobURLLogger] Hook attached.");
|
||||
}
|
||||
};
|
||||
|
||||
function toggleWidgetVisibility(node, widgetName, isVisible) {
|
||||
const widget = node.widgets.find(w => w.name === widgetName);
|
||||
if (widget) {
|
||||
widget.visible = isVisible;
|
||||
node.setDirtyCanvas(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoraNameChange(node, loraNameWidget) {
|
||||
const isNone = loraNameWidget.value === "None";
|
||||
toggleWidgetVisibility(node, "lora_model_strength", !isNone);
|
||||
toggleWidgetVisibility(node, "lora_clip_strength", !isNone);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
...ext,
|
||||
nodeCreated(node) {
|
||||
if (node.getTitle() === "Efficient Loader") {
|
||||
const loraNameWidget = node.widgets.find(w => w.name === "lora_name");
|
||||
if (loraNameWidget) {
|
||||
handleLoraNameChange(node, loraNameWidget);
|
||||
loraNameWidget.onChange = function() {
|
||||
handleLoraNameChange(node, this);
|
||||
};
|
||||
// Listen to the start of the node execution to revoke all accumulated blob URLs
|
||||
api.addEventListener("executing", ({ detail }) => {
|
||||
if (this.lastExecutedNodeId !== detail || detail === null) {
|
||||
this.blobsToRevoke.forEach(blob => {
|
||||
this.log("[BlobURLLogger] Revoking Blob URL:", blob);
|
||||
URL.revokeObjectURL(blob);
|
||||
});
|
||||
this.blobsToRevoke = []; // Clear the list after revoking all blobs
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update the last executed node ID
|
||||
this.lastExecutedNodeId = detail;
|
||||
});
|
||||
|
||||
this.log("[BlobURLLogger] Hook attached.");
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,18 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { addMenuHandler } from "./node_options/common/utils.js";
|
||||
|
||||
const LAST_SEED_BUTTON_LABEL = '🎲 Randomize / ♻️ Last Queued Seed';
|
||||
const SEED_BEHAVIOR_RANDOMIZE = 'Randomize';
|
||||
const SEED_BEHAVIOR_INCREMENT = 'Increment';
|
||||
const SEED_BEHAVIOR_DECREMENT = 'Decrement';
|
||||
|
||||
const NODE_WIDGET_MAP = {
|
||||
"KSampler (Efficient)": "seed",
|
||||
"KSampler Adv. (Efficient)": "noise_seed",
|
||||
"KSampler SDXL (Eff.)": "noise_seed"
|
||||
"KSampler SDXL (Eff.)": "noise_seed",
|
||||
"Noise Control Script": "seed",
|
||||
"HighRes-Fix Script": "seed",
|
||||
"Tiled Upscaler Script": "seed"
|
||||
};
|
||||
|
||||
const SPECIFIC_WIDTH = 325; // Set to desired width
|
||||
@@ -21,11 +28,9 @@ class SeedControl {
|
||||
this.lastSeed = -1;
|
||||
this.serializedCtx = {};
|
||||
this.node = node;
|
||||
this.holdFlag = false; // Flag to track if sampler_state was set to "Hold"
|
||||
this.usedLastSeedOnHoldRelease = false; // To track if we used the lastSeed after releasing hold
|
||||
this.seedBehavior = 'randomize'; // Default behavior
|
||||
|
||||
let controlAfterGenerateIndex;
|
||||
this.samplerStateWidget = this.node.widgets.find(w => w.name === 'sampler_state');
|
||||
|
||||
for (const [i, w] of this.node.widgets.entries()) {
|
||||
if (w.name === seedName) {
|
||||
@@ -41,10 +46,24 @@ class SeedControl {
|
||||
}
|
||||
|
||||
this.lastSeedButton = this.node.addWidget("button", LAST_SEED_BUTTON_LABEL, null, () => {
|
||||
if (this.seedWidget.value != -1) {
|
||||
const isValidValue = Number.isInteger(this.seedWidget.value) && this.seedWidget.value >= min && this.seedWidget.value <= max;
|
||||
|
||||
// Special case: if the current label is the default and seed value is -1
|
||||
if (this.lastSeedButton.name === LAST_SEED_BUTTON_LABEL && this.seedWidget.value == -1) {
|
||||
return; // Do nothing and return early
|
||||
}
|
||||
|
||||
if (isValidValue && this.seedWidget.value != -1) {
|
||||
this.lastSeed = this.seedWidget.value;
|
||||
this.seedWidget.value = -1;
|
||||
} else if (this.lastSeed !== -1) {
|
||||
this.seedWidget.value = this.lastSeed;
|
||||
} else {
|
||||
this.seedWidget.value = -1; // Set to -1 if the label didn't update due to a seed value issue
|
||||
}
|
||||
|
||||
if (isValidValue) {
|
||||
this.updateButtonLabel(); // Update the button label to reflect the change
|
||||
}
|
||||
}, { width: 50, serialize: false });
|
||||
|
||||
@@ -60,76 +79,167 @@ class SeedControl {
|
||||
const range = (max - min) / (this.seedWidget.options.step / 10);
|
||||
|
||||
this.seedWidget.serializeValue = async (node, index) => {
|
||||
const currentSeed = this.seedWidget.value;
|
||||
this.serializedCtx = {
|
||||
wasRandom: currentSeed == -1,
|
||||
};
|
||||
|
||||
// Check for the state transition and act accordingly.
|
||||
if (this.samplerStateWidget) {
|
||||
if (this.samplerStateWidget.value !== "Hold" && this.holdFlag && !this.usedLastSeedOnHoldRelease) {
|
||||
this.serializedCtx.seedUsed = this.lastSeed;
|
||||
this.usedLastSeedOnHoldRelease = true;
|
||||
this.holdFlag = false; // Reset flag for the next cycle
|
||||
}
|
||||
// Check if the button is disabled
|
||||
if (this.lastSeedButton.disabled) {
|
||||
return this.seedWidget.value;
|
||||
}
|
||||
|
||||
if (!this.usedLastSeedOnHoldRelease) {
|
||||
if (this.serializedCtx.wasRandom) {
|
||||
this.serializedCtx.seedUsed = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
|
||||
} else {
|
||||
this.serializedCtx.seedUsed = this.seedWidget.value;
|
||||
const currentSeed = this.seedWidget.value;
|
||||
this.serializedCtx = {
|
||||
wasSpecial: currentSeed == -1,
|
||||
};
|
||||
|
||||
if (this.serializedCtx.wasSpecial) {
|
||||
switch (this.seedBehavior) {
|
||||
case 'increment':
|
||||
this.serializedCtx.seedUsed = this.lastSeed + 1;
|
||||
break;
|
||||
case 'decrement':
|
||||
this.serializedCtx.seedUsed = this.lastSeed - 1;
|
||||
break;
|
||||
default:
|
||||
this.serializedCtx.seedUsed = Math.floor(Math.random() * range) * (this.seedWidget.options.step / 10) + min;
|
||||
break;
|
||||
}
|
||||
|
||||
// Ensure the seed value is an integer and remains within the accepted range
|
||||
this.serializedCtx.seedUsed = Number.isInteger(this.serializedCtx.seedUsed) ? Math.min(Math.max(this.serializedCtx.seedUsed, min), max) : this.seedWidget.value;
|
||||
|
||||
} else {
|
||||
this.serializedCtx.seedUsed = this.seedWidget.value;
|
||||
}
|
||||
|
||||
if (node && node.widgets_values) {
|
||||
node.widgets_values[index] = this.serializedCtx.seedUsed;
|
||||
}else{
|
||||
} else {
|
||||
// Update the last seed value and the button's label to show the current seed value
|
||||
this.lastSeed = this.serializedCtx.seedUsed;
|
||||
this.lastSeedButton.name = `🎲 Randomize / ♻️ ${this.lastSeed}`;
|
||||
this.updateButtonLabel();
|
||||
}
|
||||
|
||||
this.seedWidget.value = this.serializedCtx.seedUsed;
|
||||
|
||||
if (this.serializedCtx.wasRandom) {
|
||||
if (this.serializedCtx.wasSpecial) {
|
||||
this.lastSeed = this.serializedCtx.seedUsed;
|
||||
this.lastSeedButton.name = `🎲 Randomize / ♻️ ${this.lastSeed}`;
|
||||
if (this.samplerStateWidget.value === "Hold") {
|
||||
this.holdFlag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.usedLastSeedOnHoldRelease && this.samplerStateWidget.value !== "Hold") {
|
||||
// Reset the flag to ensure default behavior is restored
|
||||
this.usedLastSeedOnHoldRelease = false;
|
||||
this.updateButtonLabel();
|
||||
}
|
||||
|
||||
return this.serializedCtx.seedUsed;
|
||||
};
|
||||
|
||||
this.seedWidget.afterQueued = () => {
|
||||
if (this.serializedCtx.wasRandom) {
|
||||
// Check if the button is disabled
|
||||
if (this.lastSeedButton.disabled) {
|
||||
return; // Exit the function immediately
|
||||
}
|
||||
|
||||
if (this.serializedCtx.wasSpecial) {
|
||||
this.seedWidget.value = -1;
|
||||
}
|
||||
|
||||
|
||||
// Check if seed has changed to a non -1 value, and if so, update lastSeed
|
||||
if (this.seedWidget.value !== -1) {
|
||||
this.lastSeed = this.seedWidget.value;
|
||||
}
|
||||
|
||||
// Update the button's label to show the current last seed value
|
||||
this.lastSeedButton.name = `🎲 Randomize / ♻️ ${this.lastSeed}`;
|
||||
|
||||
this.updateButtonLabel();
|
||||
this.serializedCtx = {};
|
||||
};
|
||||
}
|
||||
|
||||
setBehavior(behavior) {
|
||||
this.seedBehavior = behavior;
|
||||
|
||||
// Capture the current seed value as lastSeed and then set the seed widget value to -1
|
||||
if (this.seedWidget.value != -1) {
|
||||
this.lastSeed = this.seedWidget.value;
|
||||
this.seedWidget.value = -1;
|
||||
}
|
||||
|
||||
this.updateButtonLabel();
|
||||
}
|
||||
|
||||
updateButtonLabel() {
|
||||
|
||||
switch (this.seedBehavior) {
|
||||
case 'increment':
|
||||
this.lastSeedButton.name = `➕ Increment / ♻️ ${this.lastSeed === -1 ? "Last Queued Seed" : this.lastSeed}`;
|
||||
break;
|
||||
case 'decrement':
|
||||
this.lastSeedButton.name = `➖ Decrement / ♻️ ${this.lastSeed === -1 ? "Last Queued Seed" : this.lastSeed}`;
|
||||
break;
|
||||
default:
|
||||
this.lastSeedButton.name = `🎲 Randomize / ♻️ ${this.lastSeed === -1 ? "Last Queued Seed" : this.lastSeed}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function showSeedBehaviorMenu(value, options, e, menu, node) {
|
||||
const behaviorOptions = [
|
||||
{
|
||||
content: "🎲 Randomize",
|
||||
callback: () => {
|
||||
node.seedControl.setBehavior('randomize');
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "➕ Increment",
|
||||
callback: () => {
|
||||
node.seedControl.setBehavior('increment');
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "➖ Decrement",
|
||||
callback: () => {
|
||||
node.seedControl.setBehavior('decrement');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
new LiteGraph.ContextMenu(behaviorOptions, {
|
||||
event: e,
|
||||
callback: null,
|
||||
parentMenu: menu,
|
||||
node: node
|
||||
});
|
||||
|
||||
return false; // This ensures the original context menu doesn't proceed
|
||||
}
|
||||
|
||||
// Extension Definition
|
||||
app.registerExtension({
|
||||
name: "efficiency.seedcontrol",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
|
||||
if (NODE_WIDGET_MAP[nodeData.name]) {
|
||||
addMenuHandler(nodeType, function (insertOption) {
|
||||
// Check conditions before showing the seed behavior option
|
||||
let showSeedOption = true;
|
||||
|
||||
if (nodeData.name === "Noise Control Script") {
|
||||
// Check for 'add_seed_noise' widget being false
|
||||
const addSeedNoiseWidget = this.widgets.find(w => w.name === 'add_seed_noise');
|
||||
if (addSeedNoiseWidget && !addSeedNoiseWidget.value) {
|
||||
showSeedOption = false;
|
||||
}
|
||||
} else if (nodeData.name === "HighRes-Fix Script") {
|
||||
// Check for 'use_same_seed' widget being true
|
||||
const useSameSeedWidget = this.widgets.find(w => w.name === 'use_same_seed');
|
||||
if (useSameSeedWidget && useSameSeedWidget.value) {
|
||||
showSeedOption = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (showSeedOption) {
|
||||
insertOption({
|
||||
content: "🌱 Seed behavior...",
|
||||
has_submenu: true,
|
||||
callback: showSeedBehaviorMenu
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||
@@ -138,4 +248,4 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
let origProps = {};
|
||||
let initialized = false;
|
||||
@@ -11,19 +11,43 @@ const doesInputWithNameExist = (node, name) => {
|
||||
return node.inputs ? node.inputs.some((input) => input.name === name) : false;
|
||||
};
|
||||
|
||||
const WIDGET_HEIGHT = 24;
|
||||
const HIDDEN_TAG = "tschide";
|
||||
// Toggle Widget + change size
|
||||
function toggleWidget(node, widget, show = false, suffix = "") {
|
||||
if (!widget || doesInputWithNameExist(node, widget.name)) return;
|
||||
|
||||
// Store the original properties of the widget if not already stored
|
||||
if (!origProps[widget.name]) {
|
||||
origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
|
||||
}
|
||||
|
||||
const origSize = node.size;
|
||||
|
||||
// Set the widget type and computeSize based on the show flag
|
||||
widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix;
|
||||
widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
|
||||
|
||||
// Recursively handle linked widgets if they exist
|
||||
widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show));
|
||||
|
||||
// Calculate the new height for the node based on its computeSize method
|
||||
const newHeight = node.computeSize()[1];
|
||||
node.setSize([node.size[0], newHeight]);
|
||||
}
|
||||
|
||||
const WIDGET_HEIGHT = 24;
|
||||
// Use for Multiline Widget Nodes (aka Efficient Loaders)
|
||||
function toggleWidget_2(node, widget, show = false, suffix = "") {
|
||||
if (!widget || doesInputWithNameExist(node, widget.name)) return;
|
||||
|
||||
const isCurrentlyVisible = widget.type !== "tschide" + suffix;
|
||||
const isCurrentlyVisible = widget.type !== HIDDEN_TAG + suffix;
|
||||
if (isCurrentlyVisible === show) return; // Early exit if widget is already in the desired state
|
||||
|
||||
if (!origProps[widget.name]) {
|
||||
origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize };
|
||||
}
|
||||
|
||||
widget.type = show ? origProps[widget.name].origType : "tschide" + suffix;
|
||||
widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix;
|
||||
widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
|
||||
|
||||
if (initialized){
|
||||
@@ -278,7 +302,18 @@ const nodeWidgetHandlers = {
|
||||
},
|
||||
"XY Input: Control Net Plot": {
|
||||
'plot_type': handleXYInputControlNetPlotPlotType
|
||||
}
|
||||
},
|
||||
"Noise Control Script": {
|
||||
'add_seed_noise': handleNoiseControlScript
|
||||
},
|
||||
"HighRes-Fix Script": {
|
||||
'upscale_type': handleHiResFixScript,
|
||||
'use_same_seed': handleHiResFixScript,
|
||||
'use_controlnet':handleHiResFixScript
|
||||
},
|
||||
"Tiled Upscaler Script": {
|
||||
'use_controlnet':handleTiledUpscalerScript
|
||||
},
|
||||
};
|
||||
|
||||
// In the main function where widgetLogic is called
|
||||
@@ -293,24 +328,142 @@ function widgetLogic(node, widget) {
|
||||
// Efficient Loader Handlers
|
||||
function handleEfficientLoaderLoraName(node, widget) {
|
||||
if (widget.value === 'None') {
|
||||
toggleWidget(node, findWidgetByName(node, 'lora_model_strength'));
|
||||
toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'));
|
||||
toggleWidget_2(node, findWidgetByName(node, 'lora_model_strength'));
|
||||
toggleWidget_2(node, findWidgetByName(node, 'lora_clip_strength'));
|
||||
} else {
|
||||
toggleWidget(node, findWidgetByName(node, 'lora_model_strength'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'), true);
|
||||
toggleWidget_2(node, findWidgetByName(node, 'lora_model_strength'), true);
|
||||
toggleWidget_2(node, findWidgetByName(node, 'lora_clip_strength'), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Eff. Loader SDXL Handlers
|
||||
function handleEffLoaderSDXLRefinerCkptName(node, widget) {
|
||||
if (widget.value === 'None') {
|
||||
toggleWidget(node, findWidgetByName(node, 'refiner_clip_skip'));
|
||||
toggleWidget(node, findWidgetByName(node, 'positive_ascore'));
|
||||
toggleWidget(node, findWidgetByName(node, 'negative_ascore'));
|
||||
toggleWidget_2(node, findWidgetByName(node, 'refiner_clip_skip'));
|
||||
toggleWidget_2(node, findWidgetByName(node, 'positive_ascore'));
|
||||
toggleWidget_2(node, findWidgetByName(node, 'negative_ascore'));
|
||||
} else {
|
||||
toggleWidget(node, findWidgetByName(node, 'refiner_clip_skip'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'positive_ascore'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'negative_ascore'), true);
|
||||
toggleWidget_2(node, findWidgetByName(node, 'refiner_clip_skip'), true);
|
||||
toggleWidget_2(node, findWidgetByName(node, 'positive_ascore'), true);
|
||||
toggleWidget_2(node, findWidgetByName(node, 'negative_ascore'), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Noise Control Script Seed Handler
|
||||
function handleNoiseControlScript(node, widget) {
|
||||
|
||||
function ensureSeedControlExists(callback) {
|
||||
if (node.seedControl && node.seedControl.lastSeedButton) {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(() => ensureSeedControlExists(callback), 0);
|
||||
}
|
||||
}
|
||||
|
||||
ensureSeedControlExists(() => {
|
||||
if (widget.value === false) {
|
||||
toggleWidget(node, findWidgetByName(node, 'seed'));
|
||||
toggleWidget(node, findWidgetByName(node, 'weight'));
|
||||
toggleWidget(node, node.seedControl.lastSeedButton);
|
||||
node.seedControl.lastSeedButton.disabled = true; // Disable the button
|
||||
} else {
|
||||
toggleWidget(node, findWidgetByName(node, 'seed'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'weight'), true);
|
||||
node.seedControl.lastSeedButton.disabled = false; // Enable the button
|
||||
toggleWidget(node, node.seedControl.lastSeedButton, true);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/// HighRes-Fix Script Handlers
|
||||
function handleHiResFixScript(node, widget) {
|
||||
|
||||
function ensureSeedControlExists(callback) {
|
||||
if (node.seedControl && node.seedControl.lastSeedButton) {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(() => ensureSeedControlExists(callback), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (findWidgetByName(node, 'upscale_type').value === "latent") {
|
||||
toggleWidget(node, findWidgetByName(node, 'pixel_upscaler'));
|
||||
|
||||
toggleWidget(node, findWidgetByName(node, 'hires_ckpt_name'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'latent_upscaler'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'use_same_seed'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'hires_steps'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'denoise'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'iterations'), true);
|
||||
|
||||
ensureSeedControlExists(() => {
|
||||
if (findWidgetByName(node, 'use_same_seed').value == true) {
|
||||
toggleWidget(node, findWidgetByName(node, 'seed'));
|
||||
toggleWidget(node, node.seedControl.lastSeedButton);
|
||||
node.seedControl.lastSeedButton.disabled = true; // Disable the button
|
||||
} else {
|
||||
toggleWidget(node, findWidgetByName(node, 'seed'), true);
|
||||
node.seedControl.lastSeedButton.disabled = false; // Enable the button
|
||||
toggleWidget(node, node.seedControl.lastSeedButton, true);
|
||||
}
|
||||
});
|
||||
|
||||
if (findWidgetByName(node, 'use_controlnet').value == '_'){
|
||||
toggleWidget(node, findWidgetByName(node, 'use_controlnet'));
|
||||
toggleWidget(node, findWidgetByName(node, 'control_net_name'));
|
||||
toggleWidget(node, findWidgetByName(node, 'strength'));
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor'));
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'));
|
||||
}
|
||||
else{
|
||||
toggleWidget(node, findWidgetByName(node, 'use_controlnet'), true);
|
||||
|
||||
if (findWidgetByName(node, 'use_controlnet').value == true){
|
||||
toggleWidget(node, findWidgetByName(node, 'control_net_name'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'strength'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'), true);
|
||||
}
|
||||
else{
|
||||
toggleWidget(node, findWidgetByName(node, 'control_net_name'));
|
||||
toggleWidget(node, findWidgetByName(node, 'strength'));
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor'));
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'));
|
||||
}
|
||||
}
|
||||
|
||||
} else if (findWidgetByName(node, 'upscale_type').value === "pixel") {
|
||||
toggleWidget(node, findWidgetByName(node, 'hires_ckpt_name'));
|
||||
toggleWidget(node, findWidgetByName(node, 'latent_upscaler'));
|
||||
toggleWidget(node, findWidgetByName(node, 'use_same_seed'));
|
||||
toggleWidget(node, findWidgetByName(node, 'hires_steps'));
|
||||
toggleWidget(node, findWidgetByName(node, 'denoise'));
|
||||
toggleWidget(node, findWidgetByName(node, 'iterations'));
|
||||
toggleWidget(node, findWidgetByName(node, 'seed'));
|
||||
ensureSeedControlExists(() => {
|
||||
toggleWidget(node, node.seedControl.lastSeedButton);
|
||||
node.seedControl.lastSeedButton.disabled = true; // Disable the button
|
||||
});
|
||||
toggleWidget(node, findWidgetByName(node, 'use_controlnet'));
|
||||
toggleWidget(node, findWidgetByName(node, 'control_net_name'));
|
||||
toggleWidget(node, findWidgetByName(node, 'strength'));
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor'));
|
||||
toggleWidget(node, findWidgetByName(node, 'preprocessor_imgs'));
|
||||
|
||||
toggleWidget(node, findWidgetByName(node, 'pixel_upscaler'), true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tiled Upscaler Script Handler
|
||||
function handleTiledUpscalerScript(node, widget) {
|
||||
if (findWidgetByName(node, 'use_controlnet').value == true){
|
||||
toggleWidget(node, findWidgetByName(node, 'tile_controlnet'), true);
|
||||
toggleWidget(node, findWidgetByName(node, 'strength'), true);
|
||||
}
|
||||
else{
|
||||
toggleWidget(node, findWidgetByName(node, 'tile_controlnet'));
|
||||
toggleWidget(node, findWidgetByName(node, 'strength'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,7 +590,7 @@ app.registerExtension({
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(() => {initialized = true;}, 2000);
|
||||
setTimeout(() => {initialized = true;}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
142
js/workflowfix.js
Normal file
142
js/workflowfix.js
Normal file
@@ -0,0 +1,142 @@
|
||||
// Detect and update Efficiency Nodes from v1.92 to v2.00 changes (Final update?)
|
||||
import { app } from '../../scripts/app.js'
|
||||
import { addNode } from "./node_options/common/utils.js";
|
||||
|
||||
const ext = {
|
||||
name: "efficiency.WorkflowFix",
|
||||
};
|
||||
|
||||
function reloadHiResFixNode(originalNode) {
|
||||
|
||||
// Safeguard against missing 'pos' property
|
||||
const position = originalNode.pos && originalNode.pos.length === 2 ? { x: originalNode.pos[0], y: originalNode.pos[1] } : { x: 0, y: 0 };
|
||||
|
||||
// Recreate the node
|
||||
const newNode = addNode("HighRes-Fix Script", originalNode, position);
|
||||
|
||||
// Transfer input connections from old node to new node
|
||||
originalNode.inputs.forEach((input, index) => {
|
||||
if (input && input.link !== null) {
|
||||
const originLinkInfo = originalNode.graph.links[input.link];
|
||||
if (originLinkInfo) {
|
||||
const originNode = originalNode.graph.getNodeById(originLinkInfo.origin_id);
|
||||
if (originNode) {
|
||||
originNode.connect(originLinkInfo.origin_slot, newNode, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Transfer output connections from old node to new node
|
||||
originalNode.outputs.forEach((output, index) => {
|
||||
if (output && output.links) {
|
||||
output.links.forEach(link => {
|
||||
const targetLinkInfo = originalNode.graph.links[link];
|
||||
if (targetLinkInfo) {
|
||||
const targetNode = originalNode.graph.getNodeById(targetLinkInfo.target_id);
|
||||
if (targetNode) {
|
||||
newNode.connect(index, targetNode, targetLinkInfo.target_slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the original node after all connections are transferred
|
||||
originalNode.graph.remove(originalNode);
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
ext.loadedGraphNode = function(node, app) {
|
||||
const originalNode = node; // This line ensures that originalNode refers to the provided node
|
||||
const kSamplerTypes = [
|
||||
"KSampler (Efficient)",
|
||||
"KSampler Adv. (Efficient)",
|
||||
"KSampler SDXL (Eff.)"
|
||||
];
|
||||
|
||||
// EFFICIENT LOADER & EFF. LOADER SDXL
|
||||
/* Changes:
|
||||
Added "token_normalization" & "weight_interpretation" widget below prompt text boxes,
|
||||
below code fixes the widget values for empty_latent_width, empty_latent_height, and batch_size
|
||||
by shifting down by 2 widget values starting from the "token_normalization" widget.
|
||||
Logic triggers when "token_normalization" is a number instead of a string.
|
||||
*/
|
||||
if (node.comfyClass === "Efficient Loader" || node.comfyClass === "Eff. Loader SDXL") {
|
||||
const tokenWidget = node.widgets.find(w => w.name === "token_normalization");
|
||||
const weightWidget = node.widgets.find(w => w.name === "weight_interpretation");
|
||||
|
||||
if (typeof tokenWidget.value === 'number') {
|
||||
console.log("[EfficiencyUpdate]", `Fixing '${node.comfyClass}' token and weight widgets:`, node);
|
||||
const index = node.widgets.indexOf(tokenWidget);
|
||||
if (index !== -1) {
|
||||
for (let i = node.widgets.length - 1; i > index + 1; i--) {
|
||||
node.widgets[i].value = node.widgets[i - 2].value;
|
||||
}
|
||||
}
|
||||
tokenWidget.value = "none";
|
||||
weightWidget.value = "comfy";
|
||||
}
|
||||
}
|
||||
|
||||
// KSAMPLER (EFFICIENT), KSAMPLER ADV. (EFFICIENT), & KSAMPLER SDXL (EFF.)
|
||||
/* Changes:
|
||||
Removed the "sampler_state" widget which cause all widget values to shift down by a factor of 1.
|
||||
Fix involves moving all widget values by -1. "vae_decode" value is lost in this process, so in
|
||||
below fix I manually set it to its default value of "true".
|
||||
*/
|
||||
else if (kSamplerTypes.includes(node.comfyClass)) {
|
||||
|
||||
const seedWidgetName = (node.comfyClass === "KSampler (Efficient)") ? "seed" : "noise_seed";
|
||||
const stepsWidgetName = (node.comfyClass === "KSampler (Efficient)") ? "steps" : "start_at_step";
|
||||
|
||||
const seedWidget = node.widgets.find(w => w.name === seedWidgetName);
|
||||
const stepsWidget = node.widgets.find(w => w.name === stepsWidgetName);
|
||||
|
||||
if (isNaN(seedWidget.value) && isNaN(stepsWidget.value)) {
|
||||
console.log("[EfficiencyUpdate]", `Fixing '${node.comfyClass}' node widgets:`, node);
|
||||
for (let i = 0; i < node.widgets.length - 1; i++) {
|
||||
node.widgets[i].value = node.widgets[i + 1].value;
|
||||
}
|
||||
node.widgets[node.widgets.length - 1].value = "true";
|
||||
}
|
||||
}
|
||||
|
||||
// HIGHRES-FIX SCRIPT
|
||||
/* Changes:
|
||||
Many new changes where added, so in order to properly update, aquired the values of the original
|
||||
widgets, reload a new node, transffer the known original values, and transffer connection.
|
||||
This fix is triggered when the upscale_type widget is neither "latent" or "pixel".
|
||||
*/
|
||||
// Check if the current node is "HighRes-Fix Script" and if any of the above fixes were applied
|
||||
else if (node.comfyClass === "HighRes-Fix Script") {
|
||||
const upscaleTypeWidget = node.widgets.find(w => w.name === "upscale_type");
|
||||
|
||||
if (upscaleTypeWidget && upscaleTypeWidget.value !== "latent" && upscaleTypeWidget.value !== "pixel") {
|
||||
console.log("[EfficiencyUpdate]", "Reloading 'HighRes-Fix Script' node:", node);
|
||||
|
||||
// Reload the node and get the new node instance
|
||||
const newNode = reloadHiResFixNode(node);
|
||||
|
||||
// Update the widgets of the new node
|
||||
const targetWidgetNames = ["latent_upscaler", "upscale_by", "hires_steps", "denoise", "iterations"];
|
||||
|
||||
// Extract the first five values of the original node
|
||||
const originalValues = originalNode.widgets.slice(0, 5).map(w => w.value);
|
||||
|
||||
targetWidgetNames.forEach((name, index) => {
|
||||
const widget = newNode.widgets.find(w => w.name === name);
|
||||
if (widget && originalValues[index] !== undefined) {
|
||||
if (name === "latent_upscaler" && typeof originalValues[index] === 'string') {
|
||||
widget.value = originalValues[index].replace("SD-Latent-Upscaler", "city96");
|
||||
} else {
|
||||
widget.value = originalValues[index];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension(ext);
|
||||
Reference in New Issue
Block a user