diff --git a/clientlibs/utils.js b/clientlibs/utils.js index 7d324f3..48e358a 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -8,6 +8,9 @@ const TIMEOUT_RANGE = { min: 5, default: 90, max: 99 }; const OUTPUT_QUALITY_RANGE = { min: 20, default: 95, max: 99 }; const CACHE_SIZE_RANGE = { min: 30, default: 150, max: 4096 }; +/** Must match `audioInferenceInputs.MAX_VIDEOS` in modules/audioInferenceInputs.py */ +const AUDIO_INFERENCE_INPUTS_MAX_VIDEOS = 4; + let openDialog = false; let lastTimeout = false; let lastOutputFormat = false; @@ -1173,6 +1176,20 @@ function audioSettingsToggleHandler(settingsNode) { const turboWidget = settingsNode.widgets.find(w => w && w.name === "turbo"); const useTextNormalizationWidget = settingsNode.widgets.find(w => w && w.name === "useTextNormalization"); const textNormalizationWidget = settingsNode.widgets.find(w => w && w.name === "textNormalization"); + const useBpmWidget = settingsNode.widgets.find(w => w && w.name === "useBpm"); + const bpmWidget = settingsNode.widgets.find(w => w && w.name === "bpm"); + const useKeyScaleWidget = settingsNode.widgets.find(w => w && w.name === "useKeyScale"); + const keyScaleWidget = settingsNode.widgets.find(w => w && w.name === "keyScale"); + const useTimeSignatureWidget = settingsNode.widgets.find(w => w && w.name === "useTimeSignature"); + const timeSignatureWidget = settingsNode.widgets.find(w => w && w.name === "timeSignature"); + const useVocalLanguageWidget = settingsNode.widgets.find(w => w && w.name === "useVocalLanguage"); + const vocalLanguageWidget = settingsNode.widgets.find(w => w && w.name === "vocalLanguage"); + const useCoverConditioningScaleWidget = settingsNode.widgets.find(w => w && w.name === "useCoverConditioningScale"); + const coverConditioningScaleWidget = settingsNode.widgets.find(w => w && w.name === "coverConditioningScale"); + const useRepaintingStartWidget = settingsNode.widgets.find(w => w && w.name === "useRepaintingStart"); + const repaintingStartWidget = settingsNode.widgets.find(w => w && w.name === "repaintingStart"); + const useRepaintingEndWidget = settingsNode.widgets.find(w => w && w.name === "useRepaintingEnd"); + const repaintingEndWidget = settingsNode.widgets.find(w => w && w.name === "repaintingEnd"); function toggleWidgetState(useWidget, paramWidget, paramName) { if (!useWidget || !paramWidget) return; @@ -1196,6 +1213,13 @@ function audioSettingsToggleHandler(settingsNode) { if (useLanguageBoostWidget && languageBoostWidget) toggleWidgetState(useLanguageBoostWidget, languageBoostWidget, "languageBoost"); if (useTurboWidget && turboWidget) toggleWidgetState(useTurboWidget, turboWidget, "turbo"); if (useTextNormalizationWidget && textNormalizationWidget) toggleWidgetState(useTextNormalizationWidget, textNormalizationWidget, "textNormalization"); + if (useBpmWidget && bpmWidget) toggleWidgetState(useBpmWidget, bpmWidget, "bpm"); + if (useKeyScaleWidget && keyScaleWidget) toggleWidgetState(useKeyScaleWidget, keyScaleWidget, "keyScale"); + if (useTimeSignatureWidget && timeSignatureWidget) toggleWidgetState(useTimeSignatureWidget, timeSignatureWidget, "timeSignature"); + if (useVocalLanguageWidget && vocalLanguageWidget) toggleWidgetState(useVocalLanguageWidget, vocalLanguageWidget, "vocalLanguage"); + if (useCoverConditioningScaleWidget && coverConditioningScaleWidget) toggleWidgetState(useCoverConditioningScaleWidget, coverConditioningScaleWidget, "coverConditioningScale"); + if (useRepaintingStartWidget && repaintingStartWidget) toggleWidgetState(useRepaintingStartWidget, repaintingStartWidget, "repaintingStart"); + if (useRepaintingEndWidget && repaintingEndWidget) toggleWidgetState(useRepaintingEndWidget, repaintingEndWidget, "repaintingEnd"); } function videoSettingsToggleHandler(settingsNode) { @@ -2797,6 +2821,8 @@ function audioModelSearchFilterHandler(audioModelSearchNode) { ], "Ace": [ "runware:ace-step@0 (ACE Step v1 3.5B)", + "runware:ace-step@v1.5-base (ACE-Step v1.5 Base)", + "runware:ace-step@v1.5-turbo (ACE-Step v1.5 Turbo)", ], "Dia": [ "runware:dia@v1.0 (Dia 1.6B)", @@ -4116,10 +4142,14 @@ function regionalPromptingRegionsToggleHandler(regionsNode) { } function audioInferenceInputsToggleHandler(audioInputsNode) { + if (!audioInputsNode?.widgets) return; + // 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"); + const useAudioWidget = audioInputsNode.widgets.find(w => w && w.name === "useAudio"); + const audioWidget = audioInputsNode.widgets.find(w => w && w.name === "Audio"); + const useVideoWidget = audioInputsNode.widgets.find(w => w && w.name === "useVideo"); + const videoWidget = audioInputsNode.widgets.find(w => w && w.name === "Video"); + const useVideosWidget = audioInputsNode.widgets.find(w => w && w.name === "useVideos"); // Helper function to toggle widget enabled state function toggleWidgetState(useWidget, paramWidget, paramName) { @@ -4163,18 +4193,34 @@ function audioInferenceInputsToggleHandler(audioInputsNode) { setTimeout(toggleEnabled, 100); } + // Set up toggle handler for single audio + if (useAudioWidget && audioWidget) { + toggleWidgetState(useAudioWidget, audioWidget, "Audio"); + } + // 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) + // Set up toggle handlers for multiple videos (Video1 … VideoN; N from widgets, fallback to AUDIO_INFERENCE_INPUTS_MAX_VIDEOS) if (useVideosWidget) { + function getAudioInferenceVideoSlotCount() { + let max = 0; + for (const w of audioInputsNode.widgets) { + if (!w?.name) continue; + const m = /^Video(\d+)$/.exec(w.name); + if (m) max = Math.max(max, parseInt(m[1], 10)); + } + return max > 0 ? max : AUDIO_INFERENCE_INPUTS_MAX_VIDEOS; + } + function toggleVideosEnabled() { const enabled = useVideosWidget.value === true; - - for (let i = 1; i <= 4; i++) { - const videoWidget = audioInputsNode.widgets.find(w => w.name === `Video${i}`); + const slotCount = getAudioInferenceVideoSlotCount(); + + for (let i = 1; i <= slotCount; i++) { + const videoWidget = audioInputsNode.widgets.find(w => w && w.name === `Video${i}`); if (videoWidget) { if (videoWidget.inputEl) { videoWidget.inputEl.disabled = !enabled; diff --git a/modules/audioInferenceInputs.py b/modules/audioInferenceInputs.py index 7d670b7..524ffca 100644 --- a/modules/audioInferenceInputs.py +++ b/modules/audioInferenceInputs.py @@ -10,6 +10,14 @@ class audioInferenceInputs: @classmethod def INPUT_TYPES(cls): optionalInputs = { + "useAudio": ("BOOLEAN", { + "tooltip": "Enable to include single audio input in API request.", + "default": False, + }), + "Audio": ("STRING", { + "tooltip": "Audio URL or mediaUUID for audio generation. Can be a direct URL or a mediaUUID from Runware Media Upload node. Only used when 'Use Audio' is enabled.", + "default": "", + }), "useVideo": ("BOOLEAN", { "tooltip": "Enable to include single video input in API request.", "default": False, @@ -37,7 +45,7 @@ class audioInferenceInputs: "optional": optionalInputs } - DESCRIPTION = "Configure custom inputs for Runware Audio Inference, including single or multiple video inputs for audio extraction or generation." + DESCRIPTION = "Configure custom inputs for Runware Audio Inference, including optional audio URL/mediaUUID, and single or multiple video inputs for audio extraction or generation." FUNCTION = "createInputs" RETURN_TYPES = ("RUNWAREAUDIOINFERENCEINPUTS",) RETURN_NAMES = ("Audio Inference Inputs",) @@ -45,12 +53,18 @@ class audioInferenceInputs: def createInputs(self, **kwargs) -> tuple[Dict[str, Any]]: """Create audio inference inputs from provided parameters""" + useAudio = kwargs.get("useAudio", False) + audio = kwargs.get("Audio", None) useVideo = kwargs.get("useVideo", False) video = kwargs.get("Video", None) useVideos = kwargs.get("useVideos", False) inputs = {} - + + # Handle single audio (inputs.audio) + if useAudio and audio is not None and audio.strip() != "": + inputs["audio"] = audio.strip() + # Handle single video (inputs.video) if useVideo and video is not None and video.strip() != "": inputs["video"] = video.strip() diff --git a/modules/audioModelSearch.py b/modules/audioModelSearch.py index 7e3a607..e5f881b 100644 --- a/modules/audioModelSearch.py +++ b/modules/audioModelSearch.py @@ -16,6 +16,8 @@ class RunwareAudioModelSearch: ], "Ace": [ "runware:ace-step@0 (ACE Step v1 3.5B)", + "runware:ace-step@v1.5-base (ACE-Step v1.5 Base)", + "runware:ace-step@v1.5-turbo (ACE-Step v1.5 Turbo)", ], "Dia": [ "runware:dia@v1.0 (Dia 1.6B)", diff --git a/modules/audioSettings.py b/modules/audioSettings.py index 4f4810c..e5a7151 100644 --- a/modules/audioSettings.py +++ b/modules/audioSettings.py @@ -1,14 +1,22 @@ """ Runware Audio Inference Settings Node -Provides lyrics, guidanceType, languageBoost, turbo, temperature, and textNormalization -settings for Runware Audio Inference. +Provides lyrics, guidanceType, languageBoost, turbo, temperature, textNormalization, +bpm, keyScale, timeSignature, vocalLanguage, coverConditioningScale, repaintingStart, +and repaintingEnd for Runware Audio Inference. """ from typing import Dict, Any +_VOCAL_LANGUAGE_CODES = [ + "ar", "az", "bg", "bn", "ca", "cs", "da", "de", "el", "en", "es", "fa", "fi", "fr", + "he", "hi", "hr", "ht", "hu", "id", "is", "it", "ja", "ko", "la", "lt", "ms", "ne", + "nl", "no", "pa", "pl", "pt", "ro", "ru", "sa", "sk", "sr", "sv", "sw", "ta", "te", + "th", "tl", "tr", "uk", "ur", "vi", "yue", "zh", "unknown", +] + class RunwareAudioSettings: - """Runware Audio Inference Settings Node (lyrics, guidanceType, languageBoost, turbo, temperature, textNormalization).""" + """Runware Audio Inference Settings Node.""" @classmethod def INPUT_TYPES(cls): @@ -41,7 +49,7 @@ class RunwareAudioSettings: "default": False, }), "guidanceType": (["apg", "cfg"], { - "tooltip": "Controls how the guidance value is used. apg = Adversarial Perceptual Guidance, cfg = Classifier-Free Guidance", + "tooltip": "Controls how the guidance value is used. See API GuidanceType: apg | cfg.", "default": "apg", }), "useLanguageBoost": ("BOOLEAN", { @@ -76,6 +84,73 @@ class RunwareAudioSettings: "label_on": "true", "label_off": "false", }), + "useBpm": ("BOOLEAN", { + "tooltip": "Enable to include bpm (tempo) in audio generation settings. If disabled, the model decides automatically.", + "default": False, + }), + "bpm": ("INT", { + "tooltip": "Beats per minute (30–300). If not set via API, the model decides. Common: 60–80 ballad, 90–110 pop, 120–130 house, 140–170 DnB. Only used when 'Use Bpm' is enabled.", + "default": 120, + "min": 30, + "max": 300, + "step": 1, + }), + "useKeyScale": ("BOOLEAN", { + "tooltip": "Enable to include keyScale (musical key and scale). Empty value lets the model decide.", + "default": False, + }), + "keyScale": ("STRING", { + "default": "", + "tooltip": "Musical key and scale. Empty string lets the model decide. Format: '{Note}{Accidental} {Mode}' (Note A–G, Accidental '' | '#' | 'b' | '♯' | '♭', Mode 'major' | 'minor'). Examples: 'C major', 'F# minor', 'B♭ major'.", + }), + "useTimeSignature": ("BOOLEAN", { + "tooltip": "Enable to include timeSignature (beats per measure). If disabled, the model decides automatically.", + "default": False, + }), + "timeSignature": ("INT", { + "default": 4, + "min": 1, + "max": 32, + "step": 1, + "tooltip": "Time signature as integer beats per bar (e.g. 2, 3, 4, 6). Only used when 'Use Time Signature' is enabled.", + }), + "useVocalLanguage": ("BOOLEAN", { + "tooltip": "Enable to include vocalLanguage (language code) for vocal generation. See dropdown for available codes.", + "default": False, + }), + "vocalLanguage": (_VOCAL_LANGUAGE_CODES, { + "tooltip": "Language code for vocals. Supports values from the dropdown list (including 'yue', 'zh', and 'unknown'). Use 'unknown' for instrumental or automatic detection. Only used when 'Use Vocal Language' is enabled.", + "default": "en", + }), + "useCoverConditioningScale": ("BOOLEAN", { + "tooltip": "Enable to include coverConditioningScale (source-audio conditioning fraction for cover tasks). Only effective when input audio is provided.", + "default": False, + }), + "coverConditioningScale": ("FLOAT", { + "tooltip": "Fraction of denoising steps using source-audio conditioning (cover task). 1.0 = all steps; 0.5 = first half source then text-only. Only used when 'Use Cover Conditioning Scale' is enabled.", + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.01, + }), + "useRepaintingStart": ("BOOLEAN", { + "tooltip": "Enable to include repaintingStart (seconds). Requires input audio. Sets repaint region with repaintingEnd; negative values prepend (outpaint before start).", + "default": False, + }), + "repaintingStart": ("FLOAT", { + "tooltip": "Start time in seconds for repaint/inpaint/outpaint region. Only used when 'Use Repainting Start' is enabled.", + "default": 0.0, + "step": 0.01, + }), + "useRepaintingEnd": ("BOOLEAN", { + "tooltip": "Enable to include repaintingEnd (seconds). Requires input audio. Values beyond source duration append (outpaint).", + "default": False, + }), + "repaintingEnd": ("FLOAT", { + "tooltip": "End time in seconds for repaint region. Only used when 'Use Repainting End' is enabled.", + "default": 0.0, + "step": 0.01, + }), } } @@ -83,7 +158,7 @@ class RunwareAudioSettings: RETURN_NAMES = ("settings",) FUNCTION = "createSettings" CATEGORY = "Runware/Audio" - DESCRIPTION = "Configure audio generation settings (lyrics, guidanceType, languageBoost, turbo, temperature, textNormalization) for Runware Audio Inference. Connect to Runware Audio Inference node." + DESCRIPTION = "Configure audio generation settings (lyrics, guidanceType, languageBoost, turbo, temperature, textNormalization, bpm, keyScale, timeSignature, vocalLanguage, coverConditioningScale, repaintingStart, repaintingEnd) for Runware Audio Inference. Connect to Runware Audio Inference node." def createSettings(self, **kwargs) -> tuple[Dict[str, Any]]: """Create audio settings dict for API""" @@ -99,6 +174,20 @@ class RunwareAudioSettings: turbo = kwargs.get("turbo", False) use_text_normalization = kwargs.get("useTextNormalization", False) text_normalization = kwargs.get("textNormalization", True) + use_bpm = kwargs.get("useBpm", False) + bpm = kwargs.get("bpm", 120) + use_key_scale = kwargs.get("useKeyScale", False) + key_scale = kwargs.get("keyScale", "") + use_time_signature = kwargs.get("useTimeSignature", False) + time_signature = kwargs.get("timeSignature", 4) + use_vocal_language = kwargs.get("useVocalLanguage", False) + vocal_language = kwargs.get("vocalLanguage", "en") + use_cover_conditioning_scale = kwargs.get("useCoverConditioningScale", False) + cover_conditioning_scale = kwargs.get("coverConditioningScale", 1.0) + use_repainting_start = kwargs.get("useRepaintingStart", False) + repainting_start = kwargs.get("repaintingStart", 0.0) + use_repainting_end = kwargs.get("useRepaintingEnd", False) + repainting_end = kwargs.get("repaintingEnd", 0.0) settings: Dict[str, Any] = {} @@ -120,6 +209,27 @@ class RunwareAudioSettings: if use_text_normalization: settings["textNormalization"] = bool(text_normalization) + if use_bpm: + settings["bpm"] = int(bpm) + + if use_key_scale and str(key_scale).strip(): + settings["keyScale"] = str(key_scale).strip() + + if use_time_signature: + settings["timeSignature"] = int(time_signature) + + if use_vocal_language: + settings["vocalLanguage"] = vocal_language + + if use_cover_conditioning_scale: + settings["coverConditioningScale"] = float(cover_conditioning_scale) + + if use_repainting_start: + settings["repaintingStart"] = float(repainting_start) + + if use_repainting_end: + settings["repaintingEnd"] = float(repainting_end) + return (settings,)