mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-19 21:08:19 +08:00
Merge branch 'master' into update-comfyui-embedded-docs-0.5.8-20260713-203133
This commit is contained in:
commit
c0e0f8cbb3
@ -709,7 +709,7 @@ def attention3_sage(q, k, v, heads, mask=None, attn_precision=None, skip_reshape
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@torch.library.custom_op("flash_attention::flash_attn", mutates_args=())
|
@torch.library.custom_op("comfy::flash_attn", mutates_args=())
|
||||||
def flash_attn_wrapper(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
|
def flash_attn_wrapper(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
|
||||||
dropout_p: float = 0.0, causal: bool = False, softmax_scale: float = -1.0) -> torch.Tensor:
|
dropout_p: float = 0.0, causal: bool = False, softmax_scale: float = -1.0) -> torch.Tensor:
|
||||||
softmax_scale_arg = None if softmax_scale == -1.0 else softmax_scale
|
softmax_scale_arg = None if softmax_scale == -1.0 else softmax_scale
|
||||||
|
|||||||
@ -197,6 +197,9 @@ class PixDiT_T2I(nn.Module):
|
|||||||
"""Hook for subclasses to inject per-block state into the patch stream (e.g. PiD's LQ gate)."""
|
"""Hook for subclasses to inject per-block state into the patch stream (e.g. PiD's LQ gate)."""
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
def _pre_pixel_blocks(self, s, **kwargs):
|
||||||
|
return s
|
||||||
|
|
||||||
def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_options={}, **kwargs):
|
def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_options={}, **kwargs):
|
||||||
H_orig, W_orig = x.shape[2], x.shape[3]
|
H_orig, W_orig = x.shape[2], x.shape[3]
|
||||||
x = comfy.ldm.common_dit.pad_to_patch_size(x, (self.patch_size, self.patch_size))
|
x = comfy.ldm.common_dit.pad_to_patch_size(x, (self.patch_size, self.patch_size))
|
||||||
@ -226,6 +229,7 @@ class PixDiT_T2I(nn.Module):
|
|||||||
s, y_emb = blk(s, y_emb, condition, pos_img, pos_txt, None, transformer_options=transformer_options)
|
s, y_emb = blk(s, y_emb, condition, pos_img, pos_txt, None, transformer_options=transformer_options)
|
||||||
s = F.silu(t_emb + s)
|
s = F.silu(t_emb + s)
|
||||||
|
|
||||||
|
s = self._pre_pixel_blocks(s, **kwargs)
|
||||||
s_cond = s.view(B * L, self.hidden_size)
|
s_cond = s.view(B * L, self.hidden_size)
|
||||||
x_pixels = self.pixel_embedder(x, patch_size=self.patch_size)
|
x_pixels = self.pixel_embedder(x, patch_size=self.patch_size)
|
||||||
for blk in self.pixel_blocks:
|
for blk in self.pixel_blocks:
|
||||||
|
|||||||
@ -13,15 +13,15 @@ from .model import PixDiT_T2I
|
|||||||
from .modules import precompute_freqs_cis_2d
|
from .modules import precompute_freqs_cis_2d
|
||||||
|
|
||||||
|
|
||||||
class SigmaAwareGatePerTokenPerDim(nn.Module):
|
class SigmaAwareGate(nn.Module):
|
||||||
"""gate = sigmoid(content_proj(cat[x, lq]) - exp(log_alpha) * sigma); out = x + gate * lq.
|
"""gate = sigmoid(content_proj(cat[x, lq]) - exp(log_alpha) * sigma); out = x + gate * lq.
|
||||||
|
|
||||||
Trained init gives ~0.88 gate at sigma=0, ~0.05 at sigma=1.
|
Trained init gives ~0.88 gate at sigma=0, ~0.05 at sigma=1.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dim: int, dtype=None, device=None, operations=None):
|
def __init__(self, dim: int, per_token: bool = False, dtype=None, device=None, operations=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.content_proj = operations.Linear(dim * 2, dim, dtype=dtype, device=device)
|
self.content_proj = operations.Linear(dim * 2, 1 if per_token else dim, dtype=dtype, device=device)
|
||||||
self.log_alpha = nn.Parameter(torch.empty((), dtype=dtype, device=device))
|
self.log_alpha = nn.Parameter(torch.empty((), dtype=dtype, device=device))
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor, lq: torch.Tensor, sigma: torch.Tensor) -> torch.Tensor:
|
def forward(self, x: torch.Tensor, lq: torch.Tensor, sigma: torch.Tensor) -> torch.Tensor:
|
||||||
@ -36,15 +36,15 @@ class SigmaAwareGatePerTokenPerDim(nn.Module):
|
|||||||
class ResBlock(nn.Module):
|
class ResBlock(nn.Module):
|
||||||
"""Pre-activation ResNet block: GN -> SiLU -> Conv -> GN -> SiLU -> Conv + skip."""
|
"""Pre-activation ResNet block: GN -> SiLU -> Conv -> GN -> SiLU -> Conv + skip."""
|
||||||
|
|
||||||
def __init__(self, channels: int, num_groups: int = 4, dtype=None, device=None, operations=None):
|
def __init__(self, channels: int, num_groups: int = 4, conv_padding_mode: str = "zeros", dtype=None, device=None, operations=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.block = nn.Sequential(
|
self.block = nn.Sequential(
|
||||||
operations.GroupNorm(num_groups, channels, dtype=dtype, device=device),
|
operations.GroupNorm(num_groups, channels, dtype=dtype, device=device),
|
||||||
nn.SiLU(),
|
nn.SiLU(),
|
||||||
operations.Conv2d(channels, channels, kernel_size=3, padding=1, dtype=dtype, device=device),
|
operations.Conv2d(channels, channels, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
|
||||||
operations.GroupNorm(num_groups, channels, dtype=dtype, device=device),
|
operations.GroupNorm(num_groups, channels, dtype=dtype, device=device),
|
||||||
nn.SiLU(),
|
nn.SiLU(),
|
||||||
operations.Conv2d(channels, channels, kernel_size=3, padding=1, dtype=dtype, device=device),
|
operations.Conv2d(channels, channels, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
|
||||||
)
|
)
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||||
@ -62,9 +62,13 @@ class LQProjection2D(nn.Module):
|
|||||||
patch_size: int = 16,
|
patch_size: int = 16,
|
||||||
sr_scale: int = 4,
|
sr_scale: int = 4,
|
||||||
latent_spatial_down_factor: int = 8,
|
latent_spatial_down_factor: int = 8,
|
||||||
|
latent_unpatchify_factor: int = 1,
|
||||||
num_res_blocks: int = 4,
|
num_res_blocks: int = 4,
|
||||||
num_outputs: int = 7,
|
num_outputs: int = 7,
|
||||||
interval: int = 2,
|
interval: int = 2,
|
||||||
|
conv_padding_mode: str = "zeros",
|
||||||
|
gate_per_token: bool = False,
|
||||||
|
pit_output: bool = False,
|
||||||
dtype=None, device=None, operations=None,
|
dtype=None, device=None, operations=None,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -74,34 +78,38 @@ class LQProjection2D(nn.Module):
|
|||||||
self.patch_size = patch_size
|
self.patch_size = patch_size
|
||||||
self.sr_scale = sr_scale
|
self.sr_scale = sr_scale
|
||||||
self.latent_spatial_down_factor = latent_spatial_down_factor
|
self.latent_spatial_down_factor = latent_spatial_down_factor
|
||||||
|
self.latent_unpatchify_factor = latent_unpatchify_factor
|
||||||
self.num_outputs = num_outputs
|
self.num_outputs = num_outputs
|
||||||
self.interval = interval
|
self.interval = interval
|
||||||
|
|
||||||
z_to_patch_ratio = (sr_scale * latent_spatial_down_factor) / patch_size
|
effective_latent_channels = latent_channels // (latent_unpatchify_factor * latent_unpatchify_factor)
|
||||||
|
effective_spatial_down_factor = latent_spatial_down_factor // latent_unpatchify_factor
|
||||||
|
z_to_patch_ratio = (sr_scale * effective_spatial_down_factor) / patch_size
|
||||||
self.z_to_patch_ratio = z_to_patch_ratio
|
self.z_to_patch_ratio = z_to_patch_ratio
|
||||||
if z_to_patch_ratio >= 1:
|
if z_to_patch_ratio >= 1:
|
||||||
self.latent_fold_factor = 0
|
self.latent_fold_factor = 0
|
||||||
latent_proj_in_ch = latent_channels
|
latent_proj_in_ch = effective_latent_channels
|
||||||
else:
|
else:
|
||||||
fold_factor = int(1 / z_to_patch_ratio)
|
fold_factor = int(1 / z_to_patch_ratio)
|
||||||
assert fold_factor * z_to_patch_ratio == 1.0
|
assert fold_factor * z_to_patch_ratio == 1.0
|
||||||
self.latent_fold_factor = fold_factor
|
self.latent_fold_factor = fold_factor
|
||||||
latent_proj_in_ch = latent_channels * fold_factor * fold_factor
|
latent_proj_in_ch = effective_latent_channels * fold_factor * fold_factor
|
||||||
|
|
||||||
layers = [
|
layers = [
|
||||||
operations.Conv2d(latent_proj_in_ch, hidden_dim, kernel_size=3, padding=1, dtype=dtype, device=device),
|
operations.Conv2d(latent_proj_in_ch, hidden_dim, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
|
||||||
nn.SiLU(),
|
nn.SiLU(),
|
||||||
operations.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1, dtype=dtype, device=device),
|
operations.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
|
||||||
]
|
]
|
||||||
for _ in range(num_res_blocks):
|
for _ in range(num_res_blocks):
|
||||||
layers.append(ResBlock(hidden_dim, dtype=dtype, device=device, operations=operations))
|
layers.append(ResBlock(hidden_dim, conv_padding_mode=conv_padding_mode, dtype=dtype, device=device, operations=operations))
|
||||||
self.latent_proj = nn.Sequential(*layers)
|
self.latent_proj = nn.Sequential(*layers)
|
||||||
|
|
||||||
self.output_heads = nn.ModuleList(
|
self.output_heads = nn.ModuleList(
|
||||||
[operations.Linear(hidden_dim, out_dim, dtype=dtype, device=device) for _ in range(num_outputs)]
|
[operations.Linear(hidden_dim, out_dim, dtype=dtype, device=device) for _ in range(num_outputs)]
|
||||||
)
|
)
|
||||||
|
self.pit_head = operations.Linear(hidden_dim, out_dim, dtype=dtype, device=device) if pit_output else None
|
||||||
self.gate_modules = nn.ModuleList(
|
self.gate_modules = nn.ModuleList(
|
||||||
[SigmaAwareGatePerTokenPerDim(out_dim, dtype=dtype, device=device, operations=operations)
|
[SigmaAwareGate(out_dim, per_token=gate_per_token, dtype=dtype, device=device, operations=operations)
|
||||||
for _ in range(num_outputs)]
|
for _ in range(num_outputs)]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -115,6 +123,11 @@ class LQProjection2D(nn.Module):
|
|||||||
return self.gate_modules[out_idx](x, lq_feature, sigma)
|
return self.gate_modules[out_idx](x, lq_feature, sigma)
|
||||||
|
|
||||||
def _align_latent_to_patch_grid(self, lq_latent: torch.Tensor, pH: int, pW: int) -> torch.Tensor:
|
def _align_latent_to_patch_grid(self, lq_latent: torch.Tensor, pH: int, pW: int) -> torch.Tensor:
|
||||||
|
f = self.latent_unpatchify_factor
|
||||||
|
if f > 1:
|
||||||
|
B, C, H, W = lq_latent.shape
|
||||||
|
lq_latent = lq_latent.reshape(B, C // (f * f), f, f, H, W)
|
||||||
|
lq_latent = lq_latent.permute(0, 1, 4, 2, 5, 3).reshape(B, C // (f * f), H * f, W * f)
|
||||||
B, z_dim = lq_latent.shape[:2]
|
B, z_dim = lq_latent.shape[:2]
|
||||||
if self.z_to_patch_ratio >= 1:
|
if self.z_to_patch_ratio >= 1:
|
||||||
if lq_latent.shape[2] != pH or lq_latent.shape[3] != pW:
|
if lq_latent.shape[2] != pH or lq_latent.shape[3] != pW:
|
||||||
@ -134,7 +147,10 @@ class LQProjection2D(nn.Module):
|
|||||||
feat = self._align_latent_to_patch_grid(lq_latent, target_pH, target_pW)
|
feat = self._align_latent_to_patch_grid(lq_latent, target_pH, target_pW)
|
||||||
B, C, H, W = feat.shape
|
B, C, H, W = feat.shape
|
||||||
tokens = feat.permute(0, 2, 3, 1).contiguous().view(B, H * W, C)
|
tokens = feat.permute(0, 2, 3, 1).contiguous().view(B, H * W, C)
|
||||||
return [head(tokens) for head in self.output_heads]
|
outputs = [head(tokens) for head in self.output_heads]
|
||||||
|
if self.pit_head is not None:
|
||||||
|
outputs.append(self.pit_head(tokens))
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
class PidNet(PixDiT_T2I):
|
class PidNet(PixDiT_T2I):
|
||||||
@ -148,6 +164,10 @@ class PidNet(PixDiT_T2I):
|
|||||||
lq_interval: int = 2,
|
lq_interval: int = 2,
|
||||||
sr_scale: int = 4,
|
sr_scale: int = 4,
|
||||||
latent_spatial_down_factor: int = 8,
|
latent_spatial_down_factor: int = 8,
|
||||||
|
lq_latent_unpatchify_factor: int = 1,
|
||||||
|
lq_conv_padding_mode: str = "zeros",
|
||||||
|
lq_gate_per_token: bool = False,
|
||||||
|
pit_lq_inject: bool = False,
|
||||||
rope_ref_h: int = 1024, # NTK ref resolution in PIXEL units: 1024px / patch=16 -> grid_ref=64.
|
rope_ref_h: int = 1024, # NTK ref resolution in PIXEL units: 1024px / patch=16 -> grid_ref=64.
|
||||||
rope_ref_w: int = 1024,
|
rope_ref_w: int = 1024,
|
||||||
image_model=None,
|
image_model=None,
|
||||||
@ -165,6 +185,8 @@ class PidNet(PixDiT_T2I):
|
|||||||
for blk in self.pixel_blocks:
|
for blk in self.pixel_blocks:
|
||||||
blk._rope_fn = _pit_rope_fn
|
blk._rope_fn = _pit_rope_fn
|
||||||
|
|
||||||
|
self.pit_lq_inject = pit_lq_inject
|
||||||
|
|
||||||
num_lq_outputs = (self.patch_depth + lq_interval - 1) // lq_interval
|
num_lq_outputs = (self.patch_depth + lq_interval - 1) // lq_interval
|
||||||
self.lq_proj = LQProjection2D(
|
self.lq_proj = LQProjection2D(
|
||||||
latent_channels=lq_latent_channels,
|
latent_channels=lq_latent_channels,
|
||||||
@ -173,13 +195,20 @@ class PidNet(PixDiT_T2I):
|
|||||||
patch_size=self.patch_size,
|
patch_size=self.patch_size,
|
||||||
sr_scale=sr_scale,
|
sr_scale=sr_scale,
|
||||||
latent_spatial_down_factor=latent_spatial_down_factor,
|
latent_spatial_down_factor=latent_spatial_down_factor,
|
||||||
|
latent_unpatchify_factor=lq_latent_unpatchify_factor,
|
||||||
num_res_blocks=lq_num_res_blocks,
|
num_res_blocks=lq_num_res_blocks,
|
||||||
num_outputs=num_lq_outputs,
|
num_outputs=num_lq_outputs,
|
||||||
interval=lq_interval,
|
interval=lq_interval,
|
||||||
|
conv_padding_mode=lq_conv_padding_mode,
|
||||||
|
gate_per_token=lq_gate_per_token,
|
||||||
|
pit_output=pit_lq_inject,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
device=device,
|
device=device,
|
||||||
operations=operations,
|
operations=operations,
|
||||||
)
|
)
|
||||||
|
self.pit_lq_gate = SigmaAwareGate(
|
||||||
|
self.hidden_size, per_token=lq_gate_per_token, dtype=dtype, device=device, operations=operations
|
||||||
|
) if pit_lq_inject else None
|
||||||
|
|
||||||
def _fetch_patch_pos(self, height, width, device, dtype, **rope_opts):
|
def _fetch_patch_pos(self, height, width, device, dtype, **rope_opts):
|
||||||
return precompute_freqs_cis_2d(
|
return precompute_freqs_cis_2d(
|
||||||
@ -197,6 +226,11 @@ class PidNet(PixDiT_T2I):
|
|||||||
return s
|
return s
|
||||||
return self.lq_proj.gate(s, pid_lq_features[out_idx], pid_degrade_sigma, out_idx)
|
return self.lq_proj.gate(s, pid_lq_features[out_idx], pid_degrade_sigma, out_idx)
|
||||||
|
|
||||||
|
def _pre_pixel_blocks(self, s, pid_pit_lq_feature=None, pid_degrade_sigma=None, **kwargs):
|
||||||
|
if pid_pit_lq_feature is None:
|
||||||
|
return s
|
||||||
|
return self.pit_lq_gate(s, pid_pit_lq_feature, pid_degrade_sigma)
|
||||||
|
|
||||||
def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_options={}, lq_latent=None, degrade_sigma=None, **kwargs):
|
def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_options={}, lq_latent=None, degrade_sigma=None, **kwargs):
|
||||||
if lq_latent is None:
|
if lq_latent is None:
|
||||||
raise ValueError("PidNet requires lq_latent — attach via PiDConditioning")
|
raise ValueError("PidNet requires lq_latent — attach via PiDConditioning")
|
||||||
@ -216,12 +250,14 @@ class PidNet(PixDiT_T2I):
|
|||||||
degrade_sigma = degrade_sigma.expand(B).contiguous()
|
degrade_sigma = degrade_sigma.expand(B).contiguous()
|
||||||
|
|
||||||
lq_features = self.lq_proj(lq_latent=lq_latent.to(x), target_pH=Hs, target_pW=Ws)
|
lq_features = self.lq_proj(lq_latent=lq_latent.to(x), target_pH=Hs, target_pW=Ws)
|
||||||
|
pit_lq_feature = lq_features.pop() if self.pit_lq_inject else None
|
||||||
|
|
||||||
return super()._forward(
|
return super()._forward(
|
||||||
x, timesteps,
|
x, timesteps,
|
||||||
context=context, attention_mask=attention_mask,
|
context=context, attention_mask=attention_mask,
|
||||||
transformer_options=transformer_options,
|
transformer_options=transformer_options,
|
||||||
pid_lq_features=lq_features,
|
pid_lq_features=lq_features,
|
||||||
|
pid_pit_lq_feature=pit_lq_feature,
|
||||||
pid_degrade_sigma=degrade_sigma,
|
pid_degrade_sigma=degrade_sigma,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -30,7 +30,7 @@ from enum import Enum
|
|||||||
import logging
|
import logging
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
import comfy.ops
|
import comfy.ops
|
||||||
ops = comfy.ops.disable_weight_init
|
ops = comfy.ops.manual_cast
|
||||||
|
|
||||||
|
|
||||||
def _seedvr2_temporal_slicing_min_size(temporal_size, temporal_overlap, temporal_scale=1):
|
def _seedvr2_temporal_slicing_min_size(temporal_size, temporal_overlap, temporal_scale=1):
|
||||||
@ -103,11 +103,10 @@ def tiled_vae(
|
|||||||
storage_device = vae_model.device
|
storage_device = vae_model.device
|
||||||
result = None
|
result = None
|
||||||
count = None
|
count = None
|
||||||
def run_temporal_chunks(spatial_tile, model=vae_model, device=storage_device):
|
def run_temporal_chunks(spatial_tile, model=vae_model):
|
||||||
device = torch.device(device)
|
t_chunk = spatial_tile.contiguous()
|
||||||
t_chunk = spatial_tile.to(device=device, dtype=next(model.parameters()).dtype, non_blocking=True).contiguous()
|
|
||||||
old_device = getattr(model, "device", None)
|
old_device = getattr(model, "device", None)
|
||||||
model.device = device
|
model.device = t_chunk.device
|
||||||
old_slicing_min_size = getattr(model, slicing_attr, None)
|
old_slicing_min_size = getattr(model, slicing_attr, None)
|
||||||
if old_slicing_min_size is not None and slicing_min_size is not None:
|
if old_slicing_min_size is not None and slicing_min_size is not None:
|
||||||
if slicing_min_size <= 0:
|
if slicing_min_size <= 0:
|
||||||
@ -397,7 +396,7 @@ class Attention(nn.Module):
|
|||||||
|
|
||||||
def causal_norm_wrapper(norm_layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
def causal_norm_wrapper(norm_layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
||||||
input_dtype = x.dtype
|
input_dtype = x.dtype
|
||||||
if isinstance(norm_layer, (ops.LayerNorm, ops.RMSNorm)):
|
if isinstance(norm_layer, (nn.LayerNorm, nn.RMSNorm)):
|
||||||
if x.ndim == 4:
|
if x.ndim == 4:
|
||||||
x = x.permute(0, 2, 3, 1)
|
x = x.permute(0, 2, 3, 1)
|
||||||
x = norm_layer(x)
|
x = norm_layer(x)
|
||||||
@ -408,14 +407,14 @@ def causal_norm_wrapper(norm_layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
|||||||
x = norm_layer(x)
|
x = norm_layer(x)
|
||||||
x = x.permute(0, 4, 1, 2, 3)
|
x = x.permute(0, 4, 1, 2, 3)
|
||||||
return x.to(input_dtype)
|
return x.to(input_dtype)
|
||||||
if isinstance(norm_layer, (ops.GroupNorm, nn.BatchNorm2d, nn.SyncBatchNorm)):
|
if isinstance(norm_layer, (nn.GroupNorm, nn.BatchNorm2d, nn.SyncBatchNorm)):
|
||||||
if x.ndim <= 4:
|
if x.ndim <= 4:
|
||||||
return norm_layer(x).to(input_dtype)
|
return norm_layer(x).to(input_dtype)
|
||||||
if x.ndim == 5:
|
if x.ndim == 5:
|
||||||
b, c, t, h, w = x.shape
|
b, c, t, h, w = x.shape
|
||||||
x = x.transpose(1, 2).reshape(b * t, c, h, w)
|
x = x.transpose(1, 2).reshape(b * t, c, h, w)
|
||||||
memory_occupy = x.numel() * x.element_size() / 1024**3
|
memory_occupy = x.numel() * x.element_size() / 1024**3
|
||||||
if isinstance(norm_layer, ops.GroupNorm) and memory_occupy > get_norm_limit():
|
if isinstance(norm_layer, nn.GroupNorm) and memory_occupy > get_norm_limit():
|
||||||
num_chunks = min(BYTEDANCE_GN_CHUNKS_FP16 if x.element_size() == 2 else BYTEDANCE_GN_CHUNKS_FP32, norm_layer.num_groups)
|
num_chunks = min(BYTEDANCE_GN_CHUNKS_FP16 if x.element_size() == 2 else BYTEDANCE_GN_CHUNKS_FP32, norm_layer.num_groups)
|
||||||
if norm_layer.num_groups % num_chunks != 0:
|
if norm_layer.num_groups % num_chunks != 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -423,9 +422,9 @@ def causal_norm_wrapper(norm_layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
|||||||
)
|
)
|
||||||
num_groups_per_chunk = norm_layer.num_groups // num_chunks
|
num_groups_per_chunk = norm_layer.num_groups // num_chunks
|
||||||
|
|
||||||
|
weights = comfy.ops.cast_to_input(norm_layer.weight, x).chunk(num_chunks, dim=0)
|
||||||
|
biases = comfy.ops.cast_to_input(norm_layer.bias, x).chunk(num_chunks, dim=0)
|
||||||
x = list(x.chunk(num_chunks, dim=1))
|
x = list(x.chunk(num_chunks, dim=1))
|
||||||
weights = norm_layer.weight.chunk(num_chunks, dim=0)
|
|
||||||
biases = norm_layer.bias.chunk(num_chunks, dim=0)
|
|
||||||
for i, (w, bias) in enumerate(zip(weights, biases)):
|
for i, (w, bias) in enumerate(zip(weights, biases)):
|
||||||
x[i] = F.group_norm(x[i], num_groups_per_chunk, w, bias, norm_layer.eps)
|
x[i] = F.group_norm(x[i], num_groups_per_chunk, w, bias, norm_layer.eps)
|
||||||
x[i] = x[i].to(input_dtype)
|
x[i] = x[i].to(input_dtype)
|
||||||
@ -1459,7 +1458,6 @@ class VideoAutoencoderKLWrapper(VideoAutoencoderKL):
|
|||||||
def _encode_with_raw_latent(self, x):
|
def _encode_with_raw_latent(self, x):
|
||||||
if x.ndim == 4:
|
if x.ndim == 4:
|
||||||
x = x.unsqueeze(2)
|
x = x.unsqueeze(2)
|
||||||
x = x.to(dtype=next(self.parameters()).dtype)
|
|
||||||
self.device = x.device
|
self.device = x.device
|
||||||
p = super().encode(x)
|
p = super().encode(x)
|
||||||
z = p.squeeze(2)
|
z = p.squeeze(2)
|
||||||
|
|||||||
@ -470,15 +470,46 @@ def detect_unet_config(state_dict, key_prefix, metadata=None):
|
|||||||
# PiD (Pixel Diffusion Decoder). Must check BEFORE plain PixelDiT_T2I.
|
# PiD (Pixel Diffusion Decoder). Must check BEFORE plain PixelDiT_T2I.
|
||||||
_lq_w_key = '{}lq_proj.latent_proj.0.weight'.format(key_prefix)
|
_lq_w_key = '{}lq_proj.latent_proj.0.weight'.format(key_prefix)
|
||||||
if _lq_w_key in state_dict_keys:
|
if _lq_w_key in state_dict_keys:
|
||||||
in_ch = int(state_dict[_lq_w_key].shape[1])
|
latent_proj_in_channels = int(state_dict[_lq_w_key].shape[1])
|
||||||
|
hidden_dim = int(state_dict[_lq_w_key].shape[0])
|
||||||
_gate_prefix = '{}lq_proj.gate_modules.'.format(key_prefix)
|
_gate_prefix = '{}lq_proj.gate_modules.'.format(key_prefix)
|
||||||
num_gates = len({k[len(_gate_prefix):].split('.')[0]
|
num_gates = len({k[len(_gate_prefix):].split('.')[0]
|
||||||
for k in state_dict_keys if k.startswith(_gate_prefix)})
|
for k in state_dict_keys if k.startswith(_gate_prefix)})
|
||||||
|
pid_v1_5 = '{}lq_proj.pit_head.weight'.format(key_prefix) in state_dict_keys
|
||||||
dit_config = {"image_model": "pid",
|
dit_config = {"image_model": "pid",
|
||||||
"lq_latent_channels": in_ch,
|
"lq_hidden_dim": hidden_dim}
|
||||||
"latent_spatial_down_factor": 16 if in_ch >= 64 else 8}
|
|
||||||
if num_gates > 0:
|
if num_gates > 0:
|
||||||
dit_config["lq_interval"] = (14 + num_gates - 1) // num_gates
|
dit_config["lq_interval"] = (14 + num_gates - 1) // num_gates
|
||||||
|
if pid_v1_5:
|
||||||
|
pid_v1_5_variants = {
|
||||||
|
16: { # Flux and QwenImage
|
||||||
|
"lq_latent_channels": 16,
|
||||||
|
"latent_spatial_down_factor": 8,
|
||||||
|
"lq_latent_unpatchify_factor": 1,
|
||||||
|
},
|
||||||
|
32: { # Flux2 after 2x latent unpatchify
|
||||||
|
"lq_latent_channels": 128,
|
||||||
|
"latent_spatial_down_factor": 16,
|
||||||
|
"lq_latent_unpatchify_factor": 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
variant = pid_v1_5_variants.get(latent_proj_in_channels)
|
||||||
|
if variant is None:
|
||||||
|
raise ValueError(f"Unsupported PiD v1.5 latent projection with {latent_proj_in_channels} input channels")
|
||||||
|
gate_weight = state_dict['{}lq_proj.gate_modules.0.content_proj.weight'.format(key_prefix)]
|
||||||
|
dit_config.update(variant)
|
||||||
|
dit_config.update({
|
||||||
|
"lq_conv_padding_mode": "replicate",
|
||||||
|
"lq_gate_per_token": gate_weight.shape[0] == 1,
|
||||||
|
"pit_lq_inject": True,
|
||||||
|
"rope_ref_h": 2048,
|
||||||
|
"rope_ref_w": 2048,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
dit_config.update({
|
||||||
|
"lq_latent_channels": latent_proj_in_channels,
|
||||||
|
"latent_spatial_down_factor": 16 if latent_proj_in_channels >= 64 else 8,
|
||||||
|
})
|
||||||
return dit_config
|
return dit_config
|
||||||
|
|
||||||
if '{}core.pixel_embedder.proj.weight'.format(key_prefix) in state_dict_keys: # PixelDiT T2I
|
if '{}core.pixel_embedder.proj.weight'.format(key_prefix) in state_dict_keys: # PixelDiT T2I
|
||||||
|
|||||||
@ -48,7 +48,7 @@ try:
|
|||||||
# older Triton lacks libdevice.rint on the HIP backend and hard-crashes the INT8 path.
|
# older Triton lacks libdevice.rint on the HIP backend and hard-crashes the INT8 path.
|
||||||
if args.disable_triton_backend:
|
if args.disable_triton_backend:
|
||||||
ck.registry.disable("triton")
|
ck.registry.disable("triton")
|
||||||
elif args.enable_triton_backend or (torch.version.hip is not None and _rocm_kitchen_arch_supported()):
|
elif args.enable_triton_backend: # or (torch.version.hip is not None and _rocm_kitchen_arch_supported()):
|
||||||
try:
|
try:
|
||||||
import triton
|
import triton
|
||||||
triton_version = tuple(int(v) for v in triton.__version__.split(".")[:2])
|
triton_version = tuple(int(v) for v in triton.__version__.split(".")[:2])
|
||||||
|
|||||||
@ -1133,7 +1133,9 @@ class GeminiImage2(IO.ComfyNode):
|
|||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_string(prompt, strip_whitespace=True, min_length=1)
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
||||||
if model == "Nano Banana 2 (Gemini 3.1 Flash Image)":
|
if model == "Nano Banana 2 (Gemini 3.1 Flash Image)":
|
||||||
model = "gemini-3.1-flash-image-preview"
|
model = "gemini-3.1-flash-image"
|
||||||
|
elif model == "gemini-3-pro-image-preview":
|
||||||
|
model = "gemini-3-pro-image"
|
||||||
|
|
||||||
parts: list[GeminiPart] = [GeminiPart(text=prompt)]
|
parts: list[GeminiPart] = [GeminiPart(text=prompt)]
|
||||||
if images is not None:
|
if images is not None:
|
||||||
@ -1507,7 +1509,7 @@ class GeminiNanoBanana2V2(IO.ComfyNode):
|
|||||||
validate_string(prompt, strip_whitespace=True, min_length=1)
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
||||||
model_choice = model["model"]
|
model_choice = model["model"]
|
||||||
if model_choice == "Nano Banana 2 (Gemini 3.1 Flash Image)":
|
if model_choice == "Nano Banana 2 (Gemini 3.1 Flash Image)":
|
||||||
model_id = "gemini-3.1-flash-image-preview"
|
model_id = "gemini-3.1-flash-image"
|
||||||
elif model_choice == "Nano Banana 2 Lite":
|
elif model_choice == "Nano Banana 2 Lite":
|
||||||
model_id = "gemini-3.1-flash-lite-image"
|
model_id = "gemini-3.1-flash-lite-image"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from comfy.comfy_api_env import normalize_comfy_api_base
|
|||||||
from comfy.deploy_environment import get_deploy_environment
|
from comfy.deploy_environment import get_deploy_environment
|
||||||
from comfy.model_management import processing_interrupted
|
from comfy.model_management import processing_interrupted
|
||||||
from comfy_api.latest import IO
|
from comfy_api.latest import IO
|
||||||
|
from comfyui_version import __version__ as comfyui_version
|
||||||
|
|
||||||
from .common_exceptions import ProcessingInterrupted
|
from .common_exceptions import ProcessingInterrupted
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ def get_comfy_api_headers(node_cls: type[IO.ComfyNode]) -> dict[str, str]:
|
|||||||
**get_auth_header(node_cls),
|
**get_auth_header(node_cls),
|
||||||
"Comfy-Env": get_deploy_environment(),
|
"Comfy-Env": get_deploy_environment(),
|
||||||
"Comfy-Usage-Source": get_usage_source(node_cls),
|
"Comfy-Usage-Source": get_usage_source(node_cls),
|
||||||
|
"Comfy-Core-Version": comfyui_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,9 @@ PREVIEWABLE_MEDIA_TYPES = frozenset({'images', 'video', 'audio', '3d', 'text'})
|
|||||||
# 3D file extensions for preview fallback (no dedicated media_type exists)
|
# 3D file extensions for preview fallback (no dedicated media_type exists)
|
||||||
THREE_D_EXTENSIONS = frozenset({'.obj', '.fbx', '.gltf', '.glb', '.usdz'})
|
THREE_D_EXTENSIONS = frozenset({'.obj', '.fbx', '.gltf', '.glb', '.usdz'})
|
||||||
|
|
||||||
|
# Text file extensions for preview fallback (the formats SaveText can produce)
|
||||||
|
TEXT_EXTENSIONS = frozenset({'.txt', '.md', '.json'})
|
||||||
|
|
||||||
|
|
||||||
def has_3d_extension(filename: str) -> bool:
|
def has_3d_extension(filename: str) -> bool:
|
||||||
lower = filename.lower()
|
lower = filename.lower()
|
||||||
@ -143,9 +146,10 @@ def is_previewable(media_type: str, item: dict) -> bool:
|
|||||||
Maintains backwards compatibility with existing logic.
|
Maintains backwards compatibility with existing logic.
|
||||||
|
|
||||||
Priority:
|
Priority:
|
||||||
1. media_type is 'images', 'video', 'audio', or '3d'
|
1. media_type is 'images', 'video', 'audio', '3d', or 'text'
|
||||||
2. format field starts with 'video/' or 'audio/'
|
2. format field starts with 'video/' or 'audio/'
|
||||||
3. filename has a 3D extension (.obj, .fbx, .gltf, .glb, .usdz)
|
3. filename has a 3D extension (.obj, .fbx, .gltf, .glb, .usdz)
|
||||||
|
4. filename has a text extension (.txt, .md, .json, ...)
|
||||||
"""
|
"""
|
||||||
if media_type in PREVIEWABLE_MEDIA_TYPES:
|
if media_type in PREVIEWABLE_MEDIA_TYPES:
|
||||||
return True
|
return True
|
||||||
@ -156,10 +160,12 @@ def is_previewable(media_type: str, item: dict) -> bool:
|
|||||||
if fmt and (fmt.startswith('video/') or fmt.startswith('audio/')):
|
if fmt and (fmt.startswith('video/') or fmt.startswith('audio/')):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check for 3D files by extension
|
# Check for 3D and text files by extension
|
||||||
filename = item.get('filename', '').lower()
|
filename = item.get('filename', '').lower()
|
||||||
if any(filename.endswith(ext) for ext in THREE_D_EXTENSIONS):
|
if any(filename.endswith(ext) for ext in THREE_D_EXTENSIONS):
|
||||||
return True
|
return True
|
||||||
|
if any(filename.endswith(ext) for ext in TEXT_EXTENSIONS):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -255,6 +261,10 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
|
|||||||
Preview priority (matching frontend):
|
Preview priority (matching frontend):
|
||||||
1. type="output" with previewable media
|
1. type="output" with previewable media
|
||||||
2. Any previewable media
|
2. Any previewable media
|
||||||
|
|
||||||
|
Text content entries (strings under 'text') are preview-only metadata,
|
||||||
|
matching the frontend's METADATA_KEYS: they can serve as the fallback
|
||||||
|
preview but are not counted as outputs.
|
||||||
"""
|
"""
|
||||||
count = 0
|
count = 0
|
||||||
preview_output = None
|
preview_output = None
|
||||||
@ -275,7 +285,6 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
|
|||||||
if normalized is None:
|
if normalized is None:
|
||||||
# Not a 3D file string — check for text preview
|
# Not a 3D file string — check for text preview
|
||||||
if media_type == 'text':
|
if media_type == 'text':
|
||||||
count += 1
|
|
||||||
if preview_output is None:
|
if preview_output is None:
|
||||||
if isinstance(item, tuple):
|
if isinstance(item, tuple):
|
||||||
text_value = item[0] if item else ''
|
text_value = item[0] if item else ''
|
||||||
|
|||||||
@ -298,6 +298,7 @@ class PreviewAudio(IO.ComfyNode):
|
|||||||
search_aliases=["play audio"],
|
search_aliases=["play audio"],
|
||||||
display_name="Preview Audio",
|
display_name="Preview Audio",
|
||||||
category="audio",
|
category="audio",
|
||||||
|
description="Preview the audio without saving it to the ComfyUI output directory.",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Audio.Input("audio"),
|
IO.Audio.Input("audio"),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFont
|
from PIL import Image, ImageDraw, ImageEnhance, ImageFont
|
||||||
@ -166,6 +168,111 @@ def boxes_to_regions(boxes, width: int, height: int) -> list:
|
|||||||
return regions
|
return regions
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_incoming_boxes(bboxes) -> list:
|
||||||
|
if isinstance(bboxes, dict):
|
||||||
|
frame = [bboxes]
|
||||||
|
elif not isinstance(bboxes, list) or not bboxes:
|
||||||
|
frame = []
|
||||||
|
elif isinstance(bboxes[0], dict):
|
||||||
|
frame = bboxes
|
||||||
|
else:
|
||||||
|
frame = bboxes[0] if isinstance(bboxes[0], list) else []
|
||||||
|
boxes = []
|
||||||
|
for box in frame:
|
||||||
|
if not isinstance(box, dict):
|
||||||
|
continue
|
||||||
|
norm = {
|
||||||
|
"x": box.get("x", 0),
|
||||||
|
"y": box.get("y", 0),
|
||||||
|
"width": box.get("width", 0),
|
||||||
|
"height": box.get("height", 0),
|
||||||
|
}
|
||||||
|
meta = box.get("metadata")
|
||||||
|
if isinstance(meta, dict):
|
||||||
|
norm["metadata"] = meta
|
||||||
|
boxes.append(norm)
|
||||||
|
return boxes
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_element(box: dict) -> bool:
|
||||||
|
bbox = box.get("bbox")
|
||||||
|
return isinstance(bbox, (list, tuple)) and len(bbox) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_bbox(box: dict) -> bool:
|
||||||
|
return all(key in box for key in ("x", "y", "width", "height"))
|
||||||
|
|
||||||
|
|
||||||
|
def elements_to_boxes(elements: list, width: int, height: int) -> list:
|
||||||
|
boxes = []
|
||||||
|
for element in elements:
|
||||||
|
if not isinstance(element, dict):
|
||||||
|
continue
|
||||||
|
bbox = element.get("bbox")
|
||||||
|
if not (isinstance(bbox, (list, tuple)) and len(bbox) == 4):
|
||||||
|
raise ValueError("bboxes element is missing a valid 'bbox' [ymin, xmin, ymax, xmax]")
|
||||||
|
try:
|
||||||
|
ymin, xmin, ymax, xmax = (float(v) / 1000.0 for v in bbox)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError("bboxes element 'bbox' must contain four numbers")
|
||||||
|
etype = "text" if element.get("type") == "text" else "obj"
|
||||||
|
boxes.append({
|
||||||
|
"x": round(min(xmin, xmax) * width),
|
||||||
|
"y": round(min(ymin, ymax) * height),
|
||||||
|
"width": round(abs(xmax - xmin) * width),
|
||||||
|
"height": round(abs(ymax - ymin) * height),
|
||||||
|
"metadata": {
|
||||||
|
"type": etype,
|
||||||
|
"text": element.get("text", "") if etype == "text" else "",
|
||||||
|
"desc": element.get("desc", ""),
|
||||||
|
"palette": element.get("color_palette", []) or [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return boxes
|
||||||
|
|
||||||
|
|
||||||
|
def boxes_from_input(data, width: int, height: int) -> list:
|
||||||
|
if data is None:
|
||||||
|
return []
|
||||||
|
if isinstance(data, str):
|
||||||
|
text = data.strip()
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except (ValueError, TypeError) as exc:
|
||||||
|
raise ValueError(f"bboxes string input is not valid JSON: {exc}") from exc
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if _looks_like_element(data):
|
||||||
|
return elements_to_boxes([data], width, height)
|
||||||
|
if _looks_like_bbox(data):
|
||||||
|
return normalize_incoming_boxes(data)
|
||||||
|
raise ValueError(
|
||||||
|
"bboxes dict must be a bounding box (x, y, width, height) or an element (with a 'bbox')"
|
||||||
|
)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise ValueError(
|
||||||
|
"bboxes input must be bounding boxes, elements, or a JSON string, "
|
||||||
|
f"got {type(data).__name__}"
|
||||||
|
)
|
||||||
|
if not data:
|
||||||
|
return []
|
||||||
|
first = data[0]
|
||||||
|
if isinstance(first, list):
|
||||||
|
return normalize_incoming_boxes(data)
|
||||||
|
if isinstance(first, dict):
|
||||||
|
if _looks_like_element(first):
|
||||||
|
return elements_to_boxes(data, width, height)
|
||||||
|
if _looks_like_bbox(first):
|
||||||
|
return normalize_incoming_boxes(data)
|
||||||
|
raise ValueError(
|
||||||
|
"bboxes items must be bounding boxes (x, y, width, height) or elements (with a 'bbox')"
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
f"bboxes list must contain bounding boxes or elements, got {type(first).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _norm_bbox(region: dict) -> list[int]:
|
def _norm_bbox(region: dict) -> list[int]:
|
||||||
def grid(value: float) -> int:
|
def grid(value: float) -> int:
|
||||||
return max(0, min(1000, round(value * 1000)))
|
return max(0, min(1000, round(value * 1000)))
|
||||||
@ -217,29 +324,48 @@ class CreateBoundingBoxes(io.ComfyNode):
|
|||||||
optional=True,
|
optional=True,
|
||||||
tooltip="Optional image used as background in the canvas and preview.",
|
tooltip="Optional image used as background in the canvas and preview.",
|
||||||
),
|
),
|
||||||
|
io.MultiType.Input(
|
||||||
|
"bboxes",
|
||||||
|
[io.BoundingBox, io.Array, io.String],
|
||||||
|
optional=True,
|
||||||
|
tooltip="Bounding boxes, elements, or a JSON string to initialize the canvas. A new upstream value initializes the canvas; edits made on the canvas take priority and are kept until the upstream value changes again.",
|
||||||
|
),
|
||||||
io.Int.Input("width", default=1024, min=64, max=16384, step=16,
|
io.Int.Input("width", default=1024, min=64, max=16384, step=16,
|
||||||
tooltip="Width of the canvas and the pixel grid for the bounding boxes."),
|
tooltip="Width of the canvas and the pixel grid for the bounding boxes."),
|
||||||
io.Int.Input("height", default=1024, min=64, max=16384, step=16,
|
io.Int.Input("height", default=1024, min=64, max=16384, step=16,
|
||||||
tooltip="Height of the canvas and the pixel grid for the bounding boxes."),
|
tooltip="Height of the canvas and the pixel grid for the bounding boxes."),
|
||||||
editor_state,
|
editor_state,
|
||||||
|
io.BoundingBoxes.Input(
|
||||||
|
"last_incoming",
|
||||||
|
optional=True,
|
||||||
|
tooltip="Internal state managed by the canvas: the upstream bboxes value that last initialized it. Leave empty to re-initialize the canvas from the bboxes input on the next run.",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
outputs=[
|
outputs=[
|
||||||
io.Image.Output(display_name="preview"),
|
io.Image.Output(display_name="preview"),
|
||||||
io.BoundingBox.Output(display_name="bboxes"),
|
io.BoundingBox.Output(display_name="bboxes"),
|
||||||
io.Array.Output(display_name="elements"),
|
io.Array.Output(display_name="elements"),
|
||||||
],
|
],
|
||||||
|
is_output_node=True,
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, width, height, editor_state=None, background=None) -> io.NodeOutput:
|
def execute(cls, width, height, editor_state=None, last_incoming=None, background=None, bboxes=None) -> io.NodeOutput:
|
||||||
regions = boxes_to_regions(editor_state, width, height)
|
incoming = boxes_from_input(bboxes, width, height)
|
||||||
|
applied = last_incoming if isinstance(last_incoming, list) else []
|
||||||
|
upstream_changed = bool(incoming) and incoming != applied
|
||||||
|
source = incoming if upstream_changed else (editor_state or [])
|
||||||
|
regions = boxes_to_regions(source, width, height)
|
||||||
preview = render_preview(regions, width, height, _bg_from_image(background))
|
preview = render_preview(regions, width, height, _bg_from_image(background))
|
||||||
|
ui = {"dims": [width, height]}
|
||||||
|
if incoming:
|
||||||
|
ui["input_bboxes"] = incoming
|
||||||
return io.NodeOutput(
|
return io.NodeOutput(
|
||||||
preview,
|
preview,
|
||||||
fractions_to_bbox_frame(regions, width, height),
|
fractions_to_bbox_frame(regions, width, height),
|
||||||
build_elements(regions),
|
build_elements(regions),
|
||||||
ui={"dims": [width, height]},
|
ui=ui,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -844,15 +844,18 @@ class ImageMergeTileList(IO.ComfyNode):
|
|||||||
# Format specifications
|
# Format specifications
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Maps (file_format, bit_depth, has_alpha) -> (numpy dtype scale, av pixel format,
|
# Maps (file_format, bit_depth, num_channels) -> (quantization scale, numpy dtype,
|
||||||
# stream pix_fmt). Keeps the encode path declarative instead of branchy.
|
# av frame pix_fmt, stream pix_fmt). Keeps the encode path declarative instead of branchy.
|
||||||
_FORMAT_SPECS = {
|
_FORMAT_SPECS = {
|
||||||
("png", "8-bit", False): {"scale": 255.0, "dtype": np.uint8, "frame_fmt": "rgb24", "stream_fmt": "rgb24"},
|
("png", "8-bit", 1): {"scale": 255.0, "dtype": np.uint8, "frame_fmt": "gray", "stream_fmt": "gray"},
|
||||||
("png", "8-bit", True): {"scale": 255.0, "dtype": np.uint8, "frame_fmt": "rgba", "stream_fmt": "rgba"},
|
("png", "8-bit", 3): {"scale": 255.0, "dtype": np.uint8, "frame_fmt": "rgb24", "stream_fmt": "rgb24"},
|
||||||
("png", "16-bit", False): {"scale": 65535.0, "dtype": np.uint16, "frame_fmt": "rgb48le", "stream_fmt": "rgb48be"},
|
("png", "8-bit", 4): {"scale": 255.0, "dtype": np.uint8, "frame_fmt": "rgba", "stream_fmt": "rgba"},
|
||||||
("png", "16-bit", True): {"scale": 65535.0, "dtype": np.uint16, "frame_fmt": "rgba64le", "stream_fmt": "rgba64be"},
|
("png", "16-bit", 1): {"scale": 65535.0, "dtype": np.uint16, "frame_fmt": "gray16le", "stream_fmt": "gray16be"},
|
||||||
("exr", "32-bit float", False): {"scale": 1.0, "dtype": np.float32, "frame_fmt": "gbrpf32le", "stream_fmt": "gbrpf32le"},
|
("png", "16-bit", 3): {"scale": 65535.0, "dtype": np.uint16, "frame_fmt": "rgb48le", "stream_fmt": "rgb48be"},
|
||||||
("exr", "32-bit float", True): {"scale": 1.0, "dtype": np.float32, "frame_fmt": "gbrapf32le", "stream_fmt": "gbrapf32le"},
|
("png", "16-bit", 4): {"scale": 65535.0, "dtype": np.uint16, "frame_fmt": "rgba64le", "stream_fmt": "rgba64be"},
|
||||||
|
("exr", "32-bit float", 1): {"scale": 1.0, "dtype": np.float32, "frame_fmt": "grayf32le", "stream_fmt": "grayf32le"},
|
||||||
|
("exr", "32-bit float", 3): {"scale": 1.0, "dtype": np.float32, "frame_fmt": "gbrpf32le", "stream_fmt": "gbrpf32le"},
|
||||||
|
("exr", "32-bit float", 4): {"scale": 1.0, "dtype": np.float32, "frame_fmt": "gbrapf32le", "stream_fmt": "gbrapf32le"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -891,10 +894,11 @@ def hlg_to_linear(t: torch.Tensor) -> torch.Tensor:
|
|||||||
return torch.cat([hlg_to_linear(rgb), alpha], dim=-1)
|
return torch.cat([hlg_to_linear(rgb), alpha], dim=-1)
|
||||||
|
|
||||||
# Piecewise: sqrt branch below 0.5, log branch above.
|
# Piecewise: sqrt branch below 0.5, log branch above.
|
||||||
# Clamp inside the log branch so negative / out-of-range values don't blow up;
|
# Clamp the log branch at the 0.5 branch point (not above it) so the
|
||||||
|
# unselected lane stays finite in exp() without altering selected values;
|
||||||
# values above 1.0 are allowed and extrapolate naturally.
|
# values above 1.0 are allowed and extrapolate naturally.
|
||||||
low = (t ** 2) / 3.0
|
low = (t ** 2) / 3.0
|
||||||
high = (torch.exp((t.clamp(min=_HLG_C) - _HLG_C) / _HLG_A) + _HLG_B) / 12.0
|
high = (torch.exp((t.clamp(min=0.5) - _HLG_C) / _HLG_A) + _HLG_B) / 12.0
|
||||||
return torch.where(t <= 0.5, low, high)
|
return torch.where(t <= 0.5, low, high)
|
||||||
|
|
||||||
|
|
||||||
@ -1087,7 +1091,8 @@ def _encode_image(
|
|||||||
bit_depth: str,
|
bit_depth: str,
|
||||||
colorspace: str,
|
colorspace: str,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Encode a single HxWxC tensor to PNG or EXR bytes in memory.
|
"""Encode a single HxWxC (or channel-less HxW grayscale) tensor to PNG or
|
||||||
|
EXR bytes in memory. Grayscale is written as single-channel PNG / Y-only EXR.
|
||||||
|
|
||||||
For EXR the input is interpreted according to `colorspace` and converted
|
For EXR the input is interpreted according to `colorspace` and converted
|
||||||
to scene-linear (EXR's convention) before writing:
|
to scene-linear (EXR's convention) before writing:
|
||||||
@ -1101,10 +1106,16 @@ def _encode_image(
|
|||||||
For PNG, colorspace selection does not modify pixels — PNG is delivered
|
For PNG, colorspace selection does not modify pixels — PNG is delivered
|
||||||
sRGB-encoded and there is no PNG path for wide-gamut HDR in this node.
|
sRGB-encoded and there is no PNG path for wide-gamut HDR in this node.
|
||||||
"""
|
"""
|
||||||
|
if img_tensor.ndim == 2:
|
||||||
|
img_tensor = img_tensor.unsqueeze(-1) # Some nodes emit grayscale as (H, W) with no channel dim, mask-style.
|
||||||
height, width, num_channels = img_tensor.shape
|
height, width, num_channels = img_tensor.shape
|
||||||
has_alpha = num_channels == 4
|
|
||||||
|
|
||||||
spec = _FORMAT_SPECS[(file_format, bit_depth, has_alpha)]
|
spec = _FORMAT_SPECS.get((file_format, bit_depth, num_channels))
|
||||||
|
if spec is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"No {file_format}/{bit_depth} encoder for {num_channels}-channel images: "
|
||||||
|
"supported channel counts are 1 (grayscale), 3 (RGB) and 4 (RGBA)."
|
||||||
|
)
|
||||||
|
|
||||||
if spec["dtype"] == np.float32:
|
if spec["dtype"] == np.float32:
|
||||||
# EXR path: preserve full range, no clamp.
|
# EXR path: preserve full range, no clamp.
|
||||||
|
|||||||
@ -92,6 +92,7 @@ class Preview3D(IO.ComfyNode):
|
|||||||
search_aliases=["view mesh", "3d viewer"],
|
search_aliases=["view mesh", "3d viewer"],
|
||||||
display_name="Preview 3D & Animation",
|
display_name="Preview 3D & Animation",
|
||||||
category="3d",
|
category="3d",
|
||||||
|
description="Preview a 3D model file without saving it to the ComfyUI output directory.",
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
is_output_node=True,
|
is_output_node=True,
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -136,6 +137,7 @@ class Preview3DAdvanced(IO.ComfyNode):
|
|||||||
display_name="Preview 3D (Advanced)",
|
display_name="Preview 3D (Advanced)",
|
||||||
search_aliases=["preview 3d", "3d viewer", "view mesh", "frame 3d", "3d camera output"],
|
search_aliases=["preview 3d", "3d viewer", "view mesh", "frame 3d", "3d camera output"],
|
||||||
category="3d",
|
category="3d",
|
||||||
|
description="Preview a 3D model file without saving it to the ComfyUI output directory.",
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
is_output_node=True,
|
is_output_node=True,
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -193,6 +195,7 @@ class PreviewGaussianSplat(IO.ComfyNode):
|
|||||||
node_id="PreviewGaussianSplat",
|
node_id="PreviewGaussianSplat",
|
||||||
display_name="Preview Splat",
|
display_name="Preview Splat",
|
||||||
category="3d",
|
category="3d",
|
||||||
|
description="Preview a gaussian splat 3D file without saving it to the ComfyUI output directory.",
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
is_output_node=True,
|
is_output_node=True,
|
||||||
search_aliases=[
|
search_aliases=[
|
||||||
@ -261,6 +264,7 @@ class PreviewPointCloud(IO.ComfyNode):
|
|||||||
node_id="PreviewPointCloud",
|
node_id="PreviewPointCloud",
|
||||||
display_name="Preview Point Cloud",
|
display_name="Preview Point Cloud",
|
||||||
category="3d",
|
category="3d",
|
||||||
|
description="Preview a point cloud 3D file without saving it to the ComfyUI output directory.",
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
is_output_node=True,
|
is_output_node=True,
|
||||||
search_aliases=[
|
search_aliases=[
|
||||||
|
|||||||
@ -419,17 +419,18 @@ class MaskPreview(IO.ComfyNode):
|
|||||||
search_aliases=["show mask", "view mask", "inspect mask", "debug mask"],
|
search_aliases=["show mask", "view mask", "inspect mask", "debug mask"],
|
||||||
display_name="Preview Mask",
|
display_name="Preview Mask",
|
||||||
category="image/mask",
|
category="image/mask",
|
||||||
description="Saves the input images to your ComfyUI output directory.",
|
description="Preview the masks without saving them to the ComfyUI output directory.",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mask.Input("mask"),
|
IO.Mask.Input("mask"),
|
||||||
],
|
],
|
||||||
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
|
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
|
||||||
is_output_node=True,
|
is_output_node=True,
|
||||||
|
outputs=[IO.Mask.Output(display_name="mask")]
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, mask, filename_prefix="ComfyUI") -> IO.NodeOutput:
|
def execute(cls, mask, filename_prefix="ComfyUI") -> IO.NodeOutput:
|
||||||
return IO.NodeOutput(ui=UI.PreviewMask(mask))
|
return IO.NodeOutput(mask, ui=UI.PreviewMask(mask))
|
||||||
|
|
||||||
|
|
||||||
class MaskExtension(ComfyExtension):
|
class MaskExtension(ComfyExtension):
|
||||||
|
|||||||
@ -18,6 +18,7 @@ class PreviewAny():
|
|||||||
|
|
||||||
CATEGORY = "utilities"
|
CATEGORY = "utilities"
|
||||||
SEARCH_ALIASES = ["show output", "inspect", "debug", "print value", "show text"]
|
SEARCH_ALIASES = ["show output", "inspect", "debug", "print value", "show text"]
|
||||||
|
DESCRIPTION = "Preview any input value as text."
|
||||||
|
|
||||||
def main(self, source=None):
|
def main(self, source=None):
|
||||||
torch.set_printoptions(edgeitems=6)
|
torch.set_printoptions(edgeitems=6)
|
||||||
|
|||||||
@ -10,11 +10,10 @@ class String(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="PrimitiveString",
|
node_id="PrimitiveString",
|
||||||
search_aliases=["text", "string", "text box", "prompt"],
|
search_aliases=["text", "string", "text box", "prompt"],
|
||||||
display_name="Text String (DEPRECATED)",
|
display_name="Text",
|
||||||
category="utilities/primitive",
|
category="utilities/primitive",
|
||||||
inputs=[io.String.Input("value")],
|
inputs=[io.String.Input("value")],
|
||||||
outputs=[io.String.Output()],
|
outputs=[io.String.Output()]
|
||||||
is_deprecated=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -28,7 +27,7 @@ class StringMultiline(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="PrimitiveStringMultiline",
|
node_id="PrimitiveStringMultiline",
|
||||||
search_aliases=["text", "string", "text multiline", "string multiline", "text box", "prompt"],
|
search_aliases=["text", "string", "text multiline", "string multiline", "text box", "prompt"],
|
||||||
display_name="Input Text",
|
display_name="Text (Multiline)",
|
||||||
category="utilities/primitive",
|
category="utilities/primitive",
|
||||||
essentials_category="Basics",
|
essentials_category="Basics",
|
||||||
inputs=[io.String.Input("value", multiline=True)],
|
inputs=[io.String.Input("value", multiline=True)],
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from typing_extensions import override
|
|||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
from comfy_api.latest import ComfyExtension, IO, Types
|
from comfy_api.latest import ComfyExtension, IO, Types, UI
|
||||||
|
|
||||||
|
|
||||||
def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None, unlit=False):
|
def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None, unlit=False):
|
||||||
@ -406,10 +406,164 @@ class SaveGLB(IO.ComfyNode):
|
|||||||
return IO.NodeOutput(ui={"3d": results})
|
return IO.NodeOutput(ui={"3d": results})
|
||||||
|
|
||||||
|
|
||||||
|
def _save_file3d_to_output(model_3d: Types.File3D, filename_prefix: str) -> str:
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||||
|
filename_prefix, folder_paths.get_output_directory()
|
||||||
|
)
|
||||||
|
ext = model_3d.format or "glb"
|
||||||
|
saved_filename = f"{filename}_{counter:05}.{ext}"
|
||||||
|
model_3d.save_to(os.path.join(full_output_folder, saved_filename))
|
||||||
|
return f"{subfolder}/{saved_filename}" if subfolder else saved_filename
|
||||||
|
|
||||||
|
|
||||||
|
def execute_save_3d_advanced(model_3d, viewport_state, width, height, filename_prefix, kwargs) -> IO.NodeOutput:
|
||||||
|
model_file = _save_file3d_to_output(model_3d, filename_prefix)
|
||||||
|
camera_info_input = kwargs.get("camera_info", None)
|
||||||
|
camera_info = camera_info_input if camera_info_input is not None else viewport_state['camera_info']
|
||||||
|
model_3d_info_input = kwargs.get("model_3d_info", None)
|
||||||
|
model_3d_info = model_3d_info_input if model_3d_info_input is not None else viewport_state.get('model_3d_info', [])
|
||||||
|
return IO.NodeOutput(
|
||||||
|
model_3d,
|
||||||
|
model_3d_info,
|
||||||
|
camera_info,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ui=UI.PreviewUI3DAdvanced(model_file, camera_info, model_3d_info),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Save3DAdvanced(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="Save3DAdvanced",
|
||||||
|
display_name="Save 3D (Advanced)",
|
||||||
|
search_aliases=["save 3d", "export 3d model", "save mesh advanced"],
|
||||||
|
category="3d",
|
||||||
|
is_experimental=True,
|
||||||
|
is_output_node=True,
|
||||||
|
inputs=[
|
||||||
|
IO.MultiType.Input(
|
||||||
|
"model_3d",
|
||||||
|
types=[
|
||||||
|
IO.File3DGLB,
|
||||||
|
IO.File3DGLTF,
|
||||||
|
IO.File3DFBX,
|
||||||
|
IO.File3DOBJ,
|
||||||
|
IO.File3DSTL,
|
||||||
|
IO.File3DUSDZ,
|
||||||
|
IO.File3DAny,
|
||||||
|
],
|
||||||
|
tooltip="3D model file from an upstream 3D node.",
|
||||||
|
),
|
||||||
|
IO.String.Input("filename_prefix", default="3d/ComfyUI"),
|
||||||
|
IO.Load3D.Input("viewport_state"),
|
||||||
|
IO.Load3DModelInfo.Input("model_3d_info", optional=True, advanced=True),
|
||||||
|
IO.Load3DCamera.Input("camera_info", optional=True, advanced=True),
|
||||||
|
IO.Int.Input("width", default=1024, min=1, max=4096, step=1),
|
||||||
|
IO.Int.Input("height", default=1024, min=1, max=4096, step=1),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.File3DAny.Output(display_name="model_3d"),
|
||||||
|
IO.Load3DModelInfo.Output(display_name="model_3d_info"),
|
||||||
|
IO.Load3DCamera.Output(display_name="camera_info"),
|
||||||
|
IO.Int.Output(display_name="width"),
|
||||||
|
IO.Int.Output(display_name="height"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, model_3d: Types.File3D, viewport_state, width: int, height: int, filename_prefix: str, **kwargs) -> IO.NodeOutput:
|
||||||
|
return execute_save_3d_advanced(model_3d, viewport_state, width, height, filename_prefix, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SaveGaussianSplat(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="SaveGaussianSplat",
|
||||||
|
display_name="Save Splat",
|
||||||
|
search_aliases=["save splat", "save gaussian splat", "export gaussian", "export splat"],
|
||||||
|
category="3d",
|
||||||
|
is_experimental=True,
|
||||||
|
is_output_node=True,
|
||||||
|
inputs=[
|
||||||
|
IO.MultiType.Input(
|
||||||
|
"model_3d",
|
||||||
|
types=[
|
||||||
|
IO.File3DSplatAny,
|
||||||
|
IO.File3DPLY,
|
||||||
|
IO.File3DSPLAT,
|
||||||
|
IO.File3DSPZ,
|
||||||
|
IO.File3DKSPLAT,
|
||||||
|
],
|
||||||
|
tooltip="A gaussian splat 3D file.",
|
||||||
|
),
|
||||||
|
IO.String.Input("filename_prefix", default="3d/ComfyUI"),
|
||||||
|
IO.Load3D.Input("viewport_state"),
|
||||||
|
IO.Load3DModelInfo.Input("model_3d_info", optional=True, advanced=True),
|
||||||
|
IO.Load3DCamera.Input("camera_info", optional=True, advanced=True),
|
||||||
|
IO.Int.Input("width", default=1024, min=1, max=4096, step=1),
|
||||||
|
IO.Int.Input("height", default=1024, min=1, max=4096, step=1),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.File3DSplatAny.Output(display_name="model_3d"),
|
||||||
|
IO.Load3DModelInfo.Output(display_name="model_3d_info"),
|
||||||
|
IO.Load3DCamera.Output(display_name="camera_info"),
|
||||||
|
IO.Int.Output(display_name="width"),
|
||||||
|
IO.Int.Output(display_name="height"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, model_3d: Types.File3D, viewport_state, width: int, height: int, filename_prefix: str, **kwargs) -> IO.NodeOutput:
|
||||||
|
return execute_save_3d_advanced(model_3d, viewport_state, width, height, filename_prefix, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SavePointCloud(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="SavePointCloud",
|
||||||
|
display_name="Save Point Cloud",
|
||||||
|
search_aliases=["save point cloud", "save pointcloud", "export point cloud"],
|
||||||
|
category="3d",
|
||||||
|
is_experimental=True,
|
||||||
|
is_output_node=True,
|
||||||
|
inputs=[
|
||||||
|
IO.MultiType.Input(
|
||||||
|
"model_3d",
|
||||||
|
types=[
|
||||||
|
IO.File3DPointCloudAny,
|
||||||
|
IO.File3DPLY,
|
||||||
|
],
|
||||||
|
tooltip="Point cloud file (.ply)",
|
||||||
|
),
|
||||||
|
IO.String.Input("filename_prefix", default="3d/ComfyUI"),
|
||||||
|
IO.Load3D.Input("viewport_state"),
|
||||||
|
IO.Load3DModelInfo.Input("model_3d_info", optional=True, advanced=True),
|
||||||
|
IO.Load3DCamera.Input("camera_info", optional=True, advanced=True),
|
||||||
|
IO.Int.Input("width", default=1024, min=1, max=4096, step=1),
|
||||||
|
IO.Int.Input("height", default=1024, min=1, max=4096, step=1),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.File3DPointCloudAny.Output(display_name="model_3d"),
|
||||||
|
IO.Load3DModelInfo.Output(display_name="model_3d_info"),
|
||||||
|
IO.Load3DCamera.Output(display_name="camera_info"),
|
||||||
|
IO.Int.Output(display_name="width"),
|
||||||
|
IO.Int.Output(display_name="height"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, model_3d: Types.File3D, viewport_state, width: int, height: int, filename_prefix: str, **kwargs) -> IO.NodeOutput:
|
||||||
|
return execute_save_3d_advanced(model_3d, viewport_state, width, height, filename_prefix, kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Save3DExtension(ComfyExtension):
|
class Save3DExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
return [SaveGLB]
|
return [SaveGLB, Save3DAdvanced, SaveGaussianSplat, SavePointCloud]
|
||||||
|
|
||||||
|
|
||||||
async def comfy_entrypoint() -> Save3DExtension:
|
async def comfy_entrypoint() -> Save3DExtension:
|
||||||
|
|||||||
71
comfy_extras/nodes_text.py
Normal file
71
comfy_extras/nodes_text.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import io, ComfyExtension, ui
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
|
|
||||||
|
class SaveTextNode(io.ComfyNode):
|
||||||
|
"""Save text content to .txt, .md, or .json."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="SaveText",
|
||||||
|
search_aliases=["save text", "write text", "export text"],
|
||||||
|
display_name="Save Text",
|
||||||
|
category="text",
|
||||||
|
description="Save text content to a file in the output directory.",
|
||||||
|
inputs=[
|
||||||
|
io.String.Input("text", force_input=True),
|
||||||
|
io.String.Input("filename_prefix", default="ComfyUI"),
|
||||||
|
io.Combo.Input("format", options=["txt", "md", "json"], default="txt"),
|
||||||
|
],
|
||||||
|
outputs=[io.String.Output(display_name="text")],
|
||||||
|
is_output_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, text, filename_prefix, format):
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||||
|
filename_prefix,
|
||||||
|
folder_paths.get_output_directory(),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
file = f"{filename}_{counter:05}.{format}"
|
||||||
|
filepath = os.path.join(full_output_folder, file)
|
||||||
|
|
||||||
|
if format == "json":
|
||||||
|
# tries to pretty print otherwise saves normally
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
else:
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
return io.NodeOutput(
|
||||||
|
text,
|
||||||
|
ui={
|
||||||
|
"text": (text,),
|
||||||
|
"files": [
|
||||||
|
ui.SavedResult(file, subfolder, io.FolderType.output)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class TextExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
SaveTextNode
|
||||||
|
]
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> TextExtension:
|
||||||
|
return TextExtension()
|
||||||
@ -81,7 +81,7 @@ class SaveVideo(io.ComfyNode):
|
|||||||
display_name="Save Video",
|
display_name="Save Video",
|
||||||
category="video",
|
category="video",
|
||||||
essentials_category="Basics",
|
essentials_category="Basics",
|
||||||
description="Saves the input images to your ComfyUI output directory.",
|
description="Saves the input videos to your ComfyUI output directory.",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Video.Input("video", tooltip="The video to save."),
|
io.Video.Input("video", tooltip="The video to save."),
|
||||||
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."),
|
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."),
|
||||||
|
|||||||
2
nodes.py
2
nodes.py
@ -1709,6 +1709,7 @@ class PreviewImage(SaveImage):
|
|||||||
self.compress_level = 1
|
self.compress_level = 1
|
||||||
|
|
||||||
SEARCH_ALIASES = ["preview", "preview image", "show image", "view image", "display image", "image viewer"]
|
SEARCH_ALIASES = ["preview", "preview image", "show image", "view image", "display image", "image viewer"]
|
||||||
|
DESCRIPTION = "Preview the images without saving them to the ComfyUI output directory."
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
@ -2504,6 +2505,7 @@ async def init_builtin_extra_nodes():
|
|||||||
"nodes_triposplat.py",
|
"nodes_triposplat.py",
|
||||||
"nodes_depth_anything_3.py",
|
"nodes_depth_anything_3.py",
|
||||||
"nodes_seed.py",
|
"nodes_seed.py",
|
||||||
|
"nodes_text.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
import_failed = []
|
import_failed = []
|
||||||
|
|||||||
@ -97,6 +97,21 @@ def _make_seedvr2_3b_shared_mm_sd():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _make_pid_v1_5_sd(latent_proj_channels=16):
|
||||||
|
sd = {
|
||||||
|
"pixel_embedder.proj.weight": torch.empty(16, 3, device="meta"),
|
||||||
|
"lq_proj.latent_proj.0.weight": torch.empty(1024, latent_proj_channels, 3, 3, device="meta"),
|
||||||
|
"lq_proj.pit_head.weight": torch.empty(1536, 1024, device="meta"),
|
||||||
|
"lq_proj.gate_modules.0.content_proj.weight": torch.empty(1, 3072, device="meta"),
|
||||||
|
"pixel_blocks.0.attn.q_norm.weight": torch.empty(72, device="meta"),
|
||||||
|
"pixel_blocks.0.adaLN_modulation.0.weight": torch.empty(24576, 1536, device="meta"),
|
||||||
|
"pixel_blocks.0.adaLN_modulation.0.bias": torch.empty(24576, device="meta"),
|
||||||
|
}
|
||||||
|
for i in range(7):
|
||||||
|
sd[f"lq_proj.gate_modules.{i}.log_alpha"] = torch.empty((), device="meta")
|
||||||
|
return sd
|
||||||
|
|
||||||
|
|
||||||
def _add_model_diffusion_prefix(sd):
|
def _add_model_diffusion_prefix(sd):
|
||||||
return {f"model.diffusion_model.{k}": v for k, v in sd.items()}
|
return {f"model.diffusion_model.{k}": v for k, v in sd.items()}
|
||||||
|
|
||||||
@ -206,6 +221,43 @@ class TestModelDetection:
|
|||||||
|
|
||||||
assert type(model_config_from_unet(sd, "model.diffusion_model.")).__name__ == "SeedVR2"
|
assert type(model_config_from_unet(sd, "model.diffusion_model.")).__name__ == "SeedVR2"
|
||||||
|
|
||||||
|
def test_pid_v1_5_detection(self):
|
||||||
|
sd = _make_pid_v1_5_sd()
|
||||||
|
unet_config = detect_unet_config(sd, "")
|
||||||
|
|
||||||
|
assert unet_config == {
|
||||||
|
"image_model": "pid",
|
||||||
|
"lq_latent_channels": 16,
|
||||||
|
"lq_hidden_dim": 1024,
|
||||||
|
"latent_spatial_down_factor": 8,
|
||||||
|
"lq_interval": 2,
|
||||||
|
"lq_latent_unpatchify_factor": 1,
|
||||||
|
"lq_conv_padding_mode": "replicate",
|
||||||
|
"lq_gate_per_token": True,
|
||||||
|
"pit_lq_inject": True,
|
||||||
|
"rope_ref_h": 2048,
|
||||||
|
"rope_ref_w": 2048,
|
||||||
|
}
|
||||||
|
assert type(model_config_from_unet_config(unet_config, sd)).__name__ == "PiD"
|
||||||
|
|
||||||
|
def test_pid_v1_5_flux2_detection(self):
|
||||||
|
unet_config = detect_unet_config(_make_pid_v1_5_sd(latent_proj_channels=32), "")
|
||||||
|
|
||||||
|
assert unet_config["lq_latent_channels"] == 128
|
||||||
|
assert unet_config["latent_spatial_down_factor"] == 16
|
||||||
|
assert unet_config["lq_latent_unpatchify_factor"] == 2
|
||||||
|
|
||||||
|
def test_pid_v1_5_pixel_adaln_conversion(self):
|
||||||
|
sd = _make_pid_v1_5_sd()
|
||||||
|
model_config = model_config_from_unet_config(detect_unet_config(sd, ""), sd)
|
||||||
|
processed = model_config.process_unet_state_dict(sd)
|
||||||
|
|
||||||
|
assert processed["pixel_blocks.0.attn.q_norm.weight"].shape == (72,)
|
||||||
|
assert processed["pixel_blocks.0.adaLN_modulation_msa.weight"].shape == (12288, 1536)
|
||||||
|
assert processed["pixel_blocks.0.adaLN_modulation_mlp.weight"].shape == (12288, 1536)
|
||||||
|
assert processed["pixel_blocks.0.adaLN_modulation_msa.bias"].shape == (12288,)
|
||||||
|
assert processed["pixel_blocks.0.adaLN_modulation_mlp.bias"].shape == (12288,)
|
||||||
|
|
||||||
def test_unet_config_and_required_keys_combination_is_unique(self):
|
def test_unet_config_and_required_keys_combination_is_unique(self):
|
||||||
"""Each model in the registry must have a unique combination of
|
"""Each model in the registry must have a unique combination of
|
||||||
``unet_config`` and ``required_keys``. If two models share the same
|
``unet_config`` and ``required_keys``. If two models share the same
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import torch
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
|
||||||
from comfy.cli_args import args as cli_args
|
from comfy.cli_args import args as cli_args
|
||||||
|
|
||||||
@ -48,3 +49,31 @@ def test_seedvr2_vae_decode_memory_covers_full_frame_lab_transfer():
|
|||||||
assert estimate == 101 * 960 * 1280 * 160
|
assert estimate == 101 * 960 * 1280 * 160
|
||||||
assert estimate > 15 * 1024 ** 3
|
assert estimate > 15 * 1024 ** 3
|
||||||
assert estimate > old_estimate * 100
|
assert estimate > old_estimate * 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_seedvr2_vae_encode_preserves_compute_dtype(monkeypatch):
|
||||||
|
wrapper = seedvr_vae.VideoAutoencoderKLWrapper.__new__(seedvr_vae.VideoAutoencoderKLWrapper)
|
||||||
|
nn.Module.__init__(wrapper)
|
||||||
|
wrapper._dummy = nn.Parameter(torch.empty(1, dtype=torch.float16))
|
||||||
|
input_dtype = None
|
||||||
|
|
||||||
|
def encode(self, x):
|
||||||
|
nonlocal input_dtype
|
||||||
|
input_dtype = x.dtype
|
||||||
|
return x
|
||||||
|
|
||||||
|
monkeypatch.setattr(seedvr_vae.VideoAutoencoderKL, "encode", encode)
|
||||||
|
|
||||||
|
x = torch.zeros((1, 3, 1, 8, 8), dtype=torch.float32)
|
||||||
|
wrapper._encode_with_raw_latent(x)
|
||||||
|
|
||||||
|
assert input_dtype == torch.float32
|
||||||
|
|
||||||
|
|
||||||
|
def test_seedvr2_vae_ops_cast_weights_to_compute_dtype():
|
||||||
|
attention = seedvr_vae.Attention(query_dim=4, heads=1, dim_head=4).to(torch.float16)
|
||||||
|
hidden_states = torch.zeros((1, 2, 4), dtype=torch.float32)
|
||||||
|
|
||||||
|
output = attention(hidden_states)
|
||||||
|
|
||||||
|
assert output.dtype == torch.float32
|
||||||
|
|||||||
@ -122,6 +122,31 @@ def test_tiled_vae_encode_uses_tensor_return_without_indexing():
|
|||||||
assert tuple(out.shape) == (2, _LATENT_CHANNELS, 1, 8, 8)
|
assert tuple(out.shape) == (2, _LATENT_CHANNELS, 1, 8, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiled_vae_preserves_compute_dtype_with_different_parameter_dtype():
|
||||||
|
class DummyVAE(nn.Module):
|
||||||
|
spatial_downsample_factor = 8
|
||||||
|
temporal_downsample_factor = 4
|
||||||
|
slicing_sample_min_size = 8
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.device = torch.device("cpu")
|
||||||
|
self._dummy = nn.Parameter(torch.zeros(1, dtype=torch.float16))
|
||||||
|
self.input_dtype = None
|
||||||
|
|
||||||
|
def encode(self, t_chunk):
|
||||||
|
self.input_dtype = t_chunk.dtype
|
||||||
|
b, _, _, h, w = t_chunk.shape
|
||||||
|
return torch.ones((b, _LATENT_CHANNELS, 1, h // 8, w // 8), dtype=t_chunk.dtype)
|
||||||
|
|
||||||
|
vae = DummyVAE()
|
||||||
|
x = torch.zeros((1, 3, 1, 64, 64), dtype=torch.float32)
|
||||||
|
|
||||||
|
tiled_vae(x, vae, tile_size=(64, 64), tile_overlap=(16, 16), encode=True)
|
||||||
|
|
||||||
|
assert vae.input_dtype == torch.float32
|
||||||
|
|
||||||
|
|
||||||
def test_tiled_vae_preserves_input_dtype_on_single_tile():
|
def test_tiled_vae_preserves_input_dtype_on_single_tile():
|
||||||
class FloatOutputVAEModel(torch.nn.Module):
|
class FloatOutputVAEModel(torch.nn.Module):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user