Merge pull request #64 from Runware/feature-KlingAvatar

Add Kling Avatar support and fix enable/disbaling id on speech/audio
This commit is contained in:
Sirshendu Ganguly
2025-12-20 20:27:31 -05:00
committed by GitHub
4 changed files with 159 additions and 15 deletions
+5 -1
View File
@@ -1,6 +1,6 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler } from "./utils.js";
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler } from "./utils.js";
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
const nodeInitList = [];
@@ -81,6 +81,10 @@ app.registerExtension({
syncSegmentToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.SETTINGS) {
settingsToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINPUT) {
audioInputToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.SPEECHINPUT) {
speechInputToggleHandler(node);
}
if(crNodeProps.colorModeOnly === true) return;
+122
View File
@@ -1684,6 +1684,8 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"klingai:7@1 (KlingAI Lip-Sync)",
"klingai:kling@o1 (Kling VIDEO O1)",
"klingai:kling-video@2.6-pro (Kling VIDEO 2.6 Pro)",
"klingai:avatar@2.0-standard (KlingAI Avatar 2.0 Standard)",
"klingai:avatar@2.0-pro (KlingAI Avatar 2.0 Pro)",
],
"Veo": [
"google:2@0 (Veo 2.0)", "google:3@0 (Veo 3.0)", "google:3@1 (Veo 3.0 Fast)",
@@ -1752,6 +1754,8 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"klingai:7@1": {"width": 0, "height": 0},
"klingai:kling@o1": {"width": 1440, "height": 1440},
"klingai:kling-video@2.6-pro": {"width": 1920, "height": 1080},
"klingai:avatar@2.0-standard": {"width": 0, "height": 0},
"klingai:avatar@2.0-pro": {"width": 0, "height": 0},
"google:2@0": {"width": 1280, "height": 720},
"google:3@0": {"width": 1280, "height": 720},
"google:3@1": {"width": 1280, "height": 720},
@@ -1811,6 +1815,8 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"klingai:7@1": null, // No resolution support
"klingai:kling@o1": null, // No standard resolution
"klingai:kling-video@2.6-pro": "1080p",
"klingai:avatar@2.0-standard": null, // No resolution support
"klingai:avatar@2.0-pro": null, // No resolution support
"google:2@0": "720p",
"google:3@0": "720p",
"google:3@1": "720p",
@@ -2517,6 +2523,120 @@ function briaProviderSettingsToggleHandler(briaNode) {
}
}
function audioInputToggleHandler(audioInputNode) {
// Find "use" parameter widget and "id" widget for Audio Input
const useIdWidget = audioInputNode.widgets.find(w => w.name === "useId");
const idWidget = audioInputNode.widgets.find(w => w.name === "id");
// Helper function to toggle widget enabled state
function toggleWidgetState(useWidget, paramWidget, paramName) {
if (!useWidget || !paramWidget) return;
function toggleEnabled() {
const enabled = useWidget.value === true;
// Disable/enable widgets using inputEl if available
if (paramWidget.inputEl) {
paramWidget.inputEl.disabled = !enabled;
paramWidget.inputEl.style.opacity = enabled ? "1" : "0.5";
paramWidget.inputEl.style.cursor = enabled ? "text" : "not-allowed";
paramWidget.inputEl.readOnly = !enabled;
}
// Also set widget property
paramWidget.disabled = !enabled;
// Fallback: try to find inputs via DOM if inputEl is not available
if (!paramWidget.inputEl) {
const nodeElement = audioInputNode.htmlElements?.widgetsContainer || audioInputNode.htmlElements;
if (nodeElement) {
const input = nodeElement.querySelector(`input[name="${paramName}"], textarea[name="${paramName}"], select[name="${paramName}"]`);
if (input) {
input.disabled = !enabled;
input.style.opacity = enabled ? "1" : "0.5";
input.style.cursor = enabled ? "text" : "not-allowed";
input.readOnly = !enabled;
if (input.tagName === "SELECT") {
input.style.pointerEvents = enabled ? "auto" : "none";
}
}
}
}
audioInputNode.setDirtyCanvas(true);
}
// Set up callback
appendWidgetCB(useWidget, () => {
setTimeout(toggleEnabled, 50);
});
// Initial call to set initial state
setTimeout(toggleEnabled, 100);
}
// Set up toggle handler for "useId" parameter
if (useIdWidget && idWidget) {
toggleWidgetState(useIdWidget, idWidget, "id");
}
}
function speechInputToggleHandler(speechInputNode) {
// Find "use" parameter widget and "id" widget for Speech Input
const useIdWidget = speechInputNode.widgets.find(w => w.name === "useId");
const idWidget = speechInputNode.widgets.find(w => w.name === "id");
// Helper function to toggle widget enabled state
function toggleWidgetState(useWidget, paramWidget, paramName) {
if (!useWidget || !paramWidget) return;
function toggleEnabled() {
const enabled = useWidget.value === true;
// Disable/enable widgets using inputEl if available
if (paramWidget.inputEl) {
paramWidget.inputEl.disabled = !enabled;
paramWidget.inputEl.style.opacity = enabled ? "1" : "0.5";
paramWidget.inputEl.style.cursor = enabled ? "text" : "not-allowed";
paramWidget.inputEl.readOnly = !enabled;
}
// Also set widget property
paramWidget.disabled = !enabled;
// Fallback: try to find inputs via DOM if inputEl is not available
if (!paramWidget.inputEl) {
const nodeElement = speechInputNode.htmlElements?.widgetsContainer || speechInputNode.htmlElements;
if (nodeElement) {
const input = nodeElement.querySelector(`input[name="${paramName}"], textarea[name="${paramName}"], select[name="${paramName}"]`);
if (input) {
input.disabled = !enabled;
input.style.opacity = enabled ? "1" : "0.5";
input.style.cursor = enabled ? "text" : "not-allowed";
input.readOnly = !enabled;
if (input.tagName === "SELECT") {
input.style.pointerEvents = enabled ? "auto" : "none";
}
}
}
}
speechInputNode.setDirtyCanvas(true);
}
// Set up callback
appendWidgetCB(useWidget, () => {
setTimeout(toggleEnabled, 50);
});
// Initial call to set initial state
setTimeout(toggleEnabled, 100);
}
// Set up toggle handler for "useId" parameter
if (useIdWidget && idWidget) {
toggleWidgetState(useIdWidget, idWidget, "id");
}
}
export {
notifyUser,
promptEnhanceHandler,
@@ -2548,6 +2668,8 @@ export {
syncProviderSettingsToggleHandler,
syncSegmentToggleHandler,
settingsToggleHandler,
audioInputToggleHandler,
speechInputToggleHandler,
};
function googleProviderSettingsToggleHandler(googleNode) {
+16 -7
View File
@@ -13,16 +13,21 @@ class RunwareAudioInput:
def INPUT_TYPES(cls):
return {
"required": {
"id": ("STRING", {
"tooltip": "Reference ID used to map audio input. Required when using segments array.",
"default": "",
}),
"source": ("STRING", {
"tooltip": "URL, base64, data URI, or UUID for the audio file. Audio formats: WAV, MP3, OGG, M4A, M3A, AAC, WMA, FLAC, MP4. Recommended: WAV/MP3 at 44.1kHz or 48kHz sampling rate.",
"default": "",
}),
},
"optional": {}
"optional": {
"useId": ("BOOLEAN", {
"tooltip": "Enable to include reference ID for mapping audio input. Required when using segments array.",
"default": False,
}),
"id": ("STRING", {
"tooltip": "Reference ID used to map audio input. Required when using segments array.",
"default": "",
}),
}
}
RETURN_TYPES = ("RUNWAREAUDIOINPUT",)
@@ -34,17 +39,21 @@ class RunwareAudioInput:
def createAudioInput(self, **kwargs) -> tuple[Dict[str, Any]]:
"""Create audio input configuration"""
useId = kwargs.get("useId", False)
audio_id = kwargs.get("id", "").strip()
source = kwargs.get("source", "").strip()
if not audio_id or not source:
if not source:
return ({},)
audioInput: Dict[str, Any] = {
"id": audio_id,
"source": source,
}
# Only include id if useId is enabled and id is provided
if useId and audio_id:
audioInput["id"] = audio_id
return (audioInput,)
+16 -7
View File
@@ -13,10 +13,6 @@ class RunwareSpeechInput:
def INPUT_TYPES(cls):
return {
"required": {
"id": ("STRING", {
"tooltip": "Reference ID used to map speech input. Required when using segments array.",
"default": "",
}),
"provider": ("STRING", {
"tooltip": "TTS provider name. Currently supports: elevenlabs",
"default": "elevenlabs",
@@ -31,7 +27,16 @@ class RunwareSpeechInput:
"default": "",
}),
},
"optional": {}
"optional": {
"useId": ("BOOLEAN", {
"tooltip": "Enable to include reference ID for mapping speech input. Required when using segments array.",
"default": False,
}),
"id": ("STRING", {
"tooltip": "Reference ID used to map speech input. Required when using segments array.",
"default": "",
}),
}
}
RETURN_TYPES = ("RUNWARESPEECHINPUT",)
@@ -43,21 +48,25 @@ class RunwareSpeechInput:
def createSpeechInput(self, **kwargs) -> tuple[Dict[str, Any]]:
"""Create speech input configuration"""
useId = kwargs.get("useId", False)
speech_id = kwargs.get("id", "").strip()
provider = kwargs.get("provider", "elevenlabs").strip()
voiceId = kwargs.get("voiceId", "").strip()
text = kwargs.get("text", "").strip()
if not speech_id or not provider or not voiceId or not text:
if not provider or not voiceId or not text:
return ({},)
speechInput: Dict[str, Any] = {
"id": speech_id,
"provider": provider,
"voiceId": voiceId,
"text": text,
}
# Only include id if useId is enabled and id is provided
if useId and speech_id:
speechInput["id"] = speech_id
return (speechInput,)