168 lines
6.1 KiB
Python
168 lines
6.1 KiB
Python
"""
|
||
Kling V3 - Unified text/image-to-video and motion control model
|
||
"""
|
||
import time
|
||
from .modelverse_api.client import ModelverseClient
|
||
from .modelverse_api.requests.kling_common import (
|
||
ASPECT_RATIOS,
|
||
CHARACTER_ORIENTATIONS,
|
||
KLING_V3_TYPES,
|
||
MODEL_KLING_V3,
|
||
MODES,
|
||
SOUNDS,
|
||
YES_NO,
|
||
)
|
||
from .modelverse_api.requests.kling_v3 import KlingV3
|
||
from comfy.comfy_types.node_typing import IO
|
||
|
||
|
||
class KlingV3Node:
|
||
"""
|
||
Kling V3 unified video generation.
|
||
Auto-routes to text-to-video, image-to-video, or motion control.
|
||
"""
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"client": ("MODELVERSE_API_CLIENT",),
|
||
"prompt": (IO.STRING, {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": "Text prompt describing the desired video",
|
||
}),
|
||
},
|
||
"optional": {
|
||
"negative_prompt": (IO.STRING, {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": "Negative prompt to avoid unwanted content",
|
||
}),
|
||
"kling_v3_type": (KLING_V3_TYPES, {
|
||
"default": "auto",
|
||
"tooltip": "Task type; auto infers from inputs (image→i2v, video→motion_control)",
|
||
}),
|
||
"first_frame_image": (IO.IMAGE, {"tooltip": "First frame image (image-to-video / motion control)"}),
|
||
"first_frame_url": (IO.STRING, {
|
||
"default": "",
|
||
"tooltip": "First frame URL (use either this OR first_frame_image, not both)",
|
||
}),
|
||
"last_frame_image": (IO.IMAGE, {"tooltip": "Last frame image (image-to-video)"}),
|
||
"last_frame_url": (IO.STRING, {
|
||
"default": "",
|
||
"tooltip": "Last frame URL (use either this OR last_frame_image, not both)",
|
||
}),
|
||
"reference_video_url": (IO.STRING, {
|
||
"default": "",
|
||
"tooltip": "Reference video URL (motion control; auto-selects motion_control when set)",
|
||
}),
|
||
"aspect_ratio": (ASPECT_RATIOS, {"default": "16:9", "tooltip": "Output aspect ratio"}),
|
||
"duration": (IO.INT, {
|
||
"default": 5, "min": 3, "max": 15, "step": 1,
|
||
"tooltip": "Video duration in seconds (3–15)",
|
||
}),
|
||
"mode": (MODES, {"default": "std", "tooltip": "std: 720p, pro: 1080p"}),
|
||
"sound": (SOUNDS, {"default": "off", "tooltip": "Generate synchronized audio"}),
|
||
"shot_type": (IO.STRING, {
|
||
"default": "",
|
||
"tooltip": "Optional shot type, e.g. 'multi' for multi-shot",
|
||
}),
|
||
"character_orientation": (CHARACTER_ORIENTATIONS, {
|
||
"default": "image",
|
||
"tooltip": "Motion control: follow image or video orientation",
|
||
}),
|
||
"keep_original_sound": (YES_NO, {
|
||
"default": "no",
|
||
"tooltip": "Motion control: keep original reference video sound",
|
||
}),
|
||
},
|
||
}
|
||
|
||
RETURN_TYPES = (IO.STRING, IO.STRING)
|
||
RETURN_NAMES = ("url", "task_id")
|
||
FUNCTION = "generate"
|
||
CATEGORY = "UCLOUD_MODELVERSE/Kling"
|
||
|
||
def generate(
|
||
self,
|
||
client,
|
||
prompt,
|
||
negative_prompt="",
|
||
kling_v3_type="auto",
|
||
first_frame_image=None,
|
||
first_frame_url="",
|
||
last_frame_image=None,
|
||
last_frame_url="",
|
||
reference_video_url="",
|
||
aspect_ratio="16:9",
|
||
duration=5,
|
||
mode="std",
|
||
sound="off",
|
||
shot_type="",
|
||
character_orientation="image",
|
||
keep_original_sound="no",
|
||
):
|
||
api_key = client.get("api_key")
|
||
if not api_key:
|
||
raise ValueError("API key is not set")
|
||
|
||
request = KlingV3(
|
||
prompt=prompt,
|
||
negative_prompt=negative_prompt,
|
||
kling_v3_type=kling_v3_type,
|
||
first_frame=first_frame_image,
|
||
first_frame_url=first_frame_url,
|
||
last_frame=last_frame_image,
|
||
last_frame_url=last_frame_url,
|
||
reference_video_url=reference_video_url,
|
||
aspect_ratio=aspect_ratio,
|
||
duration=duration,
|
||
mode=mode,
|
||
sound=sound,
|
||
shot_type=shot_type,
|
||
character_orientation=character_orientation,
|
||
keep_original_sound=keep_original_sound,
|
||
)
|
||
|
||
mv_client = ModelverseClient(api_key)
|
||
print(f"Submitting Kling V3 task: model={MODEL_KLING_V3}, type={kling_v3_type}, prompt={prompt!r}")
|
||
submit_res = mv_client.submit_task_request(request)
|
||
task_id = submit_res.get("output", {}).get("task_id")
|
||
if not task_id:
|
||
raise Exception(f"Failed to submit task: {submit_res}")
|
||
|
||
print(f"Kling V3 task submitted: {task_id}")
|
||
video_url = self._poll_task(mv_client, task_id)
|
||
return (video_url, task_id)
|
||
|
||
def _poll_task(self, mv_client, task_id, max_retries=180):
|
||
for i in range(max_retries):
|
||
status_res = mv_client.get_task_status(task_id)
|
||
task_status = status_res.get("output", {}).get("task_status")
|
||
|
||
if task_status == "Success":
|
||
urls = status_res.get("output", {}).get("urls", [])
|
||
if urls:
|
||
return urls[0]
|
||
raise Exception("Task succeeded but no video URL returned")
|
||
if task_status == "Failure":
|
||
error = status_res.get("output", {}).get("error_message", "Unknown error")
|
||
raise Exception(f"Task failed: {error}")
|
||
if task_status in ["Pending", "Running"]:
|
||
print(f"Task {task_id}: {task_status} ({i + 1}/{max_retries})")
|
||
time.sleep(5)
|
||
continue
|
||
raise Exception(f"Unknown status: {task_status}")
|
||
|
||
raise Exception("Task timed out")
|
||
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"Kling_V3": KlingV3Node,
|
||
}
|
||
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"Kling_V3": "Modelverse Kling V3",
|
||
}
|