Merge pull request #73 from Runware/feature-mireloAudioInference
Add Mirelo provider settings for audioInference, and Creatify Aurora video models
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"""
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user