Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,182 @@
import type {LGraphNode} from "@comfyorg/frontend";
import {NodeTypesString} from "../constants.js";
import {wait} from "rgthree/common/shared_utils.js";
import {describe, should, beforeEach, expect, describeRun} from "../testing/runner.js";
import {ComfyUITestEnvironment} from "../testing/comfyui_env.js";
const env = new ComfyUITestEnvironment();
function verifyInputAndOutputName(
node: LGraphNode,
index: number,
inputName: string | null,
isLinked?: boolean,
) {
if (inputName != null) {
expect(node.inputs[index]!.name).toBe(`input ${index} name`, inputName);
}
if (isLinked) {
expect(node.inputs[index]!.link).toBeANumber(`input ${index} connection`);
} else if (isLinked === false) {
expect(node.inputs[index]!.link).toBeNullOrUndefined(`input ${index} connection`);
}
if (inputName != null) {
if (inputName === "+") {
expect(node.outputs[index]).toBeUndefined(`output ${index}`);
} else {
let outputName =
inputName === "base_ctx" ? "CONTEXT" : inputName.replace(/^\+\s/, "").toUpperCase();
expect(node.outputs[index]!.name).toBe(`output ${index} name`, outputName);
}
}
}
function vertifyInputsStructure(node: LGraphNode, expectedLength: number) {
expect(node.inputs.length).toBe("inputs length", expectedLength);
expect(node.outputs.length).toBe("outputs length", expectedLength - 1);
verifyInputAndOutputName(node, expectedLength - 1, "+", false);
}
describe("TestContextDynamic", async () => {
let nodeConfig!: LGraphNode;
let nodeCtx!: LGraphNode;
let lastNode: LGraphNode | null = null;
await beforeEach(async () => {
await env.clear();
lastNode = nodeConfig = await env.addNode(NodeTypesString.KSAMPLER_CONFIG);
lastNode = nodeCtx = await env.addNode(NodeTypesString.DYNAMIC_CONTEXT);
nodeConfig.connect(0, nodeCtx, 1); // steps
nodeConfig.connect(2, nodeCtx, 2); // cfg
nodeConfig.connect(4, nodeCtx, 3); // scheduler
nodeConfig.connect(0, nodeCtx, 4); // This is the step.1
nodeConfig.connect(0, nodeCtx, 5); // This is the step.2
nodeCtx.disconnectInput(2);
nodeCtx.disconnectInput(5);
nodeConfig.connect(0, nodeCtx, 6); // This is the step.3
nodeCtx.disconnectInput(6);
await wait();
});
await should("add correct inputs", async () => {
vertifyInputsStructure(nodeCtx, 8);
let i = 0;
verifyInputAndOutputName(nodeCtx, i++, "base_ctx", false);
verifyInputAndOutputName(nodeCtx, i++, "+ steps", true);
verifyInputAndOutputName(nodeCtx, i++, "+ cfg", false);
verifyInputAndOutputName(nodeCtx, i++, "+ scheduler", true);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.1", true);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.2", false);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.3", false);
});
await should("add evaluate correct outputs", async () => {
const displayAny1 = await env.addNode(NodeTypesString.DISPLAY_ANY, {placement: "right"});
const displayAny2 = await env.addNode(NodeTypesString.DISPLAY_ANY, {placement: "under"});
const displayAny3 = await env.addNode(NodeTypesString.DISPLAY_ANY, {placement: "under"});
const displayAny4 = await env.addNode(NodeTypesString.DISPLAY_ANY, {placement: "under"});
nodeCtx.connect(1, displayAny1, 0); // steps
nodeCtx.connect(3, displayAny2, 0); // scheduler
nodeCtx.connect(4, displayAny3, 0); // steps.1
nodeCtx.connect(6, displayAny4, 0); // steps.3 (unlinked)
await env.queuePrompt();
expect(displayAny1.widgets![0]!.value).toBe("output 1", 30);
expect(displayAny2.widgets![0]!.value).toBe("output 3", '"normal"');
expect(displayAny3.widgets![0]!.value).toBe("output 4", 30);
expect(displayAny4.widgets![0]!.value).toBe("output 6", "None");
});
await describeRun("Nested", async () => {
let nodeConfig2!: LGraphNode;
let nodeCtx2!: LGraphNode;
await beforeEach(async () => {
nodeConfig2 = await env.addNode(NodeTypesString.KSAMPLER_CONFIG, {placement: "start"});
nodeConfig2.widgets = nodeConfig2.widgets || [];
nodeConfig2.widgets[0]!.value = 111;
nodeConfig2.widgets[2]!.value = 11.1;
nodeCtx2 = await env.addNode(NodeTypesString.DYNAMIC_CONTEXT, {placement: "right"});
nodeConfig2.connect(0, nodeCtx2, 1); // steps
nodeConfig2.connect(2, nodeCtx2, 2); // cfg
nodeConfig2.connect(3, nodeCtx2, 3); // sampler
nodeConfig2.connect(2, nodeCtx2, 4); // This is the cfg.1
nodeConfig2.connect(0, nodeCtx2, 5); // This is the steps.1
nodeCtx2.disconnectInput(2);
nodeCtx2.disconnectInput(5);
nodeConfig2.connect(2, nodeCtx2, 6); // This is the cfg.2
nodeCtx2.disconnectInput(6);
await wait();
});
await should("disallow context node to be connected to non-first spot.", async () => {
// Connect to first node.
let expectedInputs = 8;
nodeCtx2.connect(0, nodeCtx, expectedInputs - 1);
console.log(nodeCtx.inputs);
vertifyInputsStructure(nodeCtx, expectedInputs);
verifyInputAndOutputName(nodeCtx, 0, "base_ctx", false);
verifyInputAndOutputName(nodeCtx, nodeCtx.inputs.length - 1, null, false);
nodeCtx2.connect(0, nodeCtx, 0);
expectedInputs = 14;
vertifyInputsStructure(nodeCtx, expectedInputs);
verifyInputAndOutputName(nodeCtx, 0, "base_ctx", true);
verifyInputAndOutputName(nodeCtx, expectedInputs - 1, null, false);
});
await should("add inputs from connected above owned.", async () => {
// Connect to first node.
nodeCtx2.connect(0, nodeCtx, 0);
let expectedInputs = 14;
vertifyInputsStructure(nodeCtx, expectedInputs);
let i = 0;
verifyInputAndOutputName(nodeCtx, i++, "base_ctx", true);
verifyInputAndOutputName(nodeCtx, i++, "steps", false);
verifyInputAndOutputName(nodeCtx, i++, "cfg", false);
verifyInputAndOutputName(nodeCtx, i++, "sampler", false);
verifyInputAndOutputName(nodeCtx, i++, "cfg.1", false);
verifyInputAndOutputName(nodeCtx, i++, "steps.1", false);
verifyInputAndOutputName(nodeCtx, i++, "cfg.2", false);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.2", true);
verifyInputAndOutputName(nodeCtx, i++, "+ cfg.3", false);
verifyInputAndOutputName(nodeCtx, i++, "+ scheduler", true);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.3", true);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.4", false);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.5", false);
verifyInputAndOutputName(nodeCtx, i++, "+", false);
});
await should("add then remove inputs when disconnected.", async () => {
// Connect to first node.
nodeCtx2.connect(0, nodeCtx, 0);
let expectedInputs = 14;
expect(nodeCtx.inputs.length).toBe("inputs length", expectedInputs);
expect(nodeCtx.outputs.length).toBe("outputs length", expectedInputs - 1);
nodeCtx.disconnectInput(0);
expectedInputs = 8;
expect(nodeCtx.inputs.length).toBe("inputs length", expectedInputs);
expect(nodeCtx.outputs.length).toBe("outputs length", expectedInputs - 1);
let i = 0;
verifyInputAndOutputName(nodeCtx, i++, "base_ctx", false);
verifyInputAndOutputName(nodeCtx, i++, "+ steps", true);
verifyInputAndOutputName(nodeCtx, i++, "+ cfg", false);
verifyInputAndOutputName(nodeCtx, i++, "+ scheduler", true);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.1", true);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.2", false);
verifyInputAndOutputName(nodeCtx, i++, "+ steps.3", false);
verifyInputAndOutputName(nodeCtx, i++, "+", false);
});
});
});

View File

@@ -0,0 +1,103 @@
import type {LGraphNode} from "@comfyorg/frontend";
import {NodeTypesString} from "../constants";
import {ComfyUITestEnvironment} from "../testing/comfyui_env";
import {describe, should, beforeEach, expect, describeRun} from "../testing/runner.js";
import {pasteImageToLoadImageNode, PNG_1x1, PNG_1x2, PNG_2x1} from "../testing/utils_test.js";
const env = new ComfyUITestEnvironment();
describe("TestImageOrLatentSize", async () => {
await beforeEach(async () => {
await env.clear();
});
await describeRun("LoadImage", async () => {
let imageNode: LGraphNode;
let displayAnyW: LGraphNode;
let displayAnyH: LGraphNode;
await beforeEach(async () => {
await env.clear();
imageNode = await env.addNode("LoadImage");
const sizeNode = await env.addNode(NodeTypesString.IMAGE_OR_LATENT_SIZE);
displayAnyW = await env.addNode(NodeTypesString.DISPLAY_ANY);
displayAnyH = await env.addNode(NodeTypesString.DISPLAY_ANY);
imageNode.connect(0, sizeNode, 0);
sizeNode.connect(0, displayAnyW, 0);
sizeNode.connect(1, displayAnyH, 0);
await env.wait();
});
await should("get correct size for a 1x1 image", async () => {
await pasteImageToLoadImageNode(env, PNG_1x1, imageNode);
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 1);
expect(displayAnyH.widgets![0]!.value).toBe("height", 1);
});
await should("get correct size for a 1x2 image", async () => {
await pasteImageToLoadImageNode(env, PNG_1x2, imageNode);
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 1);
expect(displayAnyH.widgets![0]!.value).toBe("height", 2);
});
await should("get correct size for a 2x1 image", async () => {
await pasteImageToLoadImageNode(env, PNG_2x1, imageNode);
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 2);
expect(displayAnyH.widgets![0]!.value).toBe("height", 1);
});
});
await describeRun("Latent", async () => {
let latentNode: LGraphNode;
let displayAnyW: LGraphNode;
let displayAnyH: LGraphNode;
await beforeEach(async () => {
await env.clear();
latentNode = await env.addNode("EmptyLatentImage");
const sizeNode = await env.addNode(NodeTypesString.IMAGE_OR_LATENT_SIZE);
displayAnyW = await env.addNode(NodeTypesString.DISPLAY_ANY);
displayAnyH = await env.addNode(NodeTypesString.DISPLAY_ANY);
latentNode.connect(0, sizeNode, 0);
sizeNode.connect(0, displayAnyW, 0);
sizeNode.connect(1, displayAnyH, 0);
await env.wait();
latentNode.widgets![0]!.value = 16; // Width
latentNode.widgets![1]!.value = 16; // Height
latentNode.widgets![2]!.value = 1; // Batch
await env.wait();
});
await should("get correct size for a 16x16 latent", async () => {
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 16);
expect(displayAnyH.widgets![0]!.value).toBe("height", 16);
});
await should("get correct size for a 16x32 latent", async () => {
latentNode.widgets![1]!.value = 32;
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 16);
expect(displayAnyH.widgets![0]!.value).toBe("height", 32);
});
await should("get correct size for a 32x16 image", async () => {
latentNode.widgets![0]!.value = 32;
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 32);
expect(displayAnyH.widgets![0]!.value).toBe("height", 16);
});
await should("get correct size with a batch", async () => {
latentNode.widgets![0]!.value = 32;
latentNode.widgets![2]!.value = 2;
await env.queuePrompt();
expect(displayAnyW.widgets![0]!.value).toBe("width", 32);
expect(displayAnyH.widgets![0]!.value).toBe("height", 16);
});
});
});

View File

@@ -0,0 +1,162 @@
import type {LGraphNode} from "@comfyorg/frontend";
import {NodeTypesString} from "../constants";
import {ComfyUITestEnvironment} from "../testing/comfyui_env";
import {describe, should, beforeEach, expect, describeRun} from "../testing/runner.js";
import {pasteImageToLoadImageNode, PNG_1x1, PNG_1x2, PNG_2x1} from "../testing/utils_test.js";
const env = new ComfyUITestEnvironment();
function setPowerPuterValue(node: LGraphNode, outputType: string, value: string) {
// Strip as much whitespace on first non-empty line from all lines.
if (value.includes("\n")) {
value = value.replace(/^\n/gm, "");
const strip = value.match(/^(.*?)\S/)?.[1]?.length;
if (strip) {
value = value.replace(new RegExp(`^.{${strip}}`, "mg"), "");
}
}
node.widgets![1]!.value = value;
node.widgets![0]!.value = outputType;
}
describe("TestPowerPuter", async () => {
let powerPuter!: LGraphNode;
let displayAny!: LGraphNode;
await beforeEach(async () => {
await env.clear();
powerPuter = await env.addNode(NodeTypesString.POWER_PUTER);
displayAny = await env.addNode(NodeTypesString.DISPLAY_ANY);
powerPuter.connect(0, displayAny, 0);
await env.wait();
});
await should("output constants and concatenation", async () => {
const checks: Array<[string, string, string]> = [
["1", "1", "STRING"],
['"abc"', "abc", "STRING"],
["1 + 2", "3", "STRING"],
['"abc" + "xyz"', "abcxyz", "STRING"],
// INT
["1", "1", "INT"],
["1 + 2", "3", "INT"],
// FLOAT
["1", "1.0", "FLOAT"],
["1.3 + 2.8", "4.1", "FLOAT"],
// BOOLEAN
["1", "True", "BOOLEAN"],
["1 - 1", "False", "BOOLEAN"],
];
for (const data of checks) {
setPowerPuterValue(powerPuter, data[2], data[0]);
await env.queuePrompt();
expect(displayAny.widgets![0]!.value).toBe(data[0], data[1]);
}
});
await should("handle inputs", async () => {
// TODO
});
await should("handle complex inputs", async () => {
// TODO
});
await should("handle a for loop", async () => {
setPowerPuterValue(
powerPuter,
"STRING",
`
a = 0
b = ''
for n in range(4):
a += n
for m in range(2):
b += f'{str(n)}-{str(m)}.'
f'a:{a} b:{b}'
`,
);
await env.queuePrompt();
expect(displayAny.widgets![0]!.value).toBe("a:6 b:0-0.0-1.1-0.1-1.2-0.2-1.3-0.3-1.");
});
await should("handle assigning with a subscript slice", async () => {
setPowerPuterValue(
powerPuter,
"STRING",
`
a = [1,2,0]
a[a[2]] = 3
tuple(a)
`,
);
await env.queuePrompt();
expect(displayAny.widgets![0]!.value).toBe("(3, 2, 0)");
});
await should("handle aug assigning with a subscript slice", async () => {
setPowerPuterValue(
powerPuter,
"STRING",
`
a = [1,2,0]
a[a[2]] += 3
tuple(a)
`,
);
await env.queuePrompt();
expect(displayAny.widgets![0]!.value).toBe("(4, 2, 0)");
});
await should("disallow calls to some methods", async () => {
const imageNode = await pasteImageToLoadImageNode(env);
imageNode.connect(0, powerPuter, 0);
setPowerPuterValue(
powerPuter,
"STRING",
`a.numpy().tofile('/tmp/test')
`,
);
await env.queuePrompt();
// Check to see if there's an error.
expect(document.querySelector(".p-dialog-mask .p-card-body")!.textContent).toContain(
"error message",
"Disallowed access to \"tofile\" for type <class 'numpy.ndarray'>",
);
(document.querySelector(".p-dialog-mask .p-dialog-close-button")! as HTMLButtonElement).click();
});
await should("handle boolean operators correctly", async () => {
const checks: Array<[string, string, string, ('toMatchJson'|'toBe')?]> = [
// And operator all success
["1 and 42", "42", "STRING"],
["True and [42]", "[42]", "STRING", "toMatchJson"],
["a = 42\nTrue and [a]", "[42]", "STRING", "toMatchJson"],
["1 and 3 and True and [1] and 42", "42", "STRING"],
// And operator w/ a failure
["1 and 3 and True and [] and 42", "[]", "STRING", "toMatchJson"],
["1 and 0 and True and [] and 42", "0", "STRING"],
["1 and 2 and False and [] and 42", "False", "STRING"],
["b = None\n1 and 2 and True and b and 42", "None", "STRING"],
// Or operator
["1 or 42", "1", "STRING"],
["0 or 42", "42", "STRING"],
["0 or None or False or [] or 42", "42", "STRING"],
["b=42\n0 or None or False or [] or b", "42", "STRING"],
["b=42\n0 or None or False or [b] or b", "[42]", "STRING", "toMatchJson"],
["b=42\n0 or None or True or [b] or b", "True", "STRING"],
// Mix
["1 and 2 and 0 or 5", "5", "STRING"],
["None and 1 or True", "True", "STRING"],
["0 or False and True", "False", "STRING"],
];
for (const data of checks) {
setPowerPuterValue(powerPuter, data[2], data[0]);
await env.queuePrompt();
expect(displayAny.widgets![0]!.value)[data[3] || 'toBe'](data[0], data[1]);
}
});
});