diff --git a/comfy/cli_args.py b/comfy/cli_args.py index 97b348f0d..de292d9b3 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -142,6 +142,8 @@ class PerformanceFeature(enum.Enum): parser.add_argument("--fast", nargs="*", type=PerformanceFeature, help="Enable some untested and potentially quality deteriorating optimizations. --fast with no arguments enables everything. You can pass a list specific optimizations if you only want to enable specific ones. Current valid optimizations: fp16_accumulation fp8_matrix_mult cublas_ops") +parser.add_argument("--mmap-torch-files", action="store_true", help="Use mmap when loading ckpt/pt files.") + parser.add_argument("--dont-print-server", action="store_true", help="Don't print server output.") parser.add_argument("--quick-test-for-ci", action="store_true", help="Quick test for CI.") parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build: Enable convenient things that most people using the standalone windows build will probably enjoy (like auto opening the page on startup).") diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index 77ef748e8..fbdf6f554 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -1277,6 +1277,7 @@ def res_multistep(model, x, sigmas, extra_args=None, callback=None, disable=None phi1_fn = lambda t: torch.expm1(t) / t phi2_fn = lambda t: (phi1_fn(t) - 1.0) / t + old_sigma_down = None old_denoised = None uncond_denoised = None def post_cfg_function(args): @@ -1304,9 +1305,9 @@ def res_multistep(model, x, sigmas, extra_args=None, callback=None, disable=None x = x + d * dt else: # Second order multistep method in https://arxiv.org/pdf/2308.02157 - t, t_next, t_prev = t_fn(sigmas[i]), t_fn(sigma_down), t_fn(sigmas[i - 1]) + t, t_old, t_next, t_prev = t_fn(sigmas[i]), t_fn(old_sigma_down), t_fn(sigma_down), t_fn(sigmas[i - 1]) h = t_next - t - c2 = (t_prev - t) / h + c2 = (t_prev - t_old) / h phi1_val, phi2_val = phi1_fn(-h), phi2_fn(-h) b1 = torch.nan_to_num(phi1_val - phi2_val / c2, nan=0.0) @@ -1326,6 +1327,7 @@ def res_multistep(model, x, sigmas, extra_args=None, callback=None, disable=None old_denoised = uncond_denoised else: old_denoised = denoised + old_sigma_down = sigma_down return x @torch.no_grad() diff --git a/comfy/ldm/ace/attention.py b/comfy/ldm/ace/attention.py index 631d13647..f20a01669 100644 --- a/comfy/ldm/ace/attention.py +++ b/comfy/ldm/ace/attention.py @@ -19,6 +19,7 @@ import torch.nn.functional as F from torch import nn import comfy.model_management +from comfy.ldm.modules.attention import optimized_attention class Attention(nn.Module): def __init__( @@ -326,10 +327,6 @@ class CustomerAttnProcessor2_0: Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). """ - def __init__(self): - if not hasattr(F, "scaled_dot_product_attention"): - raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") - def apply_rotary_emb( self, x: torch.Tensor, @@ -435,13 +432,9 @@ class CustomerAttnProcessor2_0: attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) # the output of sdp = (batch, num_heads, seq_len, head_dim) - # TODO: add support for attn.scale when we move to Torch 2.1 - hidden_states = F.scaled_dot_product_attention( - query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False - ) - - hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) - hidden_states = hidden_states.to(query.dtype) + hidden_states = optimized_attention( + query, key, value, heads=query.shape[1], mask=attention_mask, skip_reshape=True, + ).to(query.dtype) # linear proj hidden_states = attn.to_out[0](hidden_states) diff --git a/comfy/ldm/ace/vae/music_vocoder.py b/comfy/ldm/ace/vae/music_vocoder.py index dc7c867da..2f989fa86 100755 --- a/comfy/ldm/ace/vae/music_vocoder.py +++ b/comfy/ldm/ace/vae/music_vocoder.py @@ -8,11 +8,7 @@ from typing import Callable, Tuple, List import numpy as np import torch.nn.functional as F -from torch.nn.utils import weight_norm from torch.nn.utils.parametrize import remove_parametrizations as remove_weight_norm -# from diffusers.models.modeling_utils import ModelMixin -# from diffusers.loaders import FromOriginalModelMixin -# from diffusers.configuration_utils import ConfigMixin, register_to_config from .music_log_mel import LogMelSpectrogram @@ -259,7 +255,7 @@ class ResBlock1(torch.nn.Module): self.convs1 = nn.ModuleList( [ - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -269,7 +265,7 @@ class ResBlock1(torch.nn.Module): padding=get_padding(kernel_size, dilation[0]), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -279,7 +275,7 @@ class ResBlock1(torch.nn.Module): padding=get_padding(kernel_size, dilation[1]), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -294,7 +290,7 @@ class ResBlock1(torch.nn.Module): self.convs2 = nn.ModuleList( [ - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -304,7 +300,7 @@ class ResBlock1(torch.nn.Module): padding=get_padding(kernel_size, 1), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -314,7 +310,7 @@ class ResBlock1(torch.nn.Module): padding=get_padding(kernel_size, 1), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -366,7 +362,7 @@ class HiFiGANGenerator(nn.Module): prod(upsample_rates) == hop_length ), f"hop_length must be {prod(upsample_rates)}" - self.conv_pre = weight_norm( + self.conv_pre = torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( num_mels, upsample_initial_channel, @@ -386,7 +382,7 @@ class HiFiGANGenerator(nn.Module): for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)): c_cur = upsample_initial_channel // (2 ** (i + 1)) self.ups.append( - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.ConvTranspose1d( upsample_initial_channel // (2**i), upsample_initial_channel // (2 ** (i + 1)), @@ -421,7 +417,7 @@ class HiFiGANGenerator(nn.Module): self.resblocks.append(ResBlock1(ch, k, d)) self.activation_post = post_activation() - self.conv_post = weight_norm( + self.conv_post = torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( ch, 1, diff --git a/comfy/ldm/audio/autoencoder.py b/comfy/ldm/audio/autoencoder.py index 9e7e7c876..78ed6ffa6 100644 --- a/comfy/ldm/audio/autoencoder.py +++ b/comfy/ldm/audio/autoencoder.py @@ -75,16 +75,10 @@ class SnakeBeta(nn.Module): return x def WNConv1d(*args, **kwargs): - try: - return torch.nn.utils.parametrizations.weight_norm(ops.Conv1d(*args, **kwargs)) - except: - return torch.nn.utils.weight_norm(ops.Conv1d(*args, **kwargs)) #support pytorch 2.1 and older + return torch.nn.utils.parametrizations.weight_norm(ops.Conv1d(*args, **kwargs)) def WNConvTranspose1d(*args, **kwargs): - try: - return torch.nn.utils.parametrizations.weight_norm(ops.ConvTranspose1d(*args, **kwargs)) - except: - return torch.nn.utils.weight_norm(ops.ConvTranspose1d(*args, **kwargs)) #support pytorch 2.1 and older + return torch.nn.utils.parametrizations.weight_norm(ops.ConvTranspose1d(*args, **kwargs)) def get_activation(activation: Literal["elu", "snake", "none"], antialias=False, channels=None) -> nn.Module: if activation == "elu": diff --git a/comfy/ldm/hunyuan_video/model.py b/comfy/ldm/hunyuan_video/model.py index 72af3d5bb..fbd8d4196 100644 --- a/comfy/ldm/hunyuan_video/model.py +++ b/comfy/ldm/hunyuan_video/model.py @@ -228,6 +228,7 @@ class HunyuanVideo(nn.Module): y: Tensor, guidance: Tensor = None, guiding_frame_index=None, + ref_latent=None, control=None, transformer_options={}, ) -> Tensor: @@ -238,6 +239,14 @@ class HunyuanVideo(nn.Module): img = self.img_in(img) vec = self.time_in(timestep_embedding(timesteps, 256, time_factor=1.0).to(img.dtype)) + if ref_latent is not None: + ref_latent_ids = self.img_ids(ref_latent) + ref_latent = self.img_in(ref_latent) + img = torch.cat([ref_latent, img], dim=-2) + ref_latent_ids[..., 0] = -1 + ref_latent_ids[..., 2] += (initial_shape[-1] // self.patch_size[-1]) + img_ids = torch.cat([ref_latent_ids, img_ids], dim=-2) + if guiding_frame_index is not None: token_replace_vec = self.time_in(timestep_embedding(guiding_frame_index, 256, time_factor=1.0)) vec_ = self.vector_in(y[:, :self.params.vec_in_dim]) @@ -313,6 +322,8 @@ class HunyuanVideo(nn.Module): img[:, : img_len] += add img = img[:, : img_len] + if ref_latent is not None: + img = img[:, ref_latent.shape[1]:] img = self.final_layer(img, vec, modulation_dims=modulation_dims) # (N, T, patch_size ** 2 * out_channels) @@ -324,7 +335,7 @@ class HunyuanVideo(nn.Module): img = img.reshape(initial_shape[0], self.out_channels, initial_shape[2], initial_shape[3], initial_shape[4]) return img - def forward(self, x, timestep, context, y, guidance=None, attention_mask=None, guiding_frame_index=None, control=None, transformer_options={}, **kwargs): + def img_ids(self, x): bs, c, t, h, w = x.shape patch_size = self.patch_size t_len = ((t + (patch_size[0] // 2)) // patch_size[0]) @@ -334,7 +345,11 @@ class HunyuanVideo(nn.Module): img_ids[:, :, :, 0] = img_ids[:, :, :, 0] + torch.linspace(0, t_len - 1, steps=t_len, device=x.device, dtype=x.dtype).reshape(-1, 1, 1) img_ids[:, :, :, 1] = img_ids[:, :, :, 1] + torch.linspace(0, h_len - 1, steps=h_len, device=x.device, dtype=x.dtype).reshape(1, -1, 1) img_ids[:, :, :, 2] = img_ids[:, :, :, 2] + torch.linspace(0, w_len - 1, steps=w_len, device=x.device, dtype=x.dtype).reshape(1, 1, -1) - img_ids = repeat(img_ids, "t h w c -> b (t h w) c", b=bs) + return repeat(img_ids, "t h w c -> b (t h w) c", b=bs) + + def forward(self, x, timestep, context, y, guidance=None, attention_mask=None, guiding_frame_index=None, ref_latent=None, control=None, transformer_options={}, **kwargs): + bs, c, t, h, w = x.shape + img_ids = self.img_ids(x) txt_ids = torch.zeros((bs, context.shape[1], 3), device=x.device, dtype=x.dtype) - out = self.forward_orig(x, img_ids, context, txt_ids, attention_mask, timestep, y, guidance, guiding_frame_index, control, transformer_options) + out = self.forward_orig(x, img_ids, context, txt_ids, attention_mask, timestep, y, guidance, guiding_frame_index, ref_latent, control=control, transformer_options=transformer_options) return out diff --git a/comfy/lora.py b/comfy/lora.py index fff524be2..ef110c164 100644 --- a/comfy/lora.py +++ b/comfy/lora.py @@ -286,6 +286,12 @@ def model_lora_keys_unet(model, key_map={}): key_lora = k[len("diffusion_model."):-len(".weight")].replace(".", "_") key_map["lycoris_{}".format(key_lora)] = k #SimpleTuner lycoris format + if isinstance(model, comfy.model_base.ACEStep): + for k in sdk: + if k.startswith("diffusion_model.") and k.endswith(".weight"): #Official ACE step lora format + key_lora = k[len("diffusion_model."):-len(".weight")] + key_map["{}".format(key_lora)] = k + return key_map diff --git a/comfy/model_base.py b/comfy/model_base.py index b0065fcec..f475e837e 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -924,6 +924,10 @@ class HunyuanVideo(BaseModel): if guiding_frame_index is not None: out['guiding_frame_index'] = comfy.conds.CONDRegular(torch.FloatTensor([guiding_frame_index])) + ref_latent = kwargs.get("ref_latent", None) + if ref_latent is not None: + out['ref_latent'] = comfy.conds.CONDRegular(self.process_latent_in(ref_latent)) + return out def scale_latent_inpaint(self, latent_image, **kwargs): diff --git a/comfy/ops.py b/comfy/ops.py index 032787915..431c8f89d 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -308,10 +308,10 @@ def fp8_linear(self, input): if scale_input is None: scale_input = torch.ones((), device=input.device, dtype=torch.float32) input = torch.clamp(input, min=-448, max=448, out=input) - input = input.reshape(-1, input_shape[2]).to(dtype) + input = input.reshape(-1, input_shape[2]).to(dtype).contiguous() else: scale_input = scale_input.to(input.device) - input = (input * (1.0 / scale_input).to(input_dtype)).reshape(-1, input_shape[2]).to(dtype) + input = (input * (1.0 / scale_input).to(input_dtype)).reshape(-1, input_shape[2]).to(dtype).contiguous() if bias is not None: o = torch._scaled_mm(input, w, out_dtype=input_dtype, bias=bias, scale_a=scale_input, scale_b=scale_weight) diff --git a/comfy/rmsnorm.py b/comfy/rmsnorm.py index 9d82bee1a..66ae8321d 100644 --- a/comfy/rmsnorm.py +++ b/comfy/rmsnorm.py @@ -30,7 +30,7 @@ if RMSNorm is None: def __init__( self, normalized_shape, - eps=None, + eps=1e-6, elementwise_affine=True, device=None, dtype=None, diff --git a/comfy/sd.py b/comfy/sd.py index ee350d5b5..e98a3aa87 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -451,7 +451,7 @@ class VAE: self.latent_dim = 2 self.process_output = lambda audio: audio self.process_input = lambda audio: audio - self.working_dtypes = [torch.bfloat16, torch.float32] + self.working_dtypes = [torch.bfloat16, torch.float16, torch.float32] self.disable_offload = True self.extra_1d_channel = 16 else: diff --git a/comfy/utils.py b/comfy/utils.py index a826e41bf..561e1b858 100644 --- a/comfy/utils.py +++ b/comfy/utils.py @@ -28,6 +28,9 @@ import logging import itertools from torch.nn.functional import interpolate from einops import rearrange +from comfy.cli_args import args + +MMAP_TORCH_FILES = args.mmap_torch_files ALWAYS_SAFE_LOAD = False if hasattr(torch.serialization, "add_safe_globals"): # TODO: this was added in pytorch 2.4, the unsafe path should be removed once earlier versions are deprecated @@ -67,8 +70,12 @@ def load_torch_file(ckpt, safe_load=False, device=None, return_metadata=False): raise ValueError("{}\n\nFile path: {}\n\nThe safetensors file is corrupt/incomplete. Check the file size and make sure you have copied/downloaded it correctly.".format(message, ckpt)) raise e else: + torch_args = {} + if MMAP_TORCH_FILES: + torch_args["mmap"] = True + if safe_load or ALWAYS_SAFE_LOAD: - pl_sd = torch.load(ckpt, map_location=device, weights_only=True) + pl_sd = torch.load(ckpt, map_location=device, weights_only=True, **torch_args) else: pl_sd = torch.load(ckpt, map_location=device, pickle_module=comfy.checkpoint_pickle) if "global_step" in pl_sd: diff --git a/comfy_api_nodes/apinode_utils.py b/comfy_api_nodes/apinode_utils.py index bd3b8908b..e28d7d607 100644 --- a/comfy_api_nodes/apinode_utils.py +++ b/comfy_api_nodes/apinode_utils.py @@ -1,3 +1,4 @@ +from __future__ import annotations import io import logging from typing import Optional @@ -314,7 +315,7 @@ def upload_file_to_comfyapi( file_bytes_io: BytesIO, filename: str, upload_mime_type: str, - auth_token: Optional[str] = None, + auth_kwargs: Optional[dict[str,str]] = None, ) -> str: """ Uploads a single file to ComfyUI API and returns its download URL. @@ -323,7 +324,7 @@ def upload_file_to_comfyapi( file_bytes_io: BytesIO object containing the file data. filename: The filename of the file. upload_mime_type: MIME type of the file. - auth_token: Optional authentication token. + auth_kwargs: Optional authentication token(s). Returns: The download URL for the uploaded file. @@ -337,7 +338,7 @@ def upload_file_to_comfyapi( response_model=UploadResponse, ), request=request_object, - auth_token=auth_token, + auth_kwargs=auth_kwargs, ) response: UploadResponse = operation.execute() @@ -351,7 +352,7 @@ def upload_file_to_comfyapi( def upload_video_to_comfyapi( video: VideoInput, - auth_token: Optional[str] = None, + auth_kwargs: Optional[dict[str,str]] = None, container: VideoContainer = VideoContainer.MP4, codec: VideoCodec = VideoCodec.H264, max_duration: Optional[int] = None, @@ -362,7 +363,7 @@ def upload_video_to_comfyapi( Args: video: VideoInput object (Comfy VIDEO type). - auth_token: Optional authentication token. + auth_kwargs: Optional authentication token(s). container: The video container format to use (default: MP4). codec: The video codec to use (default: H264). max_duration: Optional maximum duration of the video in seconds. If the video is longer than this, an error will be raised. @@ -390,7 +391,7 @@ def upload_video_to_comfyapi( video_bytes_io.seek(0) return upload_file_to_comfyapi( - video_bytes_io, filename, upload_mime_type, auth_token + video_bytes_io, filename, upload_mime_type, auth_kwargs ) @@ -453,7 +454,7 @@ def audio_ndarray_to_bytesio( def upload_audio_to_comfyapi( audio: AudioInput, - auth_token: Optional[str] = None, + auth_kwargs: Optional[dict[str,str]] = None, container_format: str = "mp4", codec_name: str = "aac", mime_type: str = "audio/mp4", @@ -465,7 +466,7 @@ def upload_audio_to_comfyapi( Args: audio: a Comfy `AUDIO` type (contains waveform tensor and sample_rate) - auth_token: Optional authentication token. + auth_kwargs: Optional authentication token(s). Returns: The download URL for the uploaded audio file. @@ -477,11 +478,11 @@ def upload_audio_to_comfyapi( audio_data_np, sample_rate, container_format, codec_name ) - return upload_file_to_comfyapi(audio_bytes_io, filename, mime_type, auth_token) + return upload_file_to_comfyapi(audio_bytes_io, filename, mime_type, auth_kwargs) def upload_images_to_comfyapi( - image: torch.Tensor, max_images=8, auth_token=None, mime_type: Optional[str] = None + image: torch.Tensor, max_images=8, auth_kwargs: Optional[dict[str,str]] = None, mime_type: Optional[str] = None ) -> list[str]: """ Uploads images to ComfyUI API and returns download URLs. @@ -490,7 +491,7 @@ def upload_images_to_comfyapi( Args: image: Input torch.Tensor image. max_images: Maximum number of images to upload. - auth_token: Optional authentication token. + auth_kwargs: Optional authentication token(s). mime_type: Optional MIME type for the image. """ # if batch, try to upload each file if max_images is greater than 0 @@ -521,7 +522,7 @@ def upload_images_to_comfyapi( response_model=UploadResponse, ), request=request_object, - auth_token=auth_token, + auth_kwargs=auth_kwargs, ) response = operation.execute() diff --git a/comfy_api_nodes/apis/client.py b/comfy_api_nodes/apis/client.py index 929e386d4..cff52714f 100644 --- a/comfy_api_nodes/apis/client.py +++ b/comfy_api_nodes/apis/client.py @@ -20,7 +20,8 @@ Usage Examples: # 1. Create the API client api_client = ApiClient( base_url="https://api.example.com", - api_key="your_api_key_here", + auth_token="your_auth_token_here", + comfy_api_key="your_comfy_api_key_here", timeout=30.0, verify_ssl=True ) @@ -146,12 +147,14 @@ class ApiClient: def __init__( self, base_url: str, - api_key: Optional[str] = None, + auth_token: Optional[str] = None, + comfy_api_key: Optional[str] = None, timeout: float = 3600.0, verify_ssl: bool = True, ): self.base_url = base_url - self.api_key = api_key + self.auth_token = auth_token + self.comfy_api_key = comfy_api_key self.timeout = timeout self.verify_ssl = verify_ssl @@ -201,8 +204,10 @@ class ApiClient: """Get headers for API requests, including authentication if available""" headers = {"Content-Type": "application/json", "Accept": "application/json"} - if self.api_key: - headers["Authorization"] = f"Bearer {self.api_key}" + if self.auth_token: + headers["Authorization"] = f"Bearer {self.auth_token}" + elif self.comfy_api_key: + headers["X-API-KEY"] = self.comfy_api_key return headers @@ -236,7 +241,7 @@ class ApiClient: requests.RequestException: If the request fails """ url = urljoin(self.base_url, path) - self.check_auth_token(self.api_key) + self.check_auth(self.auth_token, self.comfy_api_key) # Combine default headers with any provided headers request_headers = self.get_headers() if headers: @@ -320,11 +325,11 @@ class ApiClient: return response.json() return {} - def check_auth_token(self, auth_token): - """Verify that an auth token is present.""" - if auth_token is None: + def check_auth(self, auth_token, comfy_api_key): + """Verify that an auth token is present or comfy_api_key is present""" + if auth_token is None and comfy_api_key is None: raise Exception("Unauthorized: Please login first to use this node.") - return auth_token + return auth_token or comfy_api_key @staticmethod def upload_file( @@ -392,6 +397,8 @@ class SynchronousOperation(Generic[T, R]): files: Optional[Dict[str, Any]] = None, api_base: str | None = None, auth_token: Optional[str] = None, + comfy_api_key: Optional[str] = None, + auth_kwargs: Optional[Dict[str,str]] = None, timeout: float = 604800.0, verify_ssl: bool = True, content_type: str = "application/json", @@ -403,6 +410,10 @@ class SynchronousOperation(Generic[T, R]): self.error = None self.api_base: str = api_base or args.comfy_api_base self.auth_token = auth_token + self.comfy_api_key = comfy_api_key + if auth_kwargs is not None: + self.auth_token = auth_kwargs.get("auth_token", self.auth_token) + self.comfy_api_key = auth_kwargs.get("comfy_api_key", self.comfy_api_key) self.timeout = timeout self.verify_ssl = verify_ssl self.files = files @@ -415,7 +426,8 @@ class SynchronousOperation(Generic[T, R]): if client is None: client = ApiClient( base_url=self.api_base, - api_key=self.auth_token, + auth_token=self.auth_token, + comfy_api_key=self.comfy_api_key, timeout=self.timeout, verify_ssl=self.verify_ssl, ) @@ -502,12 +514,18 @@ class PollingOperation(Generic[T, R]): request: Optional[T] = None, api_base: str | None = None, auth_token: Optional[str] = None, + comfy_api_key: Optional[str] = None, + auth_kwargs: Optional[Dict[str,str]] = None, poll_interval: float = 5.0, ): self.poll_endpoint = poll_endpoint self.request = request self.api_base: str = api_base or args.comfy_api_base self.auth_token = auth_token + self.comfy_api_key = comfy_api_key + if auth_kwargs is not None: + self.auth_token = auth_kwargs.get("auth_token", self.auth_token) + self.comfy_api_key = auth_kwargs.get("comfy_api_key", self.comfy_api_key) self.poll_interval = poll_interval # Polling configuration @@ -528,7 +546,8 @@ class PollingOperation(Generic[T, R]): if client is None: client = ApiClient( base_url=self.api_base, - api_key=self.auth_token, + auth_token=self.auth_token, + comfy_api_key=self.comfy_api_key, ) return self._poll_until_complete(client) except Exception as e: diff --git a/comfy_api_nodes/apis/recraft_api.py b/comfy_api_nodes/apis/recraft_api.py index c0ec9d0c8..c36d95f24 100644 --- a/comfy_api_nodes/apis/recraft_api.py +++ b/comfy_api_nodes/apis/recraft_api.py @@ -81,7 +81,6 @@ class RecraftStyle: class RecraftIO: STYLEV3 = "RECRAFT_V3_STYLE" - SVG = "SVG" # TODO: if acceptable, move into ComfyUI's typing class COLOR = "RECRAFT_COLOR" CONTROLS = "RECRAFT_CONTROLS" diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 122a6ddf8..66ef1b391 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -179,6 +179,7 @@ class FluxProUltraImageNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -211,7 +212,6 @@ class FluxProUltraImageNode(ComfyNodeABC): seed=0, image_prompt=None, image_prompt_strength=0.1, - auth_token=None, **kwargs, ): if image_prompt is None: @@ -244,7 +244,7 @@ class FluxProUltraImageNode(ComfyNodeABC): None if image_prompt is None else round(image_prompt_strength, 2) ), ), - auth_token=auth_token, + auth_kwargs=kwargs, ) output_image = handle_bfl_synchronous_operation(operation) return (output_image,) @@ -319,6 +319,7 @@ class FluxProImageNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -337,7 +338,6 @@ class FluxProImageNode(ComfyNodeABC): seed=0, image_prompt=None, # image_prompt_strength=0.1, - auth_token=None, **kwargs, ): image_prompt = ( @@ -361,7 +361,7 @@ class FluxProImageNode(ComfyNodeABC): seed=seed, image_prompt=image_prompt, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) output_image = handle_bfl_synchronous_operation(operation) return (output_image,) @@ -461,6 +461,7 @@ class FluxProExpandNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -482,7 +483,6 @@ class FluxProExpandNode(ComfyNodeABC): steps: int, guidance: float, seed=0, - auth_token=None, **kwargs, ): image = convert_image_to_base64(image) @@ -506,7 +506,7 @@ class FluxProExpandNode(ComfyNodeABC): seed=seed, image=image, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) output_image = handle_bfl_synchronous_operation(operation) return (output_image,) @@ -572,6 +572,7 @@ class FluxProFillNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -590,7 +591,6 @@ class FluxProFillNode(ComfyNodeABC): steps: int, guidance: float, seed=0, - auth_token=None, **kwargs, ): # prepare mask @@ -615,7 +615,7 @@ class FluxProFillNode(ComfyNodeABC): image=image, mask=mask, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) output_image = handle_bfl_synchronous_operation(operation) return (output_image,) @@ -706,6 +706,7 @@ class FluxProCannyNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -726,7 +727,6 @@ class FluxProCannyNode(ComfyNodeABC): steps: int, guidance: float, seed=0, - auth_token=None, **kwargs, ): control_image = convert_image_to_base64(control_image[:,:,:,:3]) @@ -763,7 +763,7 @@ class FluxProCannyNode(ComfyNodeABC): canny_high_threshold=canny_high_threshold, preprocessed_image=preprocessed_image, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) output_image = handle_bfl_synchronous_operation(operation) return (output_image,) @@ -834,6 +834,7 @@ class FluxProDepthNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -852,7 +853,6 @@ class FluxProDepthNode(ComfyNodeABC): steps: int, guidance: float, seed=0, - auth_token=None, **kwargs, ): control_image = convert_image_to_base64(control_image[:,:,:,:3]) @@ -878,7 +878,7 @@ class FluxProDepthNode(ComfyNodeABC): control_image=control_image, preprocessed_image=preprocessed_image, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) output_image = handle_bfl_synchronous_operation(operation) return (output_image,) diff --git a/comfy_api_nodes/nodes_ideogram.py b/comfy_api_nodes/nodes_ideogram.py index 45c021f4a..d25468b17 100644 --- a/comfy_api_nodes/nodes_ideogram.py +++ b/comfy_api_nodes/nodes_ideogram.py @@ -234,9 +234,7 @@ def download_and_process_images(image_urls): class IdeogramV1(ComfyNodeABC): """ - Generates images synchronously using the Ideogram V1 model. - - Images links are available for a limited period of time; if you would like to keep the image, you must download it. + Generates images using the Ideogram V1 model. """ def __init__(self): @@ -303,7 +301,10 @@ class IdeogramV1(ComfyNodeABC): {"default": 1, "min": 1, "max": 8, "step": 1, "display": "number"}, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = (IO.IMAGE,) @@ -321,7 +322,7 @@ class IdeogramV1(ComfyNodeABC): seed=0, negative_prompt="", num_images=1, - auth_token=None, + **kwargs, ): # Determine the model based on turbo setting aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None) @@ -347,7 +348,7 @@ class IdeogramV1(ComfyNodeABC): negative_prompt=negative_prompt if negative_prompt else None, ) ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response = operation.execute() @@ -365,9 +366,7 @@ class IdeogramV1(ComfyNodeABC): class IdeogramV2(ComfyNodeABC): """ - Generates images synchronously using the Ideogram V2 model. - - Images links are available for a limited period of time; if you would like to keep the image, you must download it. + Generates images using the Ideogram V2 model. """ def __init__(self): @@ -458,7 +457,10 @@ class IdeogramV2(ComfyNodeABC): # }, #), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = (IO.IMAGE,) @@ -479,7 +481,7 @@ class IdeogramV2(ComfyNodeABC): negative_prompt="", num_images=1, color_palette="", - auth_token=None, + **kwargs, ): aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None) resolution = V1_V1_RES_MAP.get(resolution, None) @@ -519,7 +521,7 @@ class IdeogramV2(ComfyNodeABC): color_palette=color_palette if color_palette else None, ) ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response = operation.execute() @@ -536,10 +538,7 @@ class IdeogramV2(ComfyNodeABC): class IdeogramV3(ComfyNodeABC): """ - Generates images synchronously using the Ideogram V3 model. - - Supports both regular image generation from text prompts and image editing with mask. - Images links are available for a limited period of time; if you would like to keep the image, you must download it. + Generates images using the Ideogram V3 model. Supports both regular image generation from text prompts and image editing with mask. """ def __init__(self): @@ -621,7 +620,10 @@ class IdeogramV3(ComfyNodeABC): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = (IO.IMAGE,) @@ -641,7 +643,7 @@ class IdeogramV3(ComfyNodeABC): seed=0, num_images=1, rendering_speed="BALANCED", - auth_token=None, + **kwargs, ): # Check if both image and mask are provided for editing mode if image is not None and mask is not None: @@ -705,7 +707,7 @@ class IdeogramV3(ComfyNodeABC): "mask": mask_binary, }, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) elif image is not None or mask is not None: @@ -746,7 +748,7 @@ class IdeogramV3(ComfyNodeABC): response_model=IdeogramGenerateResponse, ), request=gen_request, - auth_token=auth_token, + auth_kwargs=kwargs, ) # Execute the operation and process response diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py index 9aa8df58b..2d0fd8883 100644 --- a/comfy_api_nodes/nodes_kling.py +++ b/comfy_api_nodes/nodes_kling.py @@ -95,7 +95,7 @@ class KlingApiError(Exception): pass -def poll_until_finished(auth_token: str, api_endpoint: ApiEndpoint[Any, R]) -> R: +def poll_until_finished(auth_kwargs: dict[str,str], api_endpoint: ApiEndpoint[Any, R]) -> R: """Polls the Kling API endpoint until the task reaches a terminal state, then returns the response.""" return PollingOperation( poll_endpoint=api_endpoint, @@ -108,7 +108,7 @@ def poll_until_finished(auth_token: str, api_endpoint: ApiEndpoint[Any, R]) -> R if response.data and response.data.task_status else None ), - auth_token=auth_token, + auth_kwargs=auth_kwargs, ).execute() @@ -184,6 +184,33 @@ def validate_image_result_response(response) -> None: raise KlingApiError(error_msg) +def validate_input_image(image: torch.Tensor) -> None: + """ + Validates the input image adheres to the expectations of the Kling API: + - The image resolution should not be less than 300*300px + - The aspect ratio of the image should be between 1:2.5 ~ 2.5:1 + + See: https://app.klingai.com/global/dev/document-api/apiReference/model/imageToVideo + """ + if len(image.shape) == 4: + height, width = image.shape[1], image.shape[2] + elif len(image.shape) == 3: + height, width = image.shape[0], image.shape[1] + else: + raise ValueError("Invalid image tensor shape.") + + # Ensure minimum resolution is met + if height < 300: + raise ValueError("Image height must be at least 300px") + if width < 300: + raise ValueError("Image width must be at least 300px") + + # Ensure aspect ratio is within acceptable range + aspect_ratio = width / height + if aspect_ratio < 1 / 2.5 or aspect_ratio > 2.5: + raise ValueError("Image aspect ratio must be between 1:2.5 and 2.5:1") + + def get_camera_control_input_config( tooltip: str, default: float = 0.0 ) -> tuple[IO, InputTypeOptions]: @@ -391,16 +418,19 @@ class KlingTextToVideoNode(KlingNodeBase): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = ("VIDEO", "STRING", "STRING") RETURN_NAMES = ("VIDEO", "video_id", "duration") DESCRIPTION = "Kling Text to Video Node" - def get_response(self, task_id: str, auth_token: str) -> KlingText2VideoResponse: + def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingText2VideoResponse: return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_TEXT_TO_VIDEO}/{task_id}", method=HttpMethod.GET, @@ -419,7 +449,7 @@ class KlingTextToVideoNode(KlingNodeBase): camera_control: Optional[KlingCameraControl] = None, model_name: Optional[str] = None, duration: Optional[str] = None, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile, str, str]: validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V) if model_name is None: @@ -441,14 +471,14 @@ class KlingTextToVideoNode(KlingNodeBase): aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio), camera_control=camera_control, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_video_result_response(final_response) video = get_video_from_response(final_response) @@ -495,7 +525,10 @@ class KlingCameraControlT2VNode(KlingTextToVideoNode): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Transform text into cinematic videos with professional camera movements that simulate real-world cinematography. Control virtual camera actions including zoom, rotation, pan, tilt, and first-person view, while maintaining focus on your original text." @@ -507,7 +540,7 @@ class KlingCameraControlT2VNode(KlingTextToVideoNode): cfg_scale: float, aspect_ratio: str, camera_control: Optional[KlingCameraControl] = None, - auth_token: Optional[str] = None, + **kwargs, ): return super().api_call( model_name=KlingVideoGenModelName.kling_v1, @@ -518,7 +551,7 @@ class KlingCameraControlT2VNode(KlingTextToVideoNode): prompt=prompt, negative_prompt=negative_prompt, camera_control=camera_control, - auth_token=auth_token, + **kwargs, ) @@ -530,7 +563,10 @@ class KlingImage2VideoNode(KlingNodeBase): return { "required": { "start_frame": model_field_to_node_input( - IO.IMAGE, KlingImage2VideoRequest, "image" + IO.IMAGE, + KlingImage2VideoRequest, + "image", + tooltip="The reference image used to generate the video.", ), "prompt": model_field_to_node_input( IO.STRING, KlingImage2VideoRequest, "prompt", multiline=True @@ -574,16 +610,19 @@ class KlingImage2VideoNode(KlingNodeBase): enum_type=KlingVideoGenDuration, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = ("VIDEO", "STRING", "STRING") RETURN_NAMES = ("VIDEO", "video_id", "duration") DESCRIPTION = "Kling Image to Video Node" - def get_response(self, task_id: str, auth_token: str) -> KlingImage2VideoResponse: + def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingImage2VideoResponse: return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_IMAGE_TO_VIDEO}/{task_id}", method=HttpMethod.GET, @@ -604,12 +643,13 @@ class KlingImage2VideoNode(KlingNodeBase): duration: str, camera_control: Optional[KlingCameraControl] = None, end_frame: Optional[torch.Tensor] = None, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V) + validate_input_image(start_frame) if camera_control is not None: - # Camera control type for image 2 video is always simple + # Camera control type for image 2 video is always `simple` camera_control.type = KlingCameraControlType.simple initial_operation = SynchronousOperation( @@ -631,18 +671,17 @@ class KlingImage2VideoNode(KlingNodeBase): negative_prompt=negative_prompt if negative_prompt else None, cfg_scale=cfg_scale, mode=KlingVideoGenMode(mode), - aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio), duration=KlingVideoGenDuration(duration), camera_control=camera_control, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_video_result_response(final_response) video = get_video_from_response(final_response) @@ -692,7 +731,10 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Transform still images into cinematic videos with professional camera movements that simulate real-world cinematography. Control virtual camera actions including zoom, rotation, pan, tilt, and first-person view, while maintaining focus on your original image." @@ -705,7 +747,7 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode): cfg_scale: float, aspect_ratio: str, camera_control: KlingCameraControl, - auth_token: Optional[str] = None, + **kwargs, ): return super().api_call( model_name=KlingVideoGenModelName.kling_v1_5, @@ -717,7 +759,7 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode): prompt=prompt, negative_prompt=negative_prompt, camera_control=camera_control, - auth_token=auth_token, + **kwargs, ) @@ -785,7 +827,10 @@ class KlingStartEndFrameNode(KlingImage2VideoNode): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Generate a video sequence that transitions between your provided start and end images. The node creates all frames in between, producing a smooth transformation from the first frame to the last." @@ -799,7 +844,7 @@ class KlingStartEndFrameNode(KlingImage2VideoNode): cfg_scale: float, aspect_ratio: str, mode: str, - auth_token: Optional[str] = None, + **kwargs, ): mode, duration, model_name = KlingStartEndFrameNode.get_mode_string_mapping()[ mode @@ -814,7 +859,7 @@ class KlingStartEndFrameNode(KlingImage2VideoNode): aspect_ratio=aspect_ratio, duration=duration, end_frame=end_frame, - auth_token=auth_token, + **kwargs, ) @@ -844,16 +889,19 @@ class KlingVideoExtendNode(KlingNodeBase): IO.STRING, KlingVideoExtendRequest, "video_id", forceInput=True ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = ("VIDEO", "STRING", "STRING") RETURN_NAMES = ("VIDEO", "video_id", "duration") DESCRIPTION = "Kling Video Extend Node. Extend videos made by other Kling nodes. The video_id is created by using other Kling Nodes." - def get_response(self, task_id: str, auth_token: str) -> KlingVideoExtendResponse: + def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingVideoExtendResponse: return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_VIDEO_EXTEND}/{task_id}", method=HttpMethod.GET, @@ -868,7 +916,7 @@ class KlingVideoExtendNode(KlingNodeBase): negative_prompt: str, cfg_scale: float, video_id: str, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile, str, str]: validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V) initial_operation = SynchronousOperation( @@ -884,14 +932,14 @@ class KlingVideoExtendNode(KlingNodeBase): cfg_scale=cfg_scale, video_id=video_id, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_video_result_response(final_response) video = get_video_from_response(final_response) @@ -904,9 +952,9 @@ class KlingVideoEffectsBase(KlingNodeBase): RETURN_TYPES = ("VIDEO", "STRING", "STRING") RETURN_NAMES = ("VIDEO", "video_id", "duration") - def get_response(self, task_id: str, auth_token: str) -> KlingVideoEffectsResponse: + def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingVideoEffectsResponse: return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_VIDEO_EFFECTS}/{task_id}", method=HttpMethod.GET, @@ -924,7 +972,7 @@ class KlingVideoEffectsBase(KlingNodeBase): image_1: torch.Tensor, image_2: Optional[torch.Tensor] = None, mode: Optional[KlingVideoGenMode] = None, - auth_token: Optional[str] = None, + **kwargs, ): if dual_character: request_input_field = KlingDualCharacterEffectInput( @@ -954,14 +1002,14 @@ class KlingVideoEffectsBase(KlingNodeBase): effect_scene=effect_scene, input=request_input_field, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_video_result_response(final_response) video = get_video_from_response(final_response) @@ -1002,7 +1050,10 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase): enum_type=KlingVideoGenDuration, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Achieve different special effects when generating a video based on the effect_scene. First image will be positioned on left side, second on right side of the composite." @@ -1017,7 +1068,7 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase): model_name: KlingCharacterEffectModelName, mode: KlingVideoGenMode, duration: KlingVideoGenDuration, - auth_token: Optional[str] = None, + **kwargs, ): video, _, duration = super().api_call( dual_character=True, @@ -1027,7 +1078,7 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase): duration=duration, image_1=image_left, image_2=image_right, - auth_token=auth_token, + **kwargs, ) return video, duration @@ -1063,7 +1114,10 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase): enum_type=KlingVideoGenDuration, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Achieve different special effects when generating a video based on the effect_scene." @@ -1074,7 +1128,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase): effect_scene: KlingSingleImageEffectsScene, model_name: KlingSingleImageEffectModelName, duration: KlingVideoGenDuration, - auth_token: Optional[str] = None, + **kwargs, ): return super().api_call( dual_character=False, @@ -1082,7 +1136,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase): model_name=model_name, duration=duration, image_1=image, - auth_token=auth_token, + **kwargs, ) @@ -1100,10 +1154,10 @@ class KlingLipSyncBase(KlingNodeBase): f"Text is too long. Maximum length is {MAX_PROMPT_LENGTH_LIP_SYNC} characters." ) - def get_response(self, task_id: str, auth_token: str) -> KlingLipSyncResponse: + def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingLipSyncResponse: """Polls the Kling API endpoint until the task reaches a terminal state.""" return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_LIP_SYNC}/{task_id}", method=HttpMethod.GET, @@ -1121,18 +1175,18 @@ class KlingLipSyncBase(KlingNodeBase): text: Optional[str] = None, voice_speed: Optional[float] = None, voice_id: Optional[str] = None, - auth_token: Optional[str] = None, + **kwargs ) -> tuple[VideoFromFile, str, str]: if text: self.validate_text(text) # Upload video to Comfy API and get download URL - video_url = upload_video_to_comfyapi(video, auth_token) + video_url = upload_video_to_comfyapi(video, auth_kwargs=kwargs) logging.info("Uploaded video to Comfy API. URL: %s", video_url) # Upload the audio file to Comfy API and get download URL if audio: - audio_url = upload_audio_to_comfyapi(audio, auth_token) + audio_url = upload_audio_to_comfyapi(audio, auth_kwargs=kwargs) logging.info("Uploaded audio to Comfy API. URL: %s", audio_url) else: audio_url = None @@ -1156,14 +1210,14 @@ class KlingLipSyncBase(KlingNodeBase): voice_id=voice_id, ), ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_video_result_response(final_response) video = get_video_from_response(final_response) @@ -1186,7 +1240,10 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase): enum_type=KlingLipSyncVoiceLanguage, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Kling Lip Sync Audio to Video Node. Syncs mouth movements in a video file to the audio content of an audio file." @@ -1196,14 +1253,14 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase): video: VideoInput, audio: AudioInput, voice_language: str, - auth_token: Optional[str] = None, + **kwargs, ): return super().api_call( video=video, audio=audio, voice_language=voice_language, mode="audio2video", - auth_token=auth_token, + **kwargs, ) @@ -1292,7 +1349,10 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase): IO.FLOAT, KlingLipSyncInputObject, "voice_speed", slider=True ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Kling Lip Sync Text to Video Node. Syncs mouth movements in a video file to a text prompt." @@ -1303,7 +1363,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase): text: str, voice: str, voice_speed: float, - auth_token: Optional[str] = None, + **kwargs, ): voice_id, voice_language = KlingLipSyncTextToVideoNode.get_voice_config()[voice] return super().api_call( @@ -1313,7 +1373,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase): voice_id=voice_id, voice_speed=voice_speed, mode="text2video", - auth_token=auth_token, + **kwargs, ) @@ -1350,16 +1410,19 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase): enum_type=KlingVirtualTryOnModelName, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Kling Virtual Try On Node. Input a human image and a cloth image to try on the cloth on the human." def get_response( - self, task_id: str, auth_token: Optional[str] = None + self, task_id: str, auth_kwargs: dict[str,str] = None ) -> KlingVirtualTryOnResponse: return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_VIRTUAL_TRY_ON}/{task_id}", method=HttpMethod.GET, @@ -1373,7 +1436,7 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase): human_image: torch.Tensor, cloth_image: torch.Tensor, model_name: KlingVirtualTryOnModelName, - auth_token: Optional[str] = None, + **kwargs, ): initial_operation = SynchronousOperation( endpoint=ApiEndpoint( @@ -1387,14 +1450,14 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase): cloth_image=tensor_to_base64_string(cloth_image), model_name=model_name, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_image_result_response(final_response) images = get_images_from_response(final_response) @@ -1462,16 +1525,19 @@ class KlingImageGenerationNode(KlingImageGenerationBase): "optional": { "image": (IO.IMAGE, {}), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } DESCRIPTION = "Kling Image Generation Node. Generate an image from a text prompt with an optional reference image." def get_response( - self, task_id: str, auth_token: Optional[str] = None + self, task_id: str, auth_kwargs: Optional[dict[str,str]] = None ) -> KlingImageGenerationsResponse: return poll_until_finished( - auth_token, + auth_kwargs, ApiEndpoint( path=f"{PATH_IMAGE_GENERATIONS}/{task_id}", method=HttpMethod.GET, @@ -1491,7 +1557,7 @@ class KlingImageGenerationNode(KlingImageGenerationBase): n: int, aspect_ratio: KlingImageGenAspectRatio, image: Optional[torch.Tensor] = None, - auth_token: Optional[str] = None, + **kwargs, ): self.validate_prompt(prompt, negative_prompt) @@ -1516,14 +1582,14 @@ class KlingImageGenerationNode(KlingImageGenerationBase): n=n, aspect_ratio=aspect_ratio, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) task_creation_response = initial_operation.execute() validate_task_creation_response(task_creation_response) task_id = task_creation_response.data.task_id - final_response = self.get_response(task_id, auth_token) + final_response = self.get_response(task_id, auth_kwargs=kwargs) validate_image_result_response(final_response) images = get_images_from_response(final_response) diff --git a/comfy_api_nodes/nodes_luma.py b/comfy_api_nodes/nodes_luma.py index 0f0d9aa80..bd33a53e0 100644 --- a/comfy_api_nodes/nodes_luma.py +++ b/comfy_api_nodes/nodes_luma.py @@ -1,4 +1,6 @@ +from __future__ import annotations from inspect import cleandoc +from typing import Optional from comfy.comfy_types.node_typing import IO, ComfyNodeABC from comfy_api.input_impl.video_types import VideoFromFile from comfy_api_nodes.apis.luma_api import ( @@ -201,6 +203,7 @@ class LumaImageGenerationNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -214,7 +217,6 @@ class LumaImageGenerationNode(ComfyNodeABC): image_luma_ref: LumaReferenceChain = None, style_image: torch.Tensor = None, character_image: torch.Tensor = None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=True, min_length=3) @@ -222,19 +224,19 @@ class LumaImageGenerationNode(ComfyNodeABC): api_image_ref = None if image_luma_ref is not None: api_image_ref = self._convert_luma_refs( - image_luma_ref, max_refs=4, auth_token=auth_token + image_luma_ref, max_refs=4, auth_kwargs=kwargs, ) # handle style_luma_ref api_style_ref = None if style_image is not None: api_style_ref = self._convert_style_image( - style_image, weight=style_image_weight, auth_token=auth_token + style_image, weight=style_image_weight, auth_kwargs=kwargs, ) # handle character_ref images character_ref = None if character_image is not None: download_urls = upload_images_to_comfyapi( - character_image, max_images=4, auth_token=auth_token + character_image, max_images=4, auth_kwargs=kwargs, ) character_ref = LumaCharacterRef( identity0=LumaImageIdentity(images=download_urls) @@ -255,7 +257,7 @@ class LumaImageGenerationNode(ComfyNodeABC): style_ref=api_style_ref, character_ref=character_ref, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api: LumaGeneration = operation.execute() @@ -269,7 +271,7 @@ class LumaImageGenerationNode(ComfyNodeABC): completed_statuses=[LumaState.completed], failed_statuses=[LumaState.failed], status_extractor=lambda x: x.state, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() @@ -278,13 +280,13 @@ class LumaImageGenerationNode(ComfyNodeABC): return (img,) def _convert_luma_refs( - self, luma_ref: LumaReferenceChain, max_refs: int, auth_token=None + self, luma_ref: LumaReferenceChain, max_refs: int, auth_kwargs: Optional[dict[str,str]] = None ): luma_urls = [] ref_count = 0 for ref in luma_ref.refs: download_urls = upload_images_to_comfyapi( - ref.image, max_images=1, auth_token=auth_token + ref.image, max_images=1, auth_kwargs=auth_kwargs ) luma_urls.append(download_urls[0]) ref_count += 1 @@ -293,12 +295,12 @@ class LumaImageGenerationNode(ComfyNodeABC): return luma_ref.create_api_model(download_urls=luma_urls, max_refs=max_refs) def _convert_style_image( - self, style_image: torch.Tensor, weight: float, auth_token=None + self, style_image: torch.Tensor, weight: float, auth_kwargs: Optional[dict[str,str]] = None ): chain = LumaReferenceChain( first_ref=LumaReference(image=style_image, weight=weight) ) - return self._convert_luma_refs(chain, max_refs=1, auth_token=auth_token) + return self._convert_luma_refs(chain, max_refs=1, auth_kwargs=auth_kwargs) class LumaImageModifyNode(ComfyNodeABC): @@ -350,6 +352,7 @@ class LumaImageModifyNode(ComfyNodeABC): "optional": {}, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -360,12 +363,11 @@ class LumaImageModifyNode(ComfyNodeABC): image: torch.Tensor, image_weight: float, seed, - auth_token=None, **kwargs, ): # first, upload image download_urls = upload_images_to_comfyapi( - image, max_images=1, auth_token=auth_token + image, max_images=1, auth_kwargs=kwargs, ) image_url = download_urls[0] # next, make Luma call with download url provided @@ -383,7 +385,7 @@ class LumaImageModifyNode(ComfyNodeABC): url=image_url, weight=round(max(min(1.0-image_weight, 0.98), 0.0), 2) ), ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api: LumaGeneration = operation.execute() @@ -397,7 +399,7 @@ class LumaImageModifyNode(ComfyNodeABC): completed_statuses=[LumaState.completed], failed_statuses=[LumaState.failed], status_extractor=lambda x: x.state, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() @@ -470,6 +472,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -483,7 +486,6 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC): loop: bool, seed, luma_concepts: LumaConceptChain = None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=False, min_length=3) @@ -506,7 +508,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC): loop=loop, concepts=luma_concepts.create_api_model() if luma_concepts else None, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api: LumaGeneration = operation.execute() @@ -520,7 +522,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC): completed_statuses=[LumaState.completed], failed_statuses=[LumaState.failed], status_extractor=lambda x: x.state, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() @@ -594,6 +596,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -608,14 +611,13 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC): first_image: torch.Tensor = None, last_image: torch.Tensor = None, luma_concepts: LumaConceptChain = None, - auth_token=None, **kwargs, ): if first_image is None and last_image is None: raise Exception( "At least one of first_image and last_image requires an input." ) - keyframes = self._convert_to_keyframes(first_image, last_image, auth_token) + keyframes = self._convert_to_keyframes(first_image, last_image, auth_kwargs=kwargs) duration = duration if model != LumaVideoModel.ray_1_6 else None resolution = resolution if model != LumaVideoModel.ray_1_6 else None @@ -636,7 +638,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC): keyframes=keyframes, concepts=luma_concepts.create_api_model() if luma_concepts else None, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api: LumaGeneration = operation.execute() @@ -650,7 +652,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC): completed_statuses=[LumaState.completed], failed_statuses=[LumaState.failed], status_extractor=lambda x: x.state, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() @@ -661,7 +663,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC): self, first_image: torch.Tensor = None, last_image: torch.Tensor = None, - auth_token=None, + auth_kwargs: Optional[dict[str,str]] = None, ): if first_image is None and last_image is None: return None @@ -669,12 +671,12 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC): frame1 = None if first_image is not None: download_urls = upload_images_to_comfyapi( - first_image, max_images=1, auth_token=auth_token + first_image, max_images=1, auth_kwargs=auth_kwargs, ) frame0 = LumaImageReference(type="image", url=download_urls[0]) if last_image is not None: download_urls = upload_images_to_comfyapi( - last_image, max_images=1, auth_token=auth_token + last_image, max_images=1, auth_kwargs=auth_kwargs, ) frame1 = LumaImageReference(type="image", url=download_urls[0]) return LumaKeyframes(frame0=frame0, frame1=frame1) diff --git a/comfy_api_nodes/nodes_minimax.py b/comfy_api_nodes/nodes_minimax.py index cacda22c6..fd64aeb0b 100644 --- a/comfy_api_nodes/nodes_minimax.py +++ b/comfy_api_nodes/nodes_minimax.py @@ -67,6 +67,7 @@ class MinimaxTextToVideoNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -84,7 +85,7 @@ class MinimaxTextToVideoNode: model="T2V-01", image: torch.Tensor=None, # used for ImageToVideo subject: torch.Tensor=None, # used for SubjectToVideo - auth_token=None, + **kwargs, ): ''' Function used between MiniMax nodes - supports T2V, I2V, and S2V, based on provided arguments. @@ -94,12 +95,12 @@ class MinimaxTextToVideoNode: # upload image, if passed in image_url = None if image is not None: - image_url = upload_images_to_comfyapi(image, max_images=1, auth_token=auth_token)[0] + image_url = upload_images_to_comfyapi(image, max_images=1, auth_kwargs=kwargs)[0] # TODO: figure out how to deal with subject properly, API returns invalid params when using S2V-01 model subject_reference = None if subject is not None: - subject_url = upload_images_to_comfyapi(subject, max_images=1, auth_token=auth_token)[0] + subject_url = upload_images_to_comfyapi(subject, max_images=1, auth_kwargs=kwargs)[0] subject_reference = [SubjectReferenceItem(image=subject_url)] @@ -118,7 +119,7 @@ class MinimaxTextToVideoNode: subject_reference=subject_reference, prompt_optimizer=None, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response = video_generate_operation.execute() @@ -137,7 +138,7 @@ class MinimaxTextToVideoNode: completed_statuses=["Success"], failed_statuses=["Fail"], status_extractor=lambda x: x.status.value, - auth_token=auth_token, + auth_kwargs=kwargs, ) task_result = video_generate_operation.execute() @@ -153,7 +154,7 @@ class MinimaxTextToVideoNode: query_params={"file_id": int(file_id)}, ), request=EmptyRequest(), - auth_token=auth_token, + auth_kwargs=kwargs, ) file_result = file_retrieve_operation.execute() @@ -221,6 +222,7 @@ class MinimaxImageToVideoNode(MinimaxTextToVideoNode): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -279,6 +281,7 @@ class MinimaxSubjectToVideoNode(MinimaxTextToVideoNode): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } diff --git a/comfy_api_nodes/nodes_openai.py b/comfy_api_nodes/nodes_openai.py index c18c65d7a..c63908be2 100644 --- a/comfy_api_nodes/nodes_openai.py +++ b/comfy_api_nodes/nodes_openai.py @@ -93,7 +93,10 @@ class OpenAIDalle2(ComfyNodeABC): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = (IO.IMAGE,) @@ -110,7 +113,7 @@ class OpenAIDalle2(ComfyNodeABC): mask=None, n=1, size="1024x1024", - auth_token=None, + **kwargs ): validate_string(prompt, strip_whitespace=False) model = "dall-e-2" @@ -168,7 +171,7 @@ class OpenAIDalle2(ComfyNodeABC): else None ), content_type=content_type, - auth_token=auth_token, + auth_kwargs=kwargs, ) response = operation.execute() @@ -236,7 +239,10 @@ class OpenAIDalle3(ComfyNodeABC): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = (IO.IMAGE,) @@ -252,7 +258,7 @@ class OpenAIDalle3(ComfyNodeABC): style="natural", quality="standard", size="1024x1024", - auth_token=None, + **kwargs ): validate_string(prompt, strip_whitespace=False) model = "dall-e-3" @@ -273,7 +279,7 @@ class OpenAIDalle3(ComfyNodeABC): style=style, seed=seed, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response = operation.execute() @@ -366,7 +372,10 @@ class OpenAIGPTImage1(ComfyNodeABC): }, ), }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, } RETURN_TYPES = (IO.IMAGE,) @@ -385,7 +394,7 @@ class OpenAIGPTImage1(ComfyNodeABC): mask=None, n=1, size="1024x1024", - auth_token=None, + **kwargs ): validate_string(prompt, strip_whitespace=False) model = "gpt-image-1" @@ -462,7 +471,7 @@ class OpenAIGPTImage1(ComfyNodeABC): ), files=files if files else None, content_type=content_type, - auth_token=auth_token, + auth_kwargs=kwargs, ) response = operation.execute() diff --git a/comfy_api_nodes/nodes_pika.py b/comfy_api_nodes/nodes_pika.py index ba4e8457d..08ec9cf07 100644 --- a/comfy_api_nodes/nodes_pika.py +++ b/comfy_api_nodes/nodes_pika.py @@ -3,6 +3,7 @@ Pika x ComfyUI API Nodes Pika API docs: https://pika-827374fb.mintlify.app/api-reference """ +from __future__ import annotations import io from typing import Optional, TypeVar @@ -120,7 +121,7 @@ class PikaNodeBase(ComfyNodeABC): RETURN_TYPES = ("VIDEO",) def poll_for_task_status( - self, task_id: str, auth_token: str + self, task_id: str, auth_kwargs: Optional[dict[str,str]] = None ) -> PikaGenerateResponse: polling_operation = PollingOperation( poll_endpoint=ApiEndpoint( @@ -139,20 +140,20 @@ class PikaNodeBase(ComfyNodeABC): progress_extractor=lambda response: ( response.progress if hasattr(response, "progress") else None ), - auth_token=auth_token, + auth_kwargs=auth_kwargs, ) return polling_operation.execute() def execute_task( self, initial_operation: SynchronousOperation[R, PikaGenerateResponse], - auth_token: Optional[str] = None, + auth_kwargs: Optional[dict[str,str]] = None, ) -> tuple[VideoFromFile]: """Executes the initial operation then polls for the task status until it is completed. Args: initial_operation: The initial operation to execute. - auth_token: The authentication token to use for the API call. + auth_kwargs: The authentication token(s) to use for the API call. Returns: A tuple containing the video file as a VIDEO output. @@ -164,7 +165,7 @@ class PikaNodeBase(ComfyNodeABC): raise PikaApiError(error_msg) task_id = initial_response.video_id - final_response = self.poll_for_task_status(task_id, auth_token) + final_response = self.poll_for_task_status(task_id, auth_kwargs) if not is_valid_video_response(final_response): error_msg = ( f"Pika task {task_id} succeeded but no video data found in response." @@ -193,6 +194,7 @@ class PikaImageToVideoV2_2(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -206,7 +208,7 @@ class PikaImageToVideoV2_2(PikaNodeBase): seed: int, resolution: str, duration: int, - auth_token: Optional[str] = None, + **kwargs ) -> tuple[VideoFromFile]: # Convert image to BytesIO image_bytes_io = tensor_to_bytesio(image) @@ -233,10 +235,10 @@ class PikaImageToVideoV2_2(PikaNodeBase): request=pika_request_data, files=pika_files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) class PikaTextToVideoNodeV2_2(PikaNodeBase): @@ -259,6 +261,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -272,7 +275,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase): resolution: str, duration: int, aspect_ratio: float, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: initial_operation = SynchronousOperation( endpoint=ApiEndpoint( @@ -289,11 +292,11 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase): duration=duration, aspectRatio=aspect_ratio, ), - auth_token=auth_token, + auth_kwargs=kwargs, content_type="application/x-www-form-urlencoded", ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) class PikaScenesV2_2(PikaNodeBase): @@ -336,6 +339,7 @@ class PikaScenesV2_2(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -355,7 +359,7 @@ class PikaScenesV2_2(PikaNodeBase): image_ingredient_3: Optional[torch.Tensor] = None, image_ingredient_4: Optional[torch.Tensor] = None, image_ingredient_5: Optional[torch.Tensor] = None, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: # Convert all passed images to BytesIO all_image_bytes_io = [] @@ -396,10 +400,10 @@ class PikaScenesV2_2(PikaNodeBase): request=pika_request_data, files=pika_files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) class PikAdditionsNode(PikaNodeBase): @@ -434,6 +438,7 @@ class PikAdditionsNode(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -446,7 +451,7 @@ class PikAdditionsNode(PikaNodeBase): prompt_text: str, negative_prompt: str, seed: int, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: # Convert video to BytesIO video_bytes_io = io.BytesIO() @@ -479,10 +484,10 @@ class PikAdditionsNode(PikaNodeBase): request=pika_request_data, files=pika_files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) class PikaSwapsNode(PikaNodeBase): @@ -526,6 +531,7 @@ class PikaSwapsNode(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -540,7 +546,7 @@ class PikaSwapsNode(PikaNodeBase): prompt_text: str, negative_prompt: str, seed: int, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: # Convert video to BytesIO video_bytes_io = io.BytesIO() @@ -583,10 +589,10 @@ class PikaSwapsNode(PikaNodeBase): request=pika_request_data, files=pika_files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) class PikaffectsNode(PikaNodeBase): @@ -630,6 +636,7 @@ class PikaffectsNode(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -642,7 +649,7 @@ class PikaffectsNode(PikaNodeBase): prompt_text: str, negative_prompt: str, seed: int, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: initial_operation = SynchronousOperation( @@ -660,10 +667,10 @@ class PikaffectsNode(PikaNodeBase): ), files={"image": ("image.png", tensor_to_bytesio(image), "image/png")}, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) class PikaStartEndFrameNode2_2(PikaNodeBase): @@ -681,6 +688,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -695,7 +703,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase): seed: int, resolution: str, duration: int, - auth_token: Optional[str] = None, + **kwargs, ) -> tuple[VideoFromFile]: pika_files = [ @@ -722,10 +730,10 @@ class PikaStartEndFrameNode2_2(PikaNodeBase): ), files=pika_files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) - return self.execute_task(initial_operation, auth_token) + return self.execute_task(initial_operation, auth_kwargs=kwargs) NODE_CLASS_MAPPINGS = { diff --git a/comfy_api_nodes/nodes_pixverse.py b/comfy_api_nodes/nodes_pixverse.py index dbb90c1dd..0c29e77c2 100644 --- a/comfy_api_nodes/nodes_pixverse.py +++ b/comfy_api_nodes/nodes_pixverse.py @@ -34,7 +34,7 @@ import requests from io import BytesIO -def upload_image_to_pixverse(image: torch.Tensor, auth_token=None): +def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None): # first, upload image to Pixverse and get image id to use in actual generation call files = { "image": tensor_to_bytesio(image) @@ -49,7 +49,7 @@ def upload_image_to_pixverse(image: torch.Tensor, auth_token=None): request=EmptyRequest(), files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=auth_kwargs, ) response_upload: PixverseImageUploadResponse = operation.execute() @@ -148,6 +148,7 @@ class PixverseTextToVideoNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -161,7 +162,6 @@ class PixverseTextToVideoNode(ComfyNodeABC): seed, negative_prompt: str=None, pixverse_template: int=None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=False) @@ -190,7 +190,7 @@ class PixverseTextToVideoNode(ComfyNodeABC): template_id=pixverse_template, seed=seed, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -207,7 +207,7 @@ class PixverseTextToVideoNode(ComfyNodeABC): completed_statuses=[PixverseStatus.successful], failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted], status_extractor=lambda x: x.Resp.status, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() @@ -278,6 +278,7 @@ class PixverseImageToVideoNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -291,11 +292,10 @@ class PixverseImageToVideoNode(ComfyNodeABC): seed, negative_prompt: str=None, pixverse_template: int=None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=False) - img_id = upload_image_to_pixverse(image, auth_token=auth_token) + img_id = upload_image_to_pixverse(image, auth_kwargs=kwargs) # 1080p is limited to 5 seconds duration # only normal motion_mode supported for 1080p or for non-5 second duration @@ -322,7 +322,7 @@ class PixverseImageToVideoNode(ComfyNodeABC): template_id=pixverse_template, seed=seed, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -339,7 +339,7 @@ class PixverseImageToVideoNode(ComfyNodeABC): completed_statuses=[PixverseStatus.successful], failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted], status_extractor=lambda x: x.Resp.status, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() @@ -407,6 +407,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -420,12 +421,11 @@ class PixverseTransitionVideoNode(ComfyNodeABC): motion_mode: str, seed, negative_prompt: str=None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=False) - first_frame_id = upload_image_to_pixverse(first_frame, auth_token=auth_token) - last_frame_id = upload_image_to_pixverse(last_frame, auth_token=auth_token) + first_frame_id = upload_image_to_pixverse(first_frame, auth_kwargs=kwargs) + last_frame_id = upload_image_to_pixverse(last_frame, auth_kwargs=kwargs) # 1080p is limited to 5 seconds duration # only normal motion_mode supported for 1080p or for non-5 second duration @@ -452,7 +452,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC): negative_prompt=negative_prompt if negative_prompt else None, seed=seed, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -469,7 +469,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC): completed_statuses=[PixverseStatus.successful], failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted], status_extractor=lambda x: x.Resp.status, - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll = operation.execute() diff --git a/comfy_api_nodes/nodes_recraft.py b/comfy_api_nodes/nodes_recraft.py index 994f377d1..767d93e3c 100644 --- a/comfy_api_nodes/nodes_recraft.py +++ b/comfy_api_nodes/nodes_recraft.py @@ -1,6 +1,7 @@ from __future__ import annotations from inspect import cleandoc from comfy.utils import ProgressBar +from comfy_extras.nodes_images import SVG # Added from comfy.comfy_types.node_typing import IO from comfy_api_nodes.apis.recraft_api import ( RecraftImageGenerationRequest, @@ -28,9 +29,6 @@ from comfy_api_nodes.apinode_utils import ( resize_mask_to_image, validate_string, ) -import folder_paths -import json -import os import torch from io import BytesIO from PIL import UnidentifiedImageError @@ -43,7 +41,7 @@ def handle_recraft_file_request( total_pixels=4096*4096, timeout=1024, request=None, - auth_token=None + auth_kwargs: dict[str,str] = None, ) -> list[BytesIO]: """ Handle sending common Recraft file-only request to get back file bytes. @@ -67,7 +65,7 @@ def handle_recraft_file_request( request=request, files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=auth_kwargs, multipart_parser=recraft_multipart_parser, ) response: RecraftImageGenerationResponse = operation.execute() @@ -162,102 +160,6 @@ class handle_recraft_image_output: raise Exception("Received output data was not an image; likely an SVG. If you used style_id, make sure it is not a Vector art style.") -class SVG: - """ - Stores SVG representations via a list of BytesIO objects. - """ - def __init__(self, data: list[BytesIO]): - self.data = data - - def combine(self, other: SVG): - return SVG(self.data + other.data) - - @staticmethod - def combine_all(svgs: list[SVG]): - all_svgs = [] - for svg in svgs: - all_svgs.extend(svg.data) - return SVG(all_svgs) - - -class SaveSVGNode: - """ - Save SVG files on disk. - """ - - def __init__(self): - self.output_dir = folder_paths.get_output_directory() - self.type = "output" - self.prefix_append = "" - - RETURN_TYPES = () - DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value - FUNCTION = "save_svg" - CATEGORY = "api node/image/Recraft" - OUTPUT_NODE = True - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "svg": (RecraftIO.SVG,), - "filename_prefix": ("STRING", {"default": "svg/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."}) - }, - "hidden": { - "prompt": "PROMPT", - "extra_pnginfo": "EXTRA_PNGINFO" - } - } - - def save_svg(self, svg: SVG, filename_prefix="svg/ComfyUI", prompt=None, extra_pnginfo=None): - filename_prefix += self.prefix_append - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) - results = list() - - # Prepare metadata JSON - metadata_dict = {} - if prompt is not None: - metadata_dict["prompt"] = prompt - if extra_pnginfo is not None: - metadata_dict.update(extra_pnginfo) - - # Convert metadata to JSON string - metadata_json = json.dumps(metadata_dict, indent=2) if metadata_dict else None - - for batch_number, svg_bytes in enumerate(svg.data): - filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) - file = f"{filename_with_batch_num}_{counter:05}_.svg" - - # Read SVG content - svg_bytes.seek(0) - svg_content = svg_bytes.read().decode('utf-8') - - # Inject metadata if available - if metadata_json: - # Create metadata element with CDATA section - metadata_element = f""" - - -""" - # Insert metadata after opening svg tag using regex - import re - svg_content = re.sub(r'(]*>)', r'\1\n' + metadata_element, svg_content) - - # Write the modified SVG to file - with open(os.path.join(full_output_folder, file), 'wb') as svg_file: - svg_file.write(svg_content.encode('utf-8')) - - results.append({ - "filename": file, - "subfolder": subfolder, - "type": self.type - }) - counter += 1 - return { "ui": { "images": results } } - - class RecraftColorRGBNode: """ Create Recraft Color by choosing specific RGB values. @@ -485,6 +387,7 @@ class RecraftTextToImageNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -497,7 +400,6 @@ class RecraftTextToImageNode: recraft_style: RecraftStyle = None, negative_prompt: str = None, recraft_controls: RecraftControls = None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=False, max_length=1000) @@ -530,7 +432,7 @@ class RecraftTextToImageNode: style_id=recraft_style.style_id, controls=controls_api, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response: RecraftImageGenerationResponse = operation.execute() images = [] @@ -620,6 +522,7 @@ class RecraftImageToImageNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -630,7 +533,6 @@ class RecraftImageToImageNode: n: int, strength: float, seed, - auth_token=None, recraft_style: RecraftStyle = None, negative_prompt: str = None, recraft_controls: RecraftControls = None, @@ -668,7 +570,7 @@ class RecraftImageToImageNode: image=image[i], path="/proxy/recraft/images/imageToImage", request=request, - auth_token=auth_token, + auth_kwargs=kwargs, ) with handle_recraft_image_output(): images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) @@ -736,6 +638,7 @@ class RecraftImageInpaintingNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -746,7 +649,6 @@ class RecraftImageInpaintingNode: prompt: str, n: int, seed, - auth_token=None, recraft_style: RecraftStyle = None, negative_prompt: str = None, **kwargs, @@ -781,7 +683,7 @@ class RecraftImageInpaintingNode: mask=mask[i:i+1], path="/proxy/recraft/images/inpaint", request=request, - auth_token=auth_token, + auth_kwargs=kwargs, ) with handle_recraft_image_output(): images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) @@ -796,8 +698,8 @@ class RecraftTextToVectorNode: Generates SVG synchronously based on prompt and resolution. """ - RETURN_TYPES = (RecraftIO.SVG,) - DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value + RETURN_TYPES = ("SVG",) # Changed + DESCRIPTION = cleandoc(__doc__ or "") if 'cleandoc' in globals() else __doc__ # Keep cleandoc if other nodes use it FUNCTION = "api_call" API_NODE = True CATEGORY = "api node/image/Recraft" @@ -860,6 +762,7 @@ class RecraftTextToVectorNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -872,7 +775,6 @@ class RecraftTextToVectorNode: seed, negative_prompt: str = None, recraft_controls: RecraftControls = None, - auth_token=None, **kwargs, ): validate_string(prompt, strip_whitespace=False, max_length=1000) @@ -903,7 +805,7 @@ class RecraftTextToVectorNode: substyle=recraft_style.substyle, controls=controls_api, ), - auth_token=auth_token, + auth_kwargs=kwargs, ) response: RecraftImageGenerationResponse = operation.execute() svg_data = [] @@ -918,8 +820,8 @@ class RecraftVectorizeImageNode: Generates SVG synchronously from an input image. """ - RETURN_TYPES = (RecraftIO.SVG,) - DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value + RETURN_TYPES = ("SVG",) # Changed + DESCRIPTION = cleandoc(__doc__ or "") if 'cleandoc' in globals() else __doc__ # Keep cleandoc if other nodes use it FUNCTION = "api_call" API_NODE = True CATEGORY = "api node/image/Recraft" @@ -934,13 +836,13 @@ class RecraftVectorizeImageNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call( self, image: torch.Tensor, - auth_token=None, **kwargs, ): svgs = [] @@ -950,7 +852,7 @@ class RecraftVectorizeImageNode: sub_bytes = handle_recraft_file_request( image=image[i], path="/proxy/recraft/images/vectorize", - auth_token=auth_token, + auth_kwargs=kwargs, ) svgs.append(SVG(sub_bytes)) pbar.update(1) @@ -1015,6 +917,7 @@ class RecraftReplaceBackgroundNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -1024,7 +927,6 @@ class RecraftReplaceBackgroundNode: prompt: str, n: int, seed, - auth_token=None, recraft_style: RecraftStyle = None, negative_prompt: str = None, **kwargs, @@ -1054,7 +956,7 @@ class RecraftReplaceBackgroundNode: image=image[i], path="/proxy/recraft/images/replaceBackground", request=request, - auth_token=auth_token, + auth_kwargs=kwargs, ) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) pbar.update(1) @@ -1084,13 +986,13 @@ class RecraftRemoveBackgroundNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call( self, image: torch.Tensor, - auth_token=None, **kwargs, ): images = [] @@ -1100,7 +1002,7 @@ class RecraftRemoveBackgroundNode: sub_bytes = handle_recraft_file_request( image=image[i], path="/proxy/recraft/images/removeBackground", - auth_token=auth_token, + auth_kwargs=kwargs, ) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) pbar.update(1) @@ -1135,13 +1037,13 @@ class RecraftCrispUpscaleNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call( self, image: torch.Tensor, - auth_token=None, **kwargs, ): images = [] @@ -1151,7 +1053,7 @@ class RecraftCrispUpscaleNode: sub_bytes = handle_recraft_file_request( image=image[i], path=self.RECRAFT_PATH, - auth_token=auth_token, + auth_kwargs=kwargs, ) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) pbar.update(1) @@ -1193,7 +1095,6 @@ NODE_CLASS_MAPPINGS = { "RecraftStyleV3InfiniteStyleLibrary": RecraftStyleInfiniteStyleLibrary, "RecraftColorRGB": RecraftColorRGBNode, "RecraftControls": RecraftControlsNode, - "SaveSVG": SaveSVGNode, } # A dictionary that contains the friendly/humanly readable titles for the nodes @@ -1213,5 +1114,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { "RecraftStyleV3InfiniteStyleLibrary": "Recraft Style - Infinite Style Library", "RecraftColorRGB": "Recraft Color RGB", "RecraftControls": "Recraft Controls", - "SaveSVG": "Save SVG", } diff --git a/comfy_api_nodes/nodes_stability.py b/comfy_api_nodes/nodes_stability.py index 52fe2417c..02e421678 100644 --- a/comfy_api_nodes/nodes_stability.py +++ b/comfy_api_nodes/nodes_stability.py @@ -120,12 +120,13 @@ class StabilityStableImageUltraNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call(self, prompt: str, aspect_ratio: str, style_preset: str, seed: int, negative_prompt: str=None, image: torch.Tensor = None, image_denoise: float=None, - auth_token=None): + **kwargs): validate_string(prompt, strip_whitespace=False) # prepare image binary if image present image_binary = None @@ -160,7 +161,7 @@ class StabilityStableImageUltraNode: ), files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -252,12 +253,13 @@ class StabilityStableImageSD_3_5Node: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call(self, model: str, prompt: str, aspect_ratio: str, style_preset: str, seed: int, cfg_scale: float, negative_prompt: str=None, image: torch.Tensor = None, image_denoise: float=None, - auth_token=None): + **kwargs): validate_string(prompt, strip_whitespace=False) # prepare image binary if image present image_binary = None @@ -298,7 +300,7 @@ class StabilityStableImageSD_3_5Node: ), files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -368,11 +370,12 @@ class StabilityUpscaleConservativeNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call(self, image: torch.Tensor, prompt: str, creativity: float, seed: int, negative_prompt: str=None, - auth_token=None): + **kwargs): validate_string(prompt, strip_whitespace=False) image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read() @@ -398,7 +401,7 @@ class StabilityUpscaleConservativeNode: ), files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -473,11 +476,12 @@ class StabilityUpscaleCreativeNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call(self, image: torch.Tensor, prompt: str, creativity: float, style_preset: str, seed: int, negative_prompt: str=None, - auth_token=None): + **kwargs): validate_string(prompt, strip_whitespace=False) image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read() @@ -506,7 +510,7 @@ class StabilityUpscaleCreativeNode: ), files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() @@ -521,7 +525,7 @@ class StabilityUpscaleCreativeNode: completed_statuses=[StabilityPollStatus.finished], failed_statuses=[StabilityPollStatus.failed], status_extractor=lambda x: get_async_dummy_status(x), - auth_token=auth_token, + auth_kwargs=kwargs, ) response_poll: StabilityResultsGetResponse = operation.execute() @@ -555,11 +559,12 @@ class StabilityUpscaleFastNode: }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } def api_call(self, image: torch.Tensor, - auth_token=None): + **kwargs): image_binary = tensor_to_bytesio(image, total_pixels=4096*4096).read() files = { @@ -576,7 +581,7 @@ class StabilityUpscaleFastNode: request=EmptyRequest(), files=files, content_type="multipart/form-data", - auth_token=auth_token, + auth_kwargs=kwargs, ) response_api = operation.execute() diff --git a/comfy_api_nodes/nodes_veo2.py b/comfy_api_nodes/nodes_veo2.py index 9233944b5..2740179c8 100644 --- a/comfy_api_nodes/nodes_veo2.py +++ b/comfy_api_nodes/nodes_veo2.py @@ -114,6 +114,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", }, } @@ -133,7 +134,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): person_generation="ALLOW", seed=0, image=None, - auth_token=None, + **kwargs, ): # Prepare the instances for the request instances = [] @@ -179,7 +180,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): instances=instances, parameters=parameters ), - auth_token=auth_token + auth_kwargs=kwargs, ) initial_response = initial_operation.execute() @@ -213,7 +214,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): request=Veo2GenVidPollRequest( operationName=operation_name ), - auth_token=auth_token, + auth_kwargs=kwargs, poll_interval=5.0 ) diff --git a/comfy_extras/nodes_apg.py b/comfy_extras/nodes_apg.py new file mode 100644 index 000000000..25b21b1b8 --- /dev/null +++ b/comfy_extras/nodes_apg.py @@ -0,0 +1,76 @@ +import torch + +def project(v0, v1): + v1 = torch.nn.functional.normalize(v1, dim=[-1, -2, -3]) + v0_parallel = (v0 * v1).sum(dim=[-1, -2, -3], keepdim=True) * v1 + v0_orthogonal = v0 - v0_parallel + return v0_parallel, v0_orthogonal + +class APG: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "eta": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "tooltip": "Controls the scale of the parallel guidance vector. Default CFG behavior at a setting of 1."}), + "norm_threshold": ("FLOAT", {"default": 5.0, "min": 0.0, "max": 50.0, "step": 0.1, "tooltip": "Normalize guidance vector to this value, normalization disable at a setting of 0."}), + "momentum": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 1.0, "step": 0.01, "tooltip":"Controls a running average of guidance during diffusion, disabled at a setting of 0."}), + } + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "sampling/custom_sampling" + + def patch(self, model, eta, norm_threshold, momentum): + running_avg = 0 + prev_sigma = None + + def pre_cfg_function(args): + nonlocal running_avg, prev_sigma + + if len(args["conds_out"]) == 1: return args["conds_out"] + + cond = args["conds_out"][0] + uncond = args["conds_out"][1] + sigma = args["sigma"][0] + cond_scale = args["cond_scale"] + + if prev_sigma is not None and sigma > prev_sigma: + running_avg = 0 + prev_sigma = sigma + + guidance = cond - uncond + + if momentum != 0: + if not torch.is_tensor(running_avg): + running_avg = guidance + else: + running_avg = momentum * running_avg + guidance + guidance = running_avg + + if norm_threshold > 0: + guidance_norm = guidance.norm(p=2, dim=[-1, -2, -3], keepdim=True) + scale = torch.minimum( + torch.ones_like(guidance_norm), + norm_threshold / guidance_norm + ) + guidance = guidance * scale + + guidance_parallel, guidance_orthogonal = project(guidance, cond) + modified_guidance = guidance_orthogonal + eta * guidance_parallel + + modified_cond = (uncond + modified_guidance) + (cond - uncond) / cond_scale + + return [modified_cond, uncond] + args["conds_out"][2:] + + m = model.clone() + m.set_model_sampler_pre_cfg_function(pre_cfg_function) + return (m,) + +NODE_CLASS_MAPPINGS = { + "APG": APG, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "APG": "Adaptive Projected Guidance", +} diff --git a/comfy_extras/nodes_audio.py b/comfy_extras/nodes_audio.py index 136ad6159..49af1eae4 100644 --- a/comfy_extras/nodes_audio.py +++ b/comfy_extras/nodes_audio.py @@ -1,5 +1,6 @@ from __future__ import annotations +import av import torchaudio import torch import comfy.model_management @@ -7,7 +8,6 @@ import folder_paths import os import io import json -import struct import random import hashlib import node_helpers @@ -90,60 +90,118 @@ class VAEDecodeAudio: return ({"waveform": audio, "sample_rate": 44100}, ) -def create_vorbis_comment_block(comment_dict, last_block): - vendor_string = b'ComfyUI' - vendor_length = len(vendor_string) +def save_audio(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None, quality="128k"): - comments = [] - for key, value in comment_dict.items(): - comment = f"{key}={value}".encode('utf-8') - comments.append(struct.pack('I', len(comment_data))[1:] + comment_data + # Opus supported sample rates + OPUS_RATES = [8000, 12000, 16000, 24000, 48000] - return comment_block + 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) -def insert_or_replace_vorbis_comment(flac_io, comment_dict): - if len(comment_dict) == 0: - return flac_io + # Use original sample rate initially + sample_rate = audio["sample_rate"] - flac_io.seek(4) + # 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 - blocks = [] - last_block = False + # Resample if necessary + if sample_rate != audio["sample_rate"]: + waveform = torchaudio.functional.resample(waveform, audio["sample_rate"], sample_rate) - while not last_block: - header = flac_io.read(4) - last_block = (header[0] & 0x80) != 0 - block_type = header[0] & 0x7F - block_length = struct.unpack('>I', b'\x00' + header[1:])[0] - block_data = flac_io.read(block_length) + # Create in-memory WAV buffer + wav_buffer = io.BytesIO() + torchaudio.save(wav_buffer, waveform, sample_rate, format="WAV") + wav_buffer.seek(0) # Rewind for reading - if block_type == 4 or block_type == 1: - pass - else: - header = bytes([(header[0] & (~0x80))]) + header[1:] - blocks.append(header + block_data) + # Use PyAV to convert and add metadata + input_container = av.open(wav_buffer) - blocks.append(create_vorbis_comment_block(comment_dict, last_block=True)) + # Create output with specified format + output_buffer = io.BytesIO() + output_container = av.open(output_buffer, mode='w', format=format) - new_flac_io = io.BytesIO() - new_flac_io.write(b'fLaC') - for block in blocks: - new_flac_io.write(block) + # Set metadata on the container + for key, value in metadata.items(): + output_container.metadata[key] = value - new_flac_io.write(flac_io.read()) - return new_flac_io + # Set up the output stream with appropriate properties + input_container.streams.audio[0] + 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) + # Copy frames from input to output + for frame in input_container.decode(audio=0): + frame.pts = None # Let PyAV handle timestamps + output_container.mux(out_stream.encode(frame)) + + # Flush encoder + output_container.mux(out_stream.encode(None)) + + # Close containers + output_container.close() + input_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({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + counter += 1 + + return { "ui": { "audio": results } } + class SaveAudio: def __init__(self): self.output_dir = folder_paths.get_output_directory() @@ -153,50 +211,70 @@ class SaveAudio: @classmethod def INPUT_TYPES(s): return {"required": { "audio": ("AUDIO", ), - "filename_prefix": ("STRING", {"default": "audio/ComfyUI"})}, + "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}), + }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } RETURN_TYPES = () - FUNCTION = "save_audio" + FUNCTION = "save_flac" OUTPUT_NODE = True CATEGORY = "audio" - def save_audio(self, audio, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): - filename_prefix += self.prefix_append - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) - results: list[FileLocator] = [] + def save_flac(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None): + return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo) - metadata = {} - if not args.disable_metadata: - if prompt is not None: - metadata["prompt"] = json.dumps(prompt) - if extra_pnginfo is not None: - for x in extra_pnginfo: - metadata[x] = json.dumps(extra_pnginfo[x]) +class SaveAudioMP3: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" - 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}_.flac" + @classmethod + def INPUT_TYPES(s): + return {"required": { "audio": ("AUDIO", ), + "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}), + "quality": (["V0", "128k", "320k"], {"default": "V0"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } - buff = io.BytesIO() - torchaudio.save(buff, waveform, audio["sample_rate"], format="FLAC") + RETURN_TYPES = () + FUNCTION = "save_mp3" - buff = insert_or_replace_vorbis_comment(buff, metadata) + OUTPUT_NODE = True - with open(os.path.join(full_output_folder, file), 'wb') as f: - f.write(buff.getbuffer()) + CATEGORY = "audio" - results.append({ - "filename": file, - "subfolder": subfolder, - "type": self.type - }) - counter += 1 + def save_mp3(self, audio, filename_prefix="ComfyUI", format="mp3", prompt=None, extra_pnginfo=None, quality="128k"): + return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo, quality) - return { "ui": { "audio": results } } +class SaveAudioOpus: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + + @classmethod + def INPUT_TYPES(s): + return {"required": { "audio": ("AUDIO", ), + "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}), + "quality": (["64k", "96k", "128k", "192k", "320k"], {"default": "128k"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "save_opus" + + OUTPUT_NODE = True + + CATEGORY = "audio" + + def save_opus(self, audio, filename_prefix="ComfyUI", format="opus", prompt=None, extra_pnginfo=None, quality="V3"): + return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo, quality) class PreviewAudio(SaveAudio): def __init__(self): @@ -248,7 +326,20 @@ NODE_CLASS_MAPPINGS = { "VAEEncodeAudio": VAEEncodeAudio, "VAEDecodeAudio": VAEDecodeAudio, "SaveAudio": SaveAudio, + "SaveAudioMP3": SaveAudioMP3, + "SaveAudioOpus": SaveAudioOpus, "LoadAudio": LoadAudio, "PreviewAudio": PreviewAudio, "ConditioningStableAudio": ConditioningStableAudio, } + +NODE_DISPLAY_NAME_MAPPINGS = { + "EmptyLatentAudio": "Empty Latent Audio", + "VAEEncodeAudio": "VAE Encode Audio", + "VAEDecodeAudio": "VAE Decode Audio", + "PreviewAudio": "Preview Audio", + "LoadAudio": "Load Audio", + "SaveAudio": "Save Audio (FLAC)", + "SaveAudioMP3": "Save Audio (MP3)", + "SaveAudioOpus": "Save Audio (Opus)", +} diff --git a/comfy_extras/nodes_hunyuan.py b/comfy_extras/nodes_hunyuan.py index 504010ad0..d7278e7a7 100644 --- a/comfy_extras/nodes_hunyuan.py +++ b/comfy_extras/nodes_hunyuan.py @@ -77,7 +77,7 @@ class HunyuanImageToVideo: "height": ("INT", {"default": 480, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), "length": ("INT", {"default": 53, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), - "guidance_type": (["v1 (concat)", "v2 (replace)"], ) + "guidance_type": (["v1 (concat)", "v2 (replace)", "custom"], ) }, "optional": {"start_image": ("IMAGE", ), }} @@ -101,10 +101,12 @@ class HunyuanImageToVideo: if guidance_type == "v1 (concat)": cond = {"concat_latent_image": concat_latent_image, "concat_mask": mask} - else: + elif guidance_type == "v2 (replace)": cond = {'guiding_frame_index': 0} latent[:, :, :concat_latent_image.shape[2]] = concat_latent_image out_latent["noise_mask"] = mask + elif guidance_type == "custom": + cond = {"ref_latent": concat_latent_image} positive = node_helpers.conditioning_set_values(positive, cond) diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index e11a4583a..77c305619 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -10,6 +10,9 @@ from PIL.PngImagePlugin import PngInfo import numpy as np import json import os +import re +from io import BytesIO +from inspect import cleandoc from comfy.comfy_types import FileLocator @@ -190,10 +193,109 @@ class SaveAnimatedPNG: return { "ui": { "images": results, "animated": (True,)} } +class SVG: + """ + Stores SVG representations via a list of BytesIO objects. + """ + def __init__(self, data: list[BytesIO]): + self.data = data + + def combine(self, other: 'SVG') -> 'SVG': + return SVG(self.data + other.data) + + @staticmethod + def combine_all(svgs: list['SVG']) -> 'SVG': + all_svgs_list: list[BytesIO] = [] + for svg_item in svgs: + all_svgs_list.extend(svg_item.data) + return SVG(all_svgs_list) + +class SaveSVGNode: + """ + Save SVG files on disk. + """ + + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + + RETURN_TYPES = () + DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value + FUNCTION = "save_svg" + CATEGORY = "image/save" # Changed + OUTPUT_NODE = True + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "svg": ("SVG",), # Changed + "filename_prefix": ("STRING", {"default": "svg/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."}) + }, + "hidden": { + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO" + } + } + + def save_svg(self, svg: SVG, filename_prefix="svg/ComfyUI", prompt=None, extra_pnginfo=None): + filename_prefix += self.prefix_append + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) + results = list() + + # Prepare metadata JSON + metadata_dict = {} + if prompt is not None: + metadata_dict["prompt"] = prompt + if extra_pnginfo is not None: + metadata_dict.update(extra_pnginfo) + + # Convert metadata to JSON string + metadata_json = json.dumps(metadata_dict, indent=2) if metadata_dict else None + + for batch_number, svg_bytes in enumerate(svg.data): + filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) + file = f"{filename_with_batch_num}_{counter:05}_.svg" + + # Read SVG content + svg_bytes.seek(0) + svg_content = svg_bytes.read().decode('utf-8') + + # Inject metadata if available + if metadata_json: + # Create metadata element with CDATA section + metadata_element = f""" + + + """ + # Insert metadata after opening svg tag using regex with a replacement function + def replacement(match): + # match.group(1) contains the captured tag + return match.group(1) + '\n' + metadata_element + + # Apply the substitution + svg_content = re.sub(r'(]*>)', replacement, svg_content, flags=re.UNICODE) + + # Write the modified SVG to file + with open(os.path.join(full_output_folder, file), 'wb') as svg_file: + svg_file.write(svg_content.encode('utf-8')) + + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + counter += 1 + return { "ui": { "images": results } } + NODE_CLASS_MAPPINGS = { "ImageCrop": ImageCrop, "RepeatImageBatch": RepeatImageBatch, "ImageFromBatch": ImageFromBatch, "SaveAnimatedWEBP": SaveAnimatedWEBP, "SaveAnimatedPNG": SaveAnimatedPNG, + "SaveSVGNode": SaveSVGNode, } diff --git a/comfy_extras/nodes_load_3d.py b/comfy_extras/nodes_load_3d.py index 53d892bc4..d5b4d9111 100644 --- a/comfy_extras/nodes_load_3d.py +++ b/comfy_extras/nodes_load_3d.py @@ -2,6 +2,10 @@ import nodes import folder_paths import os +from comfy.comfy_types import IO +from comfy_api.input_impl import VideoFromFile + + def normalize_path(path): return path.replace('\\', '/') @@ -21,8 +25,8 @@ class Load3D(): "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), }} - RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE", "LOAD3D_CAMERA") - RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart", "camera_info") + RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE", "LOAD3D_CAMERA", IO.VIDEO) + RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart", "camera_info", "recording_video") FUNCTION = "process" EXPERIMENTAL = True @@ -41,7 +45,14 @@ class Load3D(): normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path) lineart_image, ignore_mask3 = load_image_node.load_image(image=lineart_path) - return output_image, output_mask, model_file, normal_image, lineart_image, image['camera_info'] + video = None + + if image['recording'] != "": + recording_video_path = folder_paths.get_annotated_filepath(image['recording']) + + video = VideoFromFile(recording_video_path) + + return output_image, output_mask, model_file, normal_image, lineart_image, image['camera_info'], video class Load3DAnimation(): @classmethod @@ -59,8 +70,8 @@ class Load3DAnimation(): "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), }} - RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "LOAD3D_CAMERA") - RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "camera_info") + RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "LOAD3D_CAMERA", IO.VIDEO) + RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "camera_info", "recording_video") FUNCTION = "process" EXPERIMENTAL = True @@ -77,7 +88,14 @@ class Load3DAnimation(): ignore_image, output_mask = load_image_node.load_image(image=mask_path) normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path) - return output_image, output_mask, model_file, normal_image, image['camera_info'] + video = None + + if image['recording'] != "": + recording_video_path = folder_paths.get_annotated_filepath(image['recording']) + + video = VideoFromFile(recording_video_path) + + return output_image, output_mask, model_file, normal_image, image['camera_info'], video class Preview3D(): @classmethod diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py new file mode 100644 index 000000000..a852326e5 --- /dev/null +++ b/comfy_extras/nodes_string.py @@ -0,0 +1,322 @@ +import re + +from comfy.comfy_types.node_typing import IO + +class StringConcatenate(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string_a": (IO.STRING, {"multiline": True}), + "string_b": (IO.STRING, {"multiline": True}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string_a, string_b, **kwargs): + return string_a + string_b, + +class StringSubstring(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "start": (IO.INT, {}), + "end": (IO.INT, {}), + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, start, end, **kwargs): + return string[start:end], + +class StringLength(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}) + } + } + + RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("length",) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, **kwargs): + length = len(string) + + return length, + +class CaseConverter(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["UPPERCASE", "lowercase", "Capitalize", "Title Case"]}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, mode, **kwargs): + if mode == "UPPERCASE": + result = string.upper() + elif mode == "lowercase": + result = string.lower() + elif mode == "Capitalize": + result = string.capitalize() + elif mode == "Title Case": + result = string.title() + else: + result = string + + return result, + + +class StringTrim(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["Both", "Left", "Right"]}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, mode, **kwargs): + if mode == "Both": + result = string.strip() + elif mode == "Left": + result = string.lstrip() + elif mode == "Right": + result = string.rstrip() + else: + result = string + + return result, + +class StringReplace(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "find": (IO.STRING, {"multiline": True}), + "replace": (IO.STRING, {"multiline": True}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, find, replace, **kwargs): + result = string.replace(find, replace) + return result, + + +class StringContains(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "substring": (IO.STRING, {"multiline": True}), + "case_sensitive": (IO.BOOLEAN, {"default": True}) + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("contains",) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, substring, case_sensitive, **kwargs): + if case_sensitive: + contains = substring in string + else: + contains = substring.lower() in string.lower() + + return contains, + + +class StringCompare(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string_a": (IO.STRING, {"multiline": True}), + "string_b": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["Starts With", "Ends With", "Equal"]}), + "case_sensitive": (IO.BOOLEAN, {"default": True}) + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string_a, string_b, mode, case_sensitive, **kwargs): + if case_sensitive: + a = string_a + b = string_b + else: + a = string_a.lower() + b = string_b.lower() + + if mode == "Equal": + return a == b, + elif mode == "Starts With": + return a.startswith(b), + elif mode == "Ends With": + return a.endswith(b), + +class RegexMatch(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "regex_pattern": (IO.STRING, {"multiline": True}), + "case_insensitive": (IO.BOOLEAN, {"default": True}), + "multiline": (IO.BOOLEAN, {"default": False}), + "dotall": (IO.BOOLEAN, {"default": False}) + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("matches",) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, regex_pattern, case_insensitive, multiline, dotall, **kwargs): + flags = 0 + + if case_insensitive: + flags |= re.IGNORECASE + if multiline: + flags |= re.MULTILINE + if dotall: + flags |= re.DOTALL + + try: + match = re.search(regex_pattern, string, flags) + result = match is not None + + except re.error: + result = False + + return result, + + +class RegexExtract(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "regex_pattern": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["First Match", "All Matches", "First Group", "All Groups"]}), + "case_insensitive": (IO.BOOLEAN, {"default": True}), + "multiline": (IO.BOOLEAN, {"default": False}), + "dotall": (IO.BOOLEAN, {"default": False}), + "group_index": (IO.INT, {"default": 1, "min": 0, "max": 100}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, regex_pattern, mode, case_insensitive, multiline, dotall, group_index, **kwargs): + join_delimiter = "\n" + + flags = 0 + if case_insensitive: + flags |= re.IGNORECASE + if multiline: + flags |= re.MULTILINE + if dotall: + flags |= re.DOTALL + + try: + if mode == "First Match": + match = re.search(regex_pattern, string, flags) + if match: + result = match.group(0) + else: + result = "" + + elif mode == "All Matches": + matches = re.findall(regex_pattern, string, flags) + if matches: + if isinstance(matches[0], tuple): + result = join_delimiter.join([m[0] for m in matches]) + else: + result = join_delimiter.join(matches) + else: + result = "" + + elif mode == "First Group": + match = re.search(regex_pattern, string, flags) + if match and len(match.groups()) >= group_index: + result = match.group(group_index) + else: + result = "" + + elif mode == "All Groups": + matches = re.finditer(regex_pattern, string, flags) + results = [] + for match in matches: + if match.groups() and len(match.groups()) >= group_index: + results.append(match.group(group_index)) + result = join_delimiter.join(results) + else: + result = "" + + except re.error: + result = "" + + return result, + +NODE_CLASS_MAPPINGS = { + "StringConcatenate": StringConcatenate, + "StringSubstring": StringSubstring, + "StringLength": StringLength, + "CaseConverter": CaseConverter, + "StringTrim": StringTrim, + "StringReplace": StringReplace, + "StringContains": StringContains, + "StringCompare": StringCompare, + "RegexMatch": RegexMatch, + "RegexExtract": RegexExtract +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "StringConcatenate": "Concatenate", + "StringSubstring": "Substring", + "StringLength": "Length", + "CaseConverter": "Case Converter", + "StringTrim": "Trim", + "StringReplace": "Replace", + "StringContains": "Contains", + "StringCompare": "Compare", + "RegexMatch": "Regex Match", + "RegexExtract": "Regex Extract" +} diff --git a/comfyui_version.py b/comfyui_version.py index 5a73f76e4..b740b378d 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.33" +__version__ = "0.3.34" diff --git a/execution.py b/execution.py index feb61ae82..e5d1c69d9 100644 --- a/execution.py +++ b/execution.py @@ -146,6 +146,8 @@ def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, e input_data_all[x] = [unique_id] if h[x] == "AUTH_TOKEN_COMFY_ORG": input_data_all[x] = [extra_data.get("auth_token_comfy_org", None)] + if h[x] == "API_KEY_COMFY_ORG": + input_data_all[x] = [extra_data.get("api_key_comfy_org", None)] return input_data_all, missing_keys map_node_over_list = None #Don't hook this please diff --git a/nodes.py b/nodes.py index f6171acb6..0a9db1393 100644 --- a/nodes.py +++ b/nodes.py @@ -2261,8 +2261,10 @@ def init_builtin_extra_nodes(): "nodes_optimalsteps.py", "nodes_hidream.py", "nodes_fresca.py", + "nodes_apg.py", "nodes_preview_any.py", "nodes_ace.py", + "nodes_string.py", "nodes_camera_trajectory.py", ] diff --git a/pyproject.toml b/pyproject.toml index e0be329de..80061b39a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.33" +version = "0.3.34" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" diff --git a/requirements.txt b/requirements.txt index 29cf0e2ac..8f7a78984 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -comfyui-frontend-package==1.18.9 -comfyui-workflow-templates==0.1.11 +comfyui-frontend-package==1.19.9 +comfyui-workflow-templates==0.1.14 torch torchsde torchvision diff --git a/script_examples/basic_api_example.py b/script_examples/basic_api_example.py index c916e6cb9..9128420c4 100644 --- a/script_examples/basic_api_example.py +++ b/script_examples/basic_api_example.py @@ -101,6 +101,14 @@ prompt_text = """ def queue_prompt(prompt): p = {"prompt": prompt} + + # If the workflow contains API nodes, you can add a Comfy API key to the `extra_data`` field of the payload. + # p["extra_data"] = { + # "api_key_comfy_org": "comfyui-87d01e28d*******************************************************" # replace with real key + # } + # See: https://docs.comfy.org/tutorials/api-nodes/overview + # Generate a key here: https://platform.comfy.org/login + data = json.dumps(p).encode('utf-8') req = request.Request("http://127.0.0.1:8188/prompt", data=data) request.urlopen(req) diff --git a/server.py b/server.py index f64ec27d4..cb1c6a8fd 100644 --- a/server.py +++ b/server.py @@ -32,12 +32,13 @@ from app.frontend_management import FrontendManager from app.user_manager import UserManager from app.model_manager import ModelFileManager from app.custom_node_manager import CustomNodeManager -from typing import Optional +from typing import Optional, Union from api_server.routes.internal.internal_routes import InternalRoutes class BinaryEventTypes: PREVIEW_IMAGE = 1 UNENCODED_PREVIEW_IMAGE = 2 + TEXT = 3 async def send_socket_catch_exception(function, message): try: @@ -878,3 +879,15 @@ class PromptServer(): logging.warning(traceback.format_exc()) return json_data + + def send_progress_text( + self, text: Union[bytes, bytearray, str], node_id: str, sid=None + ): + if isinstance(text, str): + text = text.encode("utf-8") + node_id_bytes = str(node_id).encode("utf-8") + + # Pack the node_id length as a 4-byte unsigned integer, followed by the node_id bytes + message = struct.pack(">I", len(node_id_bytes)) + node_id_bytes + text + + self.send_sync(BinaryEventTypes.TEXT, message, sid)