Merge branch 'master' into wan_cam_dev

This commit is contained in:
George0726 2025-05-13 16:07:40 -04:00 committed by GitHub
commit 03c293a21d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1135 additions and 465 deletions

View File

@ -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("--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("--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("--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).") 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).")

View File

@ -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 phi1_fn = lambda t: torch.expm1(t) / t
phi2_fn = lambda t: (phi1_fn(t) - 1.0) / t phi2_fn = lambda t: (phi1_fn(t) - 1.0) / t
old_sigma_down = None
old_denoised = None old_denoised = None
uncond_denoised = None uncond_denoised = None
def post_cfg_function(args): 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 x = x + d * dt
else: else:
# Second order multistep method in https://arxiv.org/pdf/2308.02157 # 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 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) phi1_val, phi2_val = phi1_fn(-h), phi2_fn(-h)
b1 = torch.nan_to_num(phi1_val - phi2_val / c2, nan=0.0) 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 old_denoised = uncond_denoised
else: else:
old_denoised = denoised old_denoised = denoised
old_sigma_down = sigma_down
return x return x
@torch.no_grad() @torch.no_grad()

View File

@ -19,6 +19,7 @@ import torch.nn.functional as F
from torch import nn from torch import nn
import comfy.model_management import comfy.model_management
from comfy.ldm.modules.attention import optimized_attention
class Attention(nn.Module): class Attention(nn.Module):
def __init__( 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). 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( def apply_rotary_emb(
self, self,
x: torch.Tensor, x: torch.Tensor,
@ -435,13 +432,9 @@ class CustomerAttnProcessor2_0:
attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) 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) # 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 = optimized_attention(
hidden_states = F.scaled_dot_product_attention( query, key, value, heads=query.shape[1], mask=attention_mask, skip_reshape=True,
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False ).to(query.dtype)
)
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
hidden_states = hidden_states.to(query.dtype)
# linear proj # linear proj
hidden_states = attn.to_out[0](hidden_states) hidden_states = attn.to_out[0](hidden_states)

View File

@ -8,11 +8,7 @@ from typing import Callable, Tuple, List
import numpy as np import numpy as np
import torch.nn.functional as F 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 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 from .music_log_mel import LogMelSpectrogram
@ -259,7 +255,7 @@ class ResBlock1(torch.nn.Module):
self.convs1 = nn.ModuleList( self.convs1 = nn.ModuleList(
[ [
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
channels, channels,
channels, channels,
@ -269,7 +265,7 @@ class ResBlock1(torch.nn.Module):
padding=get_padding(kernel_size, dilation[0]), padding=get_padding(kernel_size, dilation[0]),
) )
), ),
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
channels, channels,
channels, channels,
@ -279,7 +275,7 @@ class ResBlock1(torch.nn.Module):
padding=get_padding(kernel_size, dilation[1]), padding=get_padding(kernel_size, dilation[1]),
) )
), ),
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
channels, channels,
channels, channels,
@ -294,7 +290,7 @@ class ResBlock1(torch.nn.Module):
self.convs2 = nn.ModuleList( self.convs2 = nn.ModuleList(
[ [
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
channels, channels,
channels, channels,
@ -304,7 +300,7 @@ class ResBlock1(torch.nn.Module):
padding=get_padding(kernel_size, 1), padding=get_padding(kernel_size, 1),
) )
), ),
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
channels, channels,
channels, channels,
@ -314,7 +310,7 @@ class ResBlock1(torch.nn.Module):
padding=get_padding(kernel_size, 1), padding=get_padding(kernel_size, 1),
) )
), ),
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
channels, channels,
channels, channels,
@ -366,7 +362,7 @@ class HiFiGANGenerator(nn.Module):
prod(upsample_rates) == hop_length prod(upsample_rates) == hop_length
), f"hop_length must be {prod(upsample_rates)}" ), f"hop_length must be {prod(upsample_rates)}"
self.conv_pre = weight_norm( self.conv_pre = torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
num_mels, num_mels,
upsample_initial_channel, upsample_initial_channel,
@ -386,7 +382,7 @@ class HiFiGANGenerator(nn.Module):
for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)): for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
c_cur = upsample_initial_channel // (2 ** (i + 1)) c_cur = upsample_initial_channel // (2 ** (i + 1))
self.ups.append( self.ups.append(
weight_norm( torch.nn.utils.parametrizations.weight_norm(
ops.ConvTranspose1d( ops.ConvTranspose1d(
upsample_initial_channel // (2**i), upsample_initial_channel // (2**i),
upsample_initial_channel // (2 ** (i + 1)), upsample_initial_channel // (2 ** (i + 1)),
@ -421,7 +417,7 @@ class HiFiGANGenerator(nn.Module):
self.resblocks.append(ResBlock1(ch, k, d)) self.resblocks.append(ResBlock1(ch, k, d))
self.activation_post = post_activation() self.activation_post = post_activation()
self.conv_post = weight_norm( self.conv_post = torch.nn.utils.parametrizations.weight_norm(
ops.Conv1d( ops.Conv1d(
ch, ch,
1, 1,

View File

@ -75,16 +75,10 @@ class SnakeBeta(nn.Module):
return x return x
def WNConv1d(*args, **kwargs): def WNConv1d(*args, **kwargs):
try: return torch.nn.utils.parametrizations.weight_norm(ops.Conv1d(*args, **kwargs))
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
def WNConvTranspose1d(*args, **kwargs): def WNConvTranspose1d(*args, **kwargs):
try: return torch.nn.utils.parametrizations.weight_norm(ops.ConvTranspose1d(*args, **kwargs))
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
def get_activation(activation: Literal["elu", "snake", "none"], antialias=False, channels=None) -> nn.Module: def get_activation(activation: Literal["elu", "snake", "none"], antialias=False, channels=None) -> nn.Module:
if activation == "elu": if activation == "elu":

View File

@ -228,6 +228,7 @@ class HunyuanVideo(nn.Module):
y: Tensor, y: Tensor,
guidance: Tensor = None, guidance: Tensor = None,
guiding_frame_index=None, guiding_frame_index=None,
ref_latent=None,
control=None, control=None,
transformer_options={}, transformer_options={},
) -> Tensor: ) -> Tensor:
@ -238,6 +239,14 @@ class HunyuanVideo(nn.Module):
img = self.img_in(img) img = self.img_in(img)
vec = self.time_in(timestep_embedding(timesteps, 256, time_factor=1.0).to(img.dtype)) 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: if guiding_frame_index is not None:
token_replace_vec = self.time_in(timestep_embedding(guiding_frame_index, 256, time_factor=1.0)) 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]) vec_ = self.vector_in(y[:, :self.params.vec_in_dim])
@ -313,6 +322,8 @@ class HunyuanVideo(nn.Module):
img[:, : img_len] += add img[:, : img_len] += add
img = img[:, : img_len] 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) 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]) img = img.reshape(initial_shape[0], self.out_channels, initial_shape[2], initial_shape[3], initial_shape[4])
return img 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 bs, c, t, h, w = x.shape
patch_size = self.patch_size patch_size = self.patch_size
t_len = ((t + (patch_size[0] // 2)) // patch_size[0]) 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[:, :, :, 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[:, :, :, 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[:, :, :, 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) 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 return out

View File

@ -286,6 +286,12 @@ def model_lora_keys_unet(model, key_map={}):
key_lora = k[len("diffusion_model."):-len(".weight")].replace(".", "_") key_lora = k[len("diffusion_model."):-len(".weight")].replace(".", "_")
key_map["lycoris_{}".format(key_lora)] = k #SimpleTuner lycoris format 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 return key_map

View File

@ -924,6 +924,10 @@ class HunyuanVideo(BaseModel):
if guiding_frame_index is not None: if guiding_frame_index is not None:
out['guiding_frame_index'] = comfy.conds.CONDRegular(torch.FloatTensor([guiding_frame_index])) 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 return out
def scale_latent_inpaint(self, latent_image, **kwargs): def scale_latent_inpaint(self, latent_image, **kwargs):

View File

@ -308,10 +308,10 @@ def fp8_linear(self, input):
if scale_input is None: if scale_input is None:
scale_input = torch.ones((), device=input.device, dtype=torch.float32) scale_input = torch.ones((), device=input.device, dtype=torch.float32)
input = torch.clamp(input, min=-448, max=448, out=input) 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: else:
scale_input = scale_input.to(input.device) 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: 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) o = torch._scaled_mm(input, w, out_dtype=input_dtype, bias=bias, scale_a=scale_input, scale_b=scale_weight)

View File

@ -30,7 +30,7 @@ if RMSNorm is None:
def __init__( def __init__(
self, self,
normalized_shape, normalized_shape,
eps=None, eps=1e-6,
elementwise_affine=True, elementwise_affine=True,
device=None, device=None,
dtype=None, dtype=None,

View File

@ -451,7 +451,7 @@ class VAE:
self.latent_dim = 2 self.latent_dim = 2
self.process_output = lambda audio: audio self.process_output = lambda audio: audio
self.process_input = 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.disable_offload = True
self.extra_1d_channel = 16 self.extra_1d_channel = 16
else: else:

View File

@ -28,6 +28,9 @@ import logging
import itertools import itertools
from torch.nn.functional import interpolate from torch.nn.functional import interpolate
from einops import rearrange from einops import rearrange
from comfy.cli_args import args
MMAP_TORCH_FILES = args.mmap_torch_files
ALWAYS_SAFE_LOAD = False 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 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 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 raise e
else: else:
torch_args = {}
if MMAP_TORCH_FILES:
torch_args["mmap"] = True
if safe_load or ALWAYS_SAFE_LOAD: 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: else:
pl_sd = torch.load(ckpt, map_location=device, pickle_module=comfy.checkpoint_pickle) pl_sd = torch.load(ckpt, map_location=device, pickle_module=comfy.checkpoint_pickle)
if "global_step" in pl_sd: if "global_step" in pl_sd:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import io import io
import logging import logging
from typing import Optional from typing import Optional
@ -314,7 +315,7 @@ def upload_file_to_comfyapi(
file_bytes_io: BytesIO, file_bytes_io: BytesIO,
filename: str, filename: str,
upload_mime_type: str, upload_mime_type: str,
auth_token: Optional[str] = None, auth_kwargs: Optional[dict[str,str]] = None,
) -> str: ) -> str:
""" """
Uploads a single file to ComfyUI API and returns its download URL. 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. file_bytes_io: BytesIO object containing the file data.
filename: The filename of the file. filename: The filename of the file.
upload_mime_type: MIME type of the file. upload_mime_type: MIME type of the file.
auth_token: Optional authentication token. auth_kwargs: Optional authentication token(s).
Returns: Returns:
The download URL for the uploaded file. The download URL for the uploaded file.
@ -337,7 +338,7 @@ def upload_file_to_comfyapi(
response_model=UploadResponse, response_model=UploadResponse,
), ),
request=request_object, request=request_object,
auth_token=auth_token, auth_kwargs=auth_kwargs,
) )
response: UploadResponse = operation.execute() response: UploadResponse = operation.execute()
@ -351,7 +352,7 @@ def upload_file_to_comfyapi(
def upload_video_to_comfyapi( def upload_video_to_comfyapi(
video: VideoInput, video: VideoInput,
auth_token: Optional[str] = None, auth_kwargs: Optional[dict[str,str]] = None,
container: VideoContainer = VideoContainer.MP4, container: VideoContainer = VideoContainer.MP4,
codec: VideoCodec = VideoCodec.H264, codec: VideoCodec = VideoCodec.H264,
max_duration: Optional[int] = None, max_duration: Optional[int] = None,
@ -362,7 +363,7 @@ def upload_video_to_comfyapi(
Args: Args:
video: VideoInput object (Comfy VIDEO type). 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). container: The video container format to use (default: MP4).
codec: The video codec to use (default: H264). 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. 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) video_bytes_io.seek(0)
return upload_file_to_comfyapi( 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( def upload_audio_to_comfyapi(
audio: AudioInput, audio: AudioInput,
auth_token: Optional[str] = None, auth_kwargs: Optional[dict[str,str]] = None,
container_format: str = "mp4", container_format: str = "mp4",
codec_name: str = "aac", codec_name: str = "aac",
mime_type: str = "audio/mp4", mime_type: str = "audio/mp4",
@ -465,7 +466,7 @@ def upload_audio_to_comfyapi(
Args: Args:
audio: a Comfy `AUDIO` type (contains waveform tensor and sample_rate) audio: a Comfy `AUDIO` type (contains waveform tensor and sample_rate)
auth_token: Optional authentication token. auth_kwargs: Optional authentication token(s).
Returns: Returns:
The download URL for the uploaded audio file. 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 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( 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]: ) -> list[str]:
""" """
Uploads images to ComfyUI API and returns download URLs. Uploads images to ComfyUI API and returns download URLs.
@ -490,7 +491,7 @@ def upload_images_to_comfyapi(
Args: Args:
image: Input torch.Tensor image. image: Input torch.Tensor image.
max_images: Maximum number of images to upload. 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. mime_type: Optional MIME type for the image.
""" """
# if batch, try to upload each file if max_images is greater than 0 # 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, response_model=UploadResponse,
), ),
request=request_object, request=request_object,
auth_token=auth_token, auth_kwargs=auth_kwargs,
) )
response = operation.execute() response = operation.execute()

View File

@ -20,7 +20,8 @@ Usage Examples:
# 1. Create the API client # 1. Create the API client
api_client = ApiClient( api_client = ApiClient(
base_url="https://api.example.com", 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, timeout=30.0,
verify_ssl=True verify_ssl=True
) )
@ -146,12 +147,14 @@ class ApiClient:
def __init__( def __init__(
self, self,
base_url: str, base_url: str,
api_key: Optional[str] = None, auth_token: Optional[str] = None,
comfy_api_key: Optional[str] = None,
timeout: float = 3600.0, timeout: float = 3600.0,
verify_ssl: bool = True, verify_ssl: bool = True,
): ):
self.base_url = base_url 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.timeout = timeout
self.verify_ssl = verify_ssl self.verify_ssl = verify_ssl
@ -201,8 +204,10 @@ class ApiClient:
"""Get headers for API requests, including authentication if available""" """Get headers for API requests, including authentication if available"""
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
if self.api_key: if self.auth_token:
headers["Authorization"] = f"Bearer {self.api_key}" headers["Authorization"] = f"Bearer {self.auth_token}"
elif self.comfy_api_key:
headers["X-API-KEY"] = self.comfy_api_key
return headers return headers
@ -236,7 +241,7 @@ class ApiClient:
requests.RequestException: If the request fails requests.RequestException: If the request fails
""" """
url = urljoin(self.base_url, path) 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 # Combine default headers with any provided headers
request_headers = self.get_headers() request_headers = self.get_headers()
if headers: if headers:
@ -320,11 +325,11 @@ class ApiClient:
return response.json() return response.json()
return {} return {}
def check_auth_token(self, auth_token): def check_auth(self, auth_token, comfy_api_key):
"""Verify that an auth token is present.""" """Verify that an auth token is present or comfy_api_key is present"""
if auth_token is None: if auth_token is None and comfy_api_key is None:
raise Exception("Unauthorized: Please login first to use this node.") raise Exception("Unauthorized: Please login first to use this node.")
return auth_token return auth_token or comfy_api_key
@staticmethod @staticmethod
def upload_file( def upload_file(
@ -392,6 +397,8 @@ class SynchronousOperation(Generic[T, R]):
files: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None,
api_base: str | None = None, api_base: str | None = None,
auth_token: Optional[str] = None, auth_token: Optional[str] = None,
comfy_api_key: Optional[str] = None,
auth_kwargs: Optional[Dict[str,str]] = None,
timeout: float = 604800.0, timeout: float = 604800.0,
verify_ssl: bool = True, verify_ssl: bool = True,
content_type: str = "application/json", content_type: str = "application/json",
@ -403,6 +410,10 @@ class SynchronousOperation(Generic[T, R]):
self.error = None self.error = None
self.api_base: str = api_base or args.comfy_api_base self.api_base: str = api_base or args.comfy_api_base
self.auth_token = auth_token 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.timeout = timeout
self.verify_ssl = verify_ssl self.verify_ssl = verify_ssl
self.files = files self.files = files
@ -415,7 +426,8 @@ class SynchronousOperation(Generic[T, R]):
if client is None: if client is None:
client = ApiClient( client = ApiClient(
base_url=self.api_base, 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, timeout=self.timeout,
verify_ssl=self.verify_ssl, verify_ssl=self.verify_ssl,
) )
@ -502,12 +514,18 @@ class PollingOperation(Generic[T, R]):
request: Optional[T] = None, request: Optional[T] = None,
api_base: str | None = None, api_base: str | None = None,
auth_token: Optional[str] = None, auth_token: Optional[str] = None,
comfy_api_key: Optional[str] = None,
auth_kwargs: Optional[Dict[str,str]] = None,
poll_interval: float = 5.0, poll_interval: float = 5.0,
): ):
self.poll_endpoint = poll_endpoint self.poll_endpoint = poll_endpoint
self.request = request self.request = request
self.api_base: str = api_base or args.comfy_api_base self.api_base: str = api_base or args.comfy_api_base
self.auth_token = auth_token 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 self.poll_interval = poll_interval
# Polling configuration # Polling configuration
@ -528,7 +546,8 @@ class PollingOperation(Generic[T, R]):
if client is None: if client is None:
client = ApiClient( client = ApiClient(
base_url=self.api_base, 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) return self._poll_until_complete(client)
except Exception as e: except Exception as e:

View File

@ -81,7 +81,6 @@ class RecraftStyle:
class RecraftIO: class RecraftIO:
STYLEV3 = "RECRAFT_V3_STYLE" STYLEV3 = "RECRAFT_V3_STYLE"
SVG = "SVG" # TODO: if acceptable, move into ComfyUI's typing class
COLOR = "RECRAFT_COLOR" COLOR = "RECRAFT_COLOR"
CONTROLS = "RECRAFT_CONTROLS" CONTROLS = "RECRAFT_CONTROLS"

View File

@ -179,6 +179,7 @@ class FluxProUltraImageNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -211,7 +212,6 @@ class FluxProUltraImageNode(ComfyNodeABC):
seed=0, seed=0,
image_prompt=None, image_prompt=None,
image_prompt_strength=0.1, image_prompt_strength=0.1,
auth_token=None,
**kwargs, **kwargs,
): ):
if image_prompt is None: if image_prompt is None:
@ -244,7 +244,7 @@ class FluxProUltraImageNode(ComfyNodeABC):
None if image_prompt is None else round(image_prompt_strength, 2) 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) output_image = handle_bfl_synchronous_operation(operation)
return (output_image,) return (output_image,)
@ -319,6 +319,7 @@ class FluxProImageNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -337,7 +338,6 @@ class FluxProImageNode(ComfyNodeABC):
seed=0, seed=0,
image_prompt=None, image_prompt=None,
# image_prompt_strength=0.1, # image_prompt_strength=0.1,
auth_token=None,
**kwargs, **kwargs,
): ):
image_prompt = ( image_prompt = (
@ -361,7 +361,7 @@ class FluxProImageNode(ComfyNodeABC):
seed=seed, seed=seed,
image_prompt=image_prompt, image_prompt=image_prompt,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
output_image = handle_bfl_synchronous_operation(operation) output_image = handle_bfl_synchronous_operation(operation)
return (output_image,) return (output_image,)
@ -461,6 +461,7 @@ class FluxProExpandNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -482,7 +483,6 @@ class FluxProExpandNode(ComfyNodeABC):
steps: int, steps: int,
guidance: float, guidance: float,
seed=0, seed=0,
auth_token=None,
**kwargs, **kwargs,
): ):
image = convert_image_to_base64(image) image = convert_image_to_base64(image)
@ -506,7 +506,7 @@ class FluxProExpandNode(ComfyNodeABC):
seed=seed, seed=seed,
image=image, image=image,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
output_image = handle_bfl_synchronous_operation(operation) output_image = handle_bfl_synchronous_operation(operation)
return (output_image,) return (output_image,)
@ -572,6 +572,7 @@ class FluxProFillNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -590,7 +591,6 @@ class FluxProFillNode(ComfyNodeABC):
steps: int, steps: int,
guidance: float, guidance: float,
seed=0, seed=0,
auth_token=None,
**kwargs, **kwargs,
): ):
# prepare mask # prepare mask
@ -615,7 +615,7 @@ class FluxProFillNode(ComfyNodeABC):
image=image, image=image,
mask=mask, mask=mask,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
output_image = handle_bfl_synchronous_operation(operation) output_image = handle_bfl_synchronous_operation(operation)
return (output_image,) return (output_image,)
@ -706,6 +706,7 @@ class FluxProCannyNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -726,7 +727,6 @@ class FluxProCannyNode(ComfyNodeABC):
steps: int, steps: int,
guidance: float, guidance: float,
seed=0, seed=0,
auth_token=None,
**kwargs, **kwargs,
): ):
control_image = convert_image_to_base64(control_image[:,:,:,:3]) control_image = convert_image_to_base64(control_image[:,:,:,:3])
@ -763,7 +763,7 @@ class FluxProCannyNode(ComfyNodeABC):
canny_high_threshold=canny_high_threshold, canny_high_threshold=canny_high_threshold,
preprocessed_image=preprocessed_image, preprocessed_image=preprocessed_image,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
output_image = handle_bfl_synchronous_operation(operation) output_image = handle_bfl_synchronous_operation(operation)
return (output_image,) return (output_image,)
@ -834,6 +834,7 @@ class FluxProDepthNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -852,7 +853,6 @@ class FluxProDepthNode(ComfyNodeABC):
steps: int, steps: int,
guidance: float, guidance: float,
seed=0, seed=0,
auth_token=None,
**kwargs, **kwargs,
): ):
control_image = convert_image_to_base64(control_image[:,:,:,:3]) control_image = convert_image_to_base64(control_image[:,:,:,:3])
@ -878,7 +878,7 @@ class FluxProDepthNode(ComfyNodeABC):
control_image=control_image, control_image=control_image,
preprocessed_image=preprocessed_image, preprocessed_image=preprocessed_image,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
output_image = handle_bfl_synchronous_operation(operation) output_image = handle_bfl_synchronous_operation(operation)
return (output_image,) return (output_image,)

View File

@ -234,9 +234,7 @@ def download_and_process_images(image_urls):
class IdeogramV1(ComfyNodeABC): class IdeogramV1(ComfyNodeABC):
""" """
Generates images synchronously using the Ideogram V1 model. Generates images 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.
""" """
def __init__(self): def __init__(self):
@ -303,7 +301,10 @@ class IdeogramV1(ComfyNodeABC):
{"default": 1, "min": 1, "max": 8, "step": 1, "display": "number"}, {"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,) RETURN_TYPES = (IO.IMAGE,)
@ -321,7 +322,7 @@ class IdeogramV1(ComfyNodeABC):
seed=0, seed=0,
negative_prompt="", negative_prompt="",
num_images=1, num_images=1,
auth_token=None, **kwargs,
): ):
# Determine the model based on turbo setting # Determine the model based on turbo setting
aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None) 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, negative_prompt=negative_prompt if negative_prompt else None,
) )
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response = operation.execute() response = operation.execute()
@ -365,9 +366,7 @@ class IdeogramV1(ComfyNodeABC):
class IdeogramV2(ComfyNodeABC): class IdeogramV2(ComfyNodeABC):
""" """
Generates images synchronously using the Ideogram V2 model. Generates images 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.
""" """
def __init__(self): 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,) RETURN_TYPES = (IO.IMAGE,)
@ -479,7 +481,7 @@ class IdeogramV2(ComfyNodeABC):
negative_prompt="", negative_prompt="",
num_images=1, num_images=1,
color_palette="", color_palette="",
auth_token=None, **kwargs,
): ):
aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None) aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None)
resolution = V1_V1_RES_MAP.get(resolution, 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, color_palette=color_palette if color_palette else None,
) )
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response = operation.execute() response = operation.execute()
@ -536,10 +538,7 @@ class IdeogramV2(ComfyNodeABC):
class IdeogramV3(ComfyNodeABC): class IdeogramV3(ComfyNodeABC):
""" """
Generates images synchronously using the Ideogram V3 model. Generates images using the Ideogram V3 model. Supports both regular image generation from text prompts and image editing with mask.
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.
""" """
def __init__(self): 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,) RETURN_TYPES = (IO.IMAGE,)
@ -641,7 +643,7 @@ class IdeogramV3(ComfyNodeABC):
seed=0, seed=0,
num_images=1, num_images=1,
rendering_speed="BALANCED", rendering_speed="BALANCED",
auth_token=None, **kwargs,
): ):
# Check if both image and mask are provided for editing mode # Check if both image and mask are provided for editing mode
if image is not None and mask is not None: if image is not None and mask is not None:
@ -705,7 +707,7 @@ class IdeogramV3(ComfyNodeABC):
"mask": mask_binary, "mask": mask_binary,
}, },
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=kwargs,
) )
elif image is not None or mask is not None: elif image is not None or mask is not None:
@ -746,7 +748,7 @@ class IdeogramV3(ComfyNodeABC):
response_model=IdeogramGenerateResponse, response_model=IdeogramGenerateResponse,
), ),
request=gen_request, request=gen_request,
auth_token=auth_token, auth_kwargs=kwargs,
) )
# Execute the operation and process response # Execute the operation and process response

View File

@ -95,7 +95,7 @@ class KlingApiError(Exception):
pass 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.""" """Polls the Kling API endpoint until the task reaches a terminal state, then returns the response."""
return PollingOperation( return PollingOperation(
poll_endpoint=api_endpoint, 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 if response.data and response.data.task_status
else None else None
), ),
auth_token=auth_token, auth_kwargs=auth_kwargs,
).execute() ).execute()
@ -184,6 +184,33 @@ def validate_image_result_response(response) -> None:
raise KlingApiError(error_msg) 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( def get_camera_control_input_config(
tooltip: str, default: float = 0.0 tooltip: str, default: float = 0.0
) -> tuple[IO, InputTypeOptions]: ) -> 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_TYPES = ("VIDEO", "STRING", "STRING")
RETURN_NAMES = ("VIDEO", "video_id", "duration") RETURN_NAMES = ("VIDEO", "video_id", "duration")
DESCRIPTION = "Kling Text to Video Node" 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( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_TEXT_TO_VIDEO}/{task_id}", path=f"{PATH_TEXT_TO_VIDEO}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -419,7 +449,7 @@ class KlingTextToVideoNode(KlingNodeBase):
camera_control: Optional[KlingCameraControl] = None, camera_control: Optional[KlingCameraControl] = None,
model_name: Optional[str] = None, model_name: Optional[str] = None,
duration: Optional[str] = None, duration: Optional[str] = None,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile, str, str]: ) -> tuple[VideoFromFile, str, str]:
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V) validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
if model_name is None: if model_name is None:
@ -441,14 +471,14 @@ class KlingTextToVideoNode(KlingNodeBase):
aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio), aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio),
camera_control=camera_control, camera_control=camera_control,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_video_result_response(final_response)
video = get_video_from_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." 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, cfg_scale: float,
aspect_ratio: str, aspect_ratio: str,
camera_control: Optional[KlingCameraControl] = None, camera_control: Optional[KlingCameraControl] = None,
auth_token: Optional[str] = None, **kwargs,
): ):
return super().api_call( return super().api_call(
model_name=KlingVideoGenModelName.kling_v1, model_name=KlingVideoGenModelName.kling_v1,
@ -518,7 +551,7 @@ class KlingCameraControlT2VNode(KlingTextToVideoNode):
prompt=prompt, prompt=prompt,
negative_prompt=negative_prompt, negative_prompt=negative_prompt,
camera_control=camera_control, camera_control=camera_control,
auth_token=auth_token, **kwargs,
) )
@ -530,7 +563,10 @@ class KlingImage2VideoNode(KlingNodeBase):
return { return {
"required": { "required": {
"start_frame": model_field_to_node_input( "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( "prompt": model_field_to_node_input(
IO.STRING, KlingImage2VideoRequest, "prompt", multiline=True IO.STRING, KlingImage2VideoRequest, "prompt", multiline=True
@ -574,16 +610,19 @@ class KlingImage2VideoNode(KlingNodeBase):
enum_type=KlingVideoGenDuration, 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_TYPES = ("VIDEO", "STRING", "STRING")
RETURN_NAMES = ("VIDEO", "video_id", "duration") RETURN_NAMES = ("VIDEO", "video_id", "duration")
DESCRIPTION = "Kling Image to Video Node" 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( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_IMAGE_TO_VIDEO}/{task_id}", path=f"{PATH_IMAGE_TO_VIDEO}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -604,12 +643,13 @@ class KlingImage2VideoNode(KlingNodeBase):
duration: str, duration: str,
camera_control: Optional[KlingCameraControl] = None, camera_control: Optional[KlingCameraControl] = None,
end_frame: Optional[torch.Tensor] = None, end_frame: Optional[torch.Tensor] = None,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V) validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V)
validate_input_image(start_frame)
if camera_control is not None: 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 camera_control.type = KlingCameraControlType.simple
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
@ -631,18 +671,17 @@ class KlingImage2VideoNode(KlingNodeBase):
negative_prompt=negative_prompt if negative_prompt else None, negative_prompt=negative_prompt if negative_prompt else None,
cfg_scale=cfg_scale, cfg_scale=cfg_scale,
mode=KlingVideoGenMode(mode), mode=KlingVideoGenMode(mode),
aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio),
duration=KlingVideoGenDuration(duration), duration=KlingVideoGenDuration(duration),
camera_control=camera_control, camera_control=camera_control,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_video_result_response(final_response)
video = get_video_from_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." 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, cfg_scale: float,
aspect_ratio: str, aspect_ratio: str,
camera_control: KlingCameraControl, camera_control: KlingCameraControl,
auth_token: Optional[str] = None, **kwargs,
): ):
return super().api_call( return super().api_call(
model_name=KlingVideoGenModelName.kling_v1_5, model_name=KlingVideoGenModelName.kling_v1_5,
@ -717,7 +759,7 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode):
prompt=prompt, prompt=prompt,
negative_prompt=negative_prompt, negative_prompt=negative_prompt,
camera_control=camera_control, 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." 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, cfg_scale: float,
aspect_ratio: str, aspect_ratio: str,
mode: str, mode: str,
auth_token: Optional[str] = None, **kwargs,
): ):
mode, duration, model_name = KlingStartEndFrameNode.get_mode_string_mapping()[ mode, duration, model_name = KlingStartEndFrameNode.get_mode_string_mapping()[
mode mode
@ -814,7 +859,7 @@ class KlingStartEndFrameNode(KlingImage2VideoNode):
aspect_ratio=aspect_ratio, aspect_ratio=aspect_ratio,
duration=duration, duration=duration,
end_frame=end_frame, end_frame=end_frame,
auth_token=auth_token, **kwargs,
) )
@ -844,16 +889,19 @@ class KlingVideoExtendNode(KlingNodeBase):
IO.STRING, KlingVideoExtendRequest, "video_id", forceInput=True 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_TYPES = ("VIDEO", "STRING", "STRING")
RETURN_NAMES = ("VIDEO", "video_id", "duration") 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." 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( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_VIDEO_EXTEND}/{task_id}", path=f"{PATH_VIDEO_EXTEND}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -868,7 +916,7 @@ class KlingVideoExtendNode(KlingNodeBase):
negative_prompt: str, negative_prompt: str,
cfg_scale: float, cfg_scale: float,
video_id: str, video_id: str,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile, str, str]: ) -> tuple[VideoFromFile, str, str]:
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V) validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
@ -884,14 +932,14 @@ class KlingVideoExtendNode(KlingNodeBase):
cfg_scale=cfg_scale, cfg_scale=cfg_scale,
video_id=video_id, video_id=video_id,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_video_result_response(final_response)
video = get_video_from_response(final_response) video = get_video_from_response(final_response)
@ -904,9 +952,9 @@ class KlingVideoEffectsBase(KlingNodeBase):
RETURN_TYPES = ("VIDEO", "STRING", "STRING") RETURN_TYPES = ("VIDEO", "STRING", "STRING")
RETURN_NAMES = ("VIDEO", "video_id", "duration") 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( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_VIDEO_EFFECTS}/{task_id}", path=f"{PATH_VIDEO_EFFECTS}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -924,7 +972,7 @@ class KlingVideoEffectsBase(KlingNodeBase):
image_1: torch.Tensor, image_1: torch.Tensor,
image_2: Optional[torch.Tensor] = None, image_2: Optional[torch.Tensor] = None,
mode: Optional[KlingVideoGenMode] = None, mode: Optional[KlingVideoGenMode] = None,
auth_token: Optional[str] = None, **kwargs,
): ):
if dual_character: if dual_character:
request_input_field = KlingDualCharacterEffectInput( request_input_field = KlingDualCharacterEffectInput(
@ -954,14 +1002,14 @@ class KlingVideoEffectsBase(KlingNodeBase):
effect_scene=effect_scene, effect_scene=effect_scene,
input=request_input_field, input=request_input_field,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_video_result_response(final_response)
video = get_video_from_response(final_response) video = get_video_from_response(final_response)
@ -1002,7 +1050,10 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase):
enum_type=KlingVideoGenDuration, 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." 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, model_name: KlingCharacterEffectModelName,
mode: KlingVideoGenMode, mode: KlingVideoGenMode,
duration: KlingVideoGenDuration, duration: KlingVideoGenDuration,
auth_token: Optional[str] = None, **kwargs,
): ):
video, _, duration = super().api_call( video, _, duration = super().api_call(
dual_character=True, dual_character=True,
@ -1027,7 +1078,7 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase):
duration=duration, duration=duration,
image_1=image_left, image_1=image_left,
image_2=image_right, image_2=image_right,
auth_token=auth_token, **kwargs,
) )
return video, duration return video, duration
@ -1063,7 +1114,10 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
enum_type=KlingVideoGenDuration, 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." DESCRIPTION = "Achieve different special effects when generating a video based on the effect_scene."
@ -1074,7 +1128,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
effect_scene: KlingSingleImageEffectsScene, effect_scene: KlingSingleImageEffectsScene,
model_name: KlingSingleImageEffectModelName, model_name: KlingSingleImageEffectModelName,
duration: KlingVideoGenDuration, duration: KlingVideoGenDuration,
auth_token: Optional[str] = None, **kwargs,
): ):
return super().api_call( return super().api_call(
dual_character=False, dual_character=False,
@ -1082,7 +1136,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
model_name=model_name, model_name=model_name,
duration=duration, duration=duration,
image_1=image, 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." 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.""" """Polls the Kling API endpoint until the task reaches a terminal state."""
return poll_until_finished( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_LIP_SYNC}/{task_id}", path=f"{PATH_LIP_SYNC}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -1121,18 +1175,18 @@ class KlingLipSyncBase(KlingNodeBase):
text: Optional[str] = None, text: Optional[str] = None,
voice_speed: Optional[float] = None, voice_speed: Optional[float] = None,
voice_id: Optional[str] = None, voice_id: Optional[str] = None,
auth_token: Optional[str] = None, **kwargs
) -> tuple[VideoFromFile, str, str]: ) -> tuple[VideoFromFile, str, str]:
if text: if text:
self.validate_text(text) self.validate_text(text)
# Upload video to Comfy API and get download URL # 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) logging.info("Uploaded video to Comfy API. URL: %s", video_url)
# Upload the audio file to Comfy API and get download URL # Upload the audio file to Comfy API and get download URL
if audio: 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) logging.info("Uploaded audio to Comfy API. URL: %s", audio_url)
else: else:
audio_url = None audio_url = None
@ -1156,14 +1210,14 @@ class KlingLipSyncBase(KlingNodeBase):
voice_id=voice_id, voice_id=voice_id,
), ),
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_video_result_response(final_response)
video = get_video_from_response(final_response) video = get_video_from_response(final_response)
@ -1186,7 +1240,10 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase):
enum_type=KlingLipSyncVoiceLanguage, 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." 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, video: VideoInput,
audio: AudioInput, audio: AudioInput,
voice_language: str, voice_language: str,
auth_token: Optional[str] = None, **kwargs,
): ):
return super().api_call( return super().api_call(
video=video, video=video,
audio=audio, audio=audio,
voice_language=voice_language, voice_language=voice_language,
mode="audio2video", mode="audio2video",
auth_token=auth_token, **kwargs,
) )
@ -1292,7 +1349,10 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase):
IO.FLOAT, KlingLipSyncInputObject, "voice_speed", slider=True 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." 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, text: str,
voice: str, voice: str,
voice_speed: float, voice_speed: float,
auth_token: Optional[str] = None, **kwargs,
): ):
voice_id, voice_language = KlingLipSyncTextToVideoNode.get_voice_config()[voice] voice_id, voice_language = KlingLipSyncTextToVideoNode.get_voice_config()[voice]
return super().api_call( return super().api_call(
@ -1313,7 +1373,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase):
voice_id=voice_id, voice_id=voice_id,
voice_speed=voice_speed, voice_speed=voice_speed,
mode="text2video", mode="text2video",
auth_token=auth_token, **kwargs,
) )
@ -1350,16 +1410,19 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
enum_type=KlingVirtualTryOnModelName, 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." 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( def get_response(
self, task_id: str, auth_token: Optional[str] = None self, task_id: str, auth_kwargs: dict[str,str] = None
) -> KlingVirtualTryOnResponse: ) -> KlingVirtualTryOnResponse:
return poll_until_finished( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_VIRTUAL_TRY_ON}/{task_id}", path=f"{PATH_VIRTUAL_TRY_ON}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -1373,7 +1436,7 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
human_image: torch.Tensor, human_image: torch.Tensor,
cloth_image: torch.Tensor, cloth_image: torch.Tensor,
model_name: KlingVirtualTryOnModelName, model_name: KlingVirtualTryOnModelName,
auth_token: Optional[str] = None, **kwargs,
): ):
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
endpoint=ApiEndpoint( endpoint=ApiEndpoint(
@ -1387,14 +1450,14 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
cloth_image=tensor_to_base64_string(cloth_image), cloth_image=tensor_to_base64_string(cloth_image),
model_name=model_name, model_name=model_name,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_image_result_response(final_response)
images = get_images_from_response(final_response) images = get_images_from_response(final_response)
@ -1462,16 +1525,19 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
"optional": { "optional": {
"image": (IO.IMAGE, {}), "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." DESCRIPTION = "Kling Image Generation Node. Generate an image from a text prompt with an optional reference image."
def get_response( def get_response(
self, task_id: str, auth_token: Optional[str] = None self, task_id: str, auth_kwargs: Optional[dict[str,str]] = None
) -> KlingImageGenerationsResponse: ) -> KlingImageGenerationsResponse:
return poll_until_finished( return poll_until_finished(
auth_token, auth_kwargs,
ApiEndpoint( ApiEndpoint(
path=f"{PATH_IMAGE_GENERATIONS}/{task_id}", path=f"{PATH_IMAGE_GENERATIONS}/{task_id}",
method=HttpMethod.GET, method=HttpMethod.GET,
@ -1491,7 +1557,7 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
n: int, n: int,
aspect_ratio: KlingImageGenAspectRatio, aspect_ratio: KlingImageGenAspectRatio,
image: Optional[torch.Tensor] = None, image: Optional[torch.Tensor] = None,
auth_token: Optional[str] = None, **kwargs,
): ):
self.validate_prompt(prompt, negative_prompt) self.validate_prompt(prompt, negative_prompt)
@ -1516,14 +1582,14 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
n=n, n=n,
aspect_ratio=aspect_ratio, aspect_ratio=aspect_ratio,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_creation_response = initial_operation.execute() task_creation_response = initial_operation.execute()
validate_task_creation_response(task_creation_response) validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id 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) validate_image_result_response(final_response)
images = get_images_from_response(final_response) images = get_images_from_response(final_response)

View File

@ -1,4 +1,6 @@
from __future__ import annotations
from inspect import cleandoc from inspect import cleandoc
from typing import Optional
from comfy.comfy_types.node_typing import IO, ComfyNodeABC from comfy.comfy_types.node_typing import IO, ComfyNodeABC
from comfy_api.input_impl.video_types import VideoFromFile from comfy_api.input_impl.video_types import VideoFromFile
from comfy_api_nodes.apis.luma_api import ( from comfy_api_nodes.apis.luma_api import (
@ -201,6 +203,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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, image_luma_ref: LumaReferenceChain = None,
style_image: torch.Tensor = None, style_image: torch.Tensor = None,
character_image: torch.Tensor = None, character_image: torch.Tensor = None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=True, min_length=3) validate_string(prompt, strip_whitespace=True, min_length=3)
@ -222,19 +224,19 @@ class LumaImageGenerationNode(ComfyNodeABC):
api_image_ref = None api_image_ref = None
if image_luma_ref is not None: if image_luma_ref is not None:
api_image_ref = self._convert_luma_refs( 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 # handle style_luma_ref
api_style_ref = None api_style_ref = None
if style_image is not None: if style_image is not None:
api_style_ref = self._convert_style_image( 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 # handle character_ref images
character_ref = None character_ref = None
if character_image is not None: if character_image is not None:
download_urls = upload_images_to_comfyapi( 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( character_ref = LumaCharacterRef(
identity0=LumaImageIdentity(images=download_urls) identity0=LumaImageIdentity(images=download_urls)
@ -255,7 +257,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
style_ref=api_style_ref, style_ref=api_style_ref,
character_ref=character_ref, character_ref=character_ref,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api: LumaGeneration = operation.execute() response_api: LumaGeneration = operation.execute()
@ -269,7 +271,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
completed_statuses=[LumaState.completed], completed_statuses=[LumaState.completed],
failed_statuses=[LumaState.failed], failed_statuses=[LumaState.failed],
status_extractor=lambda x: x.state, status_extractor=lambda x: x.state,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()
@ -278,13 +280,13 @@ class LumaImageGenerationNode(ComfyNodeABC):
return (img,) return (img,)
def _convert_luma_refs( 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 = [] luma_urls = []
ref_count = 0 ref_count = 0
for ref in luma_ref.refs: for ref in luma_ref.refs:
download_urls = upload_images_to_comfyapi( 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]) luma_urls.append(download_urls[0])
ref_count += 1 ref_count += 1
@ -293,12 +295,12 @@ class LumaImageGenerationNode(ComfyNodeABC):
return luma_ref.create_api_model(download_urls=luma_urls, max_refs=max_refs) return luma_ref.create_api_model(download_urls=luma_urls, max_refs=max_refs)
def _convert_style_image( 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( chain = LumaReferenceChain(
first_ref=LumaReference(image=style_image, weight=weight) 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): class LumaImageModifyNode(ComfyNodeABC):
@ -350,6 +352,7 @@ class LumaImageModifyNode(ComfyNodeABC):
"optional": {}, "optional": {},
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -360,12 +363,11 @@ class LumaImageModifyNode(ComfyNodeABC):
image: torch.Tensor, image: torch.Tensor,
image_weight: float, image_weight: float,
seed, seed,
auth_token=None,
**kwargs, **kwargs,
): ):
# first, upload image # first, upload image
download_urls = upload_images_to_comfyapi( 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] image_url = download_urls[0]
# next, make Luma call with download url provided # 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) 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() response_api: LumaGeneration = operation.execute()
@ -397,7 +399,7 @@ class LumaImageModifyNode(ComfyNodeABC):
completed_statuses=[LumaState.completed], completed_statuses=[LumaState.completed],
failed_statuses=[LumaState.failed], failed_statuses=[LumaState.failed],
status_extractor=lambda x: x.state, status_extractor=lambda x: x.state,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()
@ -470,6 +472,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -483,7 +486,6 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
loop: bool, loop: bool,
seed, seed,
luma_concepts: LumaConceptChain = None, luma_concepts: LumaConceptChain = None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False, min_length=3) validate_string(prompt, strip_whitespace=False, min_length=3)
@ -506,7 +508,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
loop=loop, loop=loop,
concepts=luma_concepts.create_api_model() if luma_concepts else None, concepts=luma_concepts.create_api_model() if luma_concepts else None,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api: LumaGeneration = operation.execute() response_api: LumaGeneration = operation.execute()
@ -520,7 +522,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
completed_statuses=[LumaState.completed], completed_statuses=[LumaState.completed],
failed_statuses=[LumaState.failed], failed_statuses=[LumaState.failed],
status_extractor=lambda x: x.state, status_extractor=lambda x: x.state,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()
@ -594,6 +596,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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, first_image: torch.Tensor = None,
last_image: torch.Tensor = None, last_image: torch.Tensor = None,
luma_concepts: LumaConceptChain = None, luma_concepts: LumaConceptChain = None,
auth_token=None,
**kwargs, **kwargs,
): ):
if first_image is None and last_image is None: if first_image is None and last_image is None:
raise Exception( raise Exception(
"At least one of first_image and last_image requires an input." "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 duration = duration if model != LumaVideoModel.ray_1_6 else None
resolution = resolution 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, keyframes=keyframes,
concepts=luma_concepts.create_api_model() if luma_concepts else None, concepts=luma_concepts.create_api_model() if luma_concepts else None,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api: LumaGeneration = operation.execute() response_api: LumaGeneration = operation.execute()
@ -650,7 +652,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
completed_statuses=[LumaState.completed], completed_statuses=[LumaState.completed],
failed_statuses=[LumaState.failed], failed_statuses=[LumaState.failed],
status_extractor=lambda x: x.state, status_extractor=lambda x: x.state,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()
@ -661,7 +663,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
self, self,
first_image: torch.Tensor = None, first_image: torch.Tensor = None,
last_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: if first_image is None and last_image is None:
return None return None
@ -669,12 +671,12 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
frame1 = None frame1 = None
if first_image is not None: if first_image is not None:
download_urls = upload_images_to_comfyapi( 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]) frame0 = LumaImageReference(type="image", url=download_urls[0])
if last_image is not None: if last_image is not None:
download_urls = upload_images_to_comfyapi( 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]) frame1 = LumaImageReference(type="image", url=download_urls[0])
return LumaKeyframes(frame0=frame0, frame1=frame1) return LumaKeyframes(frame0=frame0, frame1=frame1)

View File

@ -67,6 +67,7 @@ class MinimaxTextToVideoNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -84,7 +85,7 @@ class MinimaxTextToVideoNode:
model="T2V-01", model="T2V-01",
image: torch.Tensor=None, # used for ImageToVideo image: torch.Tensor=None, # used for ImageToVideo
subject: torch.Tensor=None, # used for SubjectToVideo 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. 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 # upload image, if passed in
image_url = None image_url = None
if image is not 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 # TODO: figure out how to deal with subject properly, API returns invalid params when using S2V-01 model
subject_reference = None subject_reference = None
if subject is not 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)] subject_reference = [SubjectReferenceItem(image=subject_url)]
@ -118,7 +119,7 @@ class MinimaxTextToVideoNode:
subject_reference=subject_reference, subject_reference=subject_reference,
prompt_optimizer=None, prompt_optimizer=None,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response = video_generate_operation.execute() response = video_generate_operation.execute()
@ -137,7 +138,7 @@ class MinimaxTextToVideoNode:
completed_statuses=["Success"], completed_statuses=["Success"],
failed_statuses=["Fail"], failed_statuses=["Fail"],
status_extractor=lambda x: x.status.value, status_extractor=lambda x: x.status.value,
auth_token=auth_token, auth_kwargs=kwargs,
) )
task_result = video_generate_operation.execute() task_result = video_generate_operation.execute()
@ -153,7 +154,7 @@ class MinimaxTextToVideoNode:
query_params={"file_id": int(file_id)}, query_params={"file_id": int(file_id)},
), ),
request=EmptyRequest(), request=EmptyRequest(),
auth_token=auth_token, auth_kwargs=kwargs,
) )
file_result = file_retrieve_operation.execute() file_result = file_retrieve_operation.execute()
@ -221,6 +222,7 @@ class MinimaxImageToVideoNode(MinimaxTextToVideoNode):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -279,6 +281,7 @@ class MinimaxSubjectToVideoNode(MinimaxTextToVideoNode):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }

View File

@ -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,) RETURN_TYPES = (IO.IMAGE,)
@ -110,7 +113,7 @@ class OpenAIDalle2(ComfyNodeABC):
mask=None, mask=None,
n=1, n=1,
size="1024x1024", size="1024x1024",
auth_token=None, **kwargs
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
model = "dall-e-2" model = "dall-e-2"
@ -168,7 +171,7 @@ class OpenAIDalle2(ComfyNodeABC):
else None else None
), ),
content_type=content_type, content_type=content_type,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response = operation.execute() 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,) RETURN_TYPES = (IO.IMAGE,)
@ -252,7 +258,7 @@ class OpenAIDalle3(ComfyNodeABC):
style="natural", style="natural",
quality="standard", quality="standard",
size="1024x1024", size="1024x1024",
auth_token=None, **kwargs
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
model = "dall-e-3" model = "dall-e-3"
@ -273,7 +279,7 @@ class OpenAIDalle3(ComfyNodeABC):
style=style, style=style,
seed=seed, seed=seed,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response = operation.execute() 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,) RETURN_TYPES = (IO.IMAGE,)
@ -385,7 +394,7 @@ class OpenAIGPTImage1(ComfyNodeABC):
mask=None, mask=None,
n=1, n=1,
size="1024x1024", size="1024x1024",
auth_token=None, **kwargs
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
model = "gpt-image-1" model = "gpt-image-1"
@ -462,7 +471,7 @@ class OpenAIGPTImage1(ComfyNodeABC):
), ),
files=files if files else None, files=files if files else None,
content_type=content_type, content_type=content_type,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response = operation.execute() response = operation.execute()

View File

@ -3,6 +3,7 @@ Pika x ComfyUI API Nodes
Pika API docs: https://pika-827374fb.mintlify.app/api-reference Pika API docs: https://pika-827374fb.mintlify.app/api-reference
""" """
from __future__ import annotations
import io import io
from typing import Optional, TypeVar from typing import Optional, TypeVar
@ -120,7 +121,7 @@ class PikaNodeBase(ComfyNodeABC):
RETURN_TYPES = ("VIDEO",) RETURN_TYPES = ("VIDEO",)
def poll_for_task_status( def poll_for_task_status(
self, task_id: str, auth_token: str self, task_id: str, auth_kwargs: Optional[dict[str,str]] = None
) -> PikaGenerateResponse: ) -> PikaGenerateResponse:
polling_operation = PollingOperation( polling_operation = PollingOperation(
poll_endpoint=ApiEndpoint( poll_endpoint=ApiEndpoint(
@ -139,20 +140,20 @@ class PikaNodeBase(ComfyNodeABC):
progress_extractor=lambda response: ( progress_extractor=lambda response: (
response.progress if hasattr(response, "progress") else None response.progress if hasattr(response, "progress") else None
), ),
auth_token=auth_token, auth_kwargs=auth_kwargs,
) )
return polling_operation.execute() return polling_operation.execute()
def execute_task( def execute_task(
self, self,
initial_operation: SynchronousOperation[R, PikaGenerateResponse], initial_operation: SynchronousOperation[R, PikaGenerateResponse],
auth_token: Optional[str] = None, auth_kwargs: Optional[dict[str,str]] = None,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
"""Executes the initial operation then polls for the task status until it is completed. """Executes the initial operation then polls for the task status until it is completed.
Args: Args:
initial_operation: The initial operation to execute. 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: Returns:
A tuple containing the video file as a VIDEO output. A tuple containing the video file as a VIDEO output.
@ -164,7 +165,7 @@ class PikaNodeBase(ComfyNodeABC):
raise PikaApiError(error_msg) raise PikaApiError(error_msg)
task_id = initial_response.video_id 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): if not is_valid_video_response(final_response):
error_msg = ( error_msg = (
f"Pika task {task_id} succeeded but no video data found in response." f"Pika task {task_id} succeeded but no video data found in response."
@ -193,6 +194,7 @@ class PikaImageToVideoV2_2(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -206,7 +208,7 @@ class PikaImageToVideoV2_2(PikaNodeBase):
seed: int, seed: int,
resolution: str, resolution: str,
duration: int, duration: int,
auth_token: Optional[str] = None, **kwargs
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
# Convert image to BytesIO # Convert image to BytesIO
image_bytes_io = tensor_to_bytesio(image) image_bytes_io = tensor_to_bytesio(image)
@ -233,10 +235,10 @@ class PikaImageToVideoV2_2(PikaNodeBase):
request=pika_request_data, request=pika_request_data,
files=pika_files, files=pika_files,
content_type="multipart/form-data", 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): class PikaTextToVideoNodeV2_2(PikaNodeBase):
@ -259,6 +261,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -272,7 +275,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase):
resolution: str, resolution: str,
duration: int, duration: int,
aspect_ratio: float, aspect_ratio: float,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
endpoint=ApiEndpoint( endpoint=ApiEndpoint(
@ -289,11 +292,11 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase):
duration=duration, duration=duration,
aspectRatio=aspect_ratio, aspectRatio=aspect_ratio,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
content_type="application/x-www-form-urlencoded", 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): class PikaScenesV2_2(PikaNodeBase):
@ -336,6 +339,7 @@ class PikaScenesV2_2(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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_3: Optional[torch.Tensor] = None,
image_ingredient_4: Optional[torch.Tensor] = None, image_ingredient_4: Optional[torch.Tensor] = None,
image_ingredient_5: Optional[torch.Tensor] = None, image_ingredient_5: Optional[torch.Tensor] = None,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
# Convert all passed images to BytesIO # Convert all passed images to BytesIO
all_image_bytes_io = [] all_image_bytes_io = []
@ -396,10 +400,10 @@ class PikaScenesV2_2(PikaNodeBase):
request=pika_request_data, request=pika_request_data,
files=pika_files, files=pika_files,
content_type="multipart/form-data", 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): class PikAdditionsNode(PikaNodeBase):
@ -434,6 +438,7 @@ class PikAdditionsNode(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -446,7 +451,7 @@ class PikAdditionsNode(PikaNodeBase):
prompt_text: str, prompt_text: str,
negative_prompt: str, negative_prompt: str,
seed: int, seed: int,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
# Convert video to BytesIO # Convert video to BytesIO
video_bytes_io = io.BytesIO() video_bytes_io = io.BytesIO()
@ -479,10 +484,10 @@ class PikAdditionsNode(PikaNodeBase):
request=pika_request_data, request=pika_request_data,
files=pika_files, files=pika_files,
content_type="multipart/form-data", 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): class PikaSwapsNode(PikaNodeBase):
@ -526,6 +531,7 @@ class PikaSwapsNode(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -540,7 +546,7 @@ class PikaSwapsNode(PikaNodeBase):
prompt_text: str, prompt_text: str,
negative_prompt: str, negative_prompt: str,
seed: int, seed: int,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
# Convert video to BytesIO # Convert video to BytesIO
video_bytes_io = io.BytesIO() video_bytes_io = io.BytesIO()
@ -583,10 +589,10 @@ class PikaSwapsNode(PikaNodeBase):
request=pika_request_data, request=pika_request_data,
files=pika_files, files=pika_files,
content_type="multipart/form-data", 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): class PikaffectsNode(PikaNodeBase):
@ -630,6 +636,7 @@ class PikaffectsNode(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -642,7 +649,7 @@ class PikaffectsNode(PikaNodeBase):
prompt_text: str, prompt_text: str,
negative_prompt: str, negative_prompt: str,
seed: int, seed: int,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
@ -660,10 +667,10 @@ class PikaffectsNode(PikaNodeBase):
), ),
files={"image": ("image.png", tensor_to_bytesio(image), "image/png")}, files={"image": ("image.png", tensor_to_bytesio(image), "image/png")},
content_type="multipart/form-data", 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): class PikaStartEndFrameNode2_2(PikaNodeBase):
@ -681,6 +688,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -695,7 +703,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase):
seed: int, seed: int,
resolution: str, resolution: str,
duration: int, duration: int,
auth_token: Optional[str] = None, **kwargs,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
pika_files = [ pika_files = [
@ -722,10 +730,10 @@ class PikaStartEndFrameNode2_2(PikaNodeBase):
), ),
files=pika_files, files=pika_files,
content_type="multipart/form-data", 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 = { NODE_CLASS_MAPPINGS = {

View File

@ -34,7 +34,7 @@ import requests
from io import BytesIO 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 # first, upload image to Pixverse and get image id to use in actual generation call
files = { files = {
"image": tensor_to_bytesio(image) "image": tensor_to_bytesio(image)
@ -49,7 +49,7 @@ def upload_image_to_pixverse(image: torch.Tensor, auth_token=None):
request=EmptyRequest(), request=EmptyRequest(),
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=auth_kwargs,
) )
response_upload: PixverseImageUploadResponse = operation.execute() response_upload: PixverseImageUploadResponse = operation.execute()
@ -148,6 +148,7 @@ class PixverseTextToVideoNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -161,7 +162,6 @@ class PixverseTextToVideoNode(ComfyNodeABC):
seed, seed,
negative_prompt: str=None, negative_prompt: str=None,
pixverse_template: int=None, pixverse_template: int=None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
@ -190,7 +190,7 @@ class PixverseTextToVideoNode(ComfyNodeABC):
template_id=pixverse_template, template_id=pixverse_template,
seed=seed, seed=seed,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -207,7 +207,7 @@ class PixverseTextToVideoNode(ComfyNodeABC):
completed_statuses=[PixverseStatus.successful], completed_statuses=[PixverseStatus.successful],
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted], failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
status_extractor=lambda x: x.Resp.status, status_extractor=lambda x: x.Resp.status,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()
@ -278,6 +278,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -291,11 +292,10 @@ class PixverseImageToVideoNode(ComfyNodeABC):
seed, seed,
negative_prompt: str=None, negative_prompt: str=None,
pixverse_template: int=None, pixverse_template: int=None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False) 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 # 1080p is limited to 5 seconds duration
# only normal motion_mode supported for 1080p or for non-5 second 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, template_id=pixverse_template,
seed=seed, seed=seed,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -339,7 +339,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
completed_statuses=[PixverseStatus.successful], completed_statuses=[PixverseStatus.successful],
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted], failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
status_extractor=lambda x: x.Resp.status, status_extractor=lambda x: x.Resp.status,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()
@ -407,6 +407,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -420,12 +421,11 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
motion_mode: str, motion_mode: str,
seed, seed,
negative_prompt: str=None, negative_prompt: str=None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
first_frame_id = upload_image_to_pixverse(first_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_token=auth_token) last_frame_id = upload_image_to_pixverse(last_frame, auth_kwargs=kwargs)
# 1080p is limited to 5 seconds duration # 1080p is limited to 5 seconds duration
# only normal motion_mode supported for 1080p or for non-5 second 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, negative_prompt=negative_prompt if negative_prompt else None,
seed=seed, seed=seed,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -469,7 +469,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
completed_statuses=[PixverseStatus.successful], completed_statuses=[PixverseStatus.successful],
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted], failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
status_extractor=lambda x: x.Resp.status, status_extractor=lambda x: x.Resp.status,
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll = operation.execute() response_poll = operation.execute()

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from inspect import cleandoc from inspect import cleandoc
from comfy.utils import ProgressBar from comfy.utils import ProgressBar
from comfy_extras.nodes_images import SVG # Added
from comfy.comfy_types.node_typing import IO from comfy.comfy_types.node_typing import IO
from comfy_api_nodes.apis.recraft_api import ( from comfy_api_nodes.apis.recraft_api import (
RecraftImageGenerationRequest, RecraftImageGenerationRequest,
@ -28,9 +29,6 @@ from comfy_api_nodes.apinode_utils import (
resize_mask_to_image, resize_mask_to_image,
validate_string, validate_string,
) )
import folder_paths
import json
import os
import torch import torch
from io import BytesIO from io import BytesIO
from PIL import UnidentifiedImageError from PIL import UnidentifiedImageError
@ -43,7 +41,7 @@ def handle_recraft_file_request(
total_pixels=4096*4096, total_pixels=4096*4096,
timeout=1024, timeout=1024,
request=None, request=None,
auth_token=None auth_kwargs: dict[str,str] = None,
) -> list[BytesIO]: ) -> list[BytesIO]:
""" """
Handle sending common Recraft file-only request to get back file bytes. Handle sending common Recraft file-only request to get back file bytes.
@ -67,7 +65,7 @@ def handle_recraft_file_request(
request=request, request=request,
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=auth_kwargs,
multipart_parser=recraft_multipart_parser, multipart_parser=recraft_multipart_parser,
) )
response: RecraftImageGenerationResponse = operation.execute() 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.") 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""" <metadata>
<![CDATA[
{metadata_json}
]]>
</metadata>
"""
# Insert metadata after opening svg tag using regex
import re
svg_content = re.sub(r'(<svg[^>]*>)', 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: class RecraftColorRGBNode:
""" """
Create Recraft Color by choosing specific RGB values. Create Recraft Color by choosing specific RGB values.
@ -485,6 +387,7 @@ class RecraftTextToImageNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -497,7 +400,6 @@ class RecraftTextToImageNode:
recraft_style: RecraftStyle = None, recraft_style: RecraftStyle = None,
negative_prompt: str = None, negative_prompt: str = None,
recraft_controls: RecraftControls = None, recraft_controls: RecraftControls = None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False, max_length=1000) validate_string(prompt, strip_whitespace=False, max_length=1000)
@ -530,7 +432,7 @@ class RecraftTextToImageNode:
style_id=recraft_style.style_id, style_id=recraft_style.style_id,
controls=controls_api, controls=controls_api,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response: RecraftImageGenerationResponse = operation.execute() response: RecraftImageGenerationResponse = operation.execute()
images = [] images = []
@ -620,6 +522,7 @@ class RecraftImageToImageNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -630,7 +533,6 @@ class RecraftImageToImageNode:
n: int, n: int,
strength: float, strength: float,
seed, seed,
auth_token=None,
recraft_style: RecraftStyle = None, recraft_style: RecraftStyle = None,
negative_prompt: str = None, negative_prompt: str = None,
recraft_controls: RecraftControls = None, recraft_controls: RecraftControls = None,
@ -668,7 +570,7 @@ class RecraftImageToImageNode:
image=image[i], image=image[i],
path="/proxy/recraft/images/imageToImage", path="/proxy/recraft/images/imageToImage",
request=request, request=request,
auth_token=auth_token, auth_kwargs=kwargs,
) )
with handle_recraft_image_output(): with handle_recraft_image_output():
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
@ -736,6 +638,7 @@ class RecraftImageInpaintingNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -746,7 +649,6 @@ class RecraftImageInpaintingNode:
prompt: str, prompt: str,
n: int, n: int,
seed, seed,
auth_token=None,
recraft_style: RecraftStyle = None, recraft_style: RecraftStyle = None,
negative_prompt: str = None, negative_prompt: str = None,
**kwargs, **kwargs,
@ -781,7 +683,7 @@ class RecraftImageInpaintingNode:
mask=mask[i:i+1], mask=mask[i:i+1],
path="/proxy/recraft/images/inpaint", path="/proxy/recraft/images/inpaint",
request=request, request=request,
auth_token=auth_token, auth_kwargs=kwargs,
) )
with handle_recraft_image_output(): with handle_recraft_image_output():
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0)) 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. Generates SVG synchronously based on prompt and resolution.
""" """
RETURN_TYPES = (RecraftIO.SVG,) RETURN_TYPES = ("SVG",) # Changed
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value DESCRIPTION = cleandoc(__doc__ or "") if 'cleandoc' in globals() else __doc__ # Keep cleandoc if other nodes use it
FUNCTION = "api_call" FUNCTION = "api_call"
API_NODE = True API_NODE = True
CATEGORY = "api node/image/Recraft" CATEGORY = "api node/image/Recraft"
@ -860,6 +762,7 @@ class RecraftTextToVectorNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -872,7 +775,6 @@ class RecraftTextToVectorNode:
seed, seed,
negative_prompt: str = None, negative_prompt: str = None,
recraft_controls: RecraftControls = None, recraft_controls: RecraftControls = None,
auth_token=None,
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False, max_length=1000) validate_string(prompt, strip_whitespace=False, max_length=1000)
@ -903,7 +805,7 @@ class RecraftTextToVectorNode:
substyle=recraft_style.substyle, substyle=recraft_style.substyle,
controls=controls_api, controls=controls_api,
), ),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response: RecraftImageGenerationResponse = operation.execute() response: RecraftImageGenerationResponse = operation.execute()
svg_data = [] svg_data = []
@ -918,8 +820,8 @@ class RecraftVectorizeImageNode:
Generates SVG synchronously from an input image. Generates SVG synchronously from an input image.
""" """
RETURN_TYPES = (RecraftIO.SVG,) RETURN_TYPES = ("SVG",) # Changed
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value DESCRIPTION = cleandoc(__doc__ or "") if 'cleandoc' in globals() else __doc__ # Keep cleandoc if other nodes use it
FUNCTION = "api_call" FUNCTION = "api_call"
API_NODE = True API_NODE = True
CATEGORY = "api node/image/Recraft" CATEGORY = "api node/image/Recraft"
@ -934,13 +836,13 @@ class RecraftVectorizeImageNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
def api_call( def api_call(
self, self,
image: torch.Tensor, image: torch.Tensor,
auth_token=None,
**kwargs, **kwargs,
): ):
svgs = [] svgs = []
@ -950,7 +852,7 @@ class RecraftVectorizeImageNode:
sub_bytes = handle_recraft_file_request( sub_bytes = handle_recraft_file_request(
image=image[i], image=image[i],
path="/proxy/recraft/images/vectorize", path="/proxy/recraft/images/vectorize",
auth_token=auth_token, auth_kwargs=kwargs,
) )
svgs.append(SVG(sub_bytes)) svgs.append(SVG(sub_bytes))
pbar.update(1) pbar.update(1)
@ -1015,6 +917,7 @@ class RecraftReplaceBackgroundNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -1024,7 +927,6 @@ class RecraftReplaceBackgroundNode:
prompt: str, prompt: str,
n: int, n: int,
seed, seed,
auth_token=None,
recraft_style: RecraftStyle = None, recraft_style: RecraftStyle = None,
negative_prompt: str = None, negative_prompt: str = None,
**kwargs, **kwargs,
@ -1054,7 +956,7 @@ class RecraftReplaceBackgroundNode:
image=image[i], image=image[i],
path="/proxy/recraft/images/replaceBackground", path="/proxy/recraft/images/replaceBackground",
request=request, 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)) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1) pbar.update(1)
@ -1084,13 +986,13 @@ class RecraftRemoveBackgroundNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
def api_call( def api_call(
self, self,
image: torch.Tensor, image: torch.Tensor,
auth_token=None,
**kwargs, **kwargs,
): ):
images = [] images = []
@ -1100,7 +1002,7 @@ class RecraftRemoveBackgroundNode:
sub_bytes = handle_recraft_file_request( sub_bytes = handle_recraft_file_request(
image=image[i], image=image[i],
path="/proxy/recraft/images/removeBackground", 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)) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1) pbar.update(1)
@ -1135,13 +1037,13 @@ class RecraftCrispUpscaleNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
def api_call( def api_call(
self, self,
image: torch.Tensor, image: torch.Tensor,
auth_token=None,
**kwargs, **kwargs,
): ):
images = [] images = []
@ -1151,7 +1053,7 @@ class RecraftCrispUpscaleNode:
sub_bytes = handle_recraft_file_request( sub_bytes = handle_recraft_file_request(
image=image[i], image=image[i],
path=self.RECRAFT_PATH, 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)) images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1) pbar.update(1)
@ -1193,7 +1095,6 @@ NODE_CLASS_MAPPINGS = {
"RecraftStyleV3InfiniteStyleLibrary": RecraftStyleInfiniteStyleLibrary, "RecraftStyleV3InfiniteStyleLibrary": RecraftStyleInfiniteStyleLibrary,
"RecraftColorRGB": RecraftColorRGBNode, "RecraftColorRGB": RecraftColorRGBNode,
"RecraftControls": RecraftControlsNode, "RecraftControls": RecraftControlsNode,
"SaveSVG": SaveSVGNode,
} }
# A dictionary that contains the friendly/humanly readable titles for the nodes # 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", "RecraftStyleV3InfiniteStyleLibrary": "Recraft Style - Infinite Style Library",
"RecraftColorRGB": "Recraft Color RGB", "RecraftColorRGB": "Recraft Color RGB",
"RecraftControls": "Recraft Controls", "RecraftControls": "Recraft Controls",
"SaveSVG": "Save SVG",
} }

View File

@ -120,12 +120,13 @@ class StabilityStableImageUltraNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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, 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, negative_prompt: str=None, image: torch.Tensor = None, image_denoise: float=None,
auth_token=None): **kwargs):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
# prepare image binary if image present # prepare image binary if image present
image_binary = None image_binary = None
@ -160,7 +161,7 @@ class StabilityStableImageUltraNode:
), ),
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -252,12 +253,13 @@ class StabilityStableImageSD_3_5Node:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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, 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, negative_prompt: str=None, image: torch.Tensor = None, image_denoise: float=None,
auth_token=None): **kwargs):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
# prepare image binary if image present # prepare image binary if image present
image_binary = None image_binary = None
@ -298,7 +300,7 @@ class StabilityStableImageSD_3_5Node:
), ),
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -368,11 +370,12 @@ class StabilityUpscaleConservativeNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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, 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) validate_string(prompt, strip_whitespace=False)
image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read() image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read()
@ -398,7 +401,7 @@ class StabilityUpscaleConservativeNode:
), ),
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -473,11 +476,12 @@ class StabilityUpscaleCreativeNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "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, 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) validate_string(prompt, strip_whitespace=False)
image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read() image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read()
@ -506,7 +510,7 @@ class StabilityUpscaleCreativeNode:
), ),
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()
@ -521,7 +525,7 @@ class StabilityUpscaleCreativeNode:
completed_statuses=[StabilityPollStatus.finished], completed_statuses=[StabilityPollStatus.finished],
failed_statuses=[StabilityPollStatus.failed], failed_statuses=[StabilityPollStatus.failed],
status_extractor=lambda x: get_async_dummy_status(x), status_extractor=lambda x: get_async_dummy_status(x),
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_poll: StabilityResultsGetResponse = operation.execute() response_poll: StabilityResultsGetResponse = operation.execute()
@ -555,11 +559,12 @@ class StabilityUpscaleFastNode:
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
def api_call(self, image: torch.Tensor, def api_call(self, image: torch.Tensor,
auth_token=None): **kwargs):
image_binary = tensor_to_bytesio(image, total_pixels=4096*4096).read() image_binary = tensor_to_bytesio(image, total_pixels=4096*4096).read()
files = { files = {
@ -576,7 +581,7 @@ class StabilityUpscaleFastNode:
request=EmptyRequest(), request=EmptyRequest(),
files=files, files=files,
content_type="multipart/form-data", content_type="multipart/form-data",
auth_token=auth_token, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = operation.execute()

View File

@ -114,6 +114,7 @@ class VeoVideoGenerationNode(ComfyNodeABC):
}, },
"hidden": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG", "auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
}, },
} }
@ -133,7 +134,7 @@ class VeoVideoGenerationNode(ComfyNodeABC):
person_generation="ALLOW", person_generation="ALLOW",
seed=0, seed=0,
image=None, image=None,
auth_token=None, **kwargs,
): ):
# Prepare the instances for the request # Prepare the instances for the request
instances = [] instances = []
@ -179,7 +180,7 @@ class VeoVideoGenerationNode(ComfyNodeABC):
instances=instances, instances=instances,
parameters=parameters parameters=parameters
), ),
auth_token=auth_token auth_kwargs=kwargs,
) )
initial_response = initial_operation.execute() initial_response = initial_operation.execute()
@ -213,7 +214,7 @@ class VeoVideoGenerationNode(ComfyNodeABC):
request=Veo2GenVidPollRequest( request=Veo2GenVidPollRequest(
operationName=operation_name operationName=operation_name
), ),
auth_token=auth_token, auth_kwargs=kwargs,
poll_interval=5.0 poll_interval=5.0
) )

76
comfy_extras/nodes_apg.py Normal file
View File

@ -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",
}

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import av
import torchaudio import torchaudio
import torch import torch
import comfy.model_management import comfy.model_management
@ -7,7 +8,6 @@ import folder_paths
import os import os
import io import io
import json import json
import struct
import random import random
import hashlib import hashlib
import node_helpers import node_helpers
@ -90,60 +90,118 @@ class VAEDecodeAudio:
return ({"waveform": audio, "sample_rate": 44100}, ) return ({"waveform": audio, "sample_rate": 44100}, )
def create_vorbis_comment_block(comment_dict, last_block): def save_audio(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None, quality="128k"):
vendor_string = b'ComfyUI'
vendor_length = len(vendor_string)
comments = [] filename_prefix += self.prefix_append
for key, value in comment_dict.items(): full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
comment = f"{key}={value}".encode('utf-8') results: list[FileLocator] = []
comments.append(struct.pack('<I', len(comment)) + comment)
user_comment_list_length = len(comments) # Prepare metadata dictionary
user_comments = b''.join(comments) 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])
comment_data = struct.pack('<I', vendor_length) + vendor_string + struct.pack('<I', user_comment_list_length) + user_comments # Opus supported sample rates
if last_block: OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
id = b'\x84'
else:
id = b'\x04'
comment_block = id + struct.pack('>I', len(comment_data))[1:] + comment_data
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): # Use original sample rate initially
if len(comment_dict) == 0: sample_rate = audio["sample_rate"]
return flac_io
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 = [] # Resample if necessary
last_block = False if sample_rate != audio["sample_rate"]:
waveform = torchaudio.functional.resample(waveform, audio["sample_rate"], sample_rate)
while not last_block: # Create in-memory WAV buffer
header = flac_io.read(4) wav_buffer = io.BytesIO()
last_block = (header[0] & 0x80) != 0 torchaudio.save(wav_buffer, waveform, sample_rate, format="WAV")
block_type = header[0] & 0x7F wav_buffer.seek(0) # Rewind for reading
block_length = struct.unpack('>I', b'\x00' + header[1:])[0]
block_data = flac_io.read(block_length)
if block_type == 4 or block_type == 1: # Use PyAV to convert and add metadata
pass input_container = av.open(wav_buffer)
else:
header = bytes([(header[0] & (~0x80))]) + header[1:]
blocks.append(header + block_data)
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() # Set metadata on the container
new_flac_io.write(b'fLaC') for key, value in metadata.items():
for block in blocks: output_container.metadata[key] = value
new_flac_io.write(block)
new_flac_io.write(flac_io.read()) # Set up the output stream with appropriate properties
return new_flac_io 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: class SaveAudio:
def __init__(self): def __init__(self):
self.output_dir = folder_paths.get_output_directory() self.output_dir = folder_paths.get_output_directory()
@ -153,50 +211,70 @@ class SaveAudio:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
return {"required": { "audio": ("AUDIO", ), return {"required": { "audio": ("AUDIO", ),
"filename_prefix": ("STRING", {"default": "audio/ComfyUI"})}, "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
} }
RETURN_TYPES = () RETURN_TYPES = ()
FUNCTION = "save_audio" FUNCTION = "save_flac"
OUTPUT_NODE = True OUTPUT_NODE = True
CATEGORY = "audio" CATEGORY = "audio"
def save_audio(self, audio, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): def save_flac(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None):
filename_prefix += self.prefix_append return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo)
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
results: list[FileLocator] = []
metadata = {} class SaveAudioMP3:
if not args.disable_metadata: def __init__(self):
if prompt is not None: self.output_dir = folder_paths.get_output_directory()
metadata["prompt"] = json.dumps(prompt) self.type = "output"
if extra_pnginfo is not None: self.prefix_append = ""
for x in extra_pnginfo:
metadata[x] = json.dumps(extra_pnginfo[x])
for (batch_number, waveform) in enumerate(audio["waveform"].cpu()): @classmethod
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) def INPUT_TYPES(s):
file = f"{filename_with_batch_num}_{counter:05}_.flac" 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() RETURN_TYPES = ()
torchaudio.save(buff, waveform, audio["sample_rate"], format="FLAC") 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: CATEGORY = "audio"
f.write(buff.getbuffer())
results.append({ def save_mp3(self, audio, filename_prefix="ComfyUI", format="mp3", prompt=None, extra_pnginfo=None, quality="128k"):
"filename": file, return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo, quality)
"subfolder": subfolder,
"type": self.type
})
counter += 1
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): class PreviewAudio(SaveAudio):
def __init__(self): def __init__(self):
@ -248,7 +326,20 @@ NODE_CLASS_MAPPINGS = {
"VAEEncodeAudio": VAEEncodeAudio, "VAEEncodeAudio": VAEEncodeAudio,
"VAEDecodeAudio": VAEDecodeAudio, "VAEDecodeAudio": VAEDecodeAudio,
"SaveAudio": SaveAudio, "SaveAudio": SaveAudio,
"SaveAudioMP3": SaveAudioMP3,
"SaveAudioOpus": SaveAudioOpus,
"LoadAudio": LoadAudio, "LoadAudio": LoadAudio,
"PreviewAudio": PreviewAudio, "PreviewAudio": PreviewAudio,
"ConditioningStableAudio": ConditioningStableAudio, "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)",
}

View File

@ -77,7 +77,7 @@ class HunyuanImageToVideo:
"height": ("INT", {"default": 480, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), "height": ("INT", {"default": 480, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}),
"length": ("INT", {"default": 53, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}), "length": ("INT", {"default": 53, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), "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", ), "optional": {"start_image": ("IMAGE", ),
}} }}
@ -101,10 +101,12 @@ class HunyuanImageToVideo:
if guidance_type == "v1 (concat)": if guidance_type == "v1 (concat)":
cond = {"concat_latent_image": concat_latent_image, "concat_mask": mask} cond = {"concat_latent_image": concat_latent_image, "concat_mask": mask}
else: elif guidance_type == "v2 (replace)":
cond = {'guiding_frame_index': 0} cond = {'guiding_frame_index': 0}
latent[:, :, :concat_latent_image.shape[2]] = concat_latent_image latent[:, :, :concat_latent_image.shape[2]] = concat_latent_image
out_latent["noise_mask"] = mask out_latent["noise_mask"] = mask
elif guidance_type == "custom":
cond = {"ref_latent": concat_latent_image}
positive = node_helpers.conditioning_set_values(positive, cond) positive = node_helpers.conditioning_set_values(positive, cond)

View File

@ -10,6 +10,9 @@ from PIL.PngImagePlugin import PngInfo
import numpy as np import numpy as np
import json import json
import os import os
import re
from io import BytesIO
from inspect import cleandoc
from comfy.comfy_types import FileLocator from comfy.comfy_types import FileLocator
@ -190,10 +193,109 @@ class SaveAnimatedPNG:
return { "ui": { "images": results, "animated": (True,)} } 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""" <metadata>
<![CDATA[
{metadata_json}
]]>
</metadata>
"""
# Insert metadata after opening svg tag using regex with a replacement function
def replacement(match):
# match.group(1) contains the captured <svg> tag
return match.group(1) + '\n' + metadata_element
# Apply the substitution
svg_content = re.sub(r'(<svg[^>]*>)', 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 = { NODE_CLASS_MAPPINGS = {
"ImageCrop": ImageCrop, "ImageCrop": ImageCrop,
"RepeatImageBatch": RepeatImageBatch, "RepeatImageBatch": RepeatImageBatch,
"ImageFromBatch": ImageFromBatch, "ImageFromBatch": ImageFromBatch,
"SaveAnimatedWEBP": SaveAnimatedWEBP, "SaveAnimatedWEBP": SaveAnimatedWEBP,
"SaveAnimatedPNG": SaveAnimatedPNG, "SaveAnimatedPNG": SaveAnimatedPNG,
"SaveSVGNode": SaveSVGNode,
} }

View File

@ -2,6 +2,10 @@ import nodes
import folder_paths import folder_paths
import os import os
from comfy.comfy_types import IO
from comfy_api.input_impl import VideoFromFile
def normalize_path(path): def normalize_path(path):
return path.replace('\\', '/') return path.replace('\\', '/')
@ -21,8 +25,8 @@ class Load3D():
"height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}),
}} }}
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE", "LOAD3D_CAMERA") RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE", "LOAD3D_CAMERA", IO.VIDEO)
RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart", "camera_info") RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart", "camera_info", "recording_video")
FUNCTION = "process" FUNCTION = "process"
EXPERIMENTAL = True EXPERIMENTAL = True
@ -41,7 +45,14 @@ class Load3D():
normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path) normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path)
lineart_image, ignore_mask3 = load_image_node.load_image(image=lineart_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(): class Load3DAnimation():
@classmethod @classmethod
@ -59,8 +70,8 @@ class Load3DAnimation():
"height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}),
}} }}
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "LOAD3D_CAMERA") RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "LOAD3D_CAMERA", IO.VIDEO)
RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "camera_info") RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "camera_info", "recording_video")
FUNCTION = "process" FUNCTION = "process"
EXPERIMENTAL = True EXPERIMENTAL = True
@ -77,7 +88,14 @@ class Load3DAnimation():
ignore_image, output_mask = load_image_node.load_image(image=mask_path) ignore_image, output_mask = load_image_node.load_image(image=mask_path)
normal_image, ignore_mask2 = load_image_node.load_image(image=normal_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(): class Preview3D():
@classmethod @classmethod

View File

@ -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"
}

View File

@ -1,3 +1,3 @@
# This file is automatically generated by the build process when version is # This file is automatically generated by the build process when version is
# updated in pyproject.toml. # updated in pyproject.toml.
__version__ = "0.3.33" __version__ = "0.3.34"

View File

@ -146,6 +146,8 @@ def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, e
input_data_all[x] = [unique_id] input_data_all[x] = [unique_id]
if h[x] == "AUTH_TOKEN_COMFY_ORG": if h[x] == "AUTH_TOKEN_COMFY_ORG":
input_data_all[x] = [extra_data.get("auth_token_comfy_org", None)] 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 return input_data_all, missing_keys
map_node_over_list = None #Don't hook this please map_node_over_list = None #Don't hook this please

View File

@ -2261,8 +2261,10 @@ def init_builtin_extra_nodes():
"nodes_optimalsteps.py", "nodes_optimalsteps.py",
"nodes_hidream.py", "nodes_hidream.py",
"nodes_fresca.py", "nodes_fresca.py",
"nodes_apg.py",
"nodes_preview_any.py", "nodes_preview_any.py",
"nodes_ace.py", "nodes_ace.py",
"nodes_string.py",
"nodes_camera_trajectory.py", "nodes_camera_trajectory.py",
] ]

View File

@ -1,6 +1,6 @@
[project] [project]
name = "ComfyUI" name = "ComfyUI"
version = "0.3.33" version = "0.3.34"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
requires-python = ">=3.9" requires-python = ">=3.9"

View File

@ -1,5 +1,5 @@
comfyui-frontend-package==1.18.9 comfyui-frontend-package==1.19.9
comfyui-workflow-templates==0.1.11 comfyui-workflow-templates==0.1.14
torch torch
torchsde torchsde
torchvision torchvision

View File

@ -101,6 +101,14 @@ prompt_text = """
def queue_prompt(prompt): def queue_prompt(prompt):
p = {"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') data = json.dumps(p).encode('utf-8')
req = request.Request("http://127.0.0.1:8188/prompt", data=data) req = request.Request("http://127.0.0.1:8188/prompt", data=data)
request.urlopen(req) request.urlopen(req)

View File

@ -32,12 +32,13 @@ from app.frontend_management import FrontendManager
from app.user_manager import UserManager from app.user_manager import UserManager
from app.model_manager import ModelFileManager from app.model_manager import ModelFileManager
from app.custom_node_manager import CustomNodeManager 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 from api_server.routes.internal.internal_routes import InternalRoutes
class BinaryEventTypes: class BinaryEventTypes:
PREVIEW_IMAGE = 1 PREVIEW_IMAGE = 1
UNENCODED_PREVIEW_IMAGE = 2 UNENCODED_PREVIEW_IMAGE = 2
TEXT = 3
async def send_socket_catch_exception(function, message): async def send_socket_catch_exception(function, message):
try: try:
@ -878,3 +879,15 @@ class PromptServer():
logging.warning(traceback.format_exc()) logging.warning(traceback.format_exc())
return json_data 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)