Compare commits

...

13 Commits

Author SHA1 Message Date
comfyanonymous
158419f3a0 ComfyUI version 0.3.34 2025-05-12 15:58:28 -04:00
comfyanonymous
640c47e7de Fix torch warning about deprecated function. (#8075)
Drop support for torch versions below 2.2 on the audio VAEs.
2025-05-12 14:32:01 -04:00
Christian Byrne
31e9e36c94 remove aspect ratio from kling request (#8062) 2025-05-12 13:32:24 -04:00
comfyanonymous
577de83ca9 ACE VAE works in fp16. (#8055) 2025-05-11 04:58:00 -04:00
Christian Byrne
3535909eb8 Add support for Comfy API keys (#8041)
* Handle Comfy API key based authorizaton (#167)

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>

* Bump frontend version to include API key features (#170)

* bump templates version

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2025-05-10 22:10:58 -04:00
Christian Byrne
235d3901fc Add method to stream text to node UI (#8018)
* show text progress preview

* include node id in message
2025-05-10 20:40:02 -04:00
comfyanonymous
d42613686f Fix issue with fp8 ops on some models. (#8045)
_scaled_mm errors when an input is non contiguous.
2025-05-10 07:52:56 -04:00
Pam
1b3bf0a5da Fix res_multistep_ancestral sampler (#8030) 2025-05-09 20:14:13 -04:00
Christian Byrne
ae60b150e5 update node tooltips and validation (#8036) 2025-05-09 20:02:45 -04:00
blepping
42da274717 Use normal ComfyUI attention in ACE-Steps model (#8023)
* Use normal ComfyUI attention in ACE-Steps model

* Let optimized_attention handle output reshape for ACE
2025-05-09 13:51:02 -04:00
thot experiment
28f178a840 move SVG to core (#7982)
* move SVG to core

* fix workflow embedding w/ unicode characters
2025-05-09 13:46:34 -04:00
comfyanonymous
8ab15c863c Add --mmap-torch-files to enable use of mmap when loading ckpt/pt (#8021) 2025-05-09 04:52:47 -04:00
comfyanonymous
924d771e18 Add ACE Step to README. (#8005) 2025-05-08 08:40:57 -04:00
29 changed files with 513 additions and 385 deletions

View File

@@ -69,9 +69,11 @@ See what ComfyUI can do with the [example workflows](https://comfyanonymous.gith
- [Hunyuan Video](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/) - [Hunyuan Video](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/)
- [Nvidia Cosmos](https://comfyanonymous.github.io/ComfyUI_examples/cosmos/) - [Nvidia Cosmos](https://comfyanonymous.github.io/ComfyUI_examples/cosmos/)
- [Wan 2.1](https://comfyanonymous.github.io/ComfyUI_examples/wan/) - [Wan 2.1](https://comfyanonymous.github.io/ComfyUI_examples/wan/)
- Audio Models
- [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- [ACE Step](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- 3D Models - 3D Models
- [Hunyuan3D 2.0](https://docs.comfy.org/tutorials/3d/hunyuan3D-2) - [Hunyuan3D 2.0](https://docs.comfy.org/tutorials/3d/hunyuan3D-2)
- [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- Asynchronous Queue system - Asynchronous Queue system
- Many optimizations: Only re-executes the parts of the workflow that changes between executions. - Many optimizations: Only re-executes the parts of the workflow that changes between executions.
- Smart memory management: can automatically run models on GPUs with as low as 1GB vram. - Smart memory management: can automatically run models on GPUs with as low as 1GB vram.

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

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

@@ -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
) )

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

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

@@ -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.18.10
comfyui-workflow-templates==0.1.11 comfyui-workflow-templates==0.1.14
torch torch
torchsde torchsde
torchvision torchvision

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)