mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-31 05:53:42 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
caf67e1f03
@ -46,6 +46,8 @@ class NodeReplaceManager:
|
||||
connections: dict[str, list[tuple[str, str, int]]] = {}
|
||||
need_replacement: set[str] = set()
|
||||
for node_number, node_struct in prompt.items():
|
||||
if "class_type" not in node_struct or "inputs" not in node_struct:
|
||||
continue
|
||||
class_type = node_struct["class_type"]
|
||||
# need replacement if not in NODE_CLASS_MAPPINGS and has replacement
|
||||
if class_type not in nodes.NODE_CLASS_MAPPINGS.keys() and self.has_replacement(class_type):
|
||||
|
||||
@ -157,11 +157,9 @@ class Embeddings1DConnector(nn.Module):
|
||||
self.num_learnable_registers = num_learnable_registers
|
||||
if self.num_learnable_registers:
|
||||
self.learnable_registers = nn.Parameter(
|
||||
torch.rand(
|
||||
torch.empty(
|
||||
self.num_learnable_registers, inner_dim, dtype=dtype, device=device
|
||||
)
|
||||
* 2.0
|
||||
- 1.0
|
||||
)
|
||||
|
||||
def get_fractional_positions(self, indices_grid):
|
||||
|
||||
@ -271,6 +271,7 @@ class ModelPatcher:
|
||||
self.is_clip = False
|
||||
self.hook_mode = comfy.hooks.EnumHookMode.MaxSpeed
|
||||
|
||||
self.cached_patcher_init: tuple[Callable, tuple] | None = None
|
||||
if not hasattr(self.model, 'model_loaded_weight_memory'):
|
||||
self.model.model_loaded_weight_memory = 0
|
||||
|
||||
@ -307,8 +308,15 @@ class ModelPatcher:
|
||||
def get_free_memory(self, device):
|
||||
return comfy.model_management.get_free_memory(device)
|
||||
|
||||
def clone(self):
|
||||
n = self.__class__(self.model, self.load_device, self.offload_device, self.model_size(), weight_inplace_update=self.weight_inplace_update)
|
||||
def clone(self, disable_dynamic=False):
|
||||
class_ = self.__class__
|
||||
model = self.model
|
||||
if self.is_dynamic() and disable_dynamic:
|
||||
class_ = ModelPatcher
|
||||
temp_model_patcher = self.cached_patcher_init[0](*self.cached_patcher_init[1], disable_dynamic=True)
|
||||
model = temp_model_patcher.model
|
||||
|
||||
n = class_(model, self.load_device, self.offload_device, self.model_size(), weight_inplace_update=self.weight_inplace_update)
|
||||
n.patches = {}
|
||||
for k in self.patches:
|
||||
n.patches[k] = self.patches[k][:]
|
||||
@ -362,6 +370,8 @@ class ModelPatcher:
|
||||
n.is_clip = self.is_clip
|
||||
n.hook_mode = self.hook_mode
|
||||
|
||||
n.cached_patcher_init = self.cached_patcher_init
|
||||
|
||||
for callback in self.get_all_callbacks(CallbacksMP.ON_CLONE):
|
||||
callback(self, n)
|
||||
return n
|
||||
|
||||
@ -827,6 +827,10 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec
|
||||
else:
|
||||
sd = {}
|
||||
|
||||
if not hasattr(self, 'weight'):
|
||||
logging.warning("Warning: state dict on uninitialized op {}".format(prefix))
|
||||
return sd
|
||||
|
||||
if self.bias is not None:
|
||||
sd["{}bias".format(prefix)] = self.bias
|
||||
|
||||
|
||||
29
comfy/sd.py
29
comfy/sd.py
@ -1530,14 +1530,24 @@ def load_checkpoint(config_path=None, ckpt_path=None, output_vae=True, output_cl
|
||||
|
||||
return (model, clip, vae)
|
||||
|
||||
def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=False, embedding_directory=None, output_model=True, model_options={}, te_model_options={}):
|
||||
def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=False, embedding_directory=None, output_model=True, model_options={}, te_model_options={}, disable_dynamic=False):
|
||||
sd, metadata = comfy.utils.load_torch_file(ckpt_path, return_metadata=True)
|
||||
out = load_state_dict_guess_config(sd, output_vae, output_clip, output_clipvision, embedding_directory, output_model, model_options, te_model_options=te_model_options, metadata=metadata)
|
||||
out = load_state_dict_guess_config(sd, output_vae, output_clip, output_clipvision, embedding_directory, output_model, model_options, te_model_options=te_model_options, metadata=metadata, disable_dynamic=disable_dynamic)
|
||||
if out is None:
|
||||
raise RuntimeError("ERROR: Could not detect model type of: {}\n{}".format(ckpt_path, model_detection_error_hint(ckpt_path, sd)))
|
||||
if output_model:
|
||||
out[0].cached_patcher_init = (load_checkpoint_guess_config_model_only, (ckpt_path, embedding_directory, model_options, te_model_options))
|
||||
return out
|
||||
|
||||
def load_state_dict_guess_config(sd, output_vae=True, output_clip=True, output_clipvision=False, embedding_directory=None, output_model=True, model_options={}, te_model_options={}, metadata=None):
|
||||
def load_checkpoint_guess_config_model_only(ckpt_path, embedding_directory=None, model_options={}, te_model_options={}, disable_dynamic=False):
|
||||
model, *_ = load_checkpoint_guess_config(ckpt_path, False, False, False,
|
||||
embedding_directory=embedding_directory,
|
||||
model_options=model_options,
|
||||
te_model_options=te_model_options,
|
||||
disable_dynamic=disable_dynamic)
|
||||
return model
|
||||
|
||||
def load_state_dict_guess_config(sd, output_vae=True, output_clip=True, output_clipvision=False, embedding_directory=None, output_model=True, model_options={}, te_model_options={}, metadata=None, disable_dynamic=False):
|
||||
clip = None
|
||||
clipvision = None
|
||||
vae = None
|
||||
@ -1586,7 +1596,8 @@ def load_state_dict_guess_config(sd, output_vae=True, output_clip=True, output_c
|
||||
if output_model:
|
||||
inital_load_device = model_management.unet_inital_load_device(parameters, unet_dtype)
|
||||
model = model_config.get_model(sd, diffusion_model_prefix, device=inital_load_device)
|
||||
model_patcher = comfy.model_patcher.CoreModelPatcher(model, load_device=load_device, offload_device=model_management.unet_offload_device())
|
||||
ModelPatcher = comfy.model_patcher.ModelPatcher if disable_dynamic else comfy.model_patcher.CoreModelPatcher
|
||||
model_patcher = ModelPatcher(model, load_device=load_device, offload_device=model_management.unet_offload_device())
|
||||
model.load_model_weights(sd, diffusion_model_prefix, assign=model_patcher.is_dynamic())
|
||||
|
||||
if output_vae:
|
||||
@ -1637,7 +1648,7 @@ def load_state_dict_guess_config(sd, output_vae=True, output_clip=True, output_c
|
||||
return (model_patcher, clip, vae, clipvision)
|
||||
|
||||
|
||||
def load_diffusion_model_state_dict(sd, model_options={}, metadata=None):
|
||||
def load_diffusion_model_state_dict(sd, model_options={}, metadata=None, disable_dynamic=False):
|
||||
"""
|
||||
Loads a UNet diffusion model from a state dictionary, supporting both diffusers and regular formats.
|
||||
|
||||
@ -1721,7 +1732,8 @@ def load_diffusion_model_state_dict(sd, model_options={}, metadata=None):
|
||||
model_config.optimizations["fp8"] = True
|
||||
|
||||
model = model_config.get_model(new_sd, "")
|
||||
model_patcher = comfy.model_patcher.CoreModelPatcher(model, load_device=load_device, offload_device=offload_device)
|
||||
ModelPatcher = comfy.model_patcher.ModelPatcher if disable_dynamic else comfy.model_patcher.CoreModelPatcher
|
||||
model_patcher = ModelPatcher(model, load_device=load_device, offload_device=offload_device)
|
||||
if not model_management.is_device_cpu(offload_device):
|
||||
model.to(offload_device)
|
||||
model.load_model_weights(new_sd, "", assign=model_patcher.is_dynamic())
|
||||
@ -1730,12 +1742,13 @@ def load_diffusion_model_state_dict(sd, model_options={}, metadata=None):
|
||||
logging.info("left over keys in diffusion model: {}".format(left_over))
|
||||
return model_patcher
|
||||
|
||||
def load_diffusion_model(unet_path, model_options={}):
|
||||
def load_diffusion_model(unet_path, model_options={}, disable_dynamic=False):
|
||||
sd, metadata = comfy.utils.load_torch_file(unet_path, return_metadata=True)
|
||||
model = load_diffusion_model_state_dict(sd, model_options=model_options, metadata=metadata)
|
||||
model = load_diffusion_model_state_dict(sd, model_options=model_options, metadata=metadata, disable_dynamic=disable_dynamic)
|
||||
if model is None:
|
||||
logging.error("ERROR UNSUPPORTED DIFFUSION MODEL {}".format(unet_path))
|
||||
raise RuntimeError("ERROR: Could not detect model type of: {}\n{}".format(unet_path, model_detection_error_hint(unet_path, sd)))
|
||||
model.cached_patcher_init = (load_diffusion_model, (unet_path, model_options))
|
||||
return model
|
||||
|
||||
def load_unet(unet_path, dtype=None):
|
||||
|
||||
@ -101,6 +101,7 @@ class LTXAVTEModel(torch.nn.Module):
|
||||
super().__init__()
|
||||
self.dtypes = set()
|
||||
self.dtypes.add(dtype)
|
||||
self.compat_mode = False
|
||||
|
||||
self.gemma3_12b = Gemma3_12BModel(device=device, dtype=dtype_llama, model_options=model_options, layer="all", layer_idx=None)
|
||||
self.dtypes.add(dtype_llama)
|
||||
@ -108,6 +109,28 @@ class LTXAVTEModel(torch.nn.Module):
|
||||
operations = self.gemma3_12b.operations # TODO
|
||||
self.text_embedding_projection = operations.Linear(3840 * 49, 3840, bias=False, dtype=dtype, device=device)
|
||||
|
||||
def enable_compat_mode(self): # TODO: remove
|
||||
from comfy.ldm.lightricks.embeddings_connector import Embeddings1DConnector
|
||||
operations = self.gemma3_12b.operations
|
||||
dtype = self.text_embedding_projection.weight.dtype
|
||||
device = self.text_embedding_projection.weight.device
|
||||
self.audio_embeddings_connector = Embeddings1DConnector(
|
||||
split_rope=True,
|
||||
double_precision_rope=True,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
operations=operations,
|
||||
)
|
||||
|
||||
self.video_embeddings_connector = Embeddings1DConnector(
|
||||
split_rope=True,
|
||||
double_precision_rope=True,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
operations=operations,
|
||||
)
|
||||
self.compat_mode = True
|
||||
|
||||
def set_clip_options(self, options):
|
||||
self.execution_device = options.get("execution_device", self.execution_device)
|
||||
self.gemma3_12b.set_clip_options(options)
|
||||
@ -129,6 +152,12 @@ class LTXAVTEModel(torch.nn.Module):
|
||||
out = out.reshape((out.shape[0], out.shape[1], -1))
|
||||
out = self.text_embedding_projection(out)
|
||||
out = out.float()
|
||||
|
||||
if self.compat_mode:
|
||||
out_vid = self.video_embeddings_connector(out)[0]
|
||||
out_audio = self.audio_embeddings_connector(out)[0]
|
||||
out = torch.concat((out_vid, out_audio), dim=-1)
|
||||
|
||||
return out.to(out_device), pooled
|
||||
|
||||
def generate(self, tokens, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed):
|
||||
@ -152,6 +181,16 @@ class LTXAVTEModel(torch.nn.Module):
|
||||
missing_all.extend([f"{prefix}{k}" for k in missing])
|
||||
unexpected_all.extend([f"{prefix}{k}" for k in unexpected])
|
||||
|
||||
if "model.diffusion_model.audio_embeddings_connector.transformer_1d_blocks.2.attn1.to_q.bias" not in sd: # TODO: remove
|
||||
ww = sd.get("model.diffusion_model.audio_embeddings_connector.transformer_1d_blocks.0.attn1.to_q.bias", None)
|
||||
if ww is not None:
|
||||
if ww.shape[0] == 3840:
|
||||
self.enable_compat_mode()
|
||||
sdv = comfy.utils.state_dict_prefix_replace(sd, {"model.diffusion_model.video_embeddings_connector.": ""}, filter_keys=True)
|
||||
self.video_embeddings_connector.load_state_dict(sdv, strict=False, assign=getattr(self, "can_assign_sd", False))
|
||||
sda = comfy.utils.state_dict_prefix_replace(sd, {"model.diffusion_model.audio_embeddings_connector.": ""}, filter_keys=True)
|
||||
self.audio_embeddings_connector.load_state_dict(sda, strict=False, assign=getattr(self, "can_assign_sd", False))
|
||||
|
||||
return (missing_all, unexpected_all)
|
||||
|
||||
def memory_estimation_function(self, token_weight_pairs, device=None):
|
||||
|
||||
@ -27,6 +27,7 @@ class Seedream4TaskCreationRequest(BaseModel):
|
||||
sequential_image_generation: str = Field("disabled")
|
||||
sequential_image_generation_options: Seedream4Options = Field(Seedream4Options(max_images=15))
|
||||
watermark: bool = Field(False)
|
||||
output_format: str | None = None
|
||||
|
||||
|
||||
class ImageTaskCreationResponse(BaseModel):
|
||||
@ -106,6 +107,7 @@ RECOMMENDED_PRESETS_SEEDREAM_4 = [
|
||||
("2496x1664 (3:2)", 2496, 1664),
|
||||
("1664x2496 (2:3)", 1664, 2496),
|
||||
("3024x1296 (21:9)", 3024, 1296),
|
||||
("3072x3072 (1:1)", 3072, 3072),
|
||||
("4096x4096 (1:1)", 4096, 4096),
|
||||
("Custom", None, None),
|
||||
]
|
||||
|
||||
@ -134,6 +134,13 @@ class ImageToVideoWithAudioRequest(BaseModel):
|
||||
shot_type: str | None = Field(None)
|
||||
|
||||
|
||||
class KlingAvatarRequest(BaseModel):
|
||||
image: str = Field(...)
|
||||
sound_file: str = Field(...)
|
||||
prompt: str | None = Field(None)
|
||||
mode: str = Field(...)
|
||||
|
||||
|
||||
class MotionControlRequest(BaseModel):
|
||||
prompt: str = Field(...)
|
||||
image_url: str = Field(...)
|
||||
|
||||
@ -37,6 +37,12 @@ from comfy_api_nodes.util import (
|
||||
|
||||
BYTEPLUS_IMAGE_ENDPOINT = "/proxy/byteplus/api/v3/images/generations"
|
||||
|
||||
SEEDREAM_MODELS = {
|
||||
"seedream 5.0 lite": "seedream-5-0-260128",
|
||||
"seedream-4-5-251128": "seedream-4-5-251128",
|
||||
"seedream-4-0-250828": "seedream-4-0-250828",
|
||||
}
|
||||
|
||||
# Long-running tasks endpoints(e.g., video)
|
||||
BYTEPLUS_TASK_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks"
|
||||
BYTEPLUS_TASK_STATUS_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks" # + /{task_id}
|
||||
@ -180,14 +186,13 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="ByteDanceSeedreamNode",
|
||||
display_name="ByteDance Seedream 4.5",
|
||||
display_name="ByteDance Seedream 5.0",
|
||||
category="api node/image/ByteDance",
|
||||
description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.",
|
||||
inputs=[
|
||||
IO.Combo.Input(
|
||||
"model",
|
||||
options=["seedream-4-5-251128", "seedream-4-0-250828"],
|
||||
tooltip="Model name",
|
||||
options=list(SEEDREAM_MODELS.keys()),
|
||||
),
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
@ -198,7 +203,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
IO.Image.Input(
|
||||
"image",
|
||||
tooltip="Input image(s) for image-to-image generation. "
|
||||
"List of 1-10 images for single or multi-reference generation.",
|
||||
"Reference image(s) for single or multi-reference generation.",
|
||||
optional=True,
|
||||
),
|
||||
IO.Combo.Input(
|
||||
@ -210,8 +215,8 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
"width",
|
||||
default=2048,
|
||||
min=1024,
|
||||
max=4096,
|
||||
step=8,
|
||||
max=6240,
|
||||
step=2,
|
||||
tooltip="Custom width for image. Value is working only if `size_preset` is set to `Custom`",
|
||||
optional=True,
|
||||
),
|
||||
@ -219,8 +224,8 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
"height",
|
||||
default=2048,
|
||||
min=1024,
|
||||
max=4096,
|
||||
step=8,
|
||||
max=4992,
|
||||
step=2,
|
||||
tooltip="Custom height for image. Value is working only if `size_preset` is set to `Custom`",
|
||||
optional=True,
|
||||
),
|
||||
@ -283,7 +288,8 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
depends_on=IO.PriceBadgeDepends(widgets=["model"]),
|
||||
expr="""
|
||||
(
|
||||
$price := $contains(widgets.model, "seedream-4-5-251128") ? 0.04 : 0.03;
|
||||
$price := $contains(widgets.model, "5.0 lite") ? 0.035 :
|
||||
$contains(widgets.model, "4-5") ? 0.04 : 0.03;
|
||||
{
|
||||
"type":"usd",
|
||||
"usd": $price,
|
||||
@ -309,6 +315,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
watermark: bool = False,
|
||||
fail_on_partial: bool = True,
|
||||
) -> IO.NodeOutput:
|
||||
model = SEEDREAM_MODELS[model]
|
||||
validate_string(prompt, strip_whitespace=True, min_length=1)
|
||||
w = h = None
|
||||
for label, tw, th in RECOMMENDED_PRESETS_SEEDREAM_4:
|
||||
@ -318,15 +325,12 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
|
||||
if w is None or h is None:
|
||||
w, h = width, height
|
||||
if not (1024 <= w <= 4096) or not (1024 <= h <= 4096):
|
||||
raise ValueError(
|
||||
f"Custom size out of range: {w}x{h}. " "Both width and height must be between 1024 and 4096 pixels."
|
||||
)
|
||||
|
||||
out_num_pixels = w * h
|
||||
mp_provided = out_num_pixels / 1_000_000.0
|
||||
if "seedream-4-5" in model and out_num_pixels < 3686400:
|
||||
if ("seedream-4-5" in model or "seedream-5-0" in model) and out_num_pixels < 3686400:
|
||||
raise ValueError(
|
||||
f"Minimum image resolution that Seedream 4.5 can generate is 3.68MP, "
|
||||
f"Minimum image resolution for the selected model is 3.68MP, "
|
||||
f"but {mp_provided:.2f}MP provided."
|
||||
)
|
||||
if "seedream-4-0" in model and out_num_pixels < 921600:
|
||||
@ -334,9 +338,18 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
f"Minimum image resolution that the selected model can generate is 0.92MP, "
|
||||
f"but {mp_provided:.2f}MP provided."
|
||||
)
|
||||
max_pixels = 10_404_496 if "seedream-5-0" in model else 16_777_216
|
||||
if out_num_pixels > max_pixels:
|
||||
raise ValueError(
|
||||
f"Maximum image resolution for the selected model is {max_pixels / 1_000_000:.2f}MP, "
|
||||
f"but {mp_provided:.2f}MP provided."
|
||||
)
|
||||
n_input_images = get_number_of_images(image) if image is not None else 0
|
||||
if n_input_images > 10:
|
||||
raise ValueError(f"Maximum of 10 reference images are supported, but {n_input_images} received.")
|
||||
max_num_of_images = 14 if model == "seedream-5-0-260128" else 10
|
||||
if n_input_images > max_num_of_images:
|
||||
raise ValueError(
|
||||
f"Maximum of {max_num_of_images} reference images are supported, but {n_input_images} received."
|
||||
)
|
||||
if sequential_image_generation == "auto" and n_input_images + max_images > 15:
|
||||
raise ValueError(
|
||||
"The maximum number of generated images plus the number of reference images cannot exceed 15."
|
||||
@ -364,6 +377,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
||||
sequential_image_generation=sequential_image_generation,
|
||||
sequential_image_generation_options=Seedream4Options(max_images=max_images),
|
||||
watermark=watermark,
|
||||
output_format="png" if model == "seedream-5-0-260128" else None,
|
||||
),
|
||||
)
|
||||
if len(response.data) == 1:
|
||||
|
||||
@ -50,6 +50,7 @@ from comfy_api_nodes.apis import (
|
||||
)
|
||||
from comfy_api_nodes.apis.kling import (
|
||||
ImageToVideoWithAudioRequest,
|
||||
KlingAvatarRequest,
|
||||
MotionControlRequest,
|
||||
MultiPromptEntry,
|
||||
OmniImageParamImage,
|
||||
@ -74,6 +75,7 @@ from comfy_api_nodes.util import (
|
||||
upload_image_to_comfyapi,
|
||||
upload_images_to_comfyapi,
|
||||
upload_video_to_comfyapi,
|
||||
validate_audio_duration,
|
||||
validate_image_aspect_ratio,
|
||||
validate_image_dimensions,
|
||||
validate_string,
|
||||
@ -3139,6 +3141,103 @@ class KlingFirstLastFrameNode(IO.ComfyNode):
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
|
||||
|
||||
class KlingAvatarNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="KlingAvatarNode",
|
||||
display_name="Kling Avatar 2.0",
|
||||
category="api node/video/Kling",
|
||||
description="Generate broadcast-style digital human videos from a single photo and an audio file.",
|
||||
inputs=[
|
||||
IO.Image.Input(
|
||||
"image",
|
||||
tooltip="Avatar reference image. "
|
||||
"Width and height must be at least 300px. Aspect ratio must be between 1:2.5 and 2.5:1.",
|
||||
),
|
||||
IO.Audio.Input(
|
||||
"sound_file",
|
||||
tooltip="Audio input. Must be between 2 and 300 seconds in duration.",
|
||||
),
|
||||
IO.Combo.Input("mode", options=["std", "pro"]),
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
multiline=True,
|
||||
default="",
|
||||
optional=True,
|
||||
tooltip="Optional prompt to define avatar actions, emotions, and camera movements.",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=2147483647,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
control_after_generate=True,
|
||||
tooltip="Seed controls whether the node should re-run; "
|
||||
"results are non-deterministic regardless of seed.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
IO.Video.Output(),
|
||||
],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(widgets=["mode"]),
|
||||
expr="""
|
||||
(
|
||||
$prices := {"std": 0.056, "pro": 0.112};
|
||||
{"type":"usd","usd": $lookup($prices, widgets.mode), "format":{"suffix":"/second"}}
|
||||
)
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
image: Input.Image,
|
||||
sound_file: Input.Audio,
|
||||
mode: str,
|
||||
seed: int,
|
||||
prompt: str = "",
|
||||
) -> IO.NodeOutput:
|
||||
validate_image_dimensions(image, min_width=300, min_height=300)
|
||||
validate_image_aspect_ratio(image, (1, 2.5), (2.5, 1))
|
||||
validate_audio_duration(sound_file, min_duration=2, max_duration=300)
|
||||
response = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/kling/v1/videos/avatar/image2video", method="POST"),
|
||||
response_model=TaskStatusResponse,
|
||||
data=KlingAvatarRequest(
|
||||
image=await upload_image_to_comfyapi(cls, image),
|
||||
sound_file=await upload_audio_to_comfyapi(
|
||||
cls, sound_file, container_format="mp3", codec_name="libmp3lame", mime_type="audio/mpeg"
|
||||
),
|
||||
prompt=prompt or None,
|
||||
mode=mode,
|
||||
),
|
||||
)
|
||||
if response.code:
|
||||
raise RuntimeError(
|
||||
f"Kling request failed. Code: {response.code}, Message: {response.message}, Data: {response.data}"
|
||||
)
|
||||
final_response = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/kling/v1/videos/avatar/image2video/{response.data.task_id}"),
|
||||
response_model=TaskStatusResponse,
|
||||
status_extractor=lambda r: (r.data.task_status if r.data else None),
|
||||
max_poll_attempts=800,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
|
||||
|
||||
class KlingExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
@ -3167,6 +3266,7 @@ class KlingExtension(ComfyExtension):
|
||||
MotionControl,
|
||||
KlingVideoNode,
|
||||
KlingFirstLastFrameNode,
|
||||
KlingAvatarNode,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import folder_paths
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import torch
|
||||
import comfy.utils
|
||||
|
||||
@ -682,6 +683,172 @@ class ImageScaleToMaxDimension(IO.ComfyNode):
|
||||
upscale = execute # TODO: remove
|
||||
|
||||
|
||||
class SplitImageToTileList(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="SplitImageToTileList",
|
||||
category="image/batch",
|
||||
search_aliases=["split image", "tile image", "slice image"],
|
||||
display_name="Split Image into List of Tiles",
|
||||
description="Splits an image into a batched list of tiles with a specified overlap.",
|
||||
inputs=[
|
||||
IO.Image.Input("image"),
|
||||
IO.Int.Input("tile_width", default=1024, min=64, max=MAX_RESOLUTION),
|
||||
IO.Int.Input("tile_height", default=1024, min=64, max=MAX_RESOLUTION),
|
||||
IO.Int.Input("overlap", default=128, min=0, max=4096),
|
||||
],
|
||||
outputs=[
|
||||
IO.Image.Output(is_output_list=True),
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_grid_coords(width, height, tile_width, tile_height, overlap):
|
||||
coords = []
|
||||
stride_x = max(1, tile_width - overlap)
|
||||
stride_y = max(1, tile_height - overlap)
|
||||
|
||||
y = 0
|
||||
while y < height:
|
||||
x = 0
|
||||
y_end = min(y + tile_height, height)
|
||||
y_start = max(0, y_end - tile_height)
|
||||
|
||||
while x < width:
|
||||
x_end = min(x + tile_width, width)
|
||||
x_start = max(0, x_end - tile_width)
|
||||
|
||||
coords.append((x_start, y_start, x_end, y_end))
|
||||
|
||||
if x_end >= width:
|
||||
break
|
||||
x += stride_x
|
||||
|
||||
if y_end >= height:
|
||||
break
|
||||
y += stride_y
|
||||
|
||||
return coords
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image, tile_width, tile_height, overlap):
|
||||
b, h, w, c = image.shape
|
||||
coords = cls.get_grid_coords(w, h, tile_width, tile_height, overlap)
|
||||
|
||||
output_list = []
|
||||
for (x_start, y_start, x_end, y_end) in coords:
|
||||
tile = image[:, y_start:y_end, x_start:x_end, :]
|
||||
output_list.append(tile)
|
||||
|
||||
return IO.NodeOutput(output_list)
|
||||
|
||||
|
||||
class ImageMergeTileList(IO.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return IO.Schema(
|
||||
node_id="ImageMergeTileList",
|
||||
display_name="Merge List of Tiles to Image",
|
||||
category="image/batch",
|
||||
search_aliases=["split image", "tile image", "slice image"],
|
||||
is_input_list=True,
|
||||
inputs=[
|
||||
IO.Image.Input("image_list"),
|
||||
IO.Int.Input("final_width", default=1024, min=64, max=32768),
|
||||
IO.Int.Input("final_height", default=1024, min=64, max=32768),
|
||||
IO.Int.Input("overlap", default=128, min=0, max=4096),
|
||||
],
|
||||
outputs=[
|
||||
IO.Image.Output(is_output_list=False),
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_grid_coords(width, height, tile_width, tile_height, overlap):
|
||||
coords = []
|
||||
stride_x = max(1, tile_width - overlap)
|
||||
stride_y = max(1, tile_height - overlap)
|
||||
|
||||
y = 0
|
||||
while y < height:
|
||||
x = 0
|
||||
y_end = min(y + tile_height, height)
|
||||
y_start = max(0, y_end - tile_height)
|
||||
|
||||
while x < width:
|
||||
x_end = min(x + tile_width, width)
|
||||
x_start = max(0, x_end - tile_width)
|
||||
|
||||
coords.append((x_start, y_start, x_end, y_end))
|
||||
|
||||
if x_end >= width:
|
||||
break
|
||||
x += stride_x
|
||||
|
||||
if y_end >= height:
|
||||
break
|
||||
y += stride_y
|
||||
|
||||
return coords
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image_list, final_width, final_height, overlap):
|
||||
w = final_width[0]
|
||||
h = final_height[0]
|
||||
ovlp = overlap[0]
|
||||
feather_str = 1.0
|
||||
|
||||
first_tile = image_list[0]
|
||||
b, t_h, t_w, c = first_tile.shape
|
||||
device = first_tile.device
|
||||
dtype = first_tile.dtype
|
||||
|
||||
coords = cls.get_grid_coords(w, h, t_w, t_h, ovlp)
|
||||
|
||||
canvas = torch.zeros((b, h, w, c), device=device, dtype=dtype)
|
||||
weights = torch.zeros((b, h, w, 1), device=device, dtype=dtype)
|
||||
|
||||
if ovlp > 0:
|
||||
y_w = torch.sin(math.pi * torch.linspace(0, 1, t_h, device=device, dtype=dtype))
|
||||
x_w = torch.sin(math.pi * torch.linspace(0, 1, t_w, device=device, dtype=dtype))
|
||||
y_w = torch.clamp(y_w, min=1e-5)
|
||||
x_w = torch.clamp(x_w, min=1e-5)
|
||||
|
||||
sine_mask = (y_w.unsqueeze(1) * x_w.unsqueeze(0)).unsqueeze(0).unsqueeze(-1)
|
||||
flat_mask = torch.ones_like(sine_mask)
|
||||
|
||||
weight_mask = torch.lerp(flat_mask, sine_mask, feather_str)
|
||||
else:
|
||||
weight_mask = torch.ones((1, t_h, t_w, 1), device=device, dtype=dtype)
|
||||
|
||||
for i, (x_start, y_start, x_end, y_end) in enumerate(coords):
|
||||
if i >= len(image_list):
|
||||
break
|
||||
|
||||
tile = image_list[i]
|
||||
|
||||
region_h = y_end - y_start
|
||||
region_w = x_end - x_start
|
||||
|
||||
real_h = min(region_h, tile.shape[1])
|
||||
real_w = min(region_w, tile.shape[2])
|
||||
|
||||
y_end_actual = y_start + real_h
|
||||
x_end_actual = x_start + real_w
|
||||
|
||||
tile_crop = tile[:, :real_h, :real_w, :]
|
||||
mask_crop = weight_mask[:, :real_h, :real_w, :]
|
||||
|
||||
canvas[:, y_start:y_end_actual, x_start:x_end_actual, :] += tile_crop * mask_crop
|
||||
weights[:, y_start:y_end_actual, x_start:x_end_actual, :] += mask_crop
|
||||
|
||||
weights[weights == 0] = 1.0
|
||||
merged_image = canvas / weights
|
||||
|
||||
return IO.NodeOutput(merged_image)
|
||||
|
||||
|
||||
class ImagesExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
@ -701,6 +868,8 @@ class ImagesExtension(ComfyExtension):
|
||||
ImageRotate,
|
||||
ImageFlip,
|
||||
ImageScaleToMaxDimension,
|
||||
SplitImageToTileList,
|
||||
ImageMergeTileList,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ class TorchCompileModel(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def execute(cls, model, backend) -> io.NodeOutput:
|
||||
m = model.clone()
|
||||
m = model.clone(disable_dynamic=True)
|
||||
set_torch_compile_wrapper(model=m, backend=backend, options={"guard_filter_fn": skip_torch_compile_dict})
|
||||
return io.NodeOutput(m)
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by the build process when version is
|
||||
# updated in pyproject.toml.
|
||||
__version__ = "0.14.1"
|
||||
__version__ = "0.15.0"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "ComfyUI"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
comfyui-frontend-package==1.39.14
|
||||
comfyui-workflow-templates==0.8.43
|
||||
comfyui-embedded-docs==0.4.1
|
||||
comfyui-frontend-package==1.39.16
|
||||
comfyui-workflow-templates==0.9.3
|
||||
comfyui-embedded-docs==0.4.3
|
||||
torch
|
||||
torchsde
|
||||
torchvision
|
||||
@ -22,7 +22,7 @@ alembic
|
||||
SQLAlchemy
|
||||
av>=14.2.0
|
||||
comfy-kitchen>=0.2.7
|
||||
comfy-aimdo>=0.2.0
|
||||
comfy-aimdo>=0.2.1
|
||||
requests
|
||||
|
||||
#non essential dependencies:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user