mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-20 14:59:28 +08:00
[Partner Nodes] feat(Kling): add support for Kling V3-Turbo model (#14528)
This commit is contained in:
parent
52257bb435
commit
191a75a2cd
@ -149,3 +149,59 @@ class MotionControlRequest(BaseModel):
|
|||||||
character_orientation: str = Field(...)
|
character_orientation: str = Field(...)
|
||||||
mode: str = Field(..., description="'pro' or 'std'")
|
mode: str = Field(..., description="'pro' or 'std'")
|
||||||
model_name: str = Field(...)
|
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,
|
OmniProImageRequest,
|
||||||
OmniProReferences2VideoRequest,
|
OmniProReferences2VideoRequest,
|
||||||
OmniProText2VideoRequest,
|
OmniProText2VideoRequest,
|
||||||
|
Kling3TurboSettings,
|
||||||
|
Kling3TurboText2VideoRequest,
|
||||||
|
Kling3TurboContent,
|
||||||
|
Kling3TurboImage2VideoRequest,
|
||||||
|
Kling3TurboCreateResponse,
|
||||||
|
Kling3TurboQueryResponse,
|
||||||
TaskStatusResponse,
|
TaskStatusResponse,
|
||||||
TextToVideoWithAudioRequest,
|
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))
|
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):
|
class KlingVideoNode(IO.ComfyNode):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -2884,7 +2951,11 @@ class KlingVideoNode(IO.ComfyNode):
|
|||||||
],
|
],
|
||||||
tooltip="Generate a series of video segments with individual prompts and durations.",
|
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(
|
IO.DynamicCombo.Input(
|
||||||
"model",
|
"model",
|
||||||
options=[
|
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.",
|
tooltip="Model and generation settings.",
|
||||||
),
|
),
|
||||||
@ -2930,6 +3012,7 @@ class KlingVideoNode(IO.ComfyNode):
|
|||||||
price_badge=IO.PriceBadge(
|
price_badge=IO.PriceBadge(
|
||||||
depends_on=IO.PriceBadgeDepends(
|
depends_on=IO.PriceBadgeDepends(
|
||||||
widgets=[
|
widgets=[
|
||||||
|
"model",
|
||||||
"model.resolution",
|
"model.resolution",
|
||||||
"generate_audio",
|
"generate_audio",
|
||||||
"multi_shot",
|
"multi_shot",
|
||||||
@ -2944,14 +3027,7 @@ class KlingVideoNode(IO.ComfyNode):
|
|||||||
),
|
),
|
||||||
expr="""
|
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");
|
$res := $lookup(widgets, "model.resolution");
|
||||||
$audio := widgets.generate_audio ? "on" : "off";
|
|
||||||
$rate := $lookup($lookup($rates, $res), $audio);
|
|
||||||
$ms := widgets.multi_shot;
|
$ms := widgets.multi_shot;
|
||||||
$isSb := $ms != "disabled";
|
$isSb := $ms != "disabled";
|
||||||
$n := $isSb ? $number($substring($ms, 0, 1)) : 0;
|
$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;
|
$d5 := $n >= 5 ? $lookup(widgets, "multi_shot.storyboard_5_duration") : 0;
|
||||||
$d6 := $n >= 6 ? $lookup(widgets, "multi_shot.storyboard_6_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");
|
$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"]
|
duration = multi_shot["duration"]
|
||||||
validate_string(multi_shot["prompt"], min_length=1, max_length=2500)
|
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:
|
if start_frame is not None:
|
||||||
validate_image_dimensions(start_frame, min_width=300, min_height=300)
|
validate_image_dimensions(start_frame, min_width=300, min_height=300)
|
||||||
validate_image_aspect_ratio(start_frame, (1, 2.5), (2.5, 1))
|
validate_image_aspect_ratio(start_frame, (1, 2.5), (2.5, 1))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user