mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-13 09:10:12 +00:00
Compare commits
14 Commits
v3/nodes_l
...
v0.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d1bbdb40 | ||
|
|
5df1427124 | ||
|
|
d1d53c14be | ||
|
|
af7b4a921d | ||
|
|
8d9faaa181 | ||
|
|
47e1e316c5 | ||
|
|
712411d539 | ||
|
|
3fa8c5686d | ||
|
|
73d9599495 | ||
|
|
44f1246c89 | ||
|
|
8f9ea49571 | ||
|
|
9ce4c3dd87 | ||
|
|
abc87d3669 | ||
|
|
f6274c06b4 |
@@ -176,8 +176,8 @@ class InputTypeOptions(TypedDict):
|
|||||||
"""COMBO type only. Specifies the configuration for a multi-select widget.
|
"""COMBO type only. Specifies the configuration for a multi-select widget.
|
||||||
Available after ComfyUI frontend v1.13.4
|
Available after ComfyUI frontend v1.13.4
|
||||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/2987"""
|
https://github.com/Comfy-Org/ComfyUI_frontend/pull/2987"""
|
||||||
gradient_stops: NotRequired[list[list[float]]]
|
gradient_stops: NotRequired[list[dict]]
|
||||||
"""Gradient color stops for gradientslider display mode. Each stop is [offset, r, g, b] (``FLOAT``)."""
|
"""Gradient color stops for gradientslider display mode. Each stop is {"offset": float, "color": [r, g, b]}."""
|
||||||
|
|
||||||
|
|
||||||
class HiddenInputTypeDict(TypedDict):
|
class HiddenInputTypeDict(TypedDict):
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ def apply_mod(tensor, m_mult, m_add=None, modulation_dims=None):
|
|||||||
return tensor * m_mult
|
return tensor * m_mult
|
||||||
else:
|
else:
|
||||||
for d in modulation_dims:
|
for d in modulation_dims:
|
||||||
tensor[:, d[0]:d[1]] *= m_mult[:, d[2]]
|
tensor[:, d[0]:d[1]] *= m_mult[:, d[2]:d[2] + 1]
|
||||||
if m_add is not None:
|
if m_add is not None:
|
||||||
tensor[:, d[0]:d[1]] += m_add[:, d[2]]
|
tensor[:, d[0]:d[1]] += m_add[:, d[2]:d[2] + 1]
|
||||||
return tensor
|
return tensor
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ class FluxParams:
|
|||||||
txt_norm: bool = False
|
txt_norm: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def invert_slices(slices, length):
|
||||||
|
sorted_slices = sorted(slices)
|
||||||
|
result = []
|
||||||
|
current = 0
|
||||||
|
|
||||||
|
for start, end in sorted_slices:
|
||||||
|
if current < start:
|
||||||
|
result.append((current, start))
|
||||||
|
current = max(current, end)
|
||||||
|
|
||||||
|
if current < length:
|
||||||
|
result.append((current, length))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class Flux(nn.Module):
|
class Flux(nn.Module):
|
||||||
"""
|
"""
|
||||||
Transformer model for flow matching on sequences.
|
Transformer model for flow matching on sequences.
|
||||||
@@ -138,6 +154,7 @@ class Flux(nn.Module):
|
|||||||
y: Tensor,
|
y: Tensor,
|
||||||
guidance: Tensor = None,
|
guidance: Tensor = None,
|
||||||
control = None,
|
control = None,
|
||||||
|
timestep_zero_index=None,
|
||||||
transformer_options={},
|
transformer_options={},
|
||||||
attn_mask: Tensor = None,
|
attn_mask: Tensor = None,
|
||||||
) -> Tensor:
|
) -> Tensor:
|
||||||
@@ -164,10 +181,6 @@ class Flux(nn.Module):
|
|||||||
txt = self.txt_norm(txt)
|
txt = self.txt_norm(txt)
|
||||||
txt = self.txt_in(txt)
|
txt = self.txt_in(txt)
|
||||||
|
|
||||||
vec_orig = vec
|
|
||||||
if self.params.global_modulation:
|
|
||||||
vec = (self.double_stream_modulation_img(vec_orig), self.double_stream_modulation_txt(vec_orig))
|
|
||||||
|
|
||||||
if "post_input" in patches:
|
if "post_input" in patches:
|
||||||
for p in patches["post_input"]:
|
for p in patches["post_input"]:
|
||||||
out = p({"img": img, "txt": txt, "img_ids": img_ids, "txt_ids": txt_ids, "transformer_options": transformer_options})
|
out = p({"img": img, "txt": txt, "img_ids": img_ids, "txt_ids": txt_ids, "transformer_options": transformer_options})
|
||||||
@@ -182,6 +195,24 @@ class Flux(nn.Module):
|
|||||||
else:
|
else:
|
||||||
pe = None
|
pe = None
|
||||||
|
|
||||||
|
vec_orig = vec
|
||||||
|
txt_vec = vec
|
||||||
|
extra_kwargs = {}
|
||||||
|
if timestep_zero_index is not None:
|
||||||
|
modulation_dims = []
|
||||||
|
batch = vec.shape[0] // 2
|
||||||
|
vec_orig = vec_orig.reshape(2, batch, vec.shape[1]).movedim(0, 1)
|
||||||
|
invert = invert_slices(timestep_zero_index, img.shape[1])
|
||||||
|
for s in invert:
|
||||||
|
modulation_dims.append((s[0], s[1], 0))
|
||||||
|
for s in timestep_zero_index:
|
||||||
|
modulation_dims.append((s[0], s[1], 1))
|
||||||
|
extra_kwargs["modulation_dims_img"] = modulation_dims
|
||||||
|
txt_vec = vec[:batch]
|
||||||
|
|
||||||
|
if self.params.global_modulation:
|
||||||
|
vec = (self.double_stream_modulation_img(vec_orig), self.double_stream_modulation_txt(txt_vec))
|
||||||
|
|
||||||
blocks_replace = patches_replace.get("dit", {})
|
blocks_replace = patches_replace.get("dit", {})
|
||||||
transformer_options["total_blocks"] = len(self.double_blocks)
|
transformer_options["total_blocks"] = len(self.double_blocks)
|
||||||
transformer_options["block_type"] = "double"
|
transformer_options["block_type"] = "double"
|
||||||
@@ -195,7 +226,8 @@ class Flux(nn.Module):
|
|||||||
vec=args["vec"],
|
vec=args["vec"],
|
||||||
pe=args["pe"],
|
pe=args["pe"],
|
||||||
attn_mask=args.get("attn_mask"),
|
attn_mask=args.get("attn_mask"),
|
||||||
transformer_options=args.get("transformer_options"))
|
transformer_options=args.get("transformer_options"),
|
||||||
|
**extra_kwargs)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
out = blocks_replace[("double_block", i)]({"img": img,
|
out = blocks_replace[("double_block", i)]({"img": img,
|
||||||
@@ -213,7 +245,8 @@ class Flux(nn.Module):
|
|||||||
vec=vec,
|
vec=vec,
|
||||||
pe=pe,
|
pe=pe,
|
||||||
attn_mask=attn_mask,
|
attn_mask=attn_mask,
|
||||||
transformer_options=transformer_options)
|
transformer_options=transformer_options,
|
||||||
|
**extra_kwargs)
|
||||||
|
|
||||||
if control is not None: # Controlnet
|
if control is not None: # Controlnet
|
||||||
control_i = control.get("input")
|
control_i = control.get("input")
|
||||||
@@ -230,6 +263,12 @@ class Flux(nn.Module):
|
|||||||
if self.params.global_modulation:
|
if self.params.global_modulation:
|
||||||
vec, _ = self.single_stream_modulation(vec_orig)
|
vec, _ = self.single_stream_modulation(vec_orig)
|
||||||
|
|
||||||
|
extra_kwargs = {}
|
||||||
|
if timestep_zero_index is not None:
|
||||||
|
lambda a: 0 if a == 0 else a + txt.shape[1]
|
||||||
|
modulation_dims_combined = list(map(lambda x: (0 if x[0] == 0 else x[0] + txt.shape[1], x[1] + txt.shape[1], x[2]), modulation_dims))
|
||||||
|
extra_kwargs["modulation_dims"] = modulation_dims_combined
|
||||||
|
|
||||||
transformer_options["total_blocks"] = len(self.single_blocks)
|
transformer_options["total_blocks"] = len(self.single_blocks)
|
||||||
transformer_options["block_type"] = "single"
|
transformer_options["block_type"] = "single"
|
||||||
transformer_options["img_slice"] = [txt.shape[1], img.shape[1]]
|
transformer_options["img_slice"] = [txt.shape[1], img.shape[1]]
|
||||||
@@ -242,7 +281,8 @@ class Flux(nn.Module):
|
|||||||
vec=args["vec"],
|
vec=args["vec"],
|
||||||
pe=args["pe"],
|
pe=args["pe"],
|
||||||
attn_mask=args.get("attn_mask"),
|
attn_mask=args.get("attn_mask"),
|
||||||
transformer_options=args.get("transformer_options"))
|
transformer_options=args.get("transformer_options"),
|
||||||
|
**extra_kwargs)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
out = blocks_replace[("single_block", i)]({"img": img,
|
out = blocks_replace[("single_block", i)]({"img": img,
|
||||||
@@ -253,7 +293,7 @@ class Flux(nn.Module):
|
|||||||
{"original_block": block_wrap})
|
{"original_block": block_wrap})
|
||||||
img = out["img"]
|
img = out["img"]
|
||||||
else:
|
else:
|
||||||
img = block(img, vec=vec, pe=pe, attn_mask=attn_mask, transformer_options=transformer_options)
|
img = block(img, vec=vec, pe=pe, attn_mask=attn_mask, transformer_options=transformer_options, **extra_kwargs)
|
||||||
|
|
||||||
if control is not None: # Controlnet
|
if control is not None: # Controlnet
|
||||||
control_o = control.get("output")
|
control_o = control.get("output")
|
||||||
@@ -264,7 +304,11 @@ class Flux(nn.Module):
|
|||||||
|
|
||||||
img = img[:, txt.shape[1] :, ...]
|
img = img[:, txt.shape[1] :, ...]
|
||||||
|
|
||||||
img = self.final_layer(img, vec_orig) # (N, T, patch_size ** 2 * out_channels)
|
extra_kwargs = {}
|
||||||
|
if timestep_zero_index is not None:
|
||||||
|
extra_kwargs["modulation_dims"] = modulation_dims
|
||||||
|
|
||||||
|
img = self.final_layer(img, vec_orig, **extra_kwargs) # (N, T, patch_size ** 2 * out_channels)
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def process_img(self, x, index=0, h_offset=0, w_offset=0, transformer_options={}):
|
def process_img(self, x, index=0, h_offset=0, w_offset=0, transformer_options={}):
|
||||||
@@ -312,13 +356,16 @@ class Flux(nn.Module):
|
|||||||
w_len = ((w_orig + (patch_size // 2)) // patch_size)
|
w_len = ((w_orig + (patch_size // 2)) // patch_size)
|
||||||
img, img_ids = self.process_img(x, transformer_options=transformer_options)
|
img, img_ids = self.process_img(x, transformer_options=transformer_options)
|
||||||
img_tokens = img.shape[1]
|
img_tokens = img.shape[1]
|
||||||
|
timestep_zero_index = None
|
||||||
if ref_latents is not None:
|
if ref_latents is not None:
|
||||||
|
ref_num_tokens = []
|
||||||
h = 0
|
h = 0
|
||||||
w = 0
|
w = 0
|
||||||
index = 0
|
index = 0
|
||||||
ref_latents_method = kwargs.get("ref_latents_method", self.params.default_ref_method)
|
ref_latents_method = kwargs.get("ref_latents_method", self.params.default_ref_method)
|
||||||
|
timestep_zero = ref_latents_method == "index_timestep_zero"
|
||||||
for ref in ref_latents:
|
for ref in ref_latents:
|
||||||
if ref_latents_method == "index":
|
if ref_latents_method in ("index", "index_timestep_zero"):
|
||||||
index += self.params.ref_index_scale
|
index += self.params.ref_index_scale
|
||||||
h_offset = 0
|
h_offset = 0
|
||||||
w_offset = 0
|
w_offset = 0
|
||||||
@@ -342,6 +389,13 @@ class Flux(nn.Module):
|
|||||||
kontext, kontext_ids = self.process_img(ref, index=index, h_offset=h_offset, w_offset=w_offset)
|
kontext, kontext_ids = self.process_img(ref, index=index, h_offset=h_offset, w_offset=w_offset)
|
||||||
img = torch.cat([img, kontext], dim=1)
|
img = torch.cat([img, kontext], dim=1)
|
||||||
img_ids = torch.cat([img_ids, kontext_ids], dim=1)
|
img_ids = torch.cat([img_ids, kontext_ids], dim=1)
|
||||||
|
ref_num_tokens.append(kontext.shape[1])
|
||||||
|
if timestep_zero:
|
||||||
|
if index > 0:
|
||||||
|
timestep = torch.cat([timestep, timestep * 0], dim=0)
|
||||||
|
timestep_zero_index = [[img_tokens, img_ids.shape[1]]]
|
||||||
|
transformer_options = transformer_options.copy()
|
||||||
|
transformer_options["reference_image_num_tokens"] = ref_num_tokens
|
||||||
|
|
||||||
txt_ids = torch.zeros((bs, context.shape[1], len(self.params.axes_dim)), device=x.device, dtype=torch.float32)
|
txt_ids = torch.zeros((bs, context.shape[1], len(self.params.axes_dim)), device=x.device, dtype=torch.float32)
|
||||||
|
|
||||||
@@ -349,6 +403,6 @@ class Flux(nn.Module):
|
|||||||
for i in self.params.txt_ids_dims:
|
for i in self.params.txt_ids_dims:
|
||||||
txt_ids[:, :, i] = torch.linspace(0, context.shape[1] - 1, steps=context.shape[1], device=x.device, dtype=torch.float32)
|
txt_ids[:, :, i] = torch.linspace(0, context.shape[1] - 1, steps=context.shape[1], device=x.device, dtype=torch.float32)
|
||||||
|
|
||||||
out = self.forward_orig(img, img_ids, context, txt_ids, timestep, y, guidance, control, transformer_options, attn_mask=kwargs.get("attention_mask", None))
|
out = self.forward_orig(img, img_ids, context, txt_ids, timestep, y, guidance, control, timestep_zero_index=timestep_zero_index, transformer_options=transformer_options, attn_mask=kwargs.get("attention_mask", None))
|
||||||
out = out[:, :img_tokens]
|
out = out[:, :img_tokens]
|
||||||
return rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h_len, w=w_len, ph=self.patch_size, pw=self.patch_size)[:,:,:h_orig,:w_orig]
|
return rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h_len, w=w_len, ph=self.patch_size, pw=self.patch_size)[:,:,:h_orig,:w_orig]
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class VideoFromFile(VideoInput):
|
|||||||
has_first_frame = False
|
has_first_frame = False
|
||||||
for frame in frames:
|
for frame in frames:
|
||||||
offset_seconds = start_time - frame.pts * audio_stream.time_base
|
offset_seconds = start_time - frame.pts * audio_stream.time_base
|
||||||
to_skip = int(offset_seconds * audio_stream.sample_rate)
|
to_skip = max(0, int(offset_seconds * audio_stream.sample_rate))
|
||||||
if to_skip < frame.samples:
|
if to_skip < frame.samples:
|
||||||
has_first_frame = True
|
has_first_frame = True
|
||||||
break
|
break
|
||||||
@@ -280,7 +280,7 @@ class VideoFromFile(VideoInput):
|
|||||||
audio_frames.append(frame.to_ndarray()[..., to_skip:])
|
audio_frames.append(frame.to_ndarray()[..., to_skip:])
|
||||||
|
|
||||||
for frame in frames:
|
for frame in frames:
|
||||||
if frame.time > start_time + self.__duration:
|
if self.__duration and frame.time > start_time + self.__duration:
|
||||||
break
|
break
|
||||||
audio_frames.append(frame.to_ndarray()) # shape: (channels, samples)
|
audio_frames.append(frame.to_ndarray()) # shape: (channels, samples)
|
||||||
if len(audio_frames) > 0:
|
if len(audio_frames) > 0:
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class Float(ComfyTypeIO):
|
|||||||
'''Float input.'''
|
'''Float input.'''
|
||||||
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
|
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
|
||||||
default: float=None, min: float=None, max: float=None, step: float=None, round: float=None,
|
default: float=None, min: float=None, max: float=None, step: float=None, round: float=None,
|
||||||
display_mode: NumberDisplay=None, gradient_stops: list[list[float]]=None,
|
display_mode: NumberDisplay=None, gradient_stops: list[dict]=None,
|
||||||
socketless: bool=None, force_input: bool=None, extra_dict=None, raw_link: bool=None, advanced: bool=None):
|
socketless: bool=None, force_input: bool=None, extra_dict=None, raw_link: bool=None, advanced: bool=None):
|
||||||
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, None, force_input, extra_dict, raw_link, advanced)
|
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, None, force_input, extra_dict, raw_link, advanced)
|
||||||
self.min = min
|
self.min = min
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import comfy.model_management
|
|||||||
import torch
|
import torch
|
||||||
import math
|
import math
|
||||||
import nodes
|
import nodes
|
||||||
|
import comfy.ldm.flux.math
|
||||||
|
|
||||||
class CLIPTextEncodeFlux(io.ComfyNode):
|
class CLIPTextEncodeFlux(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -231,6 +232,68 @@ class Flux2Scheduler(io.ComfyNode):
|
|||||||
sigmas = get_schedule(steps, round(seq_len))
|
sigmas = get_schedule(steps, round(seq_len))
|
||||||
return io.NodeOutput(sigmas)
|
return io.NodeOutput(sigmas)
|
||||||
|
|
||||||
|
class KV_Attn_Input:
|
||||||
|
def __init__(self):
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
def __call__(self, q, k, v, extra_options, **kwargs):
|
||||||
|
reference_image_num_tokens = extra_options.get("reference_image_num_tokens", [])
|
||||||
|
if len(reference_image_num_tokens) == 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
ref_toks = sum(reference_image_num_tokens)
|
||||||
|
cache_key = "{}_{}".format(extra_options["block_type"], extra_options["block_index"])
|
||||||
|
if cache_key in self.cache:
|
||||||
|
kk, vv = self.cache[cache_key]
|
||||||
|
self.set_cache = False
|
||||||
|
return {"q": q, "k": torch.cat((k, kk), dim=2), "v": torch.cat((v, vv), dim=2)}
|
||||||
|
|
||||||
|
self.cache[cache_key] = (k[:, :, -ref_toks:].clone(), v[:, :, -ref_toks:].clone())
|
||||||
|
self.set_cache = True
|
||||||
|
return {"q": q, "k": k, "v": v}
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
class FluxKVCache(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> io.Schema:
|
||||||
|
return io.Schema(
|
||||||
|
node_id="FluxKVCache",
|
||||||
|
display_name="Flux KV Cache",
|
||||||
|
description="Enables KV Cache optimization for reference images on Flux family models.",
|
||||||
|
category="",
|
||||||
|
is_experimental=True,
|
||||||
|
inputs=[
|
||||||
|
io.Model.Input("model", tooltip="The model to use KV Cache on."),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Model.Output(tooltip="The patched model with KV Cache enabled."),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, model: io.Model.Type) -> io.NodeOutput:
|
||||||
|
m = model.clone()
|
||||||
|
input_patch_obj = KV_Attn_Input()
|
||||||
|
|
||||||
|
def model_input_patch(inputs):
|
||||||
|
if len(input_patch_obj.cache) > 0:
|
||||||
|
ref_image_tokens = sum(inputs["transformer_options"].get("reference_image_num_tokens", []))
|
||||||
|
if ref_image_tokens > 0:
|
||||||
|
img = inputs["img"]
|
||||||
|
inputs["img"] = img[:, :-ref_image_tokens]
|
||||||
|
return inputs
|
||||||
|
|
||||||
|
m.set_model_attn1_patch(input_patch_obj)
|
||||||
|
m.set_model_post_input_patch(model_input_patch)
|
||||||
|
if hasattr(model.model.diffusion_model, "params"):
|
||||||
|
m.add_object_patch("diffusion_model.params.default_ref_method", "index_timestep_zero")
|
||||||
|
else:
|
||||||
|
m.add_object_patch("diffusion_model.default_ref_method", "index_timestep_zero")
|
||||||
|
|
||||||
|
return io.NodeOutput(m)
|
||||||
|
|
||||||
class FluxExtension(ComfyExtension):
|
class FluxExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
@@ -243,6 +306,7 @@ class FluxExtension(ComfyExtension):
|
|||||||
FluxKontextMultiReferenceLatentMethod,
|
FluxKontextMultiReferenceLatentMethod,
|
||||||
EmptyFlux2LatentImage,
|
EmptyFlux2LatentImage,
|
||||||
Flux2Scheduler,
|
Flux2Scheduler,
|
||||||
|
FluxKVCache,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
127
comfy_extras/nodes_painter.py
Normal file
127
comfy_extras/nodes_painter.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import folder_paths
|
||||||
|
import node_helpers
|
||||||
|
from comfy_api.latest import ComfyExtension, io, UI
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_color: str) -> tuple[float, float, float]:
|
||||||
|
hex_color = hex_color.lstrip("#")
|
||||||
|
if len(hex_color) != 6:
|
||||||
|
return (0.0, 0.0, 0.0)
|
||||||
|
r = int(hex_color[0:2], 16) / 255.0
|
||||||
|
g = int(hex_color[2:4], 16) / 255.0
|
||||||
|
b = int(hex_color[4:6], 16) / 255.0
|
||||||
|
return (r, g, b)
|
||||||
|
|
||||||
|
|
||||||
|
class PainterNode(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="Painter",
|
||||||
|
display_name="Painter",
|
||||||
|
category="image",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input(
|
||||||
|
"image",
|
||||||
|
optional=True,
|
||||||
|
tooltip="Optional base image to paint over",
|
||||||
|
),
|
||||||
|
io.String.Input(
|
||||||
|
"mask",
|
||||||
|
default="",
|
||||||
|
socketless=True,
|
||||||
|
extra_dict={"widgetType": "PAINTER", "image_upload": True},
|
||||||
|
),
|
||||||
|
io.Int.Input(
|
||||||
|
"width",
|
||||||
|
default=512,
|
||||||
|
min=64,
|
||||||
|
max=4096,
|
||||||
|
step=64,
|
||||||
|
socketless=True,
|
||||||
|
extra_dict={"hidden": True},
|
||||||
|
),
|
||||||
|
io.Int.Input(
|
||||||
|
"height",
|
||||||
|
default=512,
|
||||||
|
min=64,
|
||||||
|
max=4096,
|
||||||
|
step=64,
|
||||||
|
socketless=True,
|
||||||
|
extra_dict={"hidden": True},
|
||||||
|
),
|
||||||
|
io.Color.Input("bg_color", default="#000000"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output("IMAGE"),
|
||||||
|
io.Mask.Output("MASK"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, mask, width, height, bg_color="#000000", image=None) -> io.NodeOutput:
|
||||||
|
if image is not None:
|
||||||
|
base_image = image[:1]
|
||||||
|
h, w = base_image.shape[1], base_image.shape[2]
|
||||||
|
else:
|
||||||
|
h, w = height, width
|
||||||
|
r, g, b = hex_to_rgb(bg_color)
|
||||||
|
base_image = torch.zeros((1, h, w, 3), dtype=torch.float32)
|
||||||
|
base_image[0, :, :, 0] = r
|
||||||
|
base_image[0, :, :, 1] = g
|
||||||
|
base_image[0, :, :, 2] = b
|
||||||
|
|
||||||
|
if mask and mask.strip():
|
||||||
|
mask_path = folder_paths.get_annotated_filepath(mask)
|
||||||
|
painter_img = node_helpers.pillow(Image.open, mask_path)
|
||||||
|
painter_img = painter_img.convert("RGBA")
|
||||||
|
|
||||||
|
if painter_img.size != (w, h):
|
||||||
|
painter_img = painter_img.resize((w, h), Image.LANCZOS)
|
||||||
|
|
||||||
|
painter_np = np.array(painter_img).astype(np.float32) / 255.0
|
||||||
|
painter_rgb = painter_np[:, :, :3]
|
||||||
|
painter_alpha = painter_np[:, :, 3:4]
|
||||||
|
|
||||||
|
mask_tensor = torch.from_numpy(painter_np[:, :, 3]).unsqueeze(0)
|
||||||
|
|
||||||
|
base_np = base_image[0].cpu().numpy()
|
||||||
|
composited = painter_rgb * painter_alpha + base_np * (1.0 - painter_alpha)
|
||||||
|
out_image = torch.from_numpy(composited).unsqueeze(0)
|
||||||
|
else:
|
||||||
|
mask_tensor = torch.zeros((1, h, w), dtype=torch.float32)
|
||||||
|
out_image = base_image
|
||||||
|
|
||||||
|
return io.NodeOutput(out_image, mask_tensor, ui=UI.PreviewImage(out_image))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fingerprint_inputs(cls, mask, width, height, bg_color="#000000", image=None):
|
||||||
|
if mask and mask.strip():
|
||||||
|
mask_path = folder_paths.get_annotated_filepath(mask)
|
||||||
|
if os.path.exists(mask_path):
|
||||||
|
m = hashlib.sha256()
|
||||||
|
with open(mask_path, "rb") as f:
|
||||||
|
m.update(f.read())
|
||||||
|
return m.digest().hex()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PainterExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self):
|
||||||
|
return [PainterNode]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint():
|
||||||
|
return PainterExtension()
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# This file is automatically generated by the build process when version is
|
# This file is automatically generated by the build process when version is
|
||||||
# updated in pyproject.toml.
|
# updated in pyproject.toml.
|
||||||
__version__ = "0.16.4"
|
__version__ = "0.17.0"
|
||||||
|
|||||||
1
nodes.py
1
nodes.py
@@ -2450,6 +2450,7 @@ async def init_builtin_extra_nodes():
|
|||||||
"nodes_nag.py",
|
"nodes_nag.py",
|
||||||
"nodes_sdpose.py",
|
"nodes_sdpose.py",
|
||||||
"nodes_math.py",
|
"nodes_math.py",
|
||||||
|
"nodes_painter.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
import_failed = []
|
import_failed = []
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ComfyUI"
|
name = "ComfyUI"
|
||||||
version = "0.16.4"
|
version = "0.17.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
comfyui-frontend-package==1.39.19
|
comfyui-frontend-package==1.41.18
|
||||||
comfyui-workflow-templates==0.9.18
|
comfyui-workflow-templates==0.9.21
|
||||||
comfyui-embedded-docs==0.4.3
|
comfyui-embedded-docs==0.4.3
|
||||||
torch
|
torch
|
||||||
torchsde
|
torchsde
|
||||||
@@ -22,7 +22,7 @@ alembic
|
|||||||
SQLAlchemy
|
SQLAlchemy
|
||||||
filelock
|
filelock
|
||||||
av>=14.2.0
|
av>=14.2.0
|
||||||
comfy-kitchen>=0.2.7
|
comfy-kitchen>=0.2.8
|
||||||
comfy-aimdo>=0.2.10
|
comfy-aimdo>=0.2.10
|
||||||
requests
|
requests
|
||||||
simpleeval>=1.0.0
|
simpleeval>=1.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user