from .api import Client, ImageGenerator, ImageExpander, Image2Video, Video2Audio, Text2Audio, \ Text2Video, CameraControl, CameraControlConfig, KolorsVurtualTryOn, VideoExtend, LipSync, LipSyncInput, EffectInput, \ Effects import base64 import io import os import re import numpy import PIL import requests import torch from collections.abc import Iterable import configparser import folder_paths from comfy_extras.nodes_audio import LoadAudio from folder_paths import get_temp_directory import time import urllib.parse from pathlib import Path current_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(current_dir) config_path = os.path.join(parent_dir, 'config.ini') config = configparser.ConfigParser() config.read(config_path) def _fetch_image(url, stream=True): return requests.get(url, stream=stream).content def _tensor2images(tensor): np_imgs = numpy.clip(tensor.cpu().numpy() * 255.0, 0.0, 255.0).astype(numpy.uint8) return [PIL.Image.fromarray(np_img) for np_img in np_imgs] def _images2tensor(images): if isinstance(images, Iterable): return torch.stack([torch.from_numpy(numpy.array(image)).float() / 255.0 for image in images]) return torch.from_numpy(numpy.array(images)).unsqueeze(0).float() / 255.0 def _decode_image(data_bytes, rtn_mask=False): with io.BytesIO(data_bytes) as bytes_io: img = PIL.Image.open(bytes_io) if not rtn_mask: img = img.convert('RGB') elif 'A' in img.getbands(): img = img.getchannel('A') else: img = None return img def _encode_image(img, mask=None): if mask is not None: img = img.copy() img.putalpha(mask) with io.BytesIO() as bytes_io: if mask is not None: img.save(bytes_io, format='PNG') else: img.save(bytes_io, format='JPEG') data_bytes = bytes_io.getvalue() return data_bytes def _image_to_base64(image): if image is None: return None return base64.b64encode(_encode_image(_tensor2images(image)[0])).decode("utf-8") def _load_audio_from_url(audio_url, save_directory, filename_prefix="audio"): try: response = requests.get( audio_url, timeout=30, stream=True, headers={'User-Agent': 'Mozilla/5.0'} ) response.raise_for_status() parsed_url = urllib.parse.urlparse(audio_url) ext = Path(parsed_url.path).suffix or '.mp3' timestamp = int(time.time() * 1000) # 毫秒级时间戳 filename = f"{filename_prefix}_{timestamp}{ext}" file_path = os.path.join(save_directory, filename) os.makedirs(save_directory, exist_ok=True) with open(file_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) return file_path # 返回完整路径 except requests.exceptions.Timeout: raise Exception(f"time out: {audio_url}") except requests.exceptions.HTTPError as e: raise Exception(f"http error {e.response.status_code}: {audio_url}") except requests.exceptions.RequestException as e: raise Exception(f"download failed: {e}") except IOError as e: raise Exception(f"save failed: {e}") except Exception as e: raise Exception(f"other failed: {e}") class KLingAIAPIClient: def __init__(self): pass @classmethod def INPUT_TYPES(s): return { "required": { "access_key": ("STRING", {"multiline": False, "default": ""}), "secret_key": ("STRING", {"multiline": False, "default": ""}), "poll_interval": ("INT", {"default": "1"}), "area": (["global", "china"],), }, } RETURN_TYPES = ("KLING_AI_API_CLIENT",) RETURN_NAMES = ("client",) FUNCTION = "create_client" OUTPUT_NODE = True CATEGORY = "KLingAI" def create_client(self, access_key, secret_key, poll_interval, area): in_china = (area == "china") if access_key == "" or secret_key == "": try: klingai_api_access_key = config['API']['KLINGAI_API_ACCESS_KEY'] klingai_api_scerct_key = config['API']['KLINGAI_API_SECRET_KEY'] if klingai_api_access_key == '': raise ValueError('ACCESS_KEY is empty') if klingai_api_scerct_key == '': raise ValueError('SECRET_KEY is empty') except KeyError: raise ValueError('unable to find ACCESS_KEY or SECRET_KEY in config.ini') client = Client(klingai_api_access_key, klingai_api_scerct_key, in_china) else: client = Client(access_key, secret_key, in_china) client.poll_interval = poll_interval return (client,) class ImageGeneratorNode: @classmethod def INPUT_TYPES(s): return { "required": { "client": ("KLING_AI_API_CLIENT",), "model": (["kling-v1", "kling-v1-5", "kling-v2", "kling-v2-new", "kling-v2-1"],), "prompt": ("STRING", {"multiline": True, "default": ""}), }, "optional": { "negative_prompt": ("STRING", {"multiline": True, "default": ""}), "image": ("IMAGE",), "image_reference": (["None", "subject", "face"],), "image_fidelity": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "round": 0.01, "display": "number", "lazy": True }), "human_fidelity": ("FLOAT", { "default": 0.45, "min": 0.0, "max": 1.0, "step": 0.1, "round": 0.01, "display": "number", "lazy": True }), "image_num": ("INT", { "default": 1, "min": 0, "max": 9, "step": 1, "display": "number", "lazy": True }), "aspect_ratio": (["16:9", "9:16", "1:1", "4:3", "3:4", "3:2", "2:3"],), "resolution": ("STRING", ["1k", "2k", ], ), } } RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) FUNCTION = "generate" OUTPUT_NODE = False CATEGORY = "KLingAI" def generate(self, client, model, prompt, negative_prompt=None, image=None, image_reference="None", image_fidelity=None, human_fidelity=None, resolution=None, image_num=None, aspect_ratio=None): generator = ImageGenerator() generator.model_name = model generator.prompt = prompt generator.negative_prompt = negative_prompt generator.resolution = resolution generator.image = _image_to_base64(image) generator.image_fidelity = image_fidelity generator.aspect_ratio = aspect_ratio generator.n = image_num generator.human_fidelity = human_fidelity if model == "kling-v2-1": generator.human_fidelity = None generator.image_fidelity = None if image_reference != 'None': generator.image_reference = image_reference response = generator.run(client) imgs = None for image_info in response.task_result.images: img = _images2tensor(_decode_image(_fetch_image(image_info.url))) if imgs is None: imgs = img else: imgs = torch.cat([imgs, img], dim=0) print(f'KLing API output: {image_info.url}') return (imgs,) class ImageExpanderNode: # TODO ? @classmethod def INPUT_TYPES(s): expansion_ratio_parameter = { "default": 0, "min": 0, "max": 2, } return { "required": { "client": ("KLING_AI_API_CLIENT",), "image": ("IMAGE",), "up_expansion_ratio": ("FLOAT", expansion_ratio_parameter), "down_expansion_ratio": ("FLOAT", expansion_ratio_parameter), "left_expansion_ratio": ("FLOAT", expansion_ratio_parameter), "right_expansion_ratio": ("FLOAT", expansion_ratio_parameter), }, "optional": { "prompt": ("STRING", {"multiline": True, "default": ""}), "image_num": ("INT", { "default": 1, "min": 0, "max": 9, "step": 1, "display": "number", "lazy": True }), } } RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) FUNCTION = "generate" OUTPUT_NODE = False CATEGORY = "KLingAI" def generate(self, client, image, prompt=None, image_num=None, up_expansion_ratio=None, down_expansion_ratio=None, left_expansion_ratio=None, right_expansion_ratio=None, ): generator = ImageExpander() generator.image = _image_to_base64(image) generator.up_expansion_ratio = up_expansion_ratio generator.down_expansion_ratio = down_expansion_ratio generator.left_expansion_ratio = left_expansion_ratio generator.right_expansion_ratio = right_expansion_ratio generator.prompt = prompt generator.n = image_num response = generator.run(client) imgs = None for image_info in response.task_result.images: img = _images2tensor(_decode_image(_fetch_image(image_info.url))) if imgs is None: imgs = img else: imgs = torch.cat([imgs, img], dim=0) print(f'KLing API output: {image_info.url}') return (imgs,) class Image2VideoNode: @classmethod def INPUT_TYPES(s): return { "required": { "client": ("KLING_AI_API_CLIENT",), "model": ( ["kling-v1", "kling-v1-5", "kling-v1-6", "kling-v2-master", "kling-v2-1", "kling-v2-1-master"],), }, "optional": { "image": ("IMAGE",), "image_tail": ("IMAGE",), "prompt": ("STRING", {"multiline": True, "default": ""}), "negative_prompt": ("STRING", {"multiline": True, "default": ""}), "cfg_scale": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "round": 0.01, "display": "number", "lazy": True }), "mode": (["std", "pro"],), "duration": (["5", "10"],), "camera_control_type": ( ["None", "simple", "down_back", "forward_up", "right_turn_forward", "left_turn_forward"],), "camera_control_config": (["horizontal", "vertical", "pan", "tilt", "roll", "zoom"],), "camera_control_value": ("FLOAT", { "default": 0.5, "min": -10.0, "max": 10.0, "step": 1.0, "round": 1.0, "display": "number", "lazy": True }) } } RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("url", "video_id") FUNCTION = "generate" OUTPUT_NODE = False CATEGORY = "KLingAI" def generate(self, client, model, image=None, image_tail=None, prompt=None, negative_prompt=None, cfg_scale=None, mode=None, duration=None, camera_control_type=None, camera_control_config=None, camera_control_value=None): generator = Image2Video() generator.model_name = model generator.image = _image_to_base64(image) generator.image_tail = _image_to_base64(image_tail) generator.prompt = prompt generator.negative_prompt = negative_prompt generator.cfg_scale = cfg_scale generator.mode = mode generator.duration = duration if camera_control_type != 'None': generator.camera_control = CameraControl() generator.camera_control.type = camera_control_type if generator.camera_control.type == "simple": generator.camera_control.config = CameraControlConfig() if camera_control_config == "horizontal": generator.camera_control.config.horizontal = camera_control_value if camera_control_config == "vertical": generator.camera_control.config.vertical = camera_control_value if camera_control_config == "pan": generator.camera_control.config.pan = camera_control_value if camera_control_config == "tilt": generator.camera_control.config.tilt = camera_control_value if camera_control_config == "roll": generator.camera_control.config.roll = camera_control_value if camera_control_config == "zoom": generator.camera_control.config.zoom = camera_control_value 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 Text2VideoNode: @classmethod def INPUT_TYPES(s): return { "required": { "client": ("KLING_AI_API_CLIENT",), "model": (["kling-v1", "kling-v1-6", "kling-v2-master", "kling-v2-1", "kling-v2-1-master"],), "prompt": ("STRING", {"multiline": True, "default": ""}), }, "optional": { "negative_prompt": ("STRING", {"multiline": True, "default": ""}), "cfg_scale": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "round": 0.01, "display": "number", "lazy": True }), "mode": (["std", "pro"],), "aspect_ratio": (["16:9", "9:16", "1:1"],), "duration": (["5", "10"],), "camera_control_type": ( ["None", "simple", "down_back", "forward_up", "right_turn_forward", "left_turn_forward"],), "camera_control_config": (["horizontal", "vertical", "pan", "tilt", "roll", "zoom"],), "camera_control_value": ("FLOAT", { "default": 0.5, "min": -10.0, "max": 10.0, "step": 1.0, "round": 1.0, "display": "number", "lazy": True }) } } RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("url", "video_id") FUNCTION = "generate" OUTPUT_NODE = False CATEGORY = "KLingAI" def generate(self, client, model, prompt, negative_prompt=None, cfg_scale=None, mode=None, aspect_ratio=None, duration=None, camera_control_type=None, camera_control_config=None, camera_control_value=None): generator = Text2Video() generator.model_name = model generator.prompt = prompt generator.negative_prompt = negative_prompt generator.cfg_scale = cfg_scale generator.mode = mode generator.aspect_ratio = aspect_ratio generator.duration = duration if camera_control_type != 'None': generator.camera_control = CameraControl() generator.camera_control.type = camera_control_type if generator.camera_control.type == "simple": generator.camera_control.config = CameraControlConfig() if camera_control_config == "horizontal": generator.camera_control.config.horizontal = camera_control_value if camera_control_config == "vertical": generator.camera_control.config.vertical = camera_control_value if camera_control_config == "pan": generator.camera_control.config.pan = camera_control_value if camera_control_config == "tilt": generator.camera_control.config.tilt = camera_control_value if camera_control_config == "roll": generator.camera_control.config.roll = camera_control_value if camera_control_config == "zoom": generator.camera_control.config.zoom = camera_control_value 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 KolorsVirtualTryOnNode: @classmethod def INPUT_TYPES(s): return { "required": { "client": ("KLING_AI_API_CLIENT",), "model_name": (["kolors-virtual-try-on-v1", "kolors-virtual-try-on-v1-5"],), "human_image": ("IMAGE",), "cloth_image": ("IMAGE",), }, } RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) FUNCTION = "generate" OUTPUT_NODE = False CATEGORY = "KLingAI" def generate(self, client, model_name, human_image, cloth_image=None): generator = KolorsVurtualTryOn() generator.model_name = model_name generator.human_image = _image_to_base64(human_image) generator.cloth_image = _image_to_base64(cloth_image) response = generator.run(client) for image_info in response.task_result.images: img = _images2tensor(_decode_image(_fetch_image(image_info.url))) print(f'KLing API output: {image_info.url}') return (img,) class PreviewVideo: @classmethod def INPUT_TYPES(cls): return { "required": { "video_url": ("STRING", {"forceInput": True}), "filename_prefix": ("STRING", {"default": "KLingAI"}), "save_output": ("BOOLEAN", {"default": True}), } } OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "KLingAI" RETURN_TYPES = ("STRING",) RETURN_NAMES = ("file_path",) def run(self, video_url, filename_prefix, save_output): if not save_output: return {"ui": {"video_url": [video_url]}, "result": ('',)} output_dir = folder_paths.get_output_directory() ( full_output_folder, filename, _, _, _, ) = folder_paths.get_save_image_path(filename_prefix, output_dir) max_counter = 0 matcher = re.compile(f"{re.escape(filename)}_(\\d+)\\D*\\..+", re.IGNORECASE) for existing_file in os.listdir(full_output_folder): match = matcher.fullmatch(existing_file) if match: file_counter = int(match.group(1)) if file_counter > max_counter: max_counter = file_counter counter = max_counter + 1 file = f"{filename}_{counter:05}.mp4" file_path = os.path.join(full_output_folder, file) if type(video_url) == list: video_url = video_url[0] open(file_path, "wb").write(_fetch_image(video_url)) return {"ui": {"video_url": [video_url]}, "result": (file_path,)} class PreviewAudio(LoadAudio): @classmethod def INPUT_TYPES(cls): return { "required": { "audio_url": ("STRING", { "forceInput": True, "default": "" }), "filename_prefix": ("STRING", { "default": "KLingAI" }), "save_output": ("BOOLEAN", { "default": True }), } } RETURN_TYPES = ("AUDIO", "STRING") RETURN_NAMES = ("audio", "file_path") FUNCTION = "run" CATEGORY = "KLingAI" OUTPUT_NODE = True @classmethod def VALIDATE_INPUTS(cls, **kwargs): return True def run(self, audio_url, filename_prefix, save_output): try: if not save_output: return { "ui": {"audio_url": [audio_url]}, "result": (None, '') } temp_directory = get_temp_directory() saved_file_path = _load_audio_from_url( audio_url=audio_url, save_directory=temp_directory, filename_prefix=filename_prefix ) audio_result = super().load(saved_file_path) audio_data = audio_result[0] return { "ui": { "audio_url": [audio_url], "file_path": [saved_file_path] }, "result": (audio_data, saved_file_path) } except Exception as e: error_msg = f"[PreviewAudio] Error: {str(e)}" print(error_msg) raise Exception(error_msg) 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", "新闻播报男": "diyinnansang_DB_CN_M_04-v2", "译制片男": "yizhipiannan-v1", "元气少女": "guanxiaofang-v2", "撒娇女友": "tianmeixuemei-v1", "刀片烟嗓": "daopianyansang-v1", "乖巧正太": "mengwa-v1", "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", "The Reader": "reader_en_m-v1", "Commercial Lady": "commercial_lady_en_f-v1" } @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"], {"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",), "input": ("KLING_AI_API_LIPSYNC_INPUT",), "face_id": ("STRING", {"multiline": False, "default": ""}) }, "optional": { "video_id": ("STRING", {"multiline": False, "default": ""}), "video_url": ("STRING", {"multiline": False, "default": ""}), } } OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "KLingAI" RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("url", "video_id") def run(self, client, input, video_id=None, video_url=None): if video_id is None and video_url is None: raise Exception(f"Please input video_id or video_url.") generator = LipSync() input.video_id = video_id input.video_url = video_url 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 ('', '') class EffectNode: @classmethod def INPUT_TYPES(cls): return { "required": { "client": ("KLING_AI_API_CLIENT",), "effect_scene": ( ["baseball", "inner_voice", "a_list_look", "memory_alive", "trampoline", "trampoline_night", "pucker_up", "guess_what", "feed_mooncake", "rampage_ape", "flyer", "dishwasher", "pet_chinese_opera", "magic_fireball", "gallery_ring", "pet_moto_rider", "muscle_pet", "squeeze_scream", "pet_delivery", "running_man", "disappear", "mythic_style", "steampunk", "c4d_cartoon", "3d_cartoon_1", "3d_cartoon_2", "eagle_snatch", "hug_from_past", "firework", 'media_interview', "pet_lion", "pet_chef", "santa_gifts", "santa_hug", "girlfriend", "boyfriend", "heart_gesture_1", "pet_wizard", "smoke_smoke", "thumbs_up", "instant_kid", "dollar_rain", "cry_cry", "building_collapse", "gun_shot", "mushroom", "double_gun", "pet_warrior", "lightning_power", "jesus_hug", "shark_alert", "long_hair", "lie_flat", "polar_bear_hug", "brown_bear_hug", "jazz_jazz", "office_escape_plow", "fly_fly", "watermelon_bomb", "pet_dance", "boss_coming", "wool_curly", "pet_bee", "marry_me", "swing_swing", "day_to_night", "piggy_morph", "wig_out", "car_explosion", "ski_ski", "tiger_hug", "siblings", "construction_worker", "let’s_ride", "snatched", "magic_broom", "felt_felt", "jumpdrop", "celebration", "splashsplash", "surfsurf", "fairy_wing", "angel_wing", "dark_wing", "skateskate", "plushcut", "jelly_press", "jelly_slice", "jelly_squish", "jelly_jiggle", "pixelpixel", "yearbook", "instant_film", "anime_figure", "rocketrocket", "bloombloom", "dizzydizzy", "fuzzyfuzzy", "squish", "expansion", "hug", "kiss", "heart_gesture", "fight" ],), "model_name": (["kling-v1", "kling-v1-5", "kling-v1-6"],), "mode": (["std", "pro"],), "duration": (["5", "10"],), "image0": ("IMAGE",), }, "optional": { "image1": ("IMAGE",) } } OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "KLingAI" RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("url", "video_id") def run(self, client, effect_scene, model_name, mode, duration, image0, image1=None): generator = Effects() generator.effect_scene = effect_scene generator.input = EffectInput() if effect_scene in ["hug", "kiss", "heart_gesture", "fight"]: generator.input.model_name = 'kling-v1-6' if effect_scene == 'fight' else model_name generator.input.mode = mode generator.input.duration = duration if image1 == None or image0 == None: raise Exception("This effect needs two images.") generator.input.images = [_image_to_base64(image0), _image_to_base64(image1)] else: generator.input.mode = mode generator.input.duration = duration if image1 != None and image0 != None: raise Exception("This effect needs one image.") generator.input.image = _image_to_base64(image0) if image1 == None else _image_to_base64(image1) 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 Video2AudioNode: @classmethod def INPUT_TYPES(s): return { "required": { "client": ("KLING_AI_API_CLIENT",), "video_id": ("STRING", {"multiline": False, "default": ""}), "video_url": ("STRING", {"multiline": False, "default": ""}), }, "optional": { "sound_effect_prompt": ("STRING", {"multiline": True, "default": ""}), "bgm_prompt": ("STRING", {"multiline": True, "default": ""}), "asmr_mod": ("BOOLEAN", {"multiline": False, "default": False}), } } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING") RETURN_NAMES = ("videos_id", "videos_url", "audio_id", "audio_url_mp3") FUNCTION = "generate" OUTPUT_NODE = False CATEGORY = "KLingAI" def generate(self, client, video_id, video_url, sound_effect_prompt=None, bgm_prompt=None, asmr_mode=False, ): generator = Video2Audio() generator.video_id = video_id generator.video_url = video_url if video_id is None and video_url is None: raise Exception("Please input video_id or video_url") if video_id is not None and video_url is not None: raise Exception("Please input one of video_id or video_url") generator.sound_effect_prompt = sound_effect_prompt generator.bgm_prompt = bgm_prompt generator.asmr_mode = asmr_mode response = generator.run(client) results = [] for audio_info in response.task_result.audios: results.append({ "videos_id": audio_info["videos_id"], "videos_url": audio_info["videos_url"], "audio_id": audio_info["audio_id"], "audio_url": audio_info["audio_url"] }) return (results) class MultiImagesToVideoNode: pass class TextToAudioNode: @classmethod def INPUT_TYPES(s): return { "required": { "client": ("KLING_AI_API_CLIENT",), "prompt": ("STRING", {"multiline": True, "default": ""}), "duration": ("FLOAT", { "default": 3.0, "min": 3.0, "max": 10.0, "step": 0.1, "display": "number", },), }, } RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("id", "url") FUNCTION = "generate" OUTPUT_NODE = True CATEGORY = "KLingAI" def generate(self, client, prompt, duration, ): generator = Text2Audio() generator.prompt = prompt generator.duration = duration response = generator.run(client) url_mp3 = getattr(response.task_result.audios[0], 'url_mp3', None) if not isinstance(url_mp3, str) or not url_mp3.strip(): raise Exception(f"url_mp3 无效,当前值为:{url_mp3}") audio_id = getattr(response.task_result.audios[0], 'audio_id', None) print(f"成功提取:audio_id={audio_id}, url_mp3={url_mp3}") return (audio_id, url_mp3)