Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@

- Follow existing node conventions: `INPUT_TYPES`, `RETURN_TYPES`, `FUNCTION`,
`CATEGORY`, and registration through the local mapping used by that file.
- Treat legacy combo inputs, `io.Combo`, and `io.DynamicCombo` values as
untrusted when they affect filesystem access. Any value used as a file or
folder name, path component, format, or extension must be validated again at
the load/save boundary using an existing `folder_paths` resolver or
containment helper, or a fixed allowlist/mapping. Do not rely only on the
advertised combo options or prompt validation.
- Keep node changes backward compatible by default. Add inputs with sensible
defaults and avoid changing output types unless the request requires it.
- Model implementations should add the minimal number of ComfyUI nodes required
Expand Down
9 changes: 5 additions & 4 deletions comfy/ldm/minimax/audio_vae.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def __init__(self, channels):
self.alpha = nn.Parameter(torch.empty(1, channels, 1))

def forward(self, x):
return snake(x, self.alpha, self.alpha)
alpha = comfy.ops.cast_to_input(self.alpha, x)
return snake(x, alpha, alpha)


class SnakeBeta(nn.Module):
Expand All @@ -47,8 +48,8 @@ def __init__(self, in_features):
self.beta = nn.Parameter(torch.empty(in_features))

def forward(self, x):
alpha = torch.exp(self.alpha).view(1, -1, 1)
beta = torch.exp(self.beta).view(1, -1, 1)
alpha = torch.exp(comfy.ops.cast_to_input(self.alpha, x)).view(1, -1, 1)
beta = torch.exp(comfy.ops.cast_to_input(self.beta, x)).view(1, -1, 1)
return snake(x, alpha, beta)


Expand Down Expand Up @@ -239,7 +240,7 @@ def __init__(self, in_dim, out_dim, num_heads):
def forward(self, x):
B, N, C = x.shape
weight, _, offload_stream = comfy.ops.cast_bias_weight(self.qkv, x, offloadable=True)
qkv = F.linear(x, weight=weight, bias=torch.cat((self.q_bias, self.zero_k_bias, self.v_bias)))
qkv = F.linear(x, weight=weight, bias=comfy.ops.cast_to_input(torch.cat((self.q_bias, self.zero_k_bias, self.v_bias)), x))
comfy.ops.uncast_bias_weight(self.qkv, weight, None, offload_stream)
q, k, v = qkv.reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4).unbind(0)

Expand Down
6 changes: 3 additions & 3 deletions comfy/ldm/minimax/vae.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ def __init__(self, heads, dim_head, bias=True, eps=1e-5):
self.scale2 = nn.Parameter(torch.empty(dim))

def forward(self, x, rotary_pos_emb=None):
x = x.addcmul_(self.attn(comfy.rmsnorm.rms_norm(x, self.norm1.weight, self.norm1.eps), rotary_pos_emb), self.scale1)
return x.addcmul_(self.ff(comfy.rmsnorm.rms_norm(x, self.norm2.weight, self.norm2.eps)), self.scale2)
x = x.addcmul_(self.attn(comfy.rmsnorm.rms_norm(x, self.norm1.weight, self.norm1.eps), rotary_pos_emb), comfy.ops.cast_to_input(self.scale1, x))
return x.addcmul_(self.ff(comfy.rmsnorm.rms_norm(x, self.norm2.weight, self.norm2.eps)), comfy.ops.cast_to_input(self.scale2, x))


class ViT3DDecoder(nn.Module):
Expand Down Expand Up @@ -289,7 +289,7 @@ def forward(self, x):
num_patches = h.shape[1]
num_suffix = 1 + self.num_register_tokens

h = torch.cat([h, self.register_tokens.expand(B, -1, -1), torch.zeros_like(h[:, 0:1, :])], dim=1)
h = torch.cat([h, comfy.ops.cast_to_input(self.register_tokens, h).expand(B, -1, -1), torch.zeros_like(h[:, 0:1, :])], dim=1)

img_ids = create_token_ids((latent_T, latent_H, latent_W), x.device, x.dtype).expand(B, -1, -1)
suffix_ids = torch.zeros((B, num_suffix, 3), device=x.device, dtype=img_ids.dtype)
Expand Down
4 changes: 4 additions & 0 deletions comfy_api/latest/_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def get_save_animated_webp_ui(
class AudioSaveHelper:
"""A helper class with static methods to handle audio saving and metadata."""
_OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
_FORMATS = {"flac", "mp3", "opus"}

@staticmethod
def save_audio(
Expand All @@ -270,6 +271,9 @@ def save_audio(
format: str = "flac",
quality: str = "128k",
) -> list[SavedResult]:
if format not in AudioSaveHelper._FORMATS:
raise ValueError(f"Unsupported audio format: {format!r}")

full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
filename_prefix, _get_directory_by_folder_type(folder_type)
)
Expand Down
8 changes: 4 additions & 4 deletions comfy_extras/nodes_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def define_schema(cls):

@classmethod
def execute(cls, folder):
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
valid_extensions = [".png", ".jpg", ".jpeg", ".webp"]
image_files = [
f
Expand Down Expand Up @@ -241,7 +241,7 @@ def define_schema(cls):
def execute(cls, folder):
logging.info(f"Loading images from folder: {folder}")

sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
valid_extensions = [".png", ".jpg", ".jpeg", ".webp"]

image_files = []
Expand Down Expand Up @@ -310,7 +310,7 @@ def define_schema(cls):

@classmethod
def execute(cls, folder):
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)
video_files = sorted([
f for f in os.listdir(sub_input_dir)
if any(f.lower().endswith(ext) for ext in VALID_VIDEO_EXTENSIONS)
Expand Down Expand Up @@ -357,7 +357,7 @@ def define_schema(cls):

@classmethod
def execute(cls, folder):
sub_input_dir = os.path.join(folder_paths.get_input_directory(), folder)
sub_input_dir = secure_subfolder_path(folder_paths.get_input_directory(), folder)

video_files = []
for item in sorted(os.listdir(sub_input_dir)):
Expand Down
13 changes: 11 additions & 2 deletions comfy_extras/nodes_gaussian_splat.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,12 @@ def _mat_to_quat(m):


class SplatToFile3D(IO.ComfyNode):
FORMAT_WRITERS = {
"ply": _gaussian_ply_bytes,
"ksplat": _gaussian_ksplat_bytes,
"spz": _gaussian_spz_bytes,
}

@classmethod
def define_schema(cls):
return IO.Schema(
Expand All @@ -482,7 +488,7 @@ def define_schema(cls):
"Supports one item per batch only.",
inputs=[
IO.Splat.Input("splat"),
IO.Combo.Input("format", options=["ply", "ksplat", "spz"], # TODO: add "splat" when we have a writer for it
IO.Combo.Input("format", options=list(cls.FORMAT_WRITERS), # TODO: add "splat" when we have a writer for it
tooltip="ply: standard 3D Gaussian Splat with full spherical harmonics. "
"ksplat: mkkellogg SplatBuffer (level 0, uncompressed), base color only "
"spz: Niantic gzip-compressed (~10x smaller), base color only "
Expand All @@ -493,10 +499,13 @@ def define_schema(cls):

@classmethod
def execute(cls, splat, format="ply") -> IO.NodeOutput:
writer = cls.FORMAT_WRITERS.get(format)
if writer is None:
raise ValueError(f"Unsupported splat format: {format!r}")

if splat.positions.shape[0] > 1:
logging.warning("SplatToFile3D supports one item per batch only. Got %d; using first.", splat.positions.shape[0])
end = _real_len(splat, 0)
writer = {"ksplat": _gaussian_ksplat_bytes, "spz": _gaussian_spz_bytes}.get(format, _gaussian_ply_bytes)
data = writer(splat.positions[0, :end], splat.scales[0, :end],
splat.rotations[0, :end], splat.opacities[0, :end], splat.sh[0, :end])
return IO.NodeOutput(Types.File3D(BytesIO(data), file_format=format))
Expand Down
19 changes: 14 additions & 5 deletions nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,30 +633,39 @@ class DiffusersLoader:
SEARCH_ALIASES = ["load diffusers model"]

@classmethod
def INPUT_TYPES(cls):
def _model_paths(cls):
paths = []
for search_path in folder_paths.get_folder_paths("diffusers"):
if os.path.exists(search_path):
for root, subdir, files in os.walk(search_path, followlinks=True):
if "model_index.json" in files:
paths.append(os.path.relpath(root, start=search_path))
return paths

return {"required": {"model_path": (paths,), }}
@classmethod
def INPUT_TYPES(cls):
return {"required": {"model_path": (cls._model_paths(),), }}
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
FUNCTION = "load_checkpoint"
DEPRECATED = True

CATEGORY = "model/loaders"

def load_checkpoint(self, model_path, output_vae=True, output_clip=True):
if model_path not in self._model_paths():
raise ValueError(f"Invalid diffusers model path: {model_path!r}")

resolved_model_path = None
for search_path in folder_paths.get_folder_paths("diffusers"):
if os.path.exists(search_path):
path = os.path.join(search_path, model_path)
if os.path.exists(path):
model_path = path
if os.path.isfile(os.path.join(path, "model_index.json")):
resolved_model_path = path
break
if resolved_model_path is None:
raise FileNotFoundError(f"Diffusers model {model_path!r} not found.")

return comfy.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))
return comfy.diffusers_load.load_diffusers(resolved_model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))


class unCLIPCheckpointLoader:
Expand Down
Loading