mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-07 02:07:32 +08:00
Merge branch 'master' into fix/node-replace-missing-class-type
Some checks failed
Build package / Build Test (3.14) (push) Has been cancelled
Build package / Build Test (3.12) (push) Has been cancelled
Build package / Build Test (3.13) (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Build package / Build Test (3.10) (push) Has been cancelled
Build package / Build Test (3.11) (push) Has been cancelled
Some checks failed
Build package / Build Test (3.14) (push) Has been cancelled
Build package / Build Test (3.12) (push) Has been cancelled
Build package / Build Test (3.13) (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Build package / Build Test (3.10) (push) Has been cancelled
Build package / Build Test (3.11) (push) Has been cancelled
This commit is contained in:
commit
21013ce05a
@ -134,6 +134,13 @@ class ImageToVideoWithAudioRequest(BaseModel):
|
|||||||
shot_type: str | None = Field(None)
|
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):
|
class MotionControlRequest(BaseModel):
|
||||||
prompt: str = Field(...)
|
prompt: str = Field(...)
|
||||||
image_url: str = Field(...)
|
image_url: str = Field(...)
|
||||||
|
|||||||
@ -50,6 +50,7 @@ from comfy_api_nodes.apis import (
|
|||||||
)
|
)
|
||||||
from comfy_api_nodes.apis.kling import (
|
from comfy_api_nodes.apis.kling import (
|
||||||
ImageToVideoWithAudioRequest,
|
ImageToVideoWithAudioRequest,
|
||||||
|
KlingAvatarRequest,
|
||||||
MotionControlRequest,
|
MotionControlRequest,
|
||||||
MultiPromptEntry,
|
MultiPromptEntry,
|
||||||
OmniImageParamImage,
|
OmniImageParamImage,
|
||||||
@ -74,6 +75,7 @@ from comfy_api_nodes.util import (
|
|||||||
upload_image_to_comfyapi,
|
upload_image_to_comfyapi,
|
||||||
upload_images_to_comfyapi,
|
upload_images_to_comfyapi,
|
||||||
upload_video_to_comfyapi,
|
upload_video_to_comfyapi,
|
||||||
|
validate_audio_duration,
|
||||||
validate_image_aspect_ratio,
|
validate_image_aspect_ratio,
|
||||||
validate_image_dimensions,
|
validate_image_dimensions,
|
||||||
validate_string,
|
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))
|
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):
|
class KlingExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
@ -3167,6 +3266,7 @@ class KlingExtension(ComfyExtension):
|
|||||||
MotionControl,
|
MotionControl,
|
||||||
KlingVideoNode,
|
KlingVideoNode,
|
||||||
KlingFirstLastFrameNode,
|
KlingFirstLastFrameNode,
|
||||||
|
KlingAvatarNode,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import folder_paths
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import math
|
||||||
import torch
|
import torch
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
|
|
||||||
@ -682,6 +683,172 @@ class ImageScaleToMaxDimension(IO.ComfyNode):
|
|||||||
upscale = execute # TODO: remove
|
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):
|
class ImagesExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
@ -701,6 +868,8 @@ class ImagesExtension(ComfyExtension):
|
|||||||
ImageRotate,
|
ImageRotate,
|
||||||
ImageFlip,
|
ImageFlip,
|
||||||
ImageScaleToMaxDimension,
|
ImageScaleToMaxDimension,
|
||||||
|
SplitImageToTileList,
|
||||||
|
ImageMergeTileList,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
comfyui-frontend-package==1.39.14
|
comfyui-frontend-package==1.39.14
|
||||||
comfyui-workflow-templates==0.8.43
|
comfyui-workflow-templates==0.9.2
|
||||||
comfyui-embedded-docs==0.4.1
|
comfyui-embedded-docs==0.4.1
|
||||||
torch
|
torch
|
||||||
torchsde
|
torchsde
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user