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
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:
67
custom_nodes/ComfyUI-KJNodes/utility/fluid.py
Normal file
67
custom_nodes/ComfyUI-KJNodes/utility/fluid.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import numpy as np
|
||||
from scipy.ndimage import map_coordinates, spline_filter
|
||||
from scipy.sparse.linalg import factorized
|
||||
|
||||
from .numerical import difference, operator
|
||||
|
||||
|
||||
class Fluid:
|
||||
def __init__(self, shape, *quantities, pressure_order=1, advect_order=3):
|
||||
self.shape = shape
|
||||
self.dimensions = len(shape)
|
||||
|
||||
# Prototyping is simplified by dynamically
|
||||
# creating advected quantities as needed.
|
||||
self.quantities = quantities
|
||||
for q in quantities:
|
||||
setattr(self, q, np.zeros(shape))
|
||||
|
||||
self.indices = np.indices(shape)
|
||||
self.velocity = np.zeros((self.dimensions, *shape))
|
||||
|
||||
laplacian = operator(shape, difference(2, pressure_order))
|
||||
self.pressure_solver = factorized(laplacian)
|
||||
|
||||
self.advect_order = advect_order
|
||||
|
||||
def step(self):
|
||||
# Advection is computed backwards in time as described in Stable Fluids.
|
||||
advection_map = self.indices - self.velocity
|
||||
|
||||
# SciPy's spline filter introduces checkerboard divergence.
|
||||
# A linear blend of the filtered and unfiltered fields based
|
||||
# on some value epsilon eliminates this error.
|
||||
def advect(field, filter_epsilon=10e-2, mode='constant'):
|
||||
filtered = spline_filter(field, order=self.advect_order, mode=mode)
|
||||
field = filtered * (1 - filter_epsilon) + field * filter_epsilon
|
||||
return map_coordinates(field, advection_map, prefilter=False, order=self.advect_order, mode=mode)
|
||||
|
||||
# Apply advection to each axis of the
|
||||
# velocity field and each user-defined quantity.
|
||||
for d in range(self.dimensions):
|
||||
self.velocity[d] = advect(self.velocity[d])
|
||||
|
||||
for q in self.quantities:
|
||||
setattr(self, q, advect(getattr(self, q)))
|
||||
|
||||
# Compute the jacobian at each point in the
|
||||
# velocity field to extract curl and divergence.
|
||||
jacobian_shape = (self.dimensions,) * 2
|
||||
partials = tuple(np.gradient(d) for d in self.velocity)
|
||||
jacobian = np.stack(partials).reshape(*jacobian_shape, *self.shape)
|
||||
|
||||
divergence = jacobian.trace()
|
||||
|
||||
# If this curl calculation is extended to 3D, the y-axis value must be negated.
|
||||
# This corresponds to the coefficients of the levi-civita symbol in that dimension.
|
||||
# Higher dimensions do not have a vector -> scalar, or vector -> vector,
|
||||
# correspondence between velocity and curl due to differing isomorphisms
|
||||
# between exterior powers in dimensions != 2 or 3 respectively.
|
||||
curl_mask = np.triu(np.ones(jacobian_shape, dtype=bool), k=1)
|
||||
curl = (jacobian[curl_mask] - jacobian[curl_mask.T]).squeeze()
|
||||
|
||||
# Apply the pressure correction to the fluid's velocity field.
|
||||
pressure = self.pressure_solver(divergence.flatten()).reshape(self.shape)
|
||||
self.velocity -= np.gradient(pressure)
|
||||
|
||||
return divergence, curl, pressure
|
||||
95
custom_nodes/ComfyUI-KJNodes/utility/magictex.py
Normal file
95
custom_nodes/ComfyUI-KJNodes/utility/magictex.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Generates psychedelic color textures in the spirit of Blender's magic texture shader using Python/Numpy
|
||||
|
||||
https://github.com/cheind/magic-texture
|
||||
"""
|
||||
from typing import Tuple, Optional
|
||||
import numpy as np
|
||||
|
||||
|
||||
def coordinate_grid(shape: Tuple[int, int], dtype=np.float32):
|
||||
"""Returns a three-dimensional coordinate grid of given shape for use in `magic`."""
|
||||
x = np.linspace(-1, 1, shape[1], endpoint=True, dtype=dtype)
|
||||
y = np.linspace(-1, 1, shape[0], endpoint=True, dtype=dtype)
|
||||
X, Y = np.meshgrid(x, y)
|
||||
XYZ = np.stack((X, Y, np.ones_like(X)), -1)
|
||||
return XYZ
|
||||
|
||||
|
||||
def random_transform(coords: np.ndarray, rng: np.random.Generator = None):
|
||||
"""Returns randomly transformed coordinates"""
|
||||
H, W = coords.shape[:2]
|
||||
rng = rng or np.random.default_rng()
|
||||
m = rng.uniform(-1.0, 1.0, size=(3, 3)).astype(coords.dtype)
|
||||
return (coords.reshape(-1, 3) @ m.T).reshape(H, W, 3)
|
||||
|
||||
|
||||
def magic(
|
||||
coords: np.ndarray,
|
||||
depth: Optional[int] = None,
|
||||
distortion: Optional[int] = None,
|
||||
rng: np.random.Generator = None,
|
||||
):
|
||||
"""Returns color magic color texture.
|
||||
|
||||
The implementation is based on Blender's (https://www.blender.org/) magic
|
||||
texture shader. The following adaptions have been made:
|
||||
- we exchange the nested if-cascade by a probabilistic iterative approach
|
||||
|
||||
Kwargs
|
||||
------
|
||||
coords: HxWx3 array
|
||||
Coordinates transformed into colors by this method. See
|
||||
`magictex.coordinate_grid` to generate the default.
|
||||
depth: int (optional)
|
||||
Number of transformations applied. Higher numbers lead to more
|
||||
nested patterns. If not specified, randomly sampled.
|
||||
distortion: float (optional)
|
||||
Distortion of patterns. Larger values indicate more distortion,
|
||||
lower values tend to generate smoother patterns. If not specified,
|
||||
randomly sampled.
|
||||
rng: np.random.Generator
|
||||
Optional random generator to draw samples from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
colors: HxWx3 array
|
||||
Three channel color image in range [0,1]
|
||||
"""
|
||||
rng = rng or np.random.default_rng()
|
||||
if distortion is None:
|
||||
distortion = rng.uniform(1, 4)
|
||||
if depth is None:
|
||||
depth = rng.integers(1, 5)
|
||||
|
||||
H, W = coords.shape[:2]
|
||||
XYZ = coords
|
||||
x = np.sin((XYZ[..., 0] + XYZ[..., 1] + XYZ[..., 2]) * distortion)
|
||||
y = np.cos((-XYZ[..., 0] + XYZ[..., 1] - XYZ[..., 2]) * distortion)
|
||||
z = -np.cos((-XYZ[..., 0] - XYZ[..., 1] + XYZ[..., 2]) * distortion)
|
||||
|
||||
if depth > 0:
|
||||
x *= distortion
|
||||
y *= distortion
|
||||
z *= distortion
|
||||
y = -np.cos(x - y + z)
|
||||
y *= distortion
|
||||
|
||||
xyz = [x, y, z]
|
||||
fns = [np.cos, np.sin]
|
||||
for _ in range(1, depth):
|
||||
axis = rng.choice(3)
|
||||
fn = fns[rng.choice(2)]
|
||||
signs = rng.binomial(n=1, p=0.5, size=4) * 2 - 1
|
||||
|
||||
xyz[axis] = signs[-1] * fn(
|
||||
signs[0] * xyz[0] + signs[1] * xyz[1] + signs[2] * xyz[2]
|
||||
)
|
||||
xyz[axis] *= distortion
|
||||
|
||||
x, y, z = xyz
|
||||
x /= 2 * distortion
|
||||
y /= 2 * distortion
|
||||
z /= 2 * distortion
|
||||
c = 0.5 - np.stack((x, y, z), -1)
|
||||
np.clip(c, 0, 1.0)
|
||||
return c
|
||||
25
custom_nodes/ComfyUI-KJNodes/utility/numerical.py
Normal file
25
custom_nodes/ComfyUI-KJNodes/utility/numerical.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from functools import reduce
|
||||
from itertools import cycle
|
||||
from math import factorial
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
|
||||
def difference(derivative, accuracy=1):
|
||||
# Central differences implemented based on the article here:
|
||||
# http://web.media.mit.edu/~crtaylor/calculator.html
|
||||
derivative += 1
|
||||
radius = accuracy + derivative // 2 - 1
|
||||
points = range(-radius, radius + 1)
|
||||
coefficients = np.linalg.inv(np.vander(points))
|
||||
return coefficients[-derivative] * factorial(derivative - 1), points
|
||||
|
||||
|
||||
def operator(shape, *differences):
|
||||
# Credit to Philip Zucker for figuring out
|
||||
# that kronsum's argument order is reversed.
|
||||
# Without that bit of wisdom I'd have lost it.
|
||||
differences = zip(shape, cycle(differences))
|
||||
factors = (sp.diags(*diff, shape=(dim,) * 2) for dim, diff in differences)
|
||||
return reduce(lambda a, f: sp.kronsum(f, a, format='csc'), factors)
|
||||
88
custom_nodes/ComfyUI-KJNodes/utility/utility.py
Normal file
88
custom_nodes/ComfyUI-KJNodes/utility/utility.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, ImageColor
|
||||
from typing import Union, List
|
||||
import logging
|
||||
|
||||
# Utility functions from mtb nodes: https://github.com/melMass/comfy_mtb
|
||||
def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
|
||||
if isinstance(image, list):
|
||||
return torch.cat([pil2tensor(img) for img in image], dim=0)
|
||||
|
||||
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
|
||||
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
|
||||
if isinstance(img_np, list):
|
||||
return torch.cat([np2tensor(img) for img in img_np], dim=0)
|
||||
|
||||
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
|
||||
def tensor2np(tensor: torch.Tensor):
|
||||
if len(tensor.shape) == 3: # Single image
|
||||
return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
else: # Batch of images
|
||||
return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor]
|
||||
|
||||
def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
|
||||
batch_count = image.size(0) if len(image.shape) > 3 else 1
|
||||
if batch_count > 1:
|
||||
out = []
|
||||
for i in range(batch_count):
|
||||
out.extend(tensor2pil(image[i]))
|
||||
return out
|
||||
|
||||
return [
|
||||
Image.fromarray(
|
||||
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
|
||||
)
|
||||
]
|
||||
|
||||
def string_to_color(color_string: str) -> List[int]:
|
||||
color_list = [0, 0, 0] # Default fallback (black)
|
||||
|
||||
if ',' in color_string:
|
||||
# Handle CSV format (e.g., "255, 0, 0" or "255, 0, 0, 128" or "1.0, 0.5, 0.0")
|
||||
try:
|
||||
values = [float(channel.strip()) for channel in color_string.split(',')]
|
||||
# Convert to 0-255 range if values are in 0-1 range
|
||||
if all(0 <= v <= 1 for v in values):
|
||||
color_list = [int(v * 255) for v in values]
|
||||
else:
|
||||
color_list = [int(v) for v in values]
|
||||
except ValueError:
|
||||
logging.warning(f"Invalid color format: {color_string}. Using default black.")
|
||||
elif color_string.lstrip('#').isalnum() and not color_string.lstrip('#').replace('.', '', 1).isdigit():
|
||||
# Could be Hex format or color name
|
||||
color_string_stripped = color_string.lstrip('#')
|
||||
# Try hex first
|
||||
if len(color_string_stripped) in [6, 8] and all(c in '0123456789ABCDEFabcdef' for c in color_string_stripped):
|
||||
if len(color_string_stripped) == 6: # #RRGGBB
|
||||
color_list = [int(color_string_stripped[i:i+2], 16) for i in (0, 2, 4)]
|
||||
elif len(color_string_stripped) == 8: # #RRGGBBAA
|
||||
color_list = [int(color_string_stripped[i:i+2], 16) for i in (0, 2, 4, 6)]
|
||||
else:
|
||||
# Try color name (e.g., "red", "blue", "cyan")
|
||||
try:
|
||||
rgb = ImageColor.getrgb(color_string)
|
||||
color_list = list(rgb)
|
||||
except ValueError:
|
||||
logging.warning(f"Invalid color name or hex format: {color_string}. Using default black.")
|
||||
else:
|
||||
# Handle single value (grayscale) - can be int or float
|
||||
try:
|
||||
value = float(color_string.strip())
|
||||
# Convert to 0-255 range if it's a float between 0-1
|
||||
if 0 <= value <= 1:
|
||||
value = int(value * 255)
|
||||
else:
|
||||
value = int(value)
|
||||
color_list = [value, value, value]
|
||||
except ValueError:
|
||||
logging.warning(f"Invalid color format: {color_string}. Using default black.")
|
||||
|
||||
# Clip values to valid range
|
||||
color_list = np.clip(color_list, 0, 255).tolist()
|
||||
|
||||
return color_list
|
||||
Reference in New Issue
Block a user