Merge branch 'v3-definition' into v3-definition-wip

This commit is contained in:
kosinkadink1@gmail.com 2025-07-15 14:30:29 -05:00
commit 59e2d47cfc
12 changed files with 760 additions and 154 deletions

0
comfy_api/v3/__init__.py Normal file
View File

View File

@ -1,4 +1,4 @@
from typing import Optional, Callable from typing import Callable, Optional
def first_real_override(cls: type, name: str, *, base: type) -> Optional[Callable]: def first_real_override(cls: type, name: str, *, base: type) -> Optional[Callable]:

View File

@ -1,27 +1,30 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Literal, TypeVar, Callable, TypedDict
from typing_extensions import NotRequired
from enum import Enum
from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict
from collections import Counter
from comfy_execution.graph import ExecutionBlocker
from comfy_api.internal import ComfyNodeInternal
from comfy_api.v3.resources import Resources, ResourcesLocal
import copy import copy
from abc import ABC, abstractmethod
from collections import Counter
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Any, Callable, Literal, TypedDict, TypeVar
# used for type hinting # used for type hinting
import torch import torch
from spandrel import ImageModelDescriptor from spandrel import ImageModelDescriptor
from comfy.model_patcher import ModelPatcher from typing_extensions import NotRequired
from comfy.samplers import Sampler, CFGGuider
from comfy.sd import CLIP
from comfy.controlnet import ControlNet
from comfy.sd import VAE
from comfy.sd import StyleModel as StyleModel_
from comfy.clip_vision import ClipVisionModel from comfy.clip_vision import ClipVisionModel
from comfy.clip_vision import Output as ClipVisionOutput_ from comfy.clip_vision import Output as ClipVisionOutput_
from comfy_api.input import VideoInput from comfy.controlnet import ControlNet
from comfy.hooks import HookGroup, HookKeyframeGroup from comfy.hooks import HookGroup, HookKeyframeGroup
from comfy.model_patcher import ModelPatcher
from comfy.samplers import CFGGuider, Sampler
from comfy.sd import CLIP, VAE
from comfy.sd import StyleModel as StyleModel_
from comfy_api.input import VideoInput
from comfy_api.internal import ComfyNodeInternal
from comfy_api.v3.resources import Resources, ResourcesLocal
from comfy_execution.graph import ExecutionBlocker
# from comfy_extras.nodes_images import SVG as SVG_ # NOTE: needs to be moved before can be imported due to circular reference # from comfy_extras.nodes_images import SVG as SVG_ # NOTE: needs to be moved before can be imported due to circular reference
@ -1191,7 +1194,7 @@ class ComfyNodeV3(ComfyNodeInternal):
@classmethod @classmethod
def GET_NODE_INFO_V3(cls) -> dict[str, Any]: def GET_NODE_INFO_V3(cls) -> dict[str, Any]:
schema = cls.GET_SCHEMA() # schema = cls.GET_SCHEMA()
# TODO: finish # TODO: finish
return None return None
@ -1237,84 +1240,84 @@ class ComfyNodeV3(ComfyNodeInternal):
#-------------------------------------------- #--------------------------------------------
_DESCRIPTION = None _DESCRIPTION = None
@classproperty @classproperty
def DESCRIPTION(cls): def DESCRIPTION(cls): # noqa
if cls._DESCRIPTION is None: if cls._DESCRIPTION is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._DESCRIPTION return cls._DESCRIPTION
_CATEGORY = None _CATEGORY = None
@classproperty @classproperty
def CATEGORY(cls): def CATEGORY(cls): # noqa
if cls._CATEGORY is None: if cls._CATEGORY is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._CATEGORY return cls._CATEGORY
_EXPERIMENTAL = None _EXPERIMENTAL = None
@classproperty @classproperty
def EXPERIMENTAL(cls): def EXPERIMENTAL(cls): # noqa
if cls._EXPERIMENTAL is None: if cls._EXPERIMENTAL is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._EXPERIMENTAL return cls._EXPERIMENTAL
_DEPRECATED = None _DEPRECATED = None
@classproperty @classproperty
def DEPRECATED(cls): def DEPRECATED(cls): # noqa
if cls._DEPRECATED is None: if cls._DEPRECATED is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._DEPRECATED return cls._DEPRECATED
_API_NODE = None _API_NODE = None
@classproperty @classproperty
def API_NODE(cls): def API_NODE(cls): # noqa
if cls._API_NODE is None: if cls._API_NODE is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._API_NODE return cls._API_NODE
_OUTPUT_NODE = None _OUTPUT_NODE = None
@classproperty @classproperty
def OUTPUT_NODE(cls): def OUTPUT_NODE(cls): # noqa
if cls._OUTPUT_NODE is None: if cls._OUTPUT_NODE is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._OUTPUT_NODE return cls._OUTPUT_NODE
_INPUT_IS_LIST = None _INPUT_IS_LIST = None
@classproperty @classproperty
def INPUT_IS_LIST(cls): def INPUT_IS_LIST(cls): # noqa
if cls._INPUT_IS_LIST is None: if cls._INPUT_IS_LIST is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._INPUT_IS_LIST return cls._INPUT_IS_LIST
_OUTPUT_IS_LIST = None _OUTPUT_IS_LIST = None
@classproperty @classproperty
def OUTPUT_IS_LIST(cls): def OUTPUT_IS_LIST(cls): # noqa
if cls._OUTPUT_IS_LIST is None: if cls._OUTPUT_IS_LIST is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._OUTPUT_IS_LIST return cls._OUTPUT_IS_LIST
_RETURN_TYPES = None _RETURN_TYPES = None
@classproperty @classproperty
def RETURN_TYPES(cls): def RETURN_TYPES(cls): # noqa
if cls._RETURN_TYPES is None: if cls._RETURN_TYPES is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._RETURN_TYPES return cls._RETURN_TYPES
_RETURN_NAMES = None _RETURN_NAMES = None
@classproperty @classproperty
def RETURN_NAMES(cls): def RETURN_NAMES(cls): # noqa
if cls._RETURN_NAMES is None: if cls._RETURN_NAMES is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._RETURN_NAMES return cls._RETURN_NAMES
_OUTPUT_TOOLTIPS = None _OUTPUT_TOOLTIPS = None
@classproperty @classproperty
def OUTPUT_TOOLTIPS(cls): def OUTPUT_TOOLTIPS(cls): # noqa
if cls._OUTPUT_TOOLTIPS is None: if cls._OUTPUT_TOOLTIPS is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._OUTPUT_TOOLTIPS return cls._OUTPUT_TOOLTIPS
_NOT_IDEMPOTENT = None _NOT_IDEMPOTENT = None
@classproperty @classproperty
def NOT_IDEMPOTENT(cls): def NOT_IDEMPOTENT(cls): # noqa
if cls._NOT_IDEMPOTENT is None: if cls._NOT_IDEMPOTENT is None:
cls.GET_SCHEMA() cls.GET_SCHEMA()
return cls._NOT_IDEMPOTENT return cls._NOT_IDEMPOTENT
@ -1494,36 +1497,36 @@ class TestNode(ComfyNodeV3):
def execute(cls, **kwargs): def execute(cls, **kwargs):
pass pass
if __name__ == "__main__": # if __name__ == "__main__":
print("hello there") # print("hello there")
inputs: list[InputV3] = [ # inputs: list[InputV3] = [
Int.Input("tessfes", widgetType=String.io_type), # Int.Input("tessfes", widgetType=String.io_type),
Int.Input("my_int"), # Int.Input("my_int"),
Custom("XYZ").Input("xyz"), # Custom("XYZ").Input("xyz"),
Custom("MODEL_M").Input("model1"), # Custom("MODEL_M").Input("model1"),
Image.Input("my_image"), # Image.Input("my_image"),
Float.Input("my_float"), # Float.Input("my_float"),
MultiType.Input("my_inputs", [String, Custom("MODEL_M"), Custom("XYZ")]), # MultiType.Input("my_inputs", [String, Custom("MODEL_M"), Custom("XYZ")]),
] # ]
Custom("XYZ").Input() # Custom("XYZ").Input()
outputs: list[OutputV3] = [ # outputs: list[OutputV3] = [
Image.Output("image"), # Image.Output("image"),
Custom("XYZ").Output("xyz"), # Custom("XYZ").Output("xyz"),
] # ]
#
for c in inputs: # for c in inputs:
if isinstance(c, MultiType): # if isinstance(c, MultiType):
print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}, {[x.io_type for x in c.io_types]}") # print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}, {[x.io_type for x in c.io_types]}")
print(c.get_io_type_V1()) # print(c.get_io_type_V1())
else: # else:
print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}") # print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}")
#
for c in outputs: # for c in outputs:
print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}") # print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}")
#
zz = TestNode() # zz = TestNode()
print(zz.GET_NODE_INFO_V1()) # print(zz.GET_NODE_INFO_V1())
#
# aa = NodeInfoV1() # # aa = NodeInfoV1()
# print(asdict(aa)) # # print(asdict(aa))
# print(as_pruned_dict(aa)) # # print(as_pruned_dict(aa))

View File

@ -1,15 +1,21 @@
from __future__ import annotations from __future__ import annotations
from comfy_api.v3.io import Image, FolderType, _UIOutput, ComfyNodeV3 import json
# used for image preview import os
from comfy.cli_args import args
import folder_paths
import random import random
from io import BytesIO
import av
import numpy as np
import torchaudio
from PIL import Image as PILImage from PIL import Image as PILImage
from PIL.PngImagePlugin import PngInfo from PIL.PngImagePlugin import PngInfo
import os
import json import folder_paths
import numpy as np
# used for image preview
from comfy.cli_args import args
from comfy_api.v3.io import ComfyNodeV3, FolderType, Image, _UIOutput
class SavedResult(dict): class SavedResult(dict):
@ -63,11 +69,13 @@ class PreviewImage(_UIOutput):
"animated": (self.animated,) "animated": (self.animated,)
} }
class PreviewMask(PreviewImage): class PreviewMask(PreviewImage):
def __init__(self, mask: PreviewMask.Type, animated: bool=False, cls: ComfyNodeV3=None, **kwargs): def __init__(self, mask: PreviewMask.Type, animated: bool=False, cls: ComfyNodeV3=None, **kwargs):
preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3) preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
super().__init__(preview, animated, cls, **kwargs) super().__init__(preview, animated, cls, **kwargs)
# class UILatent(_UIOutput): # class UILatent(_UIOutput):
# def __init__(self, values: list[SavedResult | dict], **kwargs): # def __init__(self, values: list[SavedResult | dict], **kwargs):
# output_dir = folder_paths.get_temp_directory() # output_dir = folder_paths.get_temp_directory()
@ -115,13 +123,113 @@ class PreviewMask(PreviewImage):
# "latents": self.values, # "latents": self.values,
# } # }
class PreviewAudio(_UIOutput): class PreviewAudio(_UIOutput):
def __init__(self, values: list[SavedResult | dict], **kwargs): def __init__(self, audio, cls: ComfyNodeV3=None, **kwargs):
self.values = values quality = "128k"
format = "flac"
filename_prefix = "ComfyUI_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, folder_paths.get_temp_directory()
)
# Prepare metadata dictionary
metadata = {}
if not args.disable_metadata and cls is not None:
if cls.hidden.prompt is not None:
metadata["prompt"] = json.dumps(cls.hidden.prompt)
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
# Opus supported sample rates
OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
results = []
for (batch_number, waveform) in enumerate(audio["waveform"].cpu()):
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.{format}"
output_path = os.path.join(full_output_folder, file)
# Use original sample rate initially
sample_rate = audio["sample_rate"]
# Handle Opus sample rate requirements
if format == "opus":
if sample_rate > 48000:
sample_rate = 48000
elif sample_rate not in OPUS_RATES:
# Find the next highest supported rate
for rate in sorted(OPUS_RATES):
if rate > sample_rate:
sample_rate = rate
break
if sample_rate not in OPUS_RATES: # Fallback if still not supported
sample_rate = 48000
# Resample if necessary
if sample_rate != audio["sample_rate"]:
waveform = torchaudio.functional.resample(waveform, audio["sample_rate"], sample_rate)
# Create output with specified format
output_buffer = BytesIO()
output_container = av.open(output_buffer, mode='w', format=format)
# Set metadata on the container
for key, value in metadata.items():
output_container.metadata[key] = value
# Set up the output stream with appropriate properties
if format == "opus":
out_stream = output_container.add_stream("libopus", rate=sample_rate)
if quality == "64k":
out_stream.bit_rate = 64000
elif quality == "96k":
out_stream.bit_rate = 96000
elif quality == "128k":
out_stream.bit_rate = 128000
elif quality == "192k":
out_stream.bit_rate = 192000
elif quality == "320k":
out_stream.bit_rate = 320000
elif format == "mp3":
out_stream = output_container.add_stream("libmp3lame", rate=sample_rate)
if quality == "V0":
# TODO i would really love to support V3 and V5 but there doesn't seem to be a way to set the qscale level, the property below is a bool
out_stream.codec_context.qscale = 1
elif quality == "128k":
out_stream.bit_rate = 128000
elif quality == "320k":
out_stream.bit_rate = 320000
else: # format == "flac":
out_stream = output_container.add_stream("flac", rate=sample_rate)
frame = av.AudioFrame.from_ndarray(waveform.movedim(0, 1).reshape(1, -1).float().numpy(), format='flt',
layout='mono' if waveform.shape[0] == 1 else 'stereo')
frame.sample_rate = sample_rate
frame.pts = 0
output_container.mux(out_stream.encode(frame))
# Flush encoder
output_container.mux(out_stream.encode(None))
# Close containers
output_container.close()
# Write the output to file
output_buffer.seek(0)
with open(output_path, 'wb') as f:
f.write(output_buffer.getbuffer())
results.append(SavedResult(file, subfolder, FolderType.temp))
counter += 1
self.values = results
def as_dict(self): def as_dict(self):
return {"audio": self.values} return {"audio": self.values}
class PreviewUI3D(_UIOutput): class PreviewUI3D(_UIOutput):
def __init__(self, values: list[SavedResult | dict], **kwargs): def __init__(self, values: list[SavedResult | dict], **kwargs):
self.values = values self.values = values
@ -129,6 +237,7 @@ class PreviewUI3D(_UIOutput):
def as_dict(self): def as_dict(self):
return {"3d": self.values} return {"3d": self.values}
class PreviewText(_UIOutput): class PreviewText(_UIOutput):
def __init__(self, value: str, **kwargs): def __init__(self, value: str, **kwargs):
self.value = value self.value = value

View File

@ -0,0 +1,347 @@
from __future__ import annotations
import hashlib
import json
import os
from io import BytesIO
import av
import torch
import torchaudio
import comfy.model_management
import folder_paths
import node_helpers
from comfy.cli_args import args
from comfy_api.v3 import io, ui
class ConditioningStableAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="ConditioningStableAudio_V3",
category="conditioning",
inputs=[
io.Conditioning.Input(id="positive"),
io.Conditioning.Input(id="negative"),
io.Float.Input(id="seconds_start", default=0.0, min=0.0, max=1000.0, step=0.1),
io.Float.Input(id="seconds_total", default=47.0, min=0.0, max=1000.0, step=0.1),
],
outputs=[
io.Conditioning.Output(id="positive_out", display_name="positive"),
io.Conditioning.Output(id="negative_out", display_name="negative"),
],
)
@classmethod
def execute(cls, positive, negative, seconds_start, seconds_total) -> io.NodeOutput:
return io.NodeOutput(
node_helpers.conditioning_set_values(
positive, {"seconds_start": seconds_start, "seconds_total": seconds_total}
),
node_helpers.conditioning_set_values(
negative, {"seconds_start": seconds_start, "seconds_total": seconds_total}
),
)
class EmptyLatentAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="EmptyLatentAudio_V3",
category="latent/audio",
inputs=[
io.Float.Input(id="seconds", default=47.6, min=1.0, max=1000.0, step=0.1),
io.Int.Input(
id="batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch."
),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, seconds, batch_size) -> io.NodeOutput:
length = round((seconds * 44100 / 2048) / 2) * 2
latent = torch.zeros([batch_size, 64, length], device=comfy.model_management.intermediate_device())
return io.NodeOutput({"samples": latent, "type": "audio"})
class LoadAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="LoadAudio_V3", # frontend expects "LoadAudio" to work
display_name="Load Audio _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Combo.Input("audio", upload=io.UploadType.audio, options=cls.get_files_options()),
],
outputs=[io.Audio.Output()],
)
@classmethod
def get_files_options(cls) -> list[str]:
input_dir = folder_paths.get_input_directory()
return sorted(folder_paths.filter_files_content_types(os.listdir(input_dir), ["audio", "video"]))
@classmethod
def execute(cls, audio) -> io.NodeOutput:
waveform, sample_rate = torchaudio.load(folder_paths.get_annotated_filepath(audio))
return io.NodeOutput({"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate})
@classmethod
def fingerprint_inputs(s, audio):
image_path = folder_paths.get_annotated_filepath(audio)
m = hashlib.sha256()
with open(image_path, "rb") as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def validate_inputs(s, audio):
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
class PreviewAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="PreviewAudio_V3", # frontend expects "PreviewAudio" to work
display_name="Preview Audio _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Audio.Input("audio"),
],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, audio) -> io.NodeOutput:
return io.NodeOutput(ui=ui.PreviewAudio(audio, cls=cls))
class SaveAudioMP3_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="SaveAudioMP3_V3", # frontend expects "SaveAudioMP3" to work
display_name="Save Audio(MP3) _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Audio.Input("audio"),
io.String.Input("filename_prefix", default="audio/ComfyUI"),
io.Combo.Input("quality", options=["V0", "128k", "320k"], default="V0"),
],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(self, audio, filename_prefix="ComfyUI", format="mp3", quality="V0") -> io.NodeOutput:
return _save_audio(self, audio, filename_prefix, format, quality)
class SaveAudioOpus_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="SaveAudioOpus_V3", # frontend expects "SaveAudioOpus" to work
display_name="Save Audio(Opus) _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Audio.Input("audio"),
io.String.Input("filename_prefix", default="audio/ComfyUI"),
io.Combo.Input("quality", options=["64k", "96k", "128k", "192k", "320k"], default="128k"),
],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(self, audio, filename_prefix="ComfyUI", format="opus", quality="128k") -> io.NodeOutput:
return _save_audio(self, audio, filename_prefix, format, quality)
class SaveAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="SaveAudio_V3", # frontend expects "SaveAudio" to work
display_name="Save Audio _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Audio.Input("audio"),
io.String.Input("filename_prefix", default="audio/ComfyUI"),
],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, audio, filename_prefix="ComfyUI", format="flac") -> io.NodeOutput:
return _save_audio(cls, audio, filename_prefix, format)
class VAEDecodeAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="VAEDecodeAudio_V3",
category="latent/audio",
inputs=[
io.Latent.Input(id="samples"),
io.Vae.Input(id="vae"),
],
outputs=[io.Audio.Output()],
)
@classmethod
def execute(cls, vae, samples) -> io.NodeOutput:
audio = vae.decode(samples["samples"]).movedim(-1, 1)
std = torch.std(audio, dim=[1, 2], keepdim=True) * 5.0
std[std < 1.0] = 1.0
audio /= std
return io.NodeOutput({"waveform": audio, "sample_rate": 44100})
class VAEEncodeAudio_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="VAEEncodeAudio_V3",
category="latent/audio",
inputs=[
io.Audio.Input(id="audio"),
io.Vae.Input(id="vae"),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, vae, audio) -> io.NodeOutput:
sample_rate = audio["sample_rate"]
if 44100 != sample_rate:
waveform = torchaudio.functional.resample(audio["waveform"], sample_rate, 44100)
else:
waveform = audio["waveform"]
return io.NodeOutput({"samples": vae.encode(waveform.movedim(1, -1))})
def _save_audio(cls, audio, filename_prefix="ComfyUI", format="flac", quality="128k") -> io.NodeOutput:
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, folder_paths.get_output_directory()
)
# Prepare metadata dictionary
metadata = {}
if not args.disable_metadata:
if cls.hidden.prompt is not None:
metadata["prompt"] = json.dumps(cls.hidden.prompt)
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
# Opus supported sample rates
OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
results = []
for batch_number, waveform in enumerate(audio["waveform"].cpu()):
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.{format}"
output_path = os.path.join(full_output_folder, file)
# Use original sample rate initially
sample_rate = audio["sample_rate"]
# Handle Opus sample rate requirements
if format == "opus":
if sample_rate > 48000:
sample_rate = 48000
elif sample_rate not in OPUS_RATES:
# Find the next highest supported rate
for rate in sorted(OPUS_RATES):
if rate > sample_rate:
sample_rate = rate
break
if sample_rate not in OPUS_RATES: # Fallback if still not supported
sample_rate = 48000
# Resample if necessary
if sample_rate != audio["sample_rate"]:
waveform = torchaudio.functional.resample(waveform, audio["sample_rate"], sample_rate)
# Create output with specified format
output_buffer = BytesIO()
output_container = av.open(output_buffer, mode="w", format=format)
# Set metadata on the container
for key, value in metadata.items():
output_container.metadata[key] = value
# Set up the output stream with appropriate properties
if format == "opus":
out_stream = output_container.add_stream("libopus", rate=sample_rate)
if quality == "64k":
out_stream.bit_rate = 64000
elif quality == "96k":
out_stream.bit_rate = 96000
elif quality == "128k":
out_stream.bit_rate = 128000
elif quality == "192k":
out_stream.bit_rate = 192000
elif quality == "320k":
out_stream.bit_rate = 320000
elif format == "mp3":
out_stream = output_container.add_stream("libmp3lame", rate=sample_rate)
if quality == "V0":
# TODO i would really love to support V3 and V5 but there doesn't seem to be a way to set the qscale level, the property below is a bool
out_stream.codec_context.qscale = 1
elif quality == "128k":
out_stream.bit_rate = 128000
elif quality == "320k":
out_stream.bit_rate = 320000
else: # format == "flac":
out_stream = output_container.add_stream("flac", rate=sample_rate)
frame = av.AudioFrame.from_ndarray(
waveform.movedim(0, 1).reshape(1, -1).float().numpy(),
format="flt",
layout="mono" if waveform.shape[0] == 1 else "stereo",
)
frame.sample_rate = sample_rate
frame.pts = 0
output_container.mux(out_stream.encode(frame))
# Flush encoder
output_container.mux(out_stream.encode(None))
# Close containers
output_container.close()
# Write the output to file
output_buffer.seek(0)
with open(output_path, "wb") as f:
f.write(output_buffer.getbuffer())
results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
counter += 1
return io.NodeOutput(ui={"audio": results})
NODES_LIST: list[type[io.ComfyNodeV3]] = [
ConditioningStableAudio_V3,
EmptyLatentAudio_V3,
LoadAudio_V3,
PreviewAudio_V3,
SaveAudioMP3_V3,
SaveAudioOpus_V3,
SaveAudio_V3,
VAEDecodeAudio_V3,
VAEEncodeAudio_V3,
]

View File

@ -1,5 +1,5 @@
from comfy.cldm.control_types import UNION_CONTROLNET_TYPES
import comfy.utils import comfy.utils
from comfy.cldm.control_types import UNION_CONTROLNET_TYPES
from comfy_api.v3 import io from comfy_api.v3 import io
@ -27,11 +27,13 @@ class ControlNetApplyAdvanced_V3(io.ComfyNodeV3):
) )
@classmethod @classmethod
def execute(cls, positive, negative, control_net, image, strength, start_percent, end_percent, vae=None, extra_concat=[]) -> io.NodeOutput: def execute(
cls, positive, negative, control_net, image, strength, start_percent, end_percent, vae=None, extra_concat=[]
) -> io.NodeOutput:
if strength == 0: if strength == 0:
return io.NodeOutput(positive, negative) return io.NodeOutput(positive, negative)
control_hint = image.movedim(-1,1) control_hint = image.movedim(-1, 1)
cnets = {} cnets = {}
out = [] out = []
@ -40,16 +42,18 @@ class ControlNetApplyAdvanced_V3(io.ComfyNodeV3):
for t in conditioning: for t in conditioning:
d = t[1].copy() d = t[1].copy()
prev_cnet = d.get('control', None) prev_cnet = d.get("control", None)
if prev_cnet in cnets: if prev_cnet in cnets:
c_net = cnets[prev_cnet] c_net = cnets[prev_cnet]
else: else:
c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent), vae=vae, extra_concat=extra_concat) c_net = control_net.copy().set_cond_hint(
control_hint, strength, (start_percent, end_percent), vae=vae, extra_concat=extra_concat
)
c_net.set_previous_controlnet(prev_cnet) c_net.set_previous_controlnet(prev_cnet)
cnets[prev_cnet] = c_net cnets[prev_cnet] = c_net
d['control'] = c_net d["control"] = c_net
d['control_apply_to_uncond'] = False d["control_apply_to_uncond"] = False
n = [t[0], d] n = [t[0], d]
c.append(n) c.append(n)
out.append(c) out.append(c)
@ -107,7 +111,9 @@ class ControlNetInpaintingAliMamaApply_V3(ControlNetApplyAdvanced_V3):
) )
@classmethod @classmethod
def execute(cls, positive, negative, control_net, vae, image, mask, strength, start_percent, end_percent) -> io.NodeOutput: def execute(
cls, positive, negative, control_net, vae, image, mask, strength, start_percent, end_percent
) -> io.NodeOutput:
extra_concat = [] extra_concat = []
if control_net.concat_mask: if control_net.concat_mask:
mask = 1.0 - mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])) mask = 1.0 - mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
@ -115,7 +121,17 @@ class ControlNetInpaintingAliMamaApply_V3(ControlNetApplyAdvanced_V3):
image = image * mask_apply.movedim(1, -1).repeat(1, 1, 1, image.shape[3]) image = image * mask_apply.movedim(1, -1).repeat(1, 1, 1, image.shape[3])
extra_concat = [mask] extra_concat = [mask]
return super().execute(positive, negative, control_net, image, strength, start_percent, end_percent, vae=vae, extra_concat=extra_concat) return super().execute(
positive,
negative,
control_net,
image,
strength,
start_percent,
end_percent,
vae=vae,
extra_concat=extra_concat,
)
NODES_LIST: list[type[io.ComfyNodeV3]] = [ NODES_LIST: list[type[io.ComfyNodeV3]] = [

View File

@ -1,16 +1,16 @@
import hashlib
import json import json
import os import os
import torch
import hashlib
import numpy as np import numpy as np
import torch
from PIL import Image, ImageOps, ImageSequence from PIL import Image, ImageOps, ImageSequence
from PIL.PngImagePlugin import PngInfo from PIL.PngImagePlugin import PngInfo
from comfy_api.v3 import io, ui
from comfy.cli_args import args
import folder_paths import folder_paths
import node_helpers import node_helpers
from comfy.cli_args import args
from comfy_api.v3 import io, ui
class SaveImage_V3(io.ComfyNodeV3): class SaveImage_V3(io.ComfyNodeV3):
@ -29,7 +29,8 @@ class SaveImage_V3(io.ComfyNodeV3):
io.String.Input( io.String.Input(
"filename_prefix", "filename_prefix",
default="ComfyUI", default="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.", 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.",
), ),
], ],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
@ -42,8 +43,8 @@ class SaveImage_V3(io.ComfyNodeV3):
filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0] filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
) )
results = [] results = []
for (batch_number, image) in enumerate(images): for batch_number, image in enumerate(images):
i = 255. * image.cpu().numpy() i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None metadata = None
if not args.disable_metadata: if not args.disable_metadata:
@ -82,13 +83,13 @@ class SaveAnimatedPNG_V3(io.ComfyNodeV3):
@classmethod @classmethod
def execute(cls, images, fps, compress_level, filename_prefix="ComfyUI") -> io.NodeOutput: def execute(cls, images, fps, compress_level, filename_prefix="ComfyUI") -> io.NodeOutput:
full_output_folder, filename, counter, subfolder, filename_prefix = ( full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]) filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
) )
results = [] results = []
pil_images = [] pil_images = []
for image in images: for image in images:
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8)) img = Image.fromarray(np.clip(255.0 * image.cpu().numpy(), 0, 255).astype(np.uint8))
pil_images.append(img) pil_images.append(img)
metadata = None metadata = None
@ -96,19 +97,34 @@ class SaveAnimatedPNG_V3(io.ComfyNodeV3):
metadata = PngInfo() metadata = PngInfo()
if cls.hidden.prompt is not None: if cls.hidden.prompt is not None:
metadata.add( metadata.add(
b"comf", "prompt".encode("latin-1", "strict") + b"\0" + json.dumps(cls.hidden.prompt).encode("latin-1", "strict"), after_idat=True b"comf",
"prompt".encode("latin-1", "strict")
+ b"\0"
+ json.dumps(cls.hidden.prompt).encode("latin-1", "strict"),
after_idat=True,
) )
if cls.hidden.extra_pnginfo is not None: if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo: for x in cls.hidden.extra_pnginfo:
metadata.add( metadata.add(
b"comf", x.encode("latin-1", "strict") + b"\0" + json.dumps(cls.hidden.extra_pnginfo[x]).encode("latin-1", "strict"), after_idat=True b"comf",
x.encode("latin-1", "strict")
+ b"\0"
+ json.dumps(cls.hidden.extra_pnginfo[x]).encode("latin-1", "strict"),
after_idat=True,
) )
file = f"{filename}_{counter:05}_.png" file = f"{filename}_{counter:05}_.png"
pil_images[0].save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=compress_level, save_all=True, duration=int(1000.0/fps), append_images=pil_images[1:]) pil_images[0].save(
os.path.join(full_output_folder, file),
pnginfo=metadata,
compress_level=compress_level,
save_all=True,
duration=int(1000.0 / fps),
append_images=pil_images[1:],
)
results.append(ui.SavedResult(file, subfolder, io.FolderType.output)) results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
return io.NodeOutput(ui={"images": results, "animated": (True,) }) return io.NodeOutput(ui={"images": results, "animated": (True,)})
class SaveAnimatedWEBP_V3(io.ComfyNodeV3): class SaveAnimatedWEBP_V3(io.ComfyNodeV3):
@ -136,11 +152,13 @@ class SaveAnimatedWEBP_V3(io.ComfyNodeV3):
@classmethod @classmethod
def execute(cls, images, fps, filename_prefix, lossless, quality, method, num_frames=0) -> io.NodeOutput: def execute(cls, images, fps, filename_prefix, lossless, quality, method, num_frames=0) -> io.NodeOutput:
method = cls.COMPRESS_METHODS.get(method) method = cls.COMPRESS_METHODS.get(method)
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]) full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
)
results = [] results = []
pil_images = [] pil_images = []
for image in images: for image in images:
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8)) img = Image.fromarray(np.clip(255.0 * image.cpu().numpy(), 0, 255).astype(np.uint8))
pil_images.append(img) pil_images.append(img)
metadata = pil_images[0].getexif() metadata = pil_images[0].getexif()
@ -148,7 +166,7 @@ class SaveAnimatedWEBP_V3(io.ComfyNodeV3):
if cls.hidden.prompt is not None: if cls.hidden.prompt is not None:
metadata[0x0110] = "prompt:{}".format(json.dumps(cls.hidden.prompt)) metadata[0x0110] = "prompt:{}".format(json.dumps(cls.hidden.prompt))
if cls.hidden.extra_pnginfo is not None: if cls.hidden.extra_pnginfo is not None:
inital_exif = 0x010f inital_exif = 0x010F
for x in cls.hidden.extra_pnginfo: for x in cls.hidden.extra_pnginfo:
metadata[inital_exif] = "{}:{}".format(x, json.dumps(cls.hidden.extra_pnginfo[x])) metadata[inital_exif] = "{}:{}".format(x, json.dumps(cls.hidden.extra_pnginfo[x]))
inital_exif -= 1 inital_exif -= 1
@ -160,8 +178,9 @@ class SaveAnimatedWEBP_V3(io.ComfyNodeV3):
file = f"{filename}_{counter:05}_.webp" file = f"{filename}_{counter:05}_.webp"
pil_images[i].save( pil_images[i].save(
os.path.join(full_output_folder, file), os.path.join(full_output_folder, file),
save_all=True, duration=int(1000.0/fps), save_all=True,
append_images=pil_images[i + 1:i + num_frames], duration=int(1000.0 / fps),
append_images=pil_images[i + 1 : i + num_frames],
exif=metadata, exif=metadata,
lossless=lossless, lossless=lossless,
quality=quality, quality=quality,
@ -228,12 +247,12 @@ class LoadImage_V3(io.ComfyNodeV3):
output_masks = [] output_masks = []
w, h = None, None w, h = None, None
excluded_formats = ['MPO'] excluded_formats = ["MPO"]
for i in ImageSequence.Iterator(img): for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i) i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I': if i.mode == "I":
i = i.point(lambda i: i * (1 / 255)) i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB") image = i.convert("RGB")
@ -246,14 +265,14 @@ class LoadImage_V3(io.ComfyNodeV3):
image = np.array(image).astype(np.float32) / 255.0 image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,] image = torch.from_numpy(image)[None,]
if 'A' in i.getbands(): if "A" in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = np.array(i.getchannel("A")).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) mask = 1.0 - torch.from_numpy(mask)
elif i.mode == 'P' and 'transparency' in i.info: elif i.mode == "P" and "transparency" in i.info:
mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0 mask = np.array(i.convert("RGBA").getchannel("A")).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) mask = 1.0 - torch.from_numpy(mask)
else: else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu") mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
output_images.append(image) output_images.append(image)
output_masks.append(mask.unsqueeze(0)) output_masks.append(mask.unsqueeze(0))
@ -270,7 +289,7 @@ class LoadImage_V3(io.ComfyNodeV3):
def fingerprint_inputs(s, image): def fingerprint_inputs(s, image):
image_path = folder_paths.get_annotated_filepath(image) image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256() m = hashlib.sha256()
with open(image_path, 'rb') as f: with open(image_path, "rb") as f:
m.update(f.read()) m.update(f.read())
return m.digest().hex() return m.digest().hex()
@ -288,8 +307,8 @@ class LoadImageOutput_V3(io.ComfyNodeV3):
node_id="LoadImageOutput_V3", node_id="LoadImageOutput_V3",
display_name="Load Image (from Outputs) _V3", display_name="Load Image (from Outputs) _V3",
description="Load an image from the output folder. " description="Load an image from the output folder. "
"When the refresh button is clicked, the node will update the image list " "When the refresh button is clicked, the node will update the image list "
"and automatically select the first image, allowing for easy iteration.", "and automatically select the first image, allowing for easy iteration.",
category="image", category="image",
inputs=[ inputs=[
io.Combo.Input( io.Combo.Input(
@ -317,12 +336,12 @@ class LoadImageOutput_V3(io.ComfyNodeV3):
output_masks = [] output_masks = []
w, h = None, None w, h = None, None
excluded_formats = ['MPO'] excluded_formats = ["MPO"]
for i in ImageSequence.Iterator(img): for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i) i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I': if i.mode == "I":
i = i.point(lambda i: i * (1 / 255)) i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB") image = i.convert("RGB")
@ -335,12 +354,12 @@ class LoadImageOutput_V3(io.ComfyNodeV3):
image = np.array(image).astype(np.float32) / 255.0 image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,] image = torch.from_numpy(image)[None,]
if 'A' in i.getbands(): if "A" in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = np.array(i.getchannel("A")).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) mask = 1.0 - torch.from_numpy(mask)
elif i.mode == 'P' and 'transparency' in i.info: elif i.mode == "P" and "transparency" in i.info:
mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0 mask = np.array(i.convert("RGBA").getchannel("A")).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) mask = 1.0 - torch.from_numpy(mask)
else: else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
output_images.append(image) output_images.append(image)
@ -359,7 +378,7 @@ class LoadImageOutput_V3(io.ComfyNodeV3):
def fingerprint_inputs(s, image): def fingerprint_inputs(s, image):
image_path = folder_paths.get_annotated_filepath(image) image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256() m = hashlib.sha256()
with open(image_path, 'rb') as f: with open(image_path, "rb") as f:
m.update(f.read()) m.update(f.read())
return m.digest().hex() return m.digest().hex()

View File

@ -0,0 +1,104 @@
from __future__ import annotations
import sys
from comfy_api.v3 import io
class String_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="PrimitiveString_V3",
display_name="String _V3",
category="utils/primitive",
inputs=[
io.String.Input("value"),
],
outputs=[io.String.Output()],
)
@classmethod
def execute(cls, value: str) -> io.NodeOutput:
return io.NodeOutput(value)
class StringMultiline_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="PrimitiveStringMultiline_V3",
display_name="String (Multiline) _V3",
category="utils/primitive",
inputs=[
io.String.Input("value", multiline=True),
],
outputs=[io.String.Output()],
)
@classmethod
def execute(cls, value: str) -> io.NodeOutput:
return io.NodeOutput(value)
class Int_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="PrimitiveInt_V3",
display_name="Int _V3",
category="utils/primitive",
inputs=[
io.Int.Input("value", min=-sys.maxsize, max=sys.maxsize, control_after_generate=True),
],
outputs=[io.Int.Output()],
)
@classmethod
def execute(cls, value: int) -> io.NodeOutput:
return io.NodeOutput(value)
class Float_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="PrimitiveFloat_V3",
display_name="Float _V3",
category="utils/primitive",
inputs=[
io.Float.Input("value", min=-sys.maxsize, max=sys.maxsize),
],
outputs=[io.Float.Output()],
)
@classmethod
def execute(cls, value: float) -> io.NodeOutput:
return io.NodeOutput(value)
class Boolean_V3(io.ComfyNodeV3):
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="PrimitiveBoolean_V3",
display_name="Boolean _V3",
category="utils/primitive",
inputs=[
io.Boolean.Input("value"),
],
outputs=[io.Boolean.Output()],
)
@classmethod
def execute(cls, value: bool) -> io.NodeOutput:
return io.NodeOutput(value)
NODES_LIST: list[type[io.ComfyNodeV3]] = [
String_V3,
StringMultiline_V3,
Int_V3,
Float_V3,
Boolean_V3,
]

View File

@ -1,25 +1,25 @@
""" """
This file is part of ComfyUI. This file is part of ComfyUI.
Copyright (C) 2024 Stability AI Copyright (C) 2024 Stability AI
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import torch import torch
import nodes
import comfy.utils
import comfy.utils
import nodes
from comfy_api.v3 import io from comfy_api.v3 import io
@ -30,7 +30,7 @@ class StableCascade_EmptyLatentImage_V3(io.ComfyNodeV3):
node_id="StableCascade_EmptyLatentImage_V3", node_id="StableCascade_EmptyLatentImage_V3",
category="latent/stable_cascade", category="latent/stable_cascade",
inputs=[ inputs=[
io.Int.Input("width", default=1024,min=256,max=nodes.MAX_RESOLUTION, step=8), io.Int.Input("width", default=1024, min=256, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=256, max=nodes.MAX_RESOLUTION, step=8), io.Int.Input("height", default=1024, min=256, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("compression", default=42, min=4, max=128, step=1), io.Int.Input("compression", default=42, min=4, max=128, step=1),
io.Int.Input("batch_size", default=1, min=1, max=4096), io.Int.Input("batch_size", default=1, min=1, max=4096),
@ -72,9 +72,9 @@ class StableCascade_StageC_VAEEncode_V3(io.ComfyNodeV3):
out_width = (width // compression) * vae.downscale_ratio out_width = (width // compression) * vae.downscale_ratio
out_height = (height // compression) * vae.downscale_ratio out_height = (height // compression) * vae.downscale_ratio
s = comfy.utils.common_upscale(image.movedim(-1,1), out_width, out_height, "bicubic", "center").movedim(1,-1) s = comfy.utils.common_upscale(image.movedim(-1, 1), out_width, out_height, "bicubic", "center").movedim(1, -1)
c_latent = vae.encode(s[:,:,:,:3]) c_latent = vae.encode(s[:, :, :, :3])
b_latent = torch.zeros([c_latent.shape[0], 4, (height // 8) * 2, (width // 8) * 2]) b_latent = torch.zeros([c_latent.shape[0], 4, (height // 8) * 2, (width // 8) * 2])
return io.NodeOutput({"samples": c_latent}, {"samples": b_latent}) return io.NodeOutput({"samples": c_latent}, {"samples": b_latent})
@ -90,7 +90,7 @@ class StableCascade_StageB_Conditioning_V3(io.ComfyNodeV3):
io.Latent.Input("stage_c"), io.Latent.Input("stage_c"),
], ],
outputs=[ outputs=[
io.Conditioning.Output(), io.Conditioning.Output(),
], ],
) )
@ -99,7 +99,7 @@ class StableCascade_StageB_Conditioning_V3(io.ComfyNodeV3):
c = [] c = []
for t in conditioning: for t in conditioning:
d = t[1].copy() d = t[1].copy()
d['stable_cascade_prior'] = stage_c['samples'] d["stable_cascade_prior"] = stage_c["samples"]
n = [t[0], d] n = [t[0], d]
c.append(n) c.append(n)
return io.NodeOutput(c) return io.NodeOutput(c)
@ -128,7 +128,7 @@ class StableCascade_SuperResolutionControlnet_V3(io.ComfyNodeV3):
width = image.shape[-2] width = image.shape[-2]
height = image.shape[-3] height = image.shape[-3]
batch_size = image.shape[0] batch_size = image.shape[0]
controlnet_input = vae.encode(image[:,:,:,:3]).movedim(1, -1) controlnet_input = vae.encode(image[:, :, :, :3]).movedim(1, -1)
c_latent = torch.zeros([batch_size, 16, height // 16, width // 16]) c_latent = torch.zeros([batch_size, 16, height // 16, width // 16])
b_latent = torch.zeros([batch_size, 4, height // 2, width // 2]) b_latent = torch.zeros([batch_size, 4, height // 2, width // 2])

View File

@ -1,14 +1,13 @@
import hashlib import hashlib
import torch
import numpy as np import numpy as np
import torch
from PIL import Image, ImageOps, ImageSequence from PIL import Image, ImageOps, ImageSequence
from comfy_api.v3 import io
import nodes
import folder_paths import folder_paths
import node_helpers import node_helpers
import nodes
from comfy_api.v3 import io
MAX_RESOLUTION = nodes.MAX_RESOLUTION MAX_RESOLUTION = nodes.MAX_RESOLUTION
@ -51,12 +50,12 @@ class WebcamCapture_V3(io.ComfyNodeV3):
output_masks = [] output_masks = []
w, h = None, None w, h = None, None
excluded_formats = ['MPO'] excluded_formats = ["MPO"]
for i in ImageSequence.Iterator(img): for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i) i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I': if i.mode == "I":
i = i.point(lambda i: i * (1 / 255)) i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB") image = i.convert("RGB")
@ -69,12 +68,12 @@ class WebcamCapture_V3(io.ComfyNodeV3):
image = np.array(image).astype(np.float32) / 255.0 image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,] image = torch.from_numpy(image)[None,]
if 'A' in i.getbands(): if "A" in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = np.array(i.getchannel("A")).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) mask = 1.0 - torch.from_numpy(mask)
elif i.mode == 'P' and 'transparency' in i.info: elif i.mode == "P" and "transparency" in i.info:
mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0 mask = np.array(i.convert("RGBA").getchannel("A")).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) mask = 1.0 - torch.from_numpy(mask)
else: else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
output_images.append(image) output_images.append(image)
@ -93,7 +92,7 @@ class WebcamCapture_V3(io.ComfyNodeV3):
def fingerprint_inputs(s, image, width, height, capture_on_queue): def fingerprint_inputs(s, image, width, height, capture_on_queue):
image_path = folder_paths.get_annotated_filepath(image) image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256() m = hashlib.sha256()
with open(image_path, 'rb') as f: with open(image_path, "rb") as f:
m.update(f.read()) m.update(f.read())
return m.digest().hex() return m.digest().hex()

View File

@ -2299,9 +2299,11 @@ def init_builtin_extra_nodes():
"nodes_tcfg.py", "nodes_tcfg.py",
"nodes_v3_test.py", "nodes_v3_test.py",
"nodes_v1_test.py", "nodes_v1_test.py",
"v3/nodes_audio.py",
"v3/nodes_controlnet.py", "v3/nodes_controlnet.py",
"v3/nodes_images.py", "v3/nodes_images.py",
"v3/nodes_mask.py", "v3/nodes_mask.py",
"v3/nodes_primitive.py",
"v3/nodes_webcam.py", "v3/nodes_webcam.py",
"v3/nodes_stable_cascade.py", "v3/nodes_stable_cascade.py",
] ]

View File

@ -12,6 +12,8 @@ documentation = "https://docs.comfy.org/"
[tool.ruff] [tool.ruff]
lint.select = [ lint.select = [
"E", # pycodestyle errors
"I", # isort
"N805", # invalid-first-argument-name-for-method "N805", # invalid-first-argument-name-for-method
"S307", # suspicious-eval-usage "S307", # suspicious-eval-usage
"S102", # exec "S102", # exec
@ -22,3 +24,8 @@ lint.select = [
"F", "F",
] ]
exclude = ["*.ipynb"] exclude = ["*.ipynb"]
line-length = 120
lint.pycodestyle.ignore-overlong-task-comments = true
[tool.ruff.lint.per-file-ignores]
"!comfy_extras/v3/*" = ["E", "I"] # enable these rules only for V3 nodes