Merge pull request #73 from Runware/feature-mireloAudioInference

Add Mirelo provider settings for audioInference, and Creatify Aurora video models
This commit is contained in:
Sirshendu Ganguly
2026-01-12 12:49:21 -05:00
committed by GitHub
15 changed files with 451 additions and 28 deletions
+2
View File
@@ -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,
+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, 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;
+5
View File
@@ -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,
+171
View File
@@ -966,6 +966,8 @@ function audioInferenceToggleHandler(audioInferenceNode) {
const sampleRateWidget = audioInferenceNode.widgets.find(w => w.name === "sampleRate");
const useBitrateWidget = audioInferenceNode.widgets.find(w => w.name === "useBitrate");
const bitrateWidget = audioInferenceNode.widgets.find(w => w.name === "bitrate");
const useStepsWidget = audioInferenceNode.widgets.find(w => w.name === "useSteps");
const stepsWidget = audioInferenceNode.widgets.find(w => w.name === "steps");
// Helper function to toggle widget enabled state (exact same pattern)
function toggleWidgetState(useWidget, paramWidget, paramName) {
@@ -1024,6 +1026,10 @@ function audioInferenceToggleHandler(audioInferenceNode) {
if (useBitrateWidget && bitrateWidget) {
toggleWidgetState(useBitrateWidget, bitrateWidget, "bitrate");
}
if (useStepsWidget && stepsWidget) {
toggleWidgetState(useStepsWidget, stepsWidget, "steps");
}
}
function acceleratorOptionsToggleHandler(acceleratorNode) {
@@ -1748,6 +1754,10 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"Bria": [
"bria:60@1 (Bria Video Eraser)",
],
"Creatify": [
"creatify:aurora@fast (Creatify Aurora Avatar Model API (720p))",
"creatify:aurora@0 (Creatify Aurora Avatar Model API (720p))",
],
};
const MODEL_DIMENSIONS = {
@@ -1812,6 +1822,8 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"sync:lipsync-2-pro@1": {"width": 0, "height": 0},
"sync:react-1@1": {"width": 0, "height": 0},
"bria:60@1": {"width": 1280, "height": 720},
"creatify:aurora@fast": {"width": 1280, "height": 720},
"creatify:aurora@0": {"width": 1280, "height": 720},
};
const MODEL_RESOLUTIONS = {
@@ -1876,6 +1888,8 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
"sync:lipsync-2-pro@1": "720p",
"sync:react-1@1": "720p",
"bria:60@1": "720p",
"creatify:aurora@fast": "720p",
"creatify:aurora@0": "720p",
};
const DEFAULT_DIMENSIONS = {"width": 1024, "height": 576};
@@ -2086,6 +2100,9 @@ function audioModelSearchFilterHandler(audioModelSearchNode) {
"KlingAI": [
"klingai:8@1 (KlingAI Audio)",
],
"Mirelo": [
"mirelo:1@1 (Mirelo SFX 1.5)",
],
};
function filterModelList() {
@@ -2347,6 +2364,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 +3034,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 +3160,7 @@ export {
briaProviderSettingsToggleHandler,
pixverseProviderSettingsToggleHandler,
alibabaProviderSettingsToggleHandler,
mireloProviderSettingsToggleHandler,
googleProviderSettingsToggleHandler,
syncProviderSettingsToggleHandler,
syncSegmentToggleHandler,
@@ -3000,6 +3170,7 @@ export {
briaProviderMaskToggleHandler,
wanAnimateAdvancedFeatureSettingsToggleHandler,
videoAdvancedFeatureInputsToggleHandler,
audioInferenceInputsToggleHandler,
};
function googleProviderSettingsToggleHandler(googleNode) {
+129 -16
View File
@@ -5,6 +5,7 @@ import numpy as np
import librosa
import tempfile
import os
import comfy.model_management
from .utils import runwareUtils as rwUtils
@@ -72,7 +73,7 @@ class RunwareAudioInference:
"default": "URL",
"tooltip": "Output type for the generated audio"
}),
"outputFormat": (["MP3"], {
"outputFormat": (["MP3", "MP4"], {
"default": "MP3",
"tooltip": "Format of the output audio"
}),
@@ -82,6 +83,16 @@ class RunwareAudioInference:
"max": 3,
"tooltip": "Number of audio files to generate"
}),
"useSteps": ("BOOLEAN", {
"tooltip": "Enable to include steps parameter in API request. Disable if your model doesn't support steps.",
"default": False,
}),
"steps": ("INT", {
"tooltip": "Number of inference steps for audio generation. More steps generally result in higher quality but longer generation time.",
"default": 20,
"min": 1,
"max": 100,
}),
},
"optional": {
"inputs": ("RUNWAREAUDIOINFERENCEINPUTS", {
@@ -93,8 +104,8 @@ class RunwareAudioInference:
}
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
RETURN_TYPES = ("AUDIO", "VIDEO")
RETURN_NAMES = ("audio", "video")
FUNCTION = "generateAudio"
CATEGORY = "Runware/Audio"
@@ -102,15 +113,102 @@ class RunwareAudioInference:
"""Main function to generate audio using Runware API"""
params = self._extractParameters(kwargs)
genConfig = self._buildGenConfig(params)
genResult = rwUtils.inferenecRequest(genConfig)
self._logResponse(genResult)
self._validateResponse(genResult)
try:
# Debug: Print the request being sent
print(f"[DEBUG] Sending Audio Inference Request:")
print(f"[DEBUG] Request Payload: {rwUtils.safe_json_dumps(genConfig, indent=2)}")
genResult = rwUtils.inferenecRequest(genConfig)
# Debug: Print the response received
print(f"[DEBUG] Received Audio Inference Response:")
print(f"[DEBUG] Response: {rwUtils.safe_json_dumps(genResult, indent=2)}")
print(f"[Debugging] Generation config: {rwUtils.safe_json_dumps(genConfig, indent=2)}")
except Exception as e:
# Re-raise the original error without modification
raise e
audioUrl = self._extractAudioUrl(genResult)
audioObj = self._downloadAndProcessAudio(audioUrl, params["sampleRate"])
# Extract task UUID for polling
taskUUID = genConfig[0]["taskUUID"]
return (audioObj,)
# Poll for audio completion
while True:
# Check for interrupt before each poll
comfy.model_management.throw_exception_if_processing_interrupted()
# Poll for audio result
pollResult = rwUtils.pollVideoResult(taskUUID)
print(f"[Debugging] Poll result: {rwUtils.safe_json_dumps(pollResult, indent=2) if isinstance(pollResult, (dict, list)) else pollResult}")
# Check for errors first
if pollResult and "errors" in pollResult and len(pollResult["errors"]) > 0:
error_info = pollResult["errors"][0]
error_message = error_info.get("message", "Unknown error")
# Extract more detailed error info if available
if "responseContent" in error_info:
response_content = error_info["responseContent"]
# Handle both string and dict response content
if isinstance(response_content, str):
detailed_message = response_content
elif isinstance(response_content, dict):
detailed_message = response_content.get("message", str(response_content))
else:
detailed_message = str(response_content)
if detailed_message:
error_message = f"{error_message}\nProvider Error: {detailed_message}"
# Include taskUUID for debugging
task_uuid = error_info.get("taskUUID", "unknown")
raise Exception(f"Audio generation failed (Task: {task_uuid}): {error_message}")
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
audioData = pollResult["data"][0]
# Check status directly
if "status" in audioData:
status = audioData["status"]
if status == "success":
audioUrl = self._extractAudioUrl(pollResult)
hasAudio = audioUrl is not None
hasVideo = bool(audioData.get("videoURL") or audioData.get("videoBase64Data", False))
if hasAudio:
audioObj = self._downloadAndProcessAudio(audioUrl, params["sampleRate"])
print(f"[DEBUG] Audio URL found, returning audio. Audio URL: {audioUrl}")
# Return empty video object when only audio is present (prevents errors in downstream nodes)
emptyVideoObj = rwUtils.VideoObject("", width=0, height=0)
return (audioObj, emptyVideoObj)
if hasVideo:
videos = rwUtils.convertVideoB64List(pollResult, width=None, height=None)
print(f"[DEBUG] No audio URL in response, but video is present. Returning video.")
# Extract first video object from tuple (ComfyUI expects single VideoObject, not tuple)
if len(videos) > 0:
# Return empty audio object when only video is present (prevents errors in downstream nodes)
emptyAudioObj = {
"waveform": torch.zeros((1, 1, 1)), # [batch, channels, samples]
"sample_rate": params["sampleRate"]
}
return (emptyAudioObj, videos[0])
else:
raise Exception("No video object found in response")
raise Exception("No audio or video data received from API")
# If status is "processing", continue polling
# Check for interrupt before waiting
comfy.model_management.throw_exception_if_processing_interrupted()
# Wait before next poll (split into smaller chunks to allow more frequent interrupt checks)
for _ in range(10): # 10 x 0.1 second = 1 second total
comfy.model_management.throw_exception_if_processing_interrupted()
rwUtils.time.sleep(0.1)
def _extractParameters(self, kwargs):
"""Extract and validate parameters from kwargs"""
@@ -128,6 +226,8 @@ class RunwareAudioInference:
"outputFormat": kwargs.get("outputFormat", "MP3"),
"negativePrompt": kwargs.get("negativePrompt", ""),
"numberResults": kwargs.get("numberResults", 1),
"steps": kwargs.get("steps", 20),
"useSteps": kwargs.get("useSteps", False),
"inputs": kwargs.get("inputs", None),
"providerSettings": kwargs.get("providerSettings", None),
}
@@ -155,7 +255,7 @@ class RunwareAudioInference:
"model": params["model"],
"outputType": params["outputType"],
"outputFormat": params["outputFormat"],
"deliveryMethod": "sync",
"deliveryMethod": "async",
"includeCost": True,
"numberResults": params["numberResults"],
}]
@@ -190,6 +290,13 @@ class RunwareAudioInference:
else:
print(f"[DEBUG] Skipped duration")
# Add steps parameter only if enabled
if params["useSteps"]:
genConfig[0]["steps"] = params["steps"]
print(f"[DEBUG] Added steps: {params['steps']}")
else:
print(f"[DEBUG] Skipped steps")
# Add optional parameters
if params["negativePrompt"]:
genConfig[0]["negativePrompt"] = params["negativePrompt"]
@@ -241,10 +348,18 @@ class RunwareAudioInference:
raise Exception(f"Audio generation failed: {errorMessage}")
if "data" not in genResult or len(genResult["data"]) == 0:
raise Exception("No audio data received from API")
raise Exception("No data received from API")
audioData = genResult["data"][0]
hasAudio = bool(audioData.get("audioURL") or audioData.get("audioDataURI") or audioData.get("audioBase64Data"))
hasVideo = bool(audioData.get("videoURL") or audioData.get("videoBase64Data") or audioData.get("videoUUID"))
if not hasAudio and not hasVideo:
raise Exception("No audio or video data received from API")
def _extractAudioUrl(self, genResult):
"""Extract audio URL from API response"""
"""Extract audio URL from API response. Returns None if no audio URL is present (e.g., when only video is returned)."""
audioData = genResult["data"][0]
audioUrl = audioData.get("audioURL", "")
@@ -253,10 +368,8 @@ class RunwareAudioInference:
if not audioUrl:
audioUrl = audioData.get("audioBase64Data", "")
if not audioUrl:
raise Exception("No audio URL received from API")
return audioUrl
return audioUrl if audioUrl else None
def _downloadAndProcessAudio(self, audioUrl, targetSampleRate):
"""Download audio file and process it for ComfyUI"""
+44 -9
View File
@@ -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,)
+4
View File
@@ -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
@@ -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",
}
+1 -1
View File
@@ -18,7 +18,7 @@ class upscaler:
},
"optional": {
"model": ("STRING", {
"tooltip": "Model AIR ID (runware:500@1, runware:501@1, runware:502@1, runware:503@1, bria:52@1)",
"tooltip": "Model AIR ID (runware:500@1, runware:501@1, runware:502@1, runware:503@1, runware:504@1, bria:52@1)",
"default": "",
}),
# Common parameters toggles
+5
View File
@@ -596,6 +596,11 @@ class VideoObject:
def save_to(self, filename, **kwargs):
"""Save video to file by downloading from URL with retry logic"""
# If video URL is empty, this is a placeholder object (e.g., when only audio is returned)
if not self.video_url or self.video_url.strip() == "":
print(f"[Video Download] Skipping download - empty video URL (placeholder object)")
return False
max_retries = 10
retry_delays = [2, 5, 10, 15, 20] # Longer delays for retries with larger videos
+14 -1
View File
@@ -90,6 +90,10 @@ class videoModelSearch:
"Bria": [
"bria:60@1 (Bria Video Eraser)",
],
"Creatify": [
"creatify:aurora@fast (Creatify Aurora Avatar Model API (720p))",
"creatify:aurora@0 (Creatify Aurora Avatar Model API (720p))",
],
}
# Model dimensions mapping
@@ -180,6 +184,10 @@ class videoModelSearch:
# Bria Models
"bria:60@1": {"width": 0, "height": 0},
# Creatify Models
"creatify:aurora@fast": {"width": 1280, "height": 720},
"creatify:aurora@0": {"width": 1280, "height": 720},
}
# Model resolutions mapping
@@ -270,6 +278,10 @@ class videoModelSearch:
# Bria Models
"bria:60@1": "720p",
# Creatify Models
"creatify:aurora@fast": "720p",
"creatify:aurora@0": "720p",
}
MODEL_ARCHITECTURES = [
@@ -287,7 +299,8 @@ class videoModelSearch:
"Runway",
"Luma",
"Sync",
"Bria"
"Bria",
"Creatify"
]
DEFAULT_DIMENSIONS = {"width": 1024, "height": 576}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long