diff --git a/README.md b/README.md
index b67124a..5c38af2 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,18 @@ This node is used to display the try-on effect.
+### Video Extend
+This node is used to extend a video.
+
+
+
+
+### Lip Sync
+This node is used to generate a lip sync video.
+
+
+
+
## Pricing
For pricing, follow [KLing AI Pricing](https://klingai.com/dev-center).
\ No newline at end of file
diff --git a/examples/image2video.png b/examples/image2video.png
index 53797ca..de2d17d 100644
Binary files a/examples/image2video.png and b/examples/image2video.png differ
diff --git a/examples/lip_sync.png b/examples/lip_sync.png
new file mode 100644
index 0000000..f3404d9
Binary files /dev/null and b/examples/lip_sync.png differ
diff --git a/examples/video_extend.png b/examples/video_extend.png
new file mode 100644
index 0000000..f1d8a17
Binary files /dev/null and b/examples/video_extend.png differ
diff --git a/py/__init__.py b/py/__init__.py
index e2b4f1a..ca5b9d3 100644
--- a/py/__init__.py
+++ b/py/__init__.py
@@ -1,4 +1,4 @@
-from .nodes import Text2VideoNode, ImageGeneratorNode, Image2VideoNode, KLingAIAPIClient, PreviewVideo, KolorsVirtualTryOnNode
+from .nodes import Text2VideoNode, ImageGeneratorNode, Image2VideoNode, KLingAIAPIClient, PreviewVideo, KolorsVirtualTryOnNode, VideoExtendNode, LipSyncNode, LipSyncTextInputNode, LipSyncAudioInputNode
NODE_CLASS_MAPPINGS = {
'Client': KLingAIAPIClient,
@@ -6,5 +6,9 @@ NODE_CLASS_MAPPINGS = {
'Text2Video': Text2VideoNode,
'Image2Video': Image2VideoNode,
'Virtual Try On': KolorsVirtualTryOnNode,
- 'KLingAI Preview Video': PreviewVideo
+ 'KLingAI Preview Video': PreviewVideo,
+ 'Video Extend': VideoExtendNode,
+ 'Lip Sync': LipSyncNode,
+ 'Lip Sync Text Input': LipSyncTextInputNode,
+ 'Lip Sync Audio Input': LipSyncAudioInputNode,
}
\ No newline at end of file
diff --git a/py/api/__init__.py b/py/api/__init__.py
index d2d2428..2e8ddbe 100644
--- a/py/api/__init__.py
+++ b/py/api/__init__.py
@@ -4,3 +4,5 @@ from .image_generator import ImageGenerator
from .text2video import Text2Video, CameraControl, CameraControlConfig
from .image2video import Image2Video
from .virtual_try_on import KolorsVurtualTryOn
+from .video_entend import VideoExtend
+from .lip_sync import LipSync, LipSyncInput
diff --git a/py/api/lip_sync.py b/py/api/lip_sync.py
new file mode 100644
index 0000000..eefea1b
--- /dev/null
+++ b/py/api/lip_sync.py
@@ -0,0 +1,25 @@
+from .prediction import Prediction, VideoPredictionResponse
+
+class LipSyncInput(Prediction):
+ video_id: str
+ mode: str
+ text: str
+ voice_id: str
+ voice_language: str
+ voice_speed: float
+ audio_type: str
+ audio_file: str
+ audio_url: str
+
+
+class LipSync(Prediction):
+ input: LipSyncInput
+
+ def __init__(self):
+ super().__init__()
+ self._request_method = "POST"
+ self._request_path = "/v1/videos/lip-sync"
+ self._query_prediction_info_method = "GET"
+ self._query_prediction_info_path = "/v1/videos/lip-sync"
+ self._response_cls = VideoPredictionResponse
+
diff --git a/py/api/prediction.py b/py/api/prediction.py
index 79fa19e..6d1ccc1 100644
--- a/py/api/prediction.py
+++ b/py/api/prediction.py
@@ -95,7 +95,6 @@ class Prediction:
path = self._query_prediction_info_path + "/" + task_id
resp = client.request(method=self._query_prediction_info_method, path=path)
self._task_info = self._response_cls(**resp.get("data"))
- print(f'query: {path}, status: {self._task_info.task_status}')
def run(self, client: Client):
resp = client.request(method=self._request_method, path=self._request_path, json=self.to_dict())
diff --git a/py/api/video_entend.py b/py/api/video_entend.py
new file mode 100644
index 0000000..19b0b8f
--- /dev/null
+++ b/py/api/video_entend.py
@@ -0,0 +1,15 @@
+from .prediction import Prediction, VideoPredictionResponse
+
+class VideoExtend(Prediction):
+ video_id: str
+
+ prompt: str
+
+ def __init__(self):
+ super().__init__()
+ self._request_method = "POST"
+ self._request_path = "/v1/videos/video-extend"
+ self._query_prediction_info_method = "GET"
+ self._query_prediction_info_path = "/v1/videos/video-extend"
+ self._response_cls = VideoPredictionResponse
+
diff --git a/py/nodes.py b/py/nodes.py
index e02e01e..a11f789 100644
--- a/py/nodes.py
+++ b/py/nodes.py
@@ -1,4 +1,4 @@
-from .api import Client, ImageGenerator, Image2Video, Text2Video, CameraControl, CameraControlConfig, KolorsVurtualTryOn
+from .api import Client, ImageGenerator, Image2Video, Text2Video, CameraControl, CameraControlConfig, KolorsVurtualTryOn, VideoExtend, LipSync, LipSyncInput
import base64
import io
import os
@@ -203,8 +203,8 @@ class Image2VideoNode:
}
}
- RETURN_TYPES = ("STRING",)
- RETURN_NAMES = ("url",)
+ RETURN_TYPES = ("STRING", "STRING")
+ RETURN_NAMES = ("url", "video_id")
FUNCTION = "generate"
@@ -233,11 +233,11 @@ class Image2VideoNode:
generator.duration = duration
response = generator.run(client)
- result = []
for video_info in response.task_result.videos:
- result.append(video_info.url)
- print(f'KLing API output: {result}')
- return (result,)
+ print(f'KLing API output video id: {video_info.id}, url: {video_info.url}')
+ return (video_info.url, video_info.id)
+
+ return ('', '')
class Text2VideoNode:
@@ -278,8 +278,8 @@ class Text2VideoNode:
}
}
- RETURN_TYPES = ("STRING",)
- RETURN_NAMES = ("url",)
+ RETURN_TYPES = ("STRING", "STRING")
+ RETURN_NAMES = ("url", "video_id")
FUNCTION = "generate"
@@ -303,7 +303,6 @@ class Text2VideoNode:
generator = Text2Video()
generator.model = model
generator.prompt = prompt
- generator.prompt = prompt
generator.negative_prompt = negative_prompt
generator.cfg_scale = cfg_scale
generator.mode = mode
@@ -330,11 +329,11 @@ class Text2VideoNode:
response = generator.run(client)
- result = []
for video_info in response.task_result.videos:
- result.append(video_info.url)
- print(f'KLing API output: {result}')
- return (result,)
+ print(f'KLing API output video id: {video_info.id}, url: {video_info.url}')
+ return (video_info.url, video_info.id)
+
+ return ('', '')
class KolorsVirtualTryOnNode:
@@ -343,7 +342,7 @@ class KolorsVirtualTryOnNode:
return {
"required": {
"client": ("KLING_AI_API_CLIENT",),
- "model_name": (["kolors-virtual-try-on-v1"],),
+ "model_name": (["kolors-virtual-try-on-v1", "kolors-virtual-try-on-v1-5"],),
"human_image": ("IMAGE",),
"cloth_image": ("IMAGE",),
},
@@ -426,3 +425,204 @@ class PreviewVideo:
open(file_path, "wb").write(_fetch_image(video_url))
return {"ui": {"video_url": [video_url]}, "result": (file_path, )}
+
+class VideoExtendNode:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "client": ("KLING_AI_API_CLIENT",),
+ "video_id": ("STRING", {"multiline": False, "default": ""}),
+ "prompt": ("STRING", {"multiline": True, "default": ""}),
+ }
+ }
+
+ OUTPUT_NODE = True
+ FUNCTION = "run"
+ CATEGORY = "KLingAI"
+
+ RETURN_TYPES = ("STRING", "STRING")
+ RETURN_NAMES = ("url", "video_id")
+
+ def run(self, client, video_id, prompt):
+
+ generator = VideoExtend()
+ generator.video_id = video_id
+ generator.prompt = prompt
+
+ response = generator.run(client)
+
+ for video_info in response.task_result.videos:
+ print(f'KLing API output video id: {video_info.id}, url: {video_info.url}')
+ return (video_info.url, video_info.id)
+
+ return ('', '')
+
+class LipSyncTextInputNode:
+
+ audio_types = {
+ "阳光少年": "genshin_vindi2",
+ "懂事小弟": "zhinen_xuesheng",
+ "运动少年": "tiyuxi_xuedi",
+ "青春少女": "ai_shatang",
+ "温柔小妹": "genshin_klee2",
+ "元气少女": "genshin_kirara",
+ "阳光男生": "ai_kaiya",
+ "幽默小哥": "tiexin_nanyou",
+ "文艺小哥": "ai_chenjiahao_712",
+ "甜美邻家": "girlfriend_1_speech02",
+ "温柔姐姐": "chat1_female_new-3",
+ "职场女青": "girlfriend_2_speech02",
+ "活泼男童": "cartoon-boy-07",
+ "俏皮女童": "cartoon-girl-01",
+ "稳重老爸": "ai_huangyaoshi_712",
+ "温柔妈妈": "you_pingjing",
+ "严肃上司": "ai_laoguowang_712",
+ "优雅贵妇": "chengshu_jiejie",
+ "慈祥爷爷": "zhuxi_speech02",
+ "唠叨爷爷": "uk_oldman3",
+ "唠叨奶奶": "laopopo_speech02",
+ "和蔼奶奶": "heainainai_speech02",
+ "东北老铁": "dongbeilaotie_speech02",
+ "重庆小伙": "chongqingxiaohuo_speech02",
+ "四川妹子": "chuanmeizi_speech02",
+ "潮汕大叔": "chaoshandashu_speech02",
+ "台湾男生": "ai_taiwan_man2_speech02",
+ "西安掌柜": "xianzhanggui_speech02",
+ "天津姐姐": "tianjinjiejie_speech02",
+ "Sunny": "genshin_vindi2",
+ "Sage": "zhinen_xuesheng",
+ "Ace": "AOT",
+ "Blossom": "ai_shatang",
+ "Peppy": "genshin_klee2",
+ "Dove": "genshin_kirara",
+ "Shine": "ai_kaiya",
+ "Anchor": "oversea_male1",
+ "Lyric": "ai_chenjiahao_712",
+ "Melody": "girlfriend_4_speech02",
+ "Tender": "chat1_female_new-3",
+ "Siren": "chat_0407_5-1",
+ "Zippy": "cartoon-boy-07",
+ "Bud": "uk_boy1",
+ "Sprite": "cartoon-girl-01",
+ "Candy": "PeppaPig_platform",
+ "Beacon": "ai_huangzhong_712",
+ "Rock": "ai_huangyaoshi_712",
+ "Titan": "ai_laoguowang_712",
+ "Grace": "chengshu_jiejie",
+ "Helen": "you_pingjing",
+ "Lore": "calm_story1",
+ "Crag": "uk_man2",
+ "Prattle": "laopopo_speech02",
+ "Hearth": "heainainai_speech02"
+ }
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "text": ("STRING", {"multiline": True, "default": ""}),
+ "voice_id": (list(LipSyncTextInputNode.audio_types.keys()), {"multiline": False, "default": ""}),
+ "voice_language": (["zh", "en"], {"multiline": True, "default": "zh"}),
+ "voice_speed": ("FLOAT", {
+ "default": 1.0,
+ "min": 0.8,
+ "max": 2.0,
+ "step": 0.1,
+ "round": 0.01,
+ "display": "number",
+ "lazy": True
+ })
+ }
+ }
+
+ OUTPUT_NODE = True
+ FUNCTION = "run"
+ CATEGORY = "KLingAI"
+
+ RETURN_TYPES = ("KLING_AI_API_LIPSYNC_INPUT", )
+ RETURN_NAMES = ("input", )
+
+ def run(self, text, voice_id, voice_language, voice_speed):
+ input = LipSyncInput()
+ input.mode = "text2video"
+ input.text = text
+ if voice_id in LipSyncTextInputNode.audio_types:
+ input.voice_id = LipSyncTextInputNode.audio_types[voice_id]
+ else:
+ input.voice_id = voice_id
+
+ input.voice_language = voice_language
+ input.voice_speed = voice_speed
+
+ return (input, )
+
+class LipSyncAudioInputNode:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "optional": {
+ "audio_file": ("STRING", {"multiline": False, "default": ""}),
+ "audio_url": ("STRING", {"multiline": False, "default": ""}),
+ },
+ }
+
+ OUTPUT_NODE = True
+ FUNCTION = "run"
+ CATEGORY = "KLingAI"
+
+ RETURN_TYPES = ("KLING_AI_API_LIPSYNC_INPUT", )
+ RETURN_NAMES = ("input", )
+
+ def run(self, audio_file, audio_url):
+ input = LipSyncInput()
+ input.mode = "audio2video"
+ if audio_file is not None and len(audio_file) > 0:
+ input.audio_type = "file"
+ if os.path.exists(audio_file):
+ with open(audio_file, 'rb') as file:
+ file_data = file.read()
+ input.audio_file = base64.b64encode(file_data).decode('utf-8')
+ else:
+ raise Exception(f"Audio file not found: {audio_file}")
+
+ if audio_url is not None and len(audio_url) > 0:
+ input.audio_type = "url"
+ input.audio_url = audio_url
+
+ return (input, )
+
+class LipSyncNode:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "client": ("KLING_AI_API_CLIENT", ),
+ "video_id": ("STRING", {"multiline": False, "default": ""}),
+ "input": ("KLING_AI_API_LIPSYNC_INPUT", )
+ },
+ "optional": {
+
+ }
+ }
+
+ OUTPUT_NODE = True
+ FUNCTION = "run"
+ CATEGORY = "KLingAI"
+
+ RETURN_TYPES = ("STRING", "STRING")
+ RETURN_NAMES = ("url", "video_id")
+
+ def run(self, client, video_id, input):
+
+ generator = LipSync()
+ input.video_id = video_id
+ generator.input = input
+
+ response = generator.run(client)
+
+ for video_info in response.task_result.videos:
+ print(f'KLing API output video id: {video_info.id}, url: {video_info.url}')
+ return (video_info.url, video_info.id)
+
+ return ('', '')
\ No newline at end of file