支持V3omni模型

This commit is contained in:
magicwang1111
2026-07-08 15:50:39 +08:00
parent f8177df577
commit dfc62cc6c1
6 changed files with 338 additions and 20 deletions
+12 -5
View File
@@ -14,7 +14,7 @@ It bundles image generation, text-to-video, image-to-video, multi-image video, a
## Current model visibility
The plugin keeps the visible model dropdowns aligned with models verified on the active endpoint.
The plugin keeps the visible model dropdowns aligned with models confirmed for the active integration paths.
Visible image generation models:
@@ -34,6 +34,7 @@ Visible text-to-video models:
- `kling-v2-5-turbo`
- `kling-v2-6`
- `kling-v3`
- `kling-v3-omni`
Visible image-to-video models:
@@ -46,26 +47,29 @@ Visible image-to-video models:
- `kling-v2-5-turbo`
- `kling-v2-6`
- `kling-v3`
- `kling-v3-omni`
Visible multi-image video models:
- `kling-v2-1`
- `kling-v2-5-turbo`
- `kling-v3`
- `kling-v3-omni`
Visible video generation modes:
- `std`
- `pro`
- `4k` (`kling-v3` text-to-video, image-to-video, and multi-image video only; the API uses `mode: "4k"`)
- `4k` (`kling-v3` and `kling-v3-omni` text-to-video, image-to-video, and multi-image video; the API uses `mode: "4k"`)
Documented newer models such as `kling-image-o1`, `kling-video-o1`, and `kling-v3-omni` still exist in the internal capability map, but are hidden from the UI until live endpoint support is confirmed for normal accounts.
Documented newer models such as `kling-image-o1` and `kling-video-o1` still exist in the internal capability map, but are hidden from the UI until live endpoint support is confirmed for normal accounts.
## Important limitations
- Native 4K video output is exposed through the video `mode` dropdown. Models that do not support `mode=4k` will be rejected by the node capability checks before submission.
- Native preset and cloned voices are available for `kling-v2-6`; `Image2Video.custom_voice_id` sends cloned voice control through `voice_list`. The active API rejects this field for `kling-v3`.
- `kling-v3-omni` and `kling-video-o1` remain hidden until live endpoint support is confirmed for normal accounts.
- `kling-v3-omni` is visible for text-to-video, image-to-video, and multi-image video comparison. It can combine `voice_list`/`custom_voice_id` with `element_list`; reference-video requests remain guarded because native audio and `mode=4k` are rejected when `reference_video` is provided.
- `kling-video-o1` remains hidden until live endpoint support is confirmed for normal accounts.
- `Advanced Element Create` requires 1 frontal subject image plus 1-3 additional reference images of the same subject. Background images do not count as reference images.
- `Advanced Element Create` also enforces `element_name <= 20` characters and `element_description <= 100` characters.
- If you need both strong subject consistency and voiced output, the reliable flow on the current endpoint is usually: generate the bound subject video first, then add speech with `Lip Sync`, `TextToAudio`, or `Video2Audio`.
@@ -150,6 +154,7 @@ Python dependencies are listed in [requirements.txt](./requirements.txt).
- `Comfyui-Kling-Wrapper TTS`
- `Comfyui-Kling-Wrapper Custom Voice Create`
- `Comfyui-Kling-Wrapper Custom Voice Query`
- `Comfyui-Kling-Wrapper Custom Voice List Builder`
- `Comfyui-Kling-Wrapper Video2Audio`
- `Comfyui-Kling-Wrapper Advanced Element Create`
- `Comfyui-Kling-Wrapper Advanced Element Query`
@@ -219,7 +224,7 @@ When `duration` is set to `auto`, the node tries to match the uploaded reference
## Voice presets
`Text2Video` and `Image2Video` expose a `voice_preset` dropdown. `Image2Video` additionally accepts a connected `custom_voice_id`.
`Text2Video` and `Image2Video` expose a `voice_preset` dropdown. `Image2Video` additionally accepts a connected `custom_voice_id` for one custom voice, or a connected `custom_voice_list` from `Custom Voice List Builder` for multiple custom voices.
The selected preset is converted to:
@@ -229,6 +234,8 @@ The selected preset is converted to:
These presets are mainly intended for models that support native audio generation on the active endpoint, especially `kling-v2-6`.
For multiple custom voices, connect one or more `Custom Voice Create.voice_id` outputs to `Custom Voice List Builder`, then connect its `custom_voice_list` output to `Image2Video.custom_voice_list`. Set `sound=on` and reference each voice in order with prompt markers such as `<<<voice_1>>>` and `<<<voice_2>>>`. `custom_voice_list`, `custom_voice_id`, and `voice_preset` are mutually exclusive.
For official or manually supplied preset voice IDs, use the dedicated `TTS` node. It calls `/v1/audio/tts` and returns an `audio_url` that can be saved or previewed with `Preview Audio`.
## Examples
+3 -2
View File
@@ -11,17 +11,18 @@ This directory contains importable ComfyUI workflow JSON examples and small Pyth
- Replace placeholder filenames such as `example_portrait.png`, `example_subject_front.png`, `example_subject_ref_1.png`, `example_cloth.png`, `example_scene.png`, `example_lipsync_source.mp4`, `example_lipsync_audio.wav`, and `example_voice_reference.mp3` with files that exist in your ComfyUI input directory.
- Replace placeholder URLs such as `https://example.com/reference-motion.mp4` with real URLs when needed.
- Native preset and cloned voices are available for `kling-v2-6`; the active API rejects `voice_list` for `kling-v3`.
- Newer documented models such as `kling-video-o1` and `kling-v3-omni` are intentionally hidden from the visible dropdowns until live endpoint support is confirmed.
- `kling-v3-omni` is visible in the video generation dropdowns for comparison with `kling-v3`; `kling-video-o1` remains hidden until live endpoint support is confirmed.
## Important workflow notes
- `05_comfyui_kling_wrapper_text2video_v26_sound.json` is the native audio example and is built around `kling-v2-6`.
- `06_comfyui_kling_wrapper_advanced_element_subject_to_image2video.json` requires 1 frontal portrait plus 1-3 additional photos of the same subject. Background or scene images do not count as advanced-element reference images.
- If you need both subject binding and speech, generate the bound video first and then add speech with the lip-sync or audio nodes.
- `kling-v3-omni` can combine `custom_voice_id` or `custom_voice_list` with `element_list`; older models still keep stricter local guards.
- `Motion Control` accepts either a direct http(s) reference-video URL or uploaded/local video workflow inputs. For local media, use a video loader node that provides either `VIDEO` or `reference_video_frames` plus `reference_video_info`. Set `duration` to `auto` if you want it to match the uploaded motion reference length.
- `Lip Sync` now calls `identify-face` and `advanced-lip-sync`; leave `face_id` empty to use the first detected face.
- `Avatar` accepts one and only one of `AUDIO`, `audio_id`, `audio_file`, or `audio_url`.
- `Image2Video.custom_voice_id` is supported by `kling-v2-6`; it requires `sound=on` and the `<<<voice_1>>>` marker in the prompt.
- `Image2Video.custom_voice_id` is the single-custom-voice path. For multiple voices, connect `Custom Voice List Builder.custom_voice_list` to `Image2Video.custom_voice_list`, set `sound=on`, and use ordered markers such as `<<<voice_1>>>` and `<<<voice_2>>>`.
## Included files
+2 -1
View File
@@ -1,5 +1,5 @@
from .api.capabilities import NODE_PREFIX
from .nodes import AdvancedCustomElementCreateNode, AdvancedCustomElementDeleteNode, AdvancedCustomElementQueryNode, AvatarNode, EffectNode, ElementListBuilderNode, \
from .nodes import AdvancedCustomElementCreateNode, AdvancedCustomElementDeleteNode, AdvancedCustomElementQueryNode, AvatarNode, CustomVoiceListBuilderNode, EffectNode, ElementListBuilderNode, \
Image2VideoNode, ImageExpanderNode, ImageGeneratorNode, KolorsVirtualTryOnNode, LipSyncAudioInputNode, \
LipSyncNode, LipSyncTextInputNode, MotionControlNode, MultiImagesToVideoNode, MultiModalVideoEditNode, PreviewAudio, PreviewVideo, Text2VideoNode, \
TextToAudioNode, Video2AudioNode, VideoExtendNode, CustomVoiceCreateNode, CustomVoiceQueryNode, TTSNode
@@ -16,6 +16,7 @@ NODE_CLASS_MAPPINGS = {
_node_name("TTS"): TTSNode,
_node_name("Custom Voice Create"): CustomVoiceCreateNode,
_node_name("Custom Voice Query"): CustomVoiceQueryNode,
_node_name("Custom Voice List Builder"): CustomVoiceListBuilderNode,
_node_name("Avatar"): AvatarNode,
_node_name("Video2Audio"): Video2AudioNode,
_node_name("Text2Video"): Text2VideoNode,
+9 -5
View File
@@ -5,6 +5,7 @@ CLIENT_TYPE = "COMFYUI_KLING_WRAPPER_API_CLIENT"
LIPSYNC_INPUT_TYPE = "COMFYUI_KLING_WRAPPER_LIPSYNC_INPUT"
ELEMENT_TYPE = "COMFYUI_KLING_WRAPPER_ELEMENT"
ELEMENT_LIST_TYPE = "COMFYUI_KLING_WRAPPER_ELEMENT_LIST"
CUSTOM_VOICE_LIST_TYPE = "COMFYUI_KLING_WRAPPER_CUSTOM_VOICE_LIST"
DEFAULT_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "1:1"]
EXTENDED_IMAGE_ASPECT_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4", "3:2", "2:3", "21:9", "auto"]
@@ -248,8 +249,8 @@ VIDEO_MODEL_CAPABILITIES = {
"supports_image_list": True,
"supports_element_list": True,
"supports_reference_video": True,
"supports_sound": False,
"supports_voice_list": False,
"supports_sound": True,
"supports_voice_list": True,
"supports_negative_prompt": False,
"supports_cfg_scale": False,
"shot_types": ["single", "intelligence"],
@@ -258,9 +259,9 @@ VIDEO_MODEL_CAPABILITIES = {
}
# ComfyUI input options are static, so we keep the visible dropdowns aligned
# with models verified on the active endpoint. Newer documented models may
# still exist in VIDEO_MODEL_CAPABILITIES for future use, but remain hidden
# until live account support is confirmed.
# with models confirmed for the active integration paths. Some documented
# models may still exist in VIDEO_MODEL_CAPABILITIES for future use, but remain
# hidden until endpoint support is confirmed.
IMAGE_GENERATION_MODELS = [
"kling-v1",
"kling-v1-5",
@@ -278,6 +279,7 @@ TEXT_TO_VIDEO_MODELS = [
"kling-v2-5-turbo",
"kling-v2-6",
"kling-v3",
"kling-v3-omni",
]
IMAGE_TO_VIDEO_MODELS = [
@@ -290,6 +292,7 @@ IMAGE_TO_VIDEO_MODELS = [
"kling-v2-5-turbo",
"kling-v2-6",
"kling-v3",
"kling-v3-omni",
]
# Multi-image generation is routed through image2video with a primary `image`
@@ -298,6 +301,7 @@ MULTI_IMAGE_TO_VIDEO_MODELS = [
"kling-v2-1",
"kling-v2-5-turbo",
"kling-v3",
"kling-v3-omni",
]
+119 -7
View File
@@ -2,11 +2,12 @@ from .api import AdvancedLipSync, Avatar, Client, FaceIdentify, ImageGenerator,
Video2Audio, Text2Audio, Text2Video, CameraControl, CameraControlConfig, KolorsVurtualTryOn, VideoExtend, \
LipSyncInput, EffectInput, Effects, MultiImages2Video, MultiModelVideoEdit, AdvancedCustomElements, \
MotionControl, CustomVoiceCreate, CustomVoiceQuery, TTS
from .api.capabilities import DEFAULT_IMAGE_RESOLUTIONS, DEFAULT_MODES, DEFAULT_VIDEO_ASPECT_RATIOS, \
DEFAULT_VIDEO_DURATIONS, ELEMENT_LIST_TYPE, ELEMENT_TYPE, EXTENDED_IMAGE_ASPECT_RATIOS, IMAGE_GENERATION_MODELS, \
IMAGE_TO_VIDEO_MODELS, LIPSYNC_INPUT_TYPE, MULTI_IMAGE_TO_VIDEO_MODELS, NODE_CATEGORY, NODE_PREFIX, SHOT_TYPES, \
SOUND_OPTIONS, TEXT_TO_VIDEO_MODELS, VIDEO_GENERATION_MODES, get_image_capability, get_video_capability, \
validate_image_generation_request, validate_video_generation_request
from .api.capabilities import CUSTOM_VOICE_LIST_TYPE, DEFAULT_IMAGE_RESOLUTIONS, DEFAULT_MODES, \
DEFAULT_VIDEO_ASPECT_RATIOS, DEFAULT_VIDEO_DURATIONS, ELEMENT_LIST_TYPE, ELEMENT_TYPE, \
EXTENDED_IMAGE_ASPECT_RATIOS, IMAGE_GENERATION_MODELS, IMAGE_TO_VIDEO_MODELS, LIPSYNC_INPUT_TYPE, \
MULTI_IMAGE_TO_VIDEO_MODELS, NODE_CATEGORY, NODE_PREFIX, SHOT_TYPES, SOUND_OPTIONS, TEXT_TO_VIDEO_MODELS, \
VIDEO_GENERATION_MODES, get_image_capability, get_video_capability, validate_image_generation_request, \
validate_video_generation_request
from .api.exceptions import KLingAPIError
from .api.providers import (
OFFICIAL_PROVIDER,
@@ -616,6 +617,61 @@ def _build_voice_list_from_preset(voice_preset):
return [{"voice_id": voice_id}]
def _normalize_voice_reference(voice):
if voice is None:
return None
if isinstance(voice, dict):
voice_id = str(voice.get("voice_id") or voice.get("id") or "").strip()
if not voice_id:
raise ValueError("Voice payload is missing voice_id.")
return {"voice_id": voice_id}
if isinstance(voice, str):
voice_id = voice.strip()
if not voice_id:
return None
return {"voice_id": voice_id}
raise ValueError("Voice must be a dict or string.")
def _normalize_voice_list(voices):
if voices is None:
return None
if isinstance(voices, str):
cleaned = voices.strip()
if not cleaned:
return None
if cleaned[0] in "[{":
decoded = _parse_json_input(cleaned, "custom_voice_list")
voices = decoded
else:
voices = [line.strip() for line in cleaned.replace(",", "\n").splitlines()]
if isinstance(voices, dict):
voices = [voices]
normalized = []
for voice in voices:
payload = _normalize_voice_reference(voice)
if payload:
normalized.append(payload)
return normalized or None
def _validate_voice_prompt_markers(prompt, voice_list):
if not voice_list:
return
prompt_text = str(prompt or "")
missing_markers = [
f"<<<voice_{index}>>>"
for index in range(1, len(voice_list) + 1)
if f"<<<voice_{index}>>>" not in prompt_text
]
if missing_markers:
raise ValueError(
"custom_voice_list requires prompt markers: "
+ ", ".join(missing_markers)
+ "."
)
def _apply_prompt_controls(target, capability, prompt=None, negative_prompt=None, cfg_scale=None):
cleaned_prompt = _clean_optional_text(prompt)
cleaned_negative_prompt = _clean_optional_text(negative_prompt)
@@ -1960,6 +2016,7 @@ class Image2VideoNode:
"sound": (SOUND_OPTIONS,),
"voice_preset": (VOICE_PRESET_OPTIONS, {"default": DEFAULT_VOICE_PRESET}),
"custom_voice_id": ("STRING", {"forceInput": True}),
"custom_voice_list": (CUSTOM_VOICE_LIST_TYPE,),
"shot_type": (SHOT_TYPES,),
"image_list": ("IMAGE",),
"element_list": (ELEMENT_LIST_TYPE,),
@@ -1992,6 +2049,7 @@ class Image2VideoNode:
sound="off",
voice_preset=DEFAULT_VOICE_PRESET,
custom_voice_id="",
custom_voice_list=None,
shot_type="single",
image_list=None,
element_list=None,
@@ -2001,16 +2059,28 @@ class Image2VideoNode:
mode = _normalize_requested_mode(model, capability, mode)
normalized_element_list = _normalize_element_list(element_list)
preset_voice_list = _build_voice_list_from_preset(voice_preset)
normalized_custom_voice_list = _normalize_voice_list(custom_voice_list)
custom_voice_id = str(custom_voice_id or "").strip()
if custom_voice_id and preset_voice_list:
raise ValueError("Use custom_voice_id or voice_preset, not both.")
if normalized_custom_voice_list and (custom_voice_id or preset_voice_list):
raise ValueError("Use custom_voice_list, custom_voice_id, or voice_preset, not more than one.")
if custom_voice_id and sound != "on":
raise ValueError("custom_voice_id requires sound='on'.")
if normalized_custom_voice_list and sound != "on":
raise ValueError("custom_voice_list requires sound='on'.")
if custom_voice_id and "<<<voice_1>>>" not in str(prompt or ""):
raise ValueError("custom_voice_id requires the prompt marker <<<voice_1>>>.")
if custom_voice_id and normalized_element_list:
_validate_voice_prompt_markers(prompt, normalized_custom_voice_list)
if custom_voice_id and normalized_element_list and model != "kling-v3-omni":
raise ValueError("custom_voice_id cannot be used together with element_list.")
parsed_voice_list = [{"voice_id": custom_voice_id}] if custom_voice_id else preset_voice_list
if normalized_custom_voice_list and normalized_element_list and model != "kling-v3-omni":
raise ValueError("custom_voice_list cannot be used together with element_list.")
parsed_voice_list = (
normalized_custom_voice_list
or ([{"voice_id": custom_voice_id}] if custom_voice_id else None)
or preset_voice_list
)
encoded_image_list = _image_batch_to_base64_list(image_list)
has_reference_video = isinstance(reference_video, str) and bool(reference_video.strip())
has_camera_control = camera_control_type not in (None, "None")
@@ -3439,6 +3509,48 @@ class ElementListBuilderNode:
)
class CustomVoiceListBuilderNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"voice_1": ("STRING", {"forceInput": True}),
},
"optional": {
"voice_2": ("STRING", {"forceInput": True}),
"voice_3": ("STRING", {"forceInput": True}),
"voice_4": ("STRING", {"forceInput": True}),
"extra_voice_ids": ("STRING", {"multiline": True, "default": ""}),
}
}
RETURN_TYPES = (CUSTOM_VOICE_LIST_TYPE, "STRING")
RETURN_NAMES = ("custom_voice_list", "custom_voice_list_json")
FUNCTION = "build"
OUTPUT_NODE = False
CATEGORY = NODE_CATEGORY
def build(self, voice_1, voice_2=None, voice_3=None, voice_4=None, extra_voice_ids=""):
voice_refs = []
for voice_id in (voice_1, voice_2, voice_3, voice_4):
if voice_id is not None:
voice_refs.append(voice_id)
for raw_line in (extra_voice_ids or "").replace(",", "\n").splitlines():
voice_id = raw_line.strip()
if voice_id:
voice_refs.append(voice_id)
normalized = _normalize_voice_list(voice_refs)
if not normalized:
raise ValueError("At least one voice_id is required.")
return (
normalized,
json.dumps(normalized, ensure_ascii=False),
)
class MotionControlNode:
@classmethod
def INPUT_TYPES(cls):
+193
View File
@@ -0,0 +1,193 @@
import importlib
import sys
import types
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
COMFYUI_ROOT = REPO_ROOT.parent.parent
if str(COMFYUI_ROOT) not in sys.path:
sys.path.insert(0, str(COMFYUI_ROOT))
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
def _install_import_stubs():
if "torch" not in sys.modules:
sys.modules["torch"] = types.SimpleNamespace()
if "comfy_extras.nodes_audio" not in sys.modules:
audio_module = types.ModuleType("comfy_extras.nodes_audio")
audio_module.LoadAudio = object
sys.modules["comfy_extras.nodes_audio"] = audio_module
class V3OmniVisibilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
_install_import_stubs()
cls.capabilities = importlib.import_module("py.api.capabilities")
cls.nodes = importlib.import_module("py.nodes")
cls.tencent_video = importlib.import_module("py.api.providers.tencent_vod.video")
def test_v3_omni_is_visible_for_video_comparison_nodes(self):
for model_list_name in (
"TEXT_TO_VIDEO_MODELS",
"IMAGE_TO_VIDEO_MODELS",
"MULTI_IMAGE_TO_VIDEO_MODELS",
):
with self.subTest(model_list_name=model_list_name):
self.assertIn(
"kling-v3-omni",
getattr(self.capabilities, model_list_name),
)
def test_tencent_vod_v3_omni_model_version_matches_docs(self):
self.assertEqual(
self.tencent_video.TencentVodVideoProvider._resolve_model_version("kling-v3-omni"),
"3.0-Omni",
)
def test_v3_omni_reference_video_capability_stays_guarded(self):
capability = self.capabilities.validate_video_generation_request(
task_name="image2video",
model_name="kling-v3-omni",
mode="pro",
duration="5",
has_reference_video=True,
)
self.assertTrue(capability["supports_reference_video"])
self.assertTrue(capability["supports_sound"])
self.assertTrue(capability["supports_voice_list"])
def test_v3_omni_allows_custom_voice_with_element_list(self):
captured_requests = []
class FakeProvider:
def generate_image2video(self, request):
captured_requests.append(request)
return SimpleNamespace(
url="https://example.com/video.mp4",
video_id="video-omni",
final_unit_deduction=None,
)
def close(self):
pass
with mock.patch.object(self.nodes, "_create_video_provider", return_value=FakeProvider()):
url, video_id = self.nodes.Image2VideoNode().generate(
model="kling-v3-omni",
provider="official",
prompt='The subject <<<voice_1>>> says: "Hello."',
mode="pro",
duration="5",
sound="on",
custom_voice_id="voice-custom",
element_list=[{"element_id": "element-123"}],
)
self.assertEqual(url, "https://example.com/video.mp4")
self.assertEqual(video_id, "video-omni")
self.assertEqual(captured_requests[0].voice_list, [{"voice_id": "voice-custom"}])
self.assertEqual(captured_requests[0].element_list, [{"element_id": "element-123"}])
def test_custom_voice_list_builder_collects_multiple_voice_ids(self):
voice_list, voice_list_json = self.nodes.CustomVoiceListBuilderNode().build(
voice_1="voice-a",
voice_2="voice-b",
extra_voice_ids="voice-c\nvoice-d",
)
self.assertEqual(
voice_list,
[
{"voice_id": "voice-a"},
{"voice_id": "voice-b"},
{"voice_id": "voice-c"},
{"voice_id": "voice-d"},
],
)
self.assertIn("voice-d", voice_list_json)
def test_custom_voice_list_builder_is_registered(self):
mappings = importlib.import_module("py").NODE_CLASS_MAPPINGS
self.assertIn("Comfyui-Kling-Wrapper Custom Voice List Builder", mappings)
def test_v3_omni_custom_voice_list_sends_multiple_voices(self):
captured_requests = []
class FakeProvider:
def generate_image2video(self, request):
captured_requests.append(request)
return SimpleNamespace(
url="https://example.com/video.mp4",
video_id="video-omni",
final_unit_deduction=None,
)
def close(self):
pass
custom_voice_list = [
{"voice_id": "voice-a"},
{"voice_id": "voice-b"},
]
with mock.patch.object(self.nodes, "_create_video_provider", return_value=FakeProvider()):
self.nodes.Image2VideoNode().generate(
model="kling-v3-omni",
provider="official",
prompt='<<<voice_1>>> says hello. <<<voice_2>>> replies.',
mode="pro",
duration="5",
sound="on",
custom_voice_list=custom_voice_list,
element_list=[{"element_id": "element-123"}],
)
self.assertEqual(captured_requests[0].voice_list, custom_voice_list)
self.assertEqual(captured_requests[0].element_list, [{"element_id": "element-123"}])
def test_custom_voice_list_requires_matching_prompt_markers(self):
with self.assertRaisesRegex(ValueError, "<<<voice_2>>>"):
self.nodes.Image2VideoNode().generate(
model="kling-v3-omni",
provider="official",
prompt="<<<voice_1>>> says hello.",
mode="pro",
duration="5",
sound="on",
custom_voice_list=[{"voice_id": "voice-a"}, {"voice_id": "voice-b"}],
)
def test_older_custom_voice_models_still_reject_element_list(self):
with self.assertRaisesRegex(ValueError, "cannot be used together with element_list"):
self.nodes.Image2VideoNode().generate(
model="kling-v2-6",
provider="official",
prompt='The subject <<<voice_1>>> says: "Hello."',
mode="pro",
duration="5",
sound="on",
custom_voice_id="voice-custom",
element_list=[{"element_id": "element-123"}],
)
with self.assertRaisesRegex(ValueError, "custom_voice_list cannot be used together with element_list"):
self.nodes.Image2VideoNode().generate(
model="kling-v2-6",
provider="official",
prompt='<<<voice_1>>> says hello. <<<voice_2>>> replies.',
mode="pro",
duration="5",
sound="on",
custom_voice_list=[{"voice_id": "voice-a"}, {"voice_id": "voice-b"}],
element_list=[{"element_id": "element-123"}],
)
if __name__ == "__main__":
unittest.main()