支持V3omni模型
This commit is contained in:
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user