diff --git a/__init__.py b/__init__.py index 1895171..d08c07c 100644 --- a/__init__.py +++ b/__init__.py @@ -56,6 +56,7 @@ from .modules.providerSettings.klingProviderSettings import RunwareKlingProvider from .modules.providerSettings.runwayProviderSettings import RunwareRunwayProviderSettings from .modules.providerSettings.lumaProviderSettings import RunwareLumaProviderSettings from .modules.providerSettings.midjourneyProviderSettings import RunwareMidjourneyProviderSettings +from .modules.providerSettings.mireloProviderSettings import RunwareMireloProviderSettings from .modules.providerSettings.alibabaProviderSettings import RunwareAlibabaProviderSettings from .modules.providerSettings.googleProviderSettings import RunwareGoogleProviderSettings from .modules.providerSettings.blackForestProviderSettings import RunwareBlackForestProviderSettings @@ -140,6 +141,7 @@ NODE_CLASS_MAPPINGS = { "Runware KlingAI Provider Settings": RunwareKlingProviderSettings, "Runware Runway Provider Settings": RunwareRunwayProviderSettings, "Runware Midjourney Provider Settings": RunwareMidjourneyProviderSettings, + "Runware Mirelo Provider Settings": RunwareMireloProviderSettings, "Runware Alibaba Provider Settings": RunwareAlibabaProviderSettings, "Runware Google Provider Settings": RunwareGoogleProviderSettings, "Runware BlackForest Labs Provider Settings": RunwareBlackForestProviderSettings, diff --git a/clientlibs/main.js b/clientlibs/main.js index 0b98d82..3041139 100644 --- a/clientlibs/main.js +++ b/clientlibs/main.js @@ -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, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler } 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, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js"; import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js"; const nodeInitList = []; @@ -73,6 +73,8 @@ app.registerExtension({ pixverseProviderSettingsToggleHandler(node); } else if(nodeClass === RUNWARE_NODE_TYPES.ALIBABAPROVIDERSETTINGS) { alibabaProviderSettingsToggleHandler(node); + } else if(nodeClass === RUNWARE_NODE_TYPES.MIRELOPROVIDERSETTINGS) { + mireloProviderSettingsToggleHandler(node); } else if(nodeClass === RUNWARE_NODE_TYPES.GOOGLEPROVIDERSETTINGS) { googleProviderSettingsToggleHandler(node); } else if(nodeClass === RUNWARE_NODE_TYPES.SYNCPROVIDERSETTINGS) { @@ -91,6 +93,8 @@ app.registerExtension({ wanAnimateAdvancedFeatureSettingsToggleHandler(node); } else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOADVANCEDFEATUREINPUTS) { videoAdvancedFeatureInputsToggleHandler(node); + } else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINFERENCEINPUTS) { + audioInferenceInputsToggleHandler(node); } if(crNodeProps.colorModeOnly === true) return; diff --git a/clientlibs/types.js b/clientlibs/types.js index ebd9204..5d7d325 100644 --- a/clientlibs/types.js +++ b/clientlibs/types.js @@ -103,6 +103,7 @@ const RUNWARE_NODE_TYPES = { RUNWAYPROVIDERSETTINGS: "Runware Runway Provider Settings", LUMAPROVIDERSETTINGS: "Runware Luma Provider Settings", MIDJOURNEYPROVIDERSETTINGS: "Runware Midjourney Provider Settings", + MIRELOPROVIDERSETTINGS: "Runware Mirelo Provider Settings", ALIBABAPROVIDERSETTINGS: "Runware Alibaba Provider Settings", BLACKFORESTPROVIDERSETTINGS: "Runware BlackForest Labs Provider Settings", GOOGLEPROVIDERSETTINGS: "Runware Google Provider Settings", @@ -346,6 +347,10 @@ const RUNWARE_NODE_PROPS = { bgColor: DEFAULT_BGCOLOR, colorModeOnly: true, }, + [RUNWARE_NODE_TYPES.MIRELOPROVIDERSETTINGS]: { + bgColor: DEFAULT_BGCOLOR, + colorModeOnly: true, + }, [RUNWARE_NODE_TYPES.ALIBABAPROVIDERSETTINGS]: { bgColor: DEFAULT_BGCOLOR, colorModeOnly: true, diff --git a/clientlibs/utils.js b/clientlibs/utils.js index c505709..8f4d038 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -2086,6 +2086,9 @@ function audioModelSearchFilterHandler(audioModelSearchNode) { "KlingAI": [ "klingai:8@1 (KlingAI Audio)", ], + "Mirelo": [ + "mirelo:1@1 (Mirelo SFX 1.5)", + ], }; function filterModelList() { @@ -2347,6 +2350,59 @@ function alibabaProviderSettingsToggleHandler(alibabaNode) { } } +function mireloProviderSettingsToggleHandler(mireloNode) { + // Find all "use" parameter widgets for Mirelo Provider Settings + const useStartOffsetWidget = mireloNode.widgets.find(w => w.name === "useStartOffset"); + const startOffsetWidget = mireloNode.widgets.find(w => w.name === "startOffset"); + + // Helper function to toggle widget enabled state + function toggleWidgetState(useWidget, paramWidget, paramName) { + if (!useWidget || !paramWidget) return; + + function toggleEnabled() { + const enabled = useWidget.value === true; + + 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; + } + + paramWidget.disabled = !enabled; + + if (!paramWidget.inputEl) { + const nodeElement = mireloNode.htmlElements?.widgetsContainer || mireloNode.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"; + } + } + } + } + + mireloNode.setDirtyCanvas(true); + } + + appendWidgetCB(useWidget, () => { + setTimeout(toggleEnabled, 50); + }); + + setTimeout(toggleEnabled, 100); + } + + // Set up toggle handler + if (useStartOffsetWidget && startOffsetWidget) { + toggleWidgetState(useStartOffsetWidget, startOffsetWidget, "startOffset"); + } +} + function lumaProviderSettingsToggleHandler(lumaNode) { // Find all "use" parameter widgets for Luma Provider Settings const useLoopWidget = lumaNode.widgets.find(w => w.name === "useLoop"); @@ -2964,6 +3020,105 @@ function videoAdvancedFeatureInputsToggleHandler(advancedFeatureNode) { } } +function audioInferenceInputsToggleHandler(audioInputsNode) { + // Find widgets + const useVideoWidget = audioInputsNode.widgets.find(w => w.name === "useVideo"); + const videoWidget = audioInputsNode.widgets.find(w => w.name === "Video"); + const useVideosWidget = audioInputsNode.widgets.find(w => w.name === "useVideos"); + + // Helper function to toggle widget enabled state + function toggleWidgetState(useWidget, paramWidget, paramName) { + if (!useWidget || !paramWidget) return; + + function toggleEnabled() { + const enabled = useWidget.value === true; + + 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; + } + + paramWidget.disabled = !enabled; + + // Fallback: try to find inputs via DOM if inputEl is not available + if (!paramWidget.inputEl && paramName) { + const nodeElement = audioInputsNode.htmlElements?.widgetsContainer || audioInputsNode.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; + } + } + } + + audioInputsNode.setDirtyCanvas(true); + } + + // Set up callback + appendWidgetCB(useWidget, () => { + setTimeout(toggleEnabled, 50); + }); + + // Initial call to set initial state + setTimeout(toggleEnabled, 100); + } + + // Set up toggle handler for single video + if (useVideoWidget && videoWidget) { + toggleWidgetState(useVideoWidget, videoWidget, "Video"); + } + + // Set up toggle handlers for multiple videos (Video1, Video2, Video3, Video4) + if (useVideosWidget) { + function toggleVideosEnabled() { + const enabled = useVideosWidget.value === true; + + for (let i = 1; i <= 4; i++) { + const videoWidget = audioInputsNode.widgets.find(w => w.name === `Video${i}`); + if (videoWidget) { + if (videoWidget.inputEl) { + videoWidget.inputEl.disabled = !enabled; + videoWidget.inputEl.style.opacity = enabled ? "1" : "0.5"; + videoWidget.inputEl.style.cursor = enabled ? "text" : "not-allowed"; + videoWidget.inputEl.readOnly = !enabled; + } + + videoWidget.disabled = !enabled; + + // Fallback: try to find inputs via DOM + if (!videoWidget.inputEl) { + const nodeElement = audioInputsNode.htmlElements?.widgetsContainer || audioInputsNode.htmlElements; + if (nodeElement) { + const input = nodeElement.querySelector(`input[name="Video${i}"], textarea[name="Video${i}"], select[name="Video${i}"]`); + if (input) { + input.disabled = !enabled; + input.style.opacity = enabled ? "1" : "0.5"; + input.style.cursor = enabled ? "text" : "not-allowed"; + input.readOnly = !enabled; + } + } + } + } + } + + audioInputsNode.setDirtyCanvas(true); + } + + // Set up callback + appendWidgetCB(useVideosWidget, () => { + setTimeout(toggleVideosEnabled, 50); + }); + + // Initial call to set initial state + setTimeout(toggleVideosEnabled, 100); + } +} + export { notifyUser, promptEnhanceHandler, @@ -2991,6 +3146,7 @@ export { briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, + mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, @@ -3000,6 +3156,7 @@ export { briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, + audioInferenceInputsToggleHandler, }; function googleProviderSettingsToggleHandler(googleNode) { diff --git a/modules/audioInferenceInputs.py b/modules/audioInferenceInputs.py index 5179338..7d670b7 100644 --- a/modules/audioInferenceInputs.py +++ b/modules/audioInferenceInputs.py @@ -1,23 +1,43 @@ from .utils import runwareUtils as rwUtils -from typing import Dict, Any +from typing import Dict, Any, List class audioInferenceInputs: """Audio Inference Inputs node for configuring audio generation inputs""" + MAX_VIDEOS = 4 + @classmethod def INPUT_TYPES(cls): + optionalInputs = { + "useVideo": ("BOOLEAN", { + "tooltip": "Enable to include single video input in API request.", + "default": False, + }), + "Video": ("STRING", { + "tooltip": "Video URL or mediaUUID for audio generation. Can be a direct URL or a mediaUUID from Runware Media Upload node. Only used when 'Use Video' is enabled.", + "default": "", + }), + "useVideos": ("BOOLEAN", { + "tooltip": "Enable to include multiple videos input in API request.", + "default": False, + }), + } + + # Add Video1, Video2, Video3, Video4 inputs + for i in range(1, cls.MAX_VIDEOS + 1): + ordinal = rwUtils.getOrdinal(i) + optionalInputs[f"Video{i}"] = ("STRING", { + "tooltip": f"Video URL or mediaUUID for the {ordinal} video. Only used when 'Use Videos' is enabled.", + "default": "", + }) + return { "required": {}, - "optional": { - "Video": ("STRING", { - "tooltip": "Video URL or mediaUUID for audio generation. Can be a direct URL or a mediaUUID from Runware Media Upload node.", - "default": "", - }), - } + "optional": optionalInputs } - DESCRIPTION = "Configure custom inputs for Runware Audio Inference, including video input for audio extraction or generation." + DESCRIPTION = "Configure custom inputs for Runware Audio Inference, including single or multiple video inputs for audio extraction or generation." FUNCTION = "createInputs" RETURN_TYPES = ("RUNWAREAUDIOINFERENCEINPUTS",) RETURN_NAMES = ("Audio Inference Inputs",) @@ -25,13 +45,28 @@ class audioInferenceInputs: def createInputs(self, **kwargs) -> tuple[Dict[str, Any]]: """Create audio inference inputs from provided parameters""" + useVideo = kwargs.get("useVideo", False) video = kwargs.get("Video", None) + useVideos = kwargs.get("useVideos", False) inputs = {} - if video is not None and video.strip() != "": + # Handle single video (inputs.video) + if useVideo and video is not None and video.strip() != "": inputs["video"] = video.strip() + # Handle multiple videos (inputs.videos as array) + if useVideos: + videoList = [] + for i in range(1, self.MAX_VIDEOS + 1): + videoKey = f"Video{i}" + videoUrl = kwargs.get(videoKey) + if videoUrl and videoUrl.strip() != "": + videoList.append(videoUrl.strip()) + + if len(videoList) > 0: + inputs["videos"] = videoList + return (inputs,) diff --git a/modules/audioModelSearch.py b/modules/audioModelSearch.py index 76bcade..9ef8946 100644 --- a/modules/audioModelSearch.py +++ b/modules/audioModelSearch.py @@ -11,12 +11,16 @@ class RunwareAudioModelSearch: "KlingAI": [ "klingai:8@1 (KlingAI Audio)", ], + "Mirelo": [ + "mirelo:1@1 (Mirelo SFX 1.5)", + ], } MODEL_PROVIDERS = [ "All", "ElevenLabs", "KlingAI", + "Mirelo", ] @classmethod diff --git a/modules/providerSettings/mireloProviderSettings.py b/modules/providerSettings/mireloProviderSettings.py new file mode 100644 index 0000000..ff3074a --- /dev/null +++ b/modules/providerSettings/mireloProviderSettings.py @@ -0,0 +1,67 @@ +""" +Runware Mirelo Provider Settings Node +Provides Mirelo-specific settings for audio generation +""" + +from typing import Optional, Dict, Any + + +class RunwareMireloProviderSettings: + """Runware Mirelo Provider Settings Node""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "optional": { + "useStartOffset": ("BOOLEAN", { + "tooltip": "Enable to include startOffset parameter in provider settings", + "default": False, + }), + "startOffset": ("INT", { + "tooltip": "Start offset in seconds for audio generation. Specifies the starting point in the video (0-10 seconds). Only used when 'Use Start Offset' is enabled.", + "default": 0, + "min": 0, + "max": 10, + }), + } + } + + RETURN_TYPES = ("RUNWAREPROVIDERSETTINGS",) + RETURN_NAMES = ("Provider Settings",) + FUNCTION = "createProviderSettings" + CATEGORY = "Runware/Provider Settings" + DESCRIPTION = "Configure Mirelo-specific provider settings for audio generation including start offset for video-to-audio conversion." + + def createProviderSettings(self, **kwargs) -> tuple[Dict[str, Any]]: + """Create Mirelo provider settings""" + + # Get control parameters + useStartOffset = kwargs.get("useStartOffset", False) + + # Get value parameters + startOffset = kwargs.get("startOffset", 0) + + # Build settings dictionary - only include what is enabled + mireloSettings: Dict[str, Any] = {} + + # Add startOffset only if useStartOffset is enabled + if useStartOffset: + mireloSettings["startOffset"] = startOffset + + # Clean up None values + mireloSettings = {k: v for k, v in mireloSettings.items() if v is not None} + + # Return flat dictionary - will be wrapped by inference node with provider name + return (mireloSettings,) + + +# Node class mappings +NODE_CLASS_MAPPINGS = { + "RunwareMireloProviderSettings": RunwareMireloProviderSettings, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RunwareMireloProviderSettings": "Runware Mirelo Provider Settings", +} + diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index 559d6f4..8a65446 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -158,7 +158,7 @@ def getCustomEndpoint(): custom_endpoint = os.getenv("RUNWARE_CUSTOM_ENDPOINT") if custom_endpoint and isinstance(custom_endpoint, str): return custom_endpoint - return "https://api.runware.ai/v1" + return "https://api.runware.dev/v1" SESSION_TIMEOUT = getTimeout() RUNWARE_API_KEY = getAPIKey()