Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57c4fb855a | ||
|
|
68bc65783d | ||
|
|
2e7a3bf3d0 | ||
|
|
551835195e | ||
|
|
9e685f9f2d | ||
|
|
df4d4210f1 | ||
|
|
a92091a8f2 | ||
|
|
e8ea84b4cf | ||
|
|
589c3626e1 | ||
|
|
36a76e06dc | ||
|
|
20496551ad | ||
|
|
b88bb480fc |
@@ -0,0 +1,26 @@
|
|||||||
|
name: Publish to Comfy registry
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-node:
|
||||||
|
name: Publish Custom Node to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository_owner == 'kuschanow' }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Publish Custom Node
|
||||||
|
uses: Comfy-Org/publish-node-action@v1
|
||||||
|
with:
|
||||||
|
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||||
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||||
@@ -19,8 +19,8 @@ This node can flip latent and merge original and flipped version.
|
|||||||
- `latent`
|
- `latent`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Latent shift
|
## Latent shift
|
||||||
This node can shift latent along x and y-axis.
|
This node can shift latent along x and y-axis.
|
||||||
@@ -36,7 +36,7 @@ This node can shift latent along x and y-axis.
|
|||||||
- `latent`
|
- `latent`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||

|

|
||||||
|
|
||||||
## ~~TSampler with transforms (Latent Control)~~
|
## ~~TSampler with transforms (Latent Control)~~
|
||||||
Removed from version 2.0.0
|
Removed from version 2.0.0
|
||||||
@@ -55,8 +55,8 @@ exactly matches the base KSampler
|
|||||||
exactly matches the base KSampler
|
exactly matches the base KSampler
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Multiply, Mirror and Shift transform nodes parameters exactly match the corresponding `KSampler with transforms (Latent Control)` parameters.
|
Multiply, Mirror and Shift transform nodes parameters exactly match the corresponding `KSampler with transforms (Latent Control)` parameters.
|
||||||
|
|
||||||
@@ -78,9 +78,9 @@ You can apply specific offset for transform nodes.
|
|||||||
- `offset`
|
- `offset`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
You can combine different offsets to achieve interesting patterns. For example:
|
You can combine different offsets to achieve interesting patterns. For example:
|
||||||
**0 0 0 1** and **0 0 1** give this pattern: **0 0 1 1 0 1 0 1 1 0 0 1**.
|
**0 0 0 1** and **0 0 1** give this pattern: **0 0 1 1 0 1 0 1 1 0 0 1**.
|
||||||
@@ -90,7 +90,7 @@ You can combine different offsets to achieve interesting patterns. For example:
|
|||||||
Each transform node has own one-time version. They allow to make one transform action at specified step.
|
Each transform node has own one-time version. They allow to make one transform action at specified step.
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||

|

|
||||||
|
|
||||||
## Latent normalize
|
## Latent normalize
|
||||||
|
|
||||||
@@ -105,33 +105,42 @@ exactly matches the `VAE Decode` node
|
|||||||
When you multiply latent by negative or big positive (bigger than 2) number and paste this latent in sampler, you can see that the
|
When you multiply latent by negative or big positive (bigger than 2) number and paste this latent in sampler, you can see that the
|
||||||
image will be generated very poorly. This is because stable diffusion cannot work with such set of numbers (meaning the numbers contained in latent).
|
image will be generated very poorly. This is because stable diffusion cannot work with such set of numbers (meaning the numbers contained in latent).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
But you can prevent this behavior by sequential decode and encode latent using vae. Node `Latent normalize` make this process easier.
|
But you can prevent this behavior by sequential decode and encode latent using vae. Node `Latent normalize` make this process easier.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This node also change some results even if output without this node looks good.
|
This node also change some results even if output without this node looks good.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
And it very slightly changes results from latent, which have not been modified.
|
And it very slightly changes results from latent, which have not been modified.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Transform hijack
|
## Transform hijack
|
||||||
|
|
||||||
Allow you to use transforms with any samplers that you like.
|
Allow you to use transforms with any samplers that you like.
|
||||||
|
|
||||||
|
Instead of patching the latent, this node patches the **model**: it attaches the transforms
|
||||||
|
to a cloned model via a sampler post-cfg hook. Connect the returned model to any sampler
|
||||||
|
(`KSampler`, `KSamplerAdvanced`, custom sampler nodes, etc.) and the transforms will be applied
|
||||||
|
during sampling.
|
||||||
|
|
||||||
**Inputs:**
|
**Inputs:**
|
||||||
- latent
|
- model
|
||||||
- transforms
|
- transforms
|
||||||
|
|
||||||
**Outputs:**
|
**Outputs:**
|
||||||
- latent
|
- model
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||

|

|
||||||
|
|
||||||
|
> **Breaking change in 3.0.0:** `Transform hijack` now takes and returns a `MODEL` instead of a
|
||||||
|
> `LATENT`. This replaces the old global `common_ksampler` monkey-patch, which conflicted with the
|
||||||
|
> stock `KSampler` and other custom nodes. Rewire this node to your model input/output after updating.
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"LatentInterpolateTransform": "Latent interpolate transform",
|
"LatentInterpolateTransform": "Latent interpolate transform",
|
||||||
"LatentAddTransform": "Latent add transform",
|
"LatentAddTransform": "Latent add transform",
|
||||||
"OneTimeMirrorTransform": "Mirror transform (one time)",
|
"OneTimeMirrorTransform": "Mirror transform (one time)",
|
||||||
"OneTimeMultiplyTransform": "Shift transform (one time)",
|
"OneTimeMultiplyTransform": "Multiply transform (one time)",
|
||||||
"OneTimeShiftTransform": "Multiply transform (one time)",
|
"OneTimeShiftTransform": "Shift transform (one time)",
|
||||||
"OneTimeLatentInterpolateTransform": "Latent interpolate transform (one time)",
|
"OneTimeLatentInterpolateTransform": "Latent interpolate transform (one time)",
|
||||||
"OneTimeLatentAddTransform": "Latent add transform (one time)",
|
"OneTimeLatentAddTransform": "Latent add transform (one time)",
|
||||||
"TransformsCombine": "Combine transforms",
|
"TransformsCombine": "Combine transforms",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 502 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 755 KiB |
|
After Width: | Height: | Size: 777 KiB |
|
After Width: | Height: | Size: 732 KiB |
|
After Width: | Height: | Size: 597 KiB |
|
After Width: | Height: | Size: 787 KiB |
|
After Width: | Height: | Size: 789 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 565 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 290 KiB |
@@ -1,122 +1,63 @@
|
|||||||
import { app } from "/scripts/app.js";
|
import { app } from "/scripts/app.js";
|
||||||
import {addCanvas, computeCanvasSize, generatePattern, recursiveLinkUpstream, renameNodeInputs, removeNodeInputs} from "./utils.js";
|
import {addCustomCanvasWidget, generatePattern, recursiveLinkUpstream, renameNodeInputs, removeNodeInputs} from "./utils.js";
|
||||||
|
|
||||||
function drawSquares(ctx, widgetX, widgetY, squareSize, pattern) {
|
function drawSquares(ctx, startX, startY, squareSize, pattern) {
|
||||||
const actualSquareSize = squareSize - Math.floor(squareSize / 16);
|
const inset = Math.max(1, Math.floor(squareSize / 16));
|
||||||
widgetY += Math.floor(squareSize / 16) / 2;
|
const cell = squareSize - inset;
|
||||||
widgetX += Math.floor(squareSize / 16) / 2;
|
|
||||||
|
|
||||||
pattern.forEach((value, index) => {
|
pattern.forEach((value, index) => {
|
||||||
const x = widgetX + index * squareSize;
|
const x = startX + index * squareSize + inset / 2;
|
||||||
|
const y = startY + inset / 2;
|
||||||
|
|
||||||
ctx.fillStyle = value === 1 ? "#222223" : "#00000000";
|
ctx.lineWidth = inset;
|
||||||
ctx.strokeStyle = "#222223";
|
ctx.strokeStyle = "#222223";
|
||||||
ctx.lineWidth = Math.floor(squareSize / 16);
|
|
||||||
|
|
||||||
if (value === 1) {
|
if (value === 1) {
|
||||||
ctx.fillRect(x, widgetY, actualSquareSize, actualSquareSize);
|
ctx.fillStyle = "#222223";
|
||||||
|
ctx.fillRect(x, y, cell, cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.strokeRect(x, widgetY, actualSquareSize, actualSquareSize);
|
ctx.strokeRect(x, y, cell, cell);
|
||||||
|
|
||||||
if (squareSize >= 24) {
|
if (squareSize >= 24) {
|
||||||
ctx.font = `bold ${squareSize/3}px Arial`;
|
ctx.font = `bold ${squareSize / 3}px Arial`;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.text
|
|
||||||
ctx.fillStyle = value === 1 ? "#dbdbdc" : "#222223";
|
ctx.fillStyle = value === 1 ? "#dbdbdc" : "#222223";
|
||||||
ctx.fillText(value.toString(), x + actualSquareSize/2, widgetY + actualSquareSize/2);
|
ctx.fillText(value.toString(), x + cell / 2, y + cell / 2);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetWidget = {
|
// Builds the 0/1 preview pattern from the node's own widgets, or (for
|
||||||
type: "customCanvas",
|
// OffsetCombine) from every upstream offset node feeding into it.
|
||||||
name: "Offset-Canvas",
|
function getOffsetPattern(node) {
|
||||||
get value() {
|
let patterns = []
|
||||||
return this.canvas.value;
|
|
||||||
},
|
|
||||||
set value(x) {
|
|
||||||
this.canvas.value = x;
|
|
||||||
},
|
|
||||||
draw: function (ctx, node, widgetWidth, widgetY) {
|
|
||||||
if (!node.canvasHeight) {
|
|
||||||
computeCanvasSize(node, node.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
let patterns = []
|
if (node.type === "OffsetCombine") {
|
||||||
|
const connectedNodes = recursiveLinkUpstream(node, node.inputs[0].type, node.type, 0)
|
||||||
if (node.type === "OffsetCombine") {
|
for (let [node_ID] of connectedNodes) {
|
||||||
const connectedNodes = recursiveLinkUpstream(node, node.inputs[0].type, node.type, 0)
|
const connectedNode = node.graph._nodes_by_id[node_ID]
|
||||||
if (connectedNodes.length !== 0) {
|
if (connectedNode && connectedNode.type !== "OffsetCombine") {
|
||||||
for (let [node_ID, depth] of connectedNodes) {
|
patterns.push({
|
||||||
const connectedNode = node.graph._nodes_by_id[node_ID]
|
process_every: connectedNode.widgets[0].value,
|
||||||
if (connectedNode.type !== "OffsetCombine") {
|
offset: connectedNode.widgets[1].value + node.widgets[0].value,
|
||||||
const pattern = {
|
mode: connectedNode.widgets[2].value,
|
||||||
process_every: connectedNode.widgets[0].value,
|
})
|
||||||
offset: connectedNode.widgets[1].value + node.widgets[0].value,
|
|
||||||
mode: connectedNode.widgets[2].value
|
|
||||||
}
|
|
||||||
patterns.push(pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const pattern = {
|
|
||||||
process_every: node.widgets[0].value,
|
|
||||||
offset: node.widgets[1].value,
|
|
||||||
mode: node.widgets[2].value}
|
|
||||||
patterns.push(pattern)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
patterns.push({
|
||||||
|
process_every: node.widgets[0].value,
|
||||||
|
offset: node.widgets[1].value,
|
||||||
|
mode: node.widgets[2].value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const pattern = generatePattern(patterns)
|
if (patterns.length === 0) return []
|
||||||
|
|
||||||
const visible = true
|
return generatePattern(patterns)
|
||||||
const t = ctx.getTransform();
|
}
|
||||||
const margin = 10
|
|
||||||
|
|
||||||
const widgetHeight = node.canvasHeight
|
|
||||||
const width = pattern.length * 32
|
|
||||||
const height = 32
|
|
||||||
|
|
||||||
const scale = Math.min((widgetWidth-margin*2)/width, (widgetHeight-margin*2)/height)
|
|
||||||
|
|
||||||
Object.assign(this.canvas.style, {
|
|
||||||
left: `${t.e}px`,
|
|
||||||
top: `${t.f + (widgetY*t.d)}px`,
|
|
||||||
width: `${widgetWidth * t.a}px`,
|
|
||||||
height: `${widgetHeight * t.d}px`,
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 1,
|
|
||||||
fontSize: `${t.d * 10.0}px`,
|
|
||||||
pointerEvents: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
this.canvas.hidden = !visible;
|
|
||||||
|
|
||||||
let backgroundWidth = width * scale
|
|
||||||
let backgroundHeight = height * scale
|
|
||||||
|
|
||||||
let xOffset = margin
|
|
||||||
if (backgroundWidth < widgetWidth) {
|
|
||||||
xOffset += (widgetWidth-backgroundWidth)/2 - margin
|
|
||||||
}
|
|
||||||
let yOffset = margin
|
|
||||||
if (backgroundHeight < widgetHeight) {
|
|
||||||
yOffset += (widgetHeight-backgroundHeight)/2 - margin
|
|
||||||
}
|
|
||||||
|
|
||||||
let widgetX = xOffset
|
|
||||||
widgetY = widgetY + yOffset
|
|
||||||
|
|
||||||
const squareSize = backgroundWidth / pattern.length;
|
|
||||||
|
|
||||||
drawSquares(ctx, widgetX, widgetY, squareSize, pattern)
|
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff88"
|
|
||||||
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.LatentControl.TransformOffset",
|
name: "Comfy.LatentControl.TransformOffset",
|
||||||
@@ -126,7 +67,7 @@ app.registerExtension({
|
|||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||||
|
|
||||||
addCanvas(this, app, offsetWidget)
|
addCustomCanvasWidget(this, "Offset-Canvas", getOffsetPattern, drawSquares)
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@@ -142,7 +83,7 @@ app.registerExtension({
|
|||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||||
|
|
||||||
addCanvas(this, app, offsetWidget)
|
addCustomCanvasWidget(this, "Offset-Canvas", getOffsetPattern, drawSquares)
|
||||||
|
|
||||||
this.getExtraMenuOptions = function(_, options) {
|
this.getExtraMenuOptions = function(_, options) {
|
||||||
options.unshift(
|
options.unshift(
|
||||||
|
|||||||
@@ -1,52 +1,3 @@
|
|||||||
export function computeCanvasSize(node, size) {
|
|
||||||
if (node.widgets[0].last_y == null) return;
|
|
||||||
|
|
||||||
const MIN_SIZE = 64;
|
|
||||||
|
|
||||||
const inputs = node.inputs === undefined ? 0 : node.inputs.length
|
|
||||||
const outputs = node.outputs === undefined ? 0 : node.outputs.length
|
|
||||||
|
|
||||||
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(inputs, outputs) + 5;
|
|
||||||
let freeSpace = size[1] - y;
|
|
||||||
|
|
||||||
// Compute the height of all non customtext widgets
|
|
||||||
let widgetHeight = 0;
|
|
||||||
for (let i = 0; i < node.widgets.length; i++) {
|
|
||||||
const w = node.widgets[i];
|
|
||||||
if (w.type !== "customCanvas") {
|
|
||||||
if (w.computeSize) {
|
|
||||||
widgetHeight += w.computeSize()[1] + 4;
|
|
||||||
} else {
|
|
||||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See how large the canvas can be
|
|
||||||
freeSpace -= widgetHeight;
|
|
||||||
|
|
||||||
// There isnt enough space for all the widgets, increase the size of the node
|
|
||||||
if (freeSpace < MIN_SIZE) {
|
|
||||||
freeSpace = MIN_SIZE;
|
|
||||||
node.size[1] = y + widgetHeight + freeSpace;
|
|
||||||
node.graph.setDirtyCanvas(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position each of the widgets
|
|
||||||
for (const w of node.widgets) {
|
|
||||||
w.y = y;
|
|
||||||
if (w.type === "customCanvas") {
|
|
||||||
y += freeSpace;
|
|
||||||
} else if (w.computeSize) {
|
|
||||||
y += w.computeSize()[1] + 4;
|
|
||||||
} else {
|
|
||||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.canvasHeight = freeSpace;
|
|
||||||
}
|
|
||||||
|
|
||||||
function gcd(a, b) {
|
function gcd(a, b) {
|
||||||
// Функция для вычисления наибольшего общего делителя (НОД)
|
// Функция для вычисления наибольшего общего делителя (НОД)
|
||||||
while (b !== 0) {
|
while (b !== 0) {
|
||||||
@@ -123,34 +74,90 @@ export function removeNodeInputs(node, indexesToRemove) {
|
|||||||
node.removeInput(i)
|
node.removeInput(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
node.onResize(node.size)
|
node.setSize(node.computeSize())
|
||||||
|
node.graph?.setDirtyCanvas(true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addCanvas(node, app, widget) {
|
const MARGIN_X = 15; // side padding — matches the node's standard widgets
|
||||||
widget.canvas = document.createElement("canvas");
|
const MARGIN_Y = 10; // top/bottom padding around the grid (node-local px)
|
||||||
widget.canvas.className = "latent-control-custom-canvas";
|
const MIN_SQUARE = 24; // smallest square that still shows the row on tiny/long patterns
|
||||||
|
const MAX_SQUARE = 72; // largest square, so a short pattern doesn't blow up
|
||||||
|
const MIN_NODE_WIDTH = 220; // enough width for the squares to read
|
||||||
|
|
||||||
widget.parent = node;
|
// Square side that fits the given (full node) width and column count. Squares
|
||||||
document.body.appendChild(widget.canvas);
|
// shrink to fit a long pattern (down to 1px) and are capped so a short one stays
|
||||||
|
// reasonable. Guarantees squareSize * cols <= width - 2*MARGIN_X, i.e. no overflow.
|
||||||
|
function fitSquareSize(width, cols) {
|
||||||
|
const availWidth = width - MARGIN_X * 2;
|
||||||
|
return Math.max(1, Math.min(availWidth / Math.max(cols, 1), MAX_SQUARE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a self-contained canvas widget that draws a pattern preview directly onto
|
||||||
|
* the LiteGraph node context. Works with the current ComfyUI frontend (Nodes v2):
|
||||||
|
* it paints in node-local coordinates, so it scales and positions correctly with
|
||||||
|
* zoom and DPI — no DOM overlay and no manual ctx.getTransform() math.
|
||||||
|
*
|
||||||
|
* The preview auto-sizes to the space available inside the node: computeSize()
|
||||||
|
* reports exactly the height the grid needs for the current width and pattern
|
||||||
|
* length, so the node reserves the right amount of room, the squares grow/shrink
|
||||||
|
* as the node gets wider/narrower or the pattern longer/shorter, and the drawing
|
||||||
|
* always stays inside the node body.
|
||||||
|
*
|
||||||
|
* @param node the LiteGraph node
|
||||||
|
* @param name widget name
|
||||||
|
* @param getPattern (node) => number[] the 0/1 pattern to render
|
||||||
|
* @param drawSquares (ctx, startX, startY, squareSize, pattern) => void
|
||||||
|
*/
|
||||||
|
export function addCustomCanvasWidget(node, name, getPattern, drawSquares) {
|
||||||
|
const widget = {
|
||||||
|
type: "customCanvas",
|
||||||
|
name,
|
||||||
|
value: undefined,
|
||||||
|
options: { serialize: false },
|
||||||
|
computeSize() {
|
||||||
|
// Use the real node width, not the passed value: LiteGraph feeds a
|
||||||
|
// widened width to selected nodes, which would inflate the reserve.
|
||||||
|
const nodeWidth = node.size[0];
|
||||||
|
const cols = getPattern(node).length || 1;
|
||||||
|
const rowHeight = Math.max(fitSquareSize(nodeWidth, cols), MIN_SQUARE);
|
||||||
|
return [nodeWidth, rowHeight + MARGIN_Y * 2];
|
||||||
|
},
|
||||||
|
draw(ctx, node, width, widgetY) {
|
||||||
|
const pattern = getPattern(node);
|
||||||
|
if (!pattern || !pattern.length) return;
|
||||||
|
|
||||||
|
// Use node.size[0] rather than the `width` argument: on a selected node
|
||||||
|
// LiteGraph passes an enlarged width, which would push the grid right and
|
||||||
|
// peg the square size at its cap. node.size[0] is stable in both states.
|
||||||
|
const nodeWidth = node.size[0];
|
||||||
|
const cols = pattern.length;
|
||||||
|
const squareSize = fitSquareSize(nodeWidth, cols);
|
||||||
|
|
||||||
|
// Same band height computeSize reserved -> the grid stays inside the node.
|
||||||
|
const bandHeight = Math.max(squareSize, MIN_SQUARE) + MARGIN_Y * 2;
|
||||||
|
const gridWidth = squareSize * cols;
|
||||||
|
|
||||||
|
const startX = (nodeWidth - gridWidth) / 2;
|
||||||
|
const startY = widgetY + (bandHeight - squareSize) / 2;
|
||||||
|
|
||||||
|
// Light backing drawn first so empty cells stay readable on dark nodes.
|
||||||
|
ctx.fillStyle = "#ffffffcc";
|
||||||
|
ctx.fillRect(startX, startY, gridWidth, squareSize);
|
||||||
|
|
||||||
|
drawSquares(ctx, startX, startY, squareSize, pattern);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
node.addCustomWidget(widget);
|
node.addCustomWidget(widget);
|
||||||
|
|
||||||
app.canvas.onDrawBackground = function () {
|
// Widen a touch and reserve the preview height on creation. On workflow load
|
||||||
for (let n in app.graph._nodes) {
|
// LiteGraph applies the saved size after onNodeCreated, so this does not fight
|
||||||
n = graph._nodes[n];
|
// persisted sizes.
|
||||||
for (let w in n.widgets) {
|
node.size[0] = Math.max(node.size[0], MIN_NODE_WIDTH);
|
||||||
let wid = n.widgets[w];
|
node.setSize(node.computeSize());
|
||||||
if (Object.hasOwn(wid, "canvas")) {
|
|
||||||
wid.canvas.style.left = -8000 + "px";
|
|
||||||
wid.canvas.style.position = "absolute";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
node.onResize = function (size) {
|
return widget;
|
||||||
computeCanvasSize(node, size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import torch
|
|
||||||
import nodes
|
|
||||||
import comfy
|
|
||||||
from latent_preview import prepare_callback as preview_callback
|
|
||||||
|
|
||||||
|
|
||||||
class TransformContext:
|
|
||||||
original_sample_function = nodes.common_ksampler
|
|
||||||
|
|
||||||
def get_transform_sample_function(self):
|
|
||||||
def prepare_callback(model, steps, x0_output_dict=None, transforms=None):
|
|
||||||
def transform_callback(step, x0, x, total_steps):
|
|
||||||
if transforms is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for transform in transforms:
|
|
||||||
for i in range(x0.size()[0]):
|
|
||||||
x0[i] = transform["function"](step, x0[i].unsqueeze(0), total_steps, transform["params"])
|
|
||||||
|
|
||||||
preview = preview_callback(model, steps, x0_output_dict)
|
|
||||||
|
|
||||||
def callback(step, x0, x, total_steps):
|
|
||||||
transform_callback(step, x0, x, total_steps)
|
|
||||||
preview(step, x0, x, total_steps)
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def sample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0,
|
|
||||||
disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
|
|
||||||
latent_image = latent["samples"]
|
|
||||||
if disable_noise:
|
|
||||||
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
|
|
||||||
else:
|
|
||||||
batch_inds = latent["batch_index"] if "batch_index" in latent else None
|
|
||||||
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
|
|
||||||
|
|
||||||
noise_mask = None
|
|
||||||
if "noise_mask" in latent:
|
|
||||||
noise_mask = latent["noise_mask"]
|
|
||||||
|
|
||||||
callback = prepare_callback(model, steps, transforms=latent["transforms"])
|
|
||||||
disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
|
|
||||||
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
|
||||||
denoise=denoise, disable_noise=disable_noise, start_step=start_step,
|
|
||||||
last_step=last_step,
|
|
||||||
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback,
|
|
||||||
disable_pbar=disable_pbar, seed=seed)
|
|
||||||
out = latent.copy()
|
|
||||||
out["samples"] = samples
|
|
||||||
self.unhijack()
|
|
||||||
return (out,)
|
|
||||||
|
|
||||||
return sample
|
|
||||||
|
|
||||||
def hijack(self):
|
|
||||||
nodes.common_ksampler = self.get_transform_sample_function()
|
|
||||||
|
|
||||||
def unhijack(self):
|
|
||||||
nodes.common_ksampler = TransformContext.original_sample_function
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.hijack()
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
||||||
self.unhijack()
|
|
||||||
@@ -1,32 +1,20 @@
|
|||||||
from .TransformContext import TransformContext
|
from .transform_apply import attach_transforms
|
||||||
|
|
||||||
|
|
||||||
class TransformHijack:
|
class TransformHijack:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required" : {
|
"required": {
|
||||||
"latent": ("LATENT",),
|
"model": ("MODEL",),
|
||||||
"transforms": ("TRANSFORM",)
|
"transforms": ("TRANSFORM",),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("LATENT",)
|
RETURN_TYPES = ("MODEL",)
|
||||||
FUNCTION = "func"
|
FUNCTION = "func"
|
||||||
|
|
||||||
CATEGORY = "sampling/transforms"
|
CATEGORY = "sampling/transforms"
|
||||||
|
|
||||||
_context = None
|
def func(self, model, transforms):
|
||||||
_hijack_node_id = None
|
return (attach_transforms(model, transforms),)
|
||||||
|
|
||||||
def func(self, latent, transforms):
|
|
||||||
latent["transforms"] = transforms
|
|
||||||
|
|
||||||
if TransformHijack._context is None:
|
|
||||||
TransformHijack._hijack_node_id = id
|
|
||||||
TransformHijack._context = TransformContext()
|
|
||||||
else:
|
|
||||||
return (latent,)
|
|
||||||
|
|
||||||
TransformHijack._context.hijack()
|
|
||||||
return (latent,)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from .TransformContext import TransformContext
|
from .transform_apply import attach_transforms
|
||||||
from nodes import KSampler, KSamplerAdvanced
|
from nodes import KSampler, KSamplerAdvanced
|
||||||
|
|
||||||
|
|
||||||
@@ -16,17 +16,11 @@ class Transforms:
|
|||||||
|
|
||||||
FUNCTION = "func"
|
FUNCTION = "func"
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.original_function_name = self.clazz.FUNCTION
|
|
||||||
|
|
||||||
def func(self, **kwargs):
|
def func(self, **kwargs):
|
||||||
ctx = TransformContext()
|
transforms = kwargs.pop("transform_optional", None)
|
||||||
ctx.hijack()
|
if transforms:
|
||||||
latent = kwargs["latent_image"]
|
kwargs["model"] = attach_transforms(kwargs["model"], transforms)
|
||||||
latent["transforms"] = kwargs.pop("transform_optional")
|
return getattr(self, self.clazz.FUNCTION)(**kwargs)
|
||||||
kwargs["latent_image"] = latent
|
|
||||||
out = getattr(self, self.clazz.FUNCTION)(**kwargs)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def variations_factory(original_class: type, name=None) -> type:
|
def variations_factory(original_class: type, name=None) -> type:
|
||||||
@@ -34,4 +28,4 @@ def variations_factory(original_class: type, name=None) -> type:
|
|||||||
return type(name, (Transforms, original_class), {'clazz': original_class})
|
return type(name, (Transforms, original_class), {'clazz': original_class})
|
||||||
|
|
||||||
TSampler = variations_factory(KSampler)
|
TSampler = variations_factory(KSampler)
|
||||||
TSamplerAdvanced = variations_factory(KSamplerAdvanced)
|
TSamplerAdvanced = variations_factory(KSamplerAdvanced)
|
||||||
@@ -62,9 +62,7 @@ def latent_interpolate_transform(x0, params):
|
|||||||
latent = params["latent"].to(x0.device)
|
latent = params["latent"].to(x0.device)
|
||||||
|
|
||||||
if x0.shape != latent.shape:
|
if x0.shape != latent.shape:
|
||||||
latent.permute(0, 3, 1, 2)
|
|
||||||
latent = comfy.utils.common_upscale(latent, x0.shape[3], x0.shape[2], 'bicubic', crop='center')
|
latent = comfy.utils.common_upscale(latent, x0.shape[3], x0.shape[2], 'bicubic', crop='center')
|
||||||
latent.permute(0, 2, 3, 1)
|
|
||||||
|
|
||||||
x = latent * params["factor"] + x0 * (1 - params["factor"])
|
x = latent * params["factor"] + x0 * (1 - params["factor"])
|
||||||
x *= params["multiplier"]
|
x *= params["multiplier"]
|
||||||
@@ -76,9 +74,7 @@ def latent_add_transform(x0, params):
|
|||||||
latent = params["latent"].to(x0.device)
|
latent = params["latent"].to(x0.device)
|
||||||
|
|
||||||
if x0.shape != latent.shape:
|
if x0.shape != latent.shape:
|
||||||
latent.permute(0, 3, 1, 2)
|
latent = comfy.utils.common_upscale(latent, x0.shape[3], x0.shape[2], 'bicubic', crop='center')
|
||||||
latent = comfy.utils.common_upscale(latent, x0.shape[3], x0.shape[2], 'bicubic', crop='center')
|
|
||||||
latent.permute(0, 2, 3, 1)
|
|
||||||
|
|
||||||
x = x0 + latent
|
x = x0 + latent
|
||||||
x *= params["multiplier"]
|
x *= params["multiplier"]
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
def apply_transforms_to_x0(x0, step, total_steps, transforms):
|
||||||
|
x = x0.clone()
|
||||||
|
|
||||||
|
for transform in transforms:
|
||||||
|
for i in range(x.size()[0]):
|
||||||
|
x[i] = transform["function"](step, x[i].unsqueeze(0), total_steps, transform["params"])
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def _find_step(sigma, sigmas):
|
||||||
|
# sigma is a scalar tensor for the current model evaluation, sigmas is the full
|
||||||
|
# schedule. High order samplers evaluate the model at intermediate sigmas that are
|
||||||
|
# not part of the schedule; for those we return None so the transform is applied
|
||||||
|
# exactly once per step (parity with the old per-step callback).
|
||||||
|
diff = torch.abs(sigmas - sigma.to(sigmas.device))
|
||||||
|
idx = int(torch.argmin(diff).item())
|
||||||
|
if diff[idx] <= 1e-4 * max(1.0, float(sigmas[idx].abs())):
|
||||||
|
return idx
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def make_post_cfg_function(transforms):
|
||||||
|
def post_cfg_function(args):
|
||||||
|
denoised = args["denoised"]
|
||||||
|
|
||||||
|
if not transforms:
|
||||||
|
return denoised
|
||||||
|
|
||||||
|
sigmas = args["model_options"].get("transformer_options", {}).get("sample_sigmas", None)
|
||||||
|
if sigmas is None:
|
||||||
|
return denoised
|
||||||
|
|
||||||
|
step = _find_step(args["sigma"], sigmas)
|
||||||
|
if step is None:
|
||||||
|
return denoised
|
||||||
|
|
||||||
|
total_steps = len(sigmas) - 1
|
||||||
|
|
||||||
|
return apply_transforms_to_x0(denoised, step, total_steps, transforms)
|
||||||
|
|
||||||
|
return post_cfg_function
|
||||||
|
|
||||||
|
|
||||||
|
def attach_transforms(model, transforms):
|
||||||
|
m = model.clone()
|
||||||
|
m.set_model_sampler_post_cfg_function(make_post_cfg_function(transforms))
|
||||||
|
return m
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import torch
|
import torch
|
||||||
|
from nodes import PreviewImage
|
||||||
|
|
||||||
MIRROR_DIRECTIONS = ["vertically", "horizontally", "both"]
|
MIRROR_DIRECTIONS = ["vertically", "horizontally", "both"]
|
||||||
|
|
||||||
@@ -15,6 +16,9 @@ class LatentMirror:
|
|||||||
"max": 10.0,
|
"max": 10.0,
|
||||||
"step": 0.01
|
"step": 0.01
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"vae_optional": ("VAE",)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +27,7 @@ class LatentMirror:
|
|||||||
|
|
||||||
CATEGORY = "latent/advanced"
|
CATEGORY = "latent/advanced"
|
||||||
|
|
||||||
def mirror(self, latent, direction, multiplier):
|
def mirror(self, latent, direction, multiplier, vae_optional = None):
|
||||||
l = latent.copy()
|
l = latent.copy()
|
||||||
if direction == "vertically" or direction == "both":
|
if direction == "vertically" or direction == "both":
|
||||||
l["samples"] = torch.flip(l["samples"], dims=[2]) + l["samples"]
|
l["samples"] = torch.flip(l["samples"], dims=[2]) + l["samples"]
|
||||||
@@ -31,4 +35,8 @@ class LatentMirror:
|
|||||||
l["samples"] = torch.flip(l["samples"], dims=[3]) + l["samples"]
|
l["samples"] = torch.flip(l["samples"], dims=[3]) + l["samples"]
|
||||||
|
|
||||||
l["samples"] *= multiplier
|
l["samples"] *= multiplier
|
||||||
|
|
||||||
|
if vae_optional:
|
||||||
|
return {"result": (l,), "ui": PreviewImage().save_images(vae_optional.decode(l["samples"]))["ui"]}
|
||||||
|
|
||||||
return (l,)
|
return (l,)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import torch
|
import torch
|
||||||
|
from nodes import PreviewImage
|
||||||
|
|
||||||
|
|
||||||
class LatentShift:
|
class LatentShift:
|
||||||
@@ -19,6 +20,9 @@ class LatentShift:
|
|||||||
"max": 1,
|
"max": 1,
|
||||||
"step": 0.01
|
"step": 0.01
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"vae_optional": ("VAE",)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@ class LatentShift:
|
|||||||
|
|
||||||
CATEGORY = "latent/advanced"
|
CATEGORY = "latent/advanced"
|
||||||
|
|
||||||
def shift(self, latent, x_shift, y_shift):
|
def shift(self, latent, x_shift, y_shift, vae_optional = None):
|
||||||
l = latent.copy()
|
l = latent.copy()
|
||||||
|
|
||||||
if x_shift != 0:
|
if x_shift != 0:
|
||||||
@@ -35,4 +39,8 @@ class LatentShift:
|
|||||||
if y_shift != 0:
|
if y_shift != 0:
|
||||||
l["samples"] = torch.roll(l["samples"], shifts=int(l["samples"].size()[2] * y_shift), dims=[2])
|
l["samples"] = torch.roll(l["samples"], shifts=int(l["samples"].size()[2] * y_shift), dims=[2])
|
||||||
|
|
||||||
|
if vae_optional:
|
||||||
|
return {"result": (l,), "ui": PreviewImage().save_images(vae_optional.decode(l["samples"]))["ui"]}
|
||||||
|
|
||||||
return (l,)
|
return (l,)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-advanced-latent-control"
|
||||||
|
description = "This custom node helps to transform latent in different ways."
|
||||||
|
version = "3.1.0"
|
||||||
|
license = "LICENSE"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/RomanKuschanow/ComfyUI-Advanced-Latent-Control"
|
||||||
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "kuschanow"
|
||||||
|
DisplayName = "ComfyUI-Advanced-Latent-Control"
|
||||||
|
Icon = ""
|
||||||