mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-19 04:57:26 +08:00
Compare commits
2 Commits
cloud-open
...
v0.25.1
| Author | SHA1 | Date | |
|---|---|---|---|
| eca4757d65 | |||
| a043f1c8bd |
@ -149,3 +149,59 @@ class MotionControlRequest(BaseModel):
|
||||
character_orientation: str = Field(...)
|
||||
mode: str = Field(..., description="'pro' or 'std'")
|
||||
model_name: str = Field(...)
|
||||
|
||||
|
||||
class Kling3TurboSettings(BaseModel):
|
||||
resolution: str = Field("720p", description="'720p' or '1080p'")
|
||||
aspect_ratio: str | None = Field(None, description="'16:9'/'9:16'/'1:1'; text-to-video only")
|
||||
duration: int = Field(5, description="3-15 second")
|
||||
|
||||
|
||||
class Kling3TurboText2VideoRequest(BaseModel):
|
||||
prompt: str = Field(..., description="<=3072 chars; may use multi-shot 'shot n, m, words; ...'")
|
||||
settings: Kling3TurboSettings | None = Field(None)
|
||||
|
||||
|
||||
class Kling3TurboContent(BaseModel):
|
||||
type: str = Field(..., description="'prompt' or 'first_frame'")
|
||||
text: str | None = Field(None, description="for type=prompt; <=2500 chars")
|
||||
url: str | None = Field(None, description="for type=first_frame")
|
||||
|
||||
|
||||
class Kling3TurboImage2VideoRequest(BaseModel):
|
||||
contents: list[Kling3TurboContent] = Field(..., description="prompt + first_frame materials")
|
||||
settings: Kling3TurboSettings | None = Field(None)
|
||||
|
||||
|
||||
class Kling3TurboCreateData(BaseModel):
|
||||
id: str | None = Field(None, description="Task ID")
|
||||
status: str | None = Field(None)
|
||||
message: str | None = Field(None)
|
||||
|
||||
|
||||
class Kling3TurboCreateResponse(BaseModel):
|
||||
code: int | None = Field(None)
|
||||
message: str | None = Field(None)
|
||||
request_id: str | None = Field(None)
|
||||
data: Kling3TurboCreateData | None = Field(None)
|
||||
|
||||
|
||||
class Kling3TurboOutput(BaseModel):
|
||||
type: str | None = Field(None, description="'video', 'image', 'audio', ...")
|
||||
id: str | None = Field(None)
|
||||
url: str | None = Field(None)
|
||||
duration: str | None = Field(None)
|
||||
|
||||
|
||||
class Kling3TurboTaskData(BaseModel):
|
||||
id: str | None = Field(None)
|
||||
status: str | None = Field(None, description="submitted | processing | succeeded | failed")
|
||||
message: str | None = Field(None)
|
||||
outputs: list[Kling3TurboOutput] | None = Field(None)
|
||||
|
||||
|
||||
class Kling3TurboQueryResponse(BaseModel):
|
||||
code: int | None = Field(None)
|
||||
message: str | None = Field(None)
|
||||
request_id: str | None = Field(None)
|
||||
data: list[Kling3TurboTaskData] | None = Field(None)
|
||||
|
||||
@ -60,6 +60,12 @@ from comfy_api_nodes.apis.kling import (
|
||||
OmniProImageRequest,
|
||||
OmniProReferences2VideoRequest,
|
||||
OmniProText2VideoRequest,
|
||||
Kling3TurboSettings,
|
||||
Kling3TurboText2VideoRequest,
|
||||
Kling3TurboContent,
|
||||
Kling3TurboImage2VideoRequest,
|
||||
Kling3TurboCreateResponse,
|
||||
Kling3TurboQueryResponse,
|
||||
TaskStatusResponse,
|
||||
TextToVideoWithAudioRequest,
|
||||
)
|
||||
@ -2847,6 +2853,67 @@ class MotionControl(IO.ComfyNode):
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
|
||||
|
||||
def build_turbo_shot_prompt(multi_prompt: list[MultiPromptEntry]) -> str:
|
||||
"""Render storyboard entries into the Turbo multi-shot prompt 'shot n, m, words; ...'."""
|
||||
return "; ".join(f"shot {i}, {int(e.duration)}, {e.prompt}" for i, e in enumerate(multi_prompt, 1)) + ";"
|
||||
|
||||
|
||||
def _turbo_video_url(response: Kling3TurboQueryResponse) -> str:
|
||||
"""Extract the result video URL from a /tasks response (data[].outputs[] where type == 'video')."""
|
||||
task = response.data[0] if response.data else None
|
||||
if task and task.outputs:
|
||||
for output in task.outputs:
|
||||
if output.type == "video" and output.url:
|
||||
return output.url
|
||||
raise RuntimeError(f"Kling 3.0 Turbo task finished without a video output: {response.model_dump()}")
|
||||
|
||||
|
||||
async def execute_kling_turbo(
|
||||
cls: type[IO.ComfyNode],
|
||||
*,
|
||||
prompt: str,
|
||||
resolution: str,
|
||||
aspect_ratio: str,
|
||||
duration: int,
|
||||
start_frame: torch.Tensor | None,
|
||||
) -> IO.NodeOutput:
|
||||
"""Create + poll a Kling 3.0 Turbo task. Image-to-video when start_frame is given, else text-to-video."""
|
||||
if start_frame is not None:
|
||||
validate_image_dimensions(start_frame, min_width=300, min_height=300)
|
||||
validate_image_aspect_ratio(start_frame, (1, 2.5), (2.5, 1))
|
||||
contents = [Kling3TurboContent(type="first_frame", url=tensor_to_base64_string(start_frame))]
|
||||
if prompt:
|
||||
contents.insert(0, Kling3TurboContent(type="prompt", text=prompt))
|
||||
create = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/kling/image-to-video/kling-3.0-turbo", method="POST"),
|
||||
response_model=Kling3TurboCreateResponse,
|
||||
data=Kling3TurboImage2VideoRequest(
|
||||
contents=contents,
|
||||
settings=Kling3TurboSettings(resolution=resolution, duration=duration), # i2v: no aspect_ratio
|
||||
),
|
||||
)
|
||||
else:
|
||||
create = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/kling/text-to-video/kling-3.0-turbo", method="POST"),
|
||||
response_model=Kling3TurboCreateResponse,
|
||||
data=Kling3TurboText2VideoRequest(
|
||||
prompt=prompt,
|
||||
settings=Kling3TurboSettings(resolution=resolution, aspect_ratio=aspect_ratio, duration=duration),
|
||||
),
|
||||
)
|
||||
if not (create.data and create.data.id):
|
||||
raise RuntimeError(f"Kling 3.0 Turbo create failed. Code: {create.code}, Message: {create.message}")
|
||||
final_response = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/kling/tasks", query_params={"task_ids": create.data.id}),
|
||||
response_model=Kling3TurboQueryResponse,
|
||||
status_extractor=lambda r: (r.data[0].status if r.data else None),
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(_turbo_video_url(final_response)))
|
||||
|
||||
|
||||
class KlingVideoNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
@ -2884,7 +2951,11 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
],
|
||||
tooltip="Generate a series of video segments with individual prompts and durations.",
|
||||
),
|
||||
IO.Boolean.Input("generate_audio", default=True),
|
||||
IO.Boolean.Input(
|
||||
"generate_audio",
|
||||
default=True,
|
||||
tooltip="'kling-3.0-turbo' always generates native audio, so the audio toggle is ignored.",
|
||||
),
|
||||
IO.DynamicCombo.Input(
|
||||
"model",
|
||||
options=[
|
||||
@ -2899,6 +2970,17 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
),
|
||||
],
|
||||
),
|
||||
IO.DynamicCombo.Option(
|
||||
"kling-3.0-turbo",
|
||||
[
|
||||
IO.Combo.Input("resolution", options=["1080p", "720p"], default="720p"),
|
||||
IO.Combo.Input(
|
||||
"aspect_ratio",
|
||||
options=["16:9", "9:16", "1:1"],
|
||||
tooltip="Ignored in image-to-video mode.",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
tooltip="Model and generation settings.",
|
||||
),
|
||||
@ -2930,6 +3012,7 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(
|
||||
widgets=[
|
||||
"model",
|
||||
"model.resolution",
|
||||
"generate_audio",
|
||||
"multi_shot",
|
||||
@ -2944,14 +3027,7 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
),
|
||||
expr="""
|
||||
(
|
||||
$rates := {
|
||||
"4k": {"off": 0.42, "on": 0.42},
|
||||
"1080p": {"off": 0.112, "on": 0.168},
|
||||
"720p": {"off": 0.084, "on": 0.126}
|
||||
};
|
||||
$res := $lookup(widgets, "model.resolution");
|
||||
$audio := widgets.generate_audio ? "on" : "off";
|
||||
$rate := $lookup($lookup($rates, $res), $audio);
|
||||
$ms := widgets.multi_shot;
|
||||
$isSb := $ms != "disabled";
|
||||
$n := $isSb ? $number($substring($ms, 0, 1)) : 0;
|
||||
@ -2962,7 +3038,18 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
$d5 := $n >= 5 ? $lookup(widgets, "multi_shot.storyboard_5_duration") : 0;
|
||||
$d6 := $n >= 6 ? $lookup(widgets, "multi_shot.storyboard_6_duration") : 0;
|
||||
$dur := $isSb ? $d1 + $d2 + $d3 + $d4 + $d5 + $d6 : $lookup(widgets, "multi_shot.duration");
|
||||
{"type":"usd","usd": $rate * $dur}
|
||||
widgets.model = "kling-3.0-turbo"
|
||||
? {"type":"usd","usd": ($res = "1080p" ? 0.14 : 0.112) * $dur}
|
||||
: (
|
||||
$rates := {
|
||||
"4k": {"off": 0.42, "on": 0.42},
|
||||
"1080p": {"off": 0.112, "on": 0.168},
|
||||
"720p": {"off": 0.084, "on": 0.126}
|
||||
};
|
||||
$audio := widgets.generate_audio ? "on" : "off";
|
||||
$rate := $lookup($lookup($rates, $res), $audio);
|
||||
{"type":"usd","usd": $rate * $dur}
|
||||
)
|
||||
)
|
||||
""",
|
||||
),
|
||||
@ -3015,6 +3102,17 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
duration = multi_shot["duration"]
|
||||
validate_string(multi_shot["prompt"], min_length=1, max_length=2500)
|
||||
|
||||
if model["model"] == "kling-3.0-turbo":
|
||||
turbo_prompt = build_turbo_shot_prompt(multi_prompt_list) if custom_multi_shot else multi_shot["prompt"]
|
||||
return await execute_kling_turbo(
|
||||
cls,
|
||||
prompt=turbo_prompt,
|
||||
resolution=model["resolution"],
|
||||
aspect_ratio=model["aspect_ratio"],
|
||||
duration=duration,
|
||||
start_frame=start_frame,
|
||||
)
|
||||
|
||||
if start_frame is not None:
|
||||
validate_image_dimensions(start_frame, min_width=300, min_height=300)
|
||||
validate_image_aspect_ratio(start_frame, (1, 2.5), (2.5, 1))
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by the build process when version is
|
||||
# updated in pyproject.toml.
|
||||
__version__ = "0.25.0"
|
||||
__version__ = "0.25.1"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "ComfyUI"
|
||||
version = "0.25.0"
|
||||
version = "0.25.1"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user