Refactor: Use librosa instead of torchaudio, Add dedicated handlers for all use parameters and fix widget ordering
- Use librosa instead of torchaudio for mediaStorage - Created dedicated toggle handlers for Image Inference, Video Inference, Upscaler, Audio Inference, Accelerator Options, Bytedance Provider Settings, and OpenAI Provider Settings - Fixed Audio Inference widget ordering (use parameters now appear before their corresponding parameters) - All handlers use the same proven pattern from videoInferenceDimensionsHandler - Improved widget disabling/enabling functionality across all nodes
This commit is contained in:
+13
-11
@@ -28,25 +28,26 @@ from .modules.imageInferenceInputs import imageInferenceInputs
|
||||
from .modules.videoInference import txt2vid
|
||||
from .modules.videoModelSearch import videoModelSearch
|
||||
from .modules.frameImages import RunwareFrameImages
|
||||
from .modules.providerSettings import RunwareProviderSettings
|
||||
from .modules.pixverseProviderSettings import RunwarePixverseProviderSettings
|
||||
from .modules.openaiProviderSettings import RunwareOpenAIProviderSettings
|
||||
from .modules.bytedanceImageProviderSettings import RunwareBytedanceProviderSettings
|
||||
from .modules.briaImageProviderSettings import RunwareBriaProviderSettings
|
||||
from .modules.providerSettings.providerSettings import RunwareProviderSettings
|
||||
from .modules.providerSettings.pixverseProviderSettings import RunwarePixverseProviderSettings
|
||||
from .modules.providerSettings.openaiProviderSettings import RunwareOpenAIProviderSettings
|
||||
from .modules.providerSettings.bytedanceImageProviderSettings import RunwareBytedanceProviderSettings
|
||||
from .modules.providerSettings.briaImageProviderSettings import RunwareBriaProviderSettings
|
||||
from .modules.audioInference import RunwareAudioInference
|
||||
from .modules.audioModelSearch import RunwareAudioModelSearch
|
||||
from .modules.audioSections import RunwareAudioSections
|
||||
from .modules.elevenlabsProviderSettings import RunwareElevenLabsProviderSettings
|
||||
from .modules.elevenlabsProviderSettingsSections import RunwareElevenLabsProviderSettingsSections
|
||||
from .modules.providerSettings.elevenlabsProviderSettings import RunwareElevenLabsProviderSettings
|
||||
from .modules.providerSettings.elevenlabsProviderSettingsSections import RunwareElevenLabsProviderSettingsSections
|
||||
from .modules.acceleratorOptions import acceleratorOptions
|
||||
from .modules.viduProviderSettings import RunwareViduProviderSettings
|
||||
from .modules.providerSettings.viduProviderSettings import RunwareViduProviderSettings
|
||||
from .modules.mediaUpload import runwareMediaUpload
|
||||
from .modules.inputAudios import inputAudios
|
||||
from .modules.referenceVideos import referenceVideos
|
||||
from .modules.videoInferenceInputs import videoInferenceInputs
|
||||
from .modules.lightricksProviderSettings import RunwareLightricksProviderSettings
|
||||
from .modules.minimaxProviderSettings import RunwareMiniMaxProviderSettings
|
||||
from .modules.klingProviderSettings import RunwareKlingProviderSettings
|
||||
from .modules.providerSettings.lightricksProviderSettings import RunwareLightricksProviderSettings
|
||||
from .modules.providerSettings.minimaxProviderSettings import RunwareMiniMaxProviderSettings
|
||||
from .modules.providerSettings.klingProviderSettings import RunwareKlingProviderSettings
|
||||
from .modules.providerSettings.runwayProviderSettings import RunwareRunwayProviderSettings
|
||||
from .modules.vectorize import vectorize
|
||||
from .modules.videoBgRemoval import videoBgRemoval
|
||||
from .modules.videoUpscaler import videoUpscaler
|
||||
@@ -114,6 +115,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Runware Lightricks Provider Settings": RunwareLightricksProviderSettings,
|
||||
"Runware MiniMax Provider Settings": RunwareMiniMaxProviderSettings,
|
||||
"Runware KlingAI Provider Settings": RunwareKlingProviderSettings,
|
||||
"Runware Runway Provider Settings": RunwareRunwayProviderSettings,
|
||||
"Runware Vectorize": vectorize,
|
||||
"Runware Video Background Removal": videoBgRemoval,
|
||||
"Runware Video Upscaler": videoUpscaler,
|
||||
|
||||
+22
-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 } from "./utils.js";
|
||||
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, videoTranscriptionHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, useParameterToggleHandler, imageInferenceToggleHandler, upscalerToggleHandler, audioInferenceToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler } from "./utils.js";
|
||||
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
|
||||
|
||||
const nodeInitList = [];
|
||||
@@ -45,6 +45,27 @@ app.registerExtension({
|
||||
const nodeWidgets = node.widgets;
|
||||
if(nodeWidgets.length <= 0) return;
|
||||
|
||||
// Handle node-specific handlers first - each node has its own dedicated handler
|
||||
if(nodeClass === RUNWARE_NODE_TYPES.IMAGEINFERENCE) {
|
||||
imageInferenceToggleHandler(node);
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINFERENCE) {
|
||||
videoInferenceDimensionsHandler(node);
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.UPSCALER) {
|
||||
upscalerToggleHandler(node);
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINFERENCE) {
|
||||
audioInferenceToggleHandler(node);
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.ACCELERATOROPTIONS) {
|
||||
acceleratorOptionsToggleHandler(node);
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.BYTEDANCEPROVIDERSETTINGS) {
|
||||
bytedanceProviderSettingsToggleHandler(node);
|
||||
} else if(nodeClass === RUNWARE_NODE_TYPES.OPENAIPROVIDERSETTINGS) {
|
||||
openaiProviderSettingsToggleHandler(node);
|
||||
}
|
||||
|
||||
if(nodeClass === RUNWARE_NODE_TYPES.VIDEOMODELSEARCH) {
|
||||
videoModelSearchFilterHandler(node);
|
||||
}
|
||||
|
||||
for(const nodeWidget of nodeWidgets) {
|
||||
const widgetName = nodeWidget.name;
|
||||
const widgetType = nodeWidget.type;
|
||||
|
||||
@@ -95,6 +95,7 @@ const RUNWARE_NODE_TYPES = {
|
||||
VIDEOINFERENCEINPUTS: "Runware Video Inference Inputs",
|
||||
LIGHTRICKSPROVIDERSETTINGS: "Runware Lightricks Provider Settings",
|
||||
KLINGPROVIDERSETTINGS: "Runware KlingAI Provider Settings",
|
||||
RUNWAYPROVIDERSETTINGS: "Runware Runway Provider Settings",
|
||||
VECTORIZE: "Runware Vectorize",
|
||||
VIDEOBGREMOVAL: "Runware Video Background Removal",
|
||||
VIDEOUPSCALER: "Runware Video Upscaler",
|
||||
@@ -298,6 +299,10 @@ const RUNWARE_NODE_PROPS = {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.RUNWAYPROVIDERSETTINGS]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.VECTORIZE]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
},
|
||||
|
||||
+1691
-629
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
class acceleratorOptions:
|
||||
"""Accelerator Options node for configuring caching options for faster generation"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
# FBCache parameters
|
||||
"fbcache": ("BOOLEAN", {
|
||||
"tooltip": "Enable Frame Buffer Cache for faster generation",
|
||||
"default": None,
|
||||
@@ -22,8 +21,6 @@ class acceleratorOptions:
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
}),
|
||||
|
||||
# TeaCache parameters
|
||||
"teaCache": ("BOOLEAN", {
|
||||
"tooltip": "Enable TeaCache for transformer-based models (Flux, SD3). Does not work with UNet models like SDXL or SD1.5. Particularly effective for iterative editing workflows.",
|
||||
"default": None,
|
||||
@@ -39,8 +36,6 @@ class acceleratorOptions:
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
}),
|
||||
|
||||
# DeepCache parameters
|
||||
"deepCache": ("BOOLEAN", {
|
||||
"tooltip": "Enable DeepCache for UNet-based models (SDXL, SD1.5). Not applicable to transformer models. Provides performance improvements for high-throughput scenarios.",
|
||||
"default": None,
|
||||
@@ -61,8 +56,6 @@ class acceleratorOptions:
|
||||
"min": 0,
|
||||
"max": 10,
|
||||
}),
|
||||
|
||||
# Cache step control parameters (step numbers)
|
||||
"useCacheSteps": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include cache step control parameters (step numbers) in API request.",
|
||||
"default": False,
|
||||
@@ -79,8 +72,6 @@ class acceleratorOptions:
|
||||
"min": 1,
|
||||
"max": 200,
|
||||
}),
|
||||
|
||||
# Cache step control parameters (percentages)
|
||||
"useCachePercentageSteps": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include cache step control parameters (percentages) in API request.",
|
||||
"default": False,
|
||||
@@ -117,63 +108,65 @@ class acceleratorOptions:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def acceleratorOptions(self, **kwargs):
|
||||
# Get all optional parameters
|
||||
fbcache = kwargs.get("fbcache")
|
||||
useCacheDistance = kwargs.get("useCacheDistance", False)
|
||||
cacheDistance = kwargs.get("cacheDistance", 0.0)
|
||||
teaCache = kwargs.get("teaCache")
|
||||
useTeaCacheDistance = kwargs.get("useTeaCacheDistance", False)
|
||||
teaCacheDistance = kwargs.get("teaCacheDistance", 0.0)
|
||||
deepCache = kwargs.get("deepCache")
|
||||
|
||||
# Get toggle and value parameters
|
||||
useDeepCacheOptions = kwargs.get("useDeepCacheOptions", False)
|
||||
deepCacheInterval = kwargs.get("deepCacheInterval", 1)
|
||||
deepCacheBranchId = kwargs.get("deepCacheBranchId", 0)
|
||||
useCacheSteps = kwargs.get("useCacheSteps", False)
|
||||
cacheStartStep = kwargs.get("cacheStartStep", 0)
|
||||
cacheEndStep = kwargs.get("cacheEndStep", 1)
|
||||
useCachePercentageSteps = kwargs.get("useCachePercentageSteps", False)
|
||||
cacheStartStepPercentage = kwargs.get("cacheStartStepPercentage", 0)
|
||||
cacheEndStepPercentage = kwargs.get("cacheEndStepPercentage", 1)
|
||||
useCacheMaxConsecutiveSteps = kwargs.get("useCacheMaxConsecutiveSteps", False)
|
||||
cacheMaxConsecutiveSteps = kwargs.get("cacheMaxConsecutiveSteps", 1)
|
||||
"""Build accelerator options dictionary"""
|
||||
params = self._extractParameters(kwargs)
|
||||
acceleratorOptions = self._buildAcceleratorOptions(params)
|
||||
return (acceleratorOptions,)
|
||||
|
||||
# Build accelerator options dictionary
|
||||
def _extractParameters(self, kwargs):
|
||||
"""Extract all parameters from kwargs"""
|
||||
return {
|
||||
"fbcache": kwargs.get("fbcache"),
|
||||
"useCacheDistance": kwargs.get("useCacheDistance", False),
|
||||
"cacheDistance": kwargs.get("cacheDistance", 0.0),
|
||||
"teaCache": kwargs.get("teaCache"),
|
||||
"useTeaCacheDistance": kwargs.get("useTeaCacheDistance", False),
|
||||
"teaCacheDistance": kwargs.get("teaCacheDistance", 0.0),
|
||||
"deepCache": kwargs.get("deepCache"),
|
||||
"useDeepCacheOptions": kwargs.get("useDeepCacheOptions", False),
|
||||
"deepCacheInterval": kwargs.get("deepCacheInterval", 1),
|
||||
"deepCacheBranchId": kwargs.get("deepCacheBranchId", 0),
|
||||
"useCacheSteps": kwargs.get("useCacheSteps", False),
|
||||
"cacheStartStep": kwargs.get("cacheStartStep", 0),
|
||||
"cacheEndStep": kwargs.get("cacheEndStep", 1),
|
||||
"useCachePercentageSteps": kwargs.get("useCachePercentageSteps", False),
|
||||
"cacheStartStepPercentage": kwargs.get("cacheStartStepPercentage", 0),
|
||||
"cacheEndStepPercentage": kwargs.get("cacheEndStepPercentage", 1),
|
||||
"useCacheMaxConsecutiveSteps": kwargs.get("useCacheMaxConsecutiveSteps", False),
|
||||
"cacheMaxConsecutiveSteps": kwargs.get("cacheMaxConsecutiveSteps", 1),
|
||||
}
|
||||
|
||||
def _buildAcceleratorOptions(self, params):
|
||||
"""Build accelerator options dictionary from parameters"""
|
||||
acceleratorOptions = {}
|
||||
|
||||
# FBCache options
|
||||
if fbcache is True:
|
||||
acceleratorOptions["fbcache"] = fbcache
|
||||
if useCacheDistance:
|
||||
acceleratorOptions["cacheDistance"] = cacheDistance
|
||||
if params["fbcache"] is True:
|
||||
acceleratorOptions["fbcache"] = params["fbcache"]
|
||||
if params["useCacheDistance"]:
|
||||
acceleratorOptions["cacheDistance"] = params["cacheDistance"]
|
||||
|
||||
# TeaCache options
|
||||
if teaCache is True:
|
||||
acceleratorOptions["teaCache"] = teaCache
|
||||
if useTeaCacheDistance:
|
||||
acceleratorOptions["teaCacheDistance"] = teaCacheDistance
|
||||
if params["teaCache"] is True:
|
||||
acceleratorOptions["teaCache"] = params["teaCache"]
|
||||
if params["useTeaCacheDistance"]:
|
||||
acceleratorOptions["teaCacheDistance"] = params["teaCacheDistance"]
|
||||
|
||||
# DeepCache options
|
||||
if deepCache is True:
|
||||
acceleratorOptions["deepCache"] = deepCache
|
||||
if useDeepCacheOptions:
|
||||
acceleratorOptions["deepCacheInterval"] = deepCacheInterval
|
||||
acceleratorOptions["deepCacheBranchId"] = deepCacheBranchId
|
||||
if params["deepCache"] is True:
|
||||
acceleratorOptions["deepCache"] = params["deepCache"]
|
||||
if params["useDeepCacheOptions"]:
|
||||
acceleratorOptions["deepCacheInterval"] = params["deepCacheInterval"]
|
||||
acceleratorOptions["deepCacheBranchId"] = params["deepCacheBranchId"]
|
||||
|
||||
# Cache step control options (step numbers)
|
||||
if useCacheSteps:
|
||||
if cacheStartStep > 0:
|
||||
acceleratorOptions["cacheStartStep"] = cacheStartStep
|
||||
acceleratorOptions["cacheEndStep"] = cacheEndStep
|
||||
if params["useCacheSteps"]:
|
||||
if params["cacheStartStep"] > 0:
|
||||
acceleratorOptions["cacheStartStep"] = params["cacheStartStep"]
|
||||
acceleratorOptions["cacheEndStep"] = params["cacheEndStep"]
|
||||
|
||||
# Cache step control options (percentages)
|
||||
if useCachePercentageSteps:
|
||||
if cacheStartStepPercentage > 0:
|
||||
acceleratorOptions["cacheStartStepPercentage"] = cacheStartStepPercentage
|
||||
acceleratorOptions["cacheEndStepPercentage"] = cacheEndStepPercentage
|
||||
if params["useCachePercentageSteps"]:
|
||||
if params["cacheStartStepPercentage"] > 0:
|
||||
acceleratorOptions["cacheStartStepPercentage"] = params["cacheStartStepPercentage"]
|
||||
acceleratorOptions["cacheEndStepPercentage"] = params["cacheEndStepPercentage"]
|
||||
|
||||
if useCacheMaxConsecutiveSteps:
|
||||
acceleratorOptions["cacheMaxConsecutiveSteps"] = cacheMaxConsecutiveSteps
|
||||
if params["useCacheMaxConsecutiveSteps"]:
|
||||
acceleratorOptions["cacheMaxConsecutiveSteps"] = params["cacheMaxConsecutiveSteps"]
|
||||
|
||||
return (acceleratorOptions,)
|
||||
return acceleratorOptions
|
||||
|
||||
+22
-1
@@ -1,6 +1,9 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class apiManager:
|
||||
"""API Manager node for configuring Runware API settings"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -60,4 +63,22 @@ class apiManager:
|
||||
|
||||
DESCRIPTION = "API Managers is a Runware Utility That helps you define or change your API Key, Session Timeout, Image Output Format & Quality, and Image Caching settings From The ComfyUI Interface Without having to adjust the env file locally."
|
||||
CATEGORY = "Runware"
|
||||
RETURN_TYPES = ()
|
||||
RETURN_TYPES = ()
|
||||
|
||||
def manageApiSettings(self, **kwargs):
|
||||
"""Manage API settings"""
|
||||
apiKey = kwargs.get("API Key")
|
||||
maxTimeout = kwargs.get("Max Timeout")
|
||||
imageOutputQuality = kwargs.get("Image Output Quality")
|
||||
imageOutputFormat = kwargs.get("Image Output Format")
|
||||
enableImagesCaching = kwargs.get("Enable Images Caching")
|
||||
minImageCacheSize = kwargs.get("Min Image Cache Size")
|
||||
|
||||
rwUtils.setApiKey(apiKey)
|
||||
rwUtils.setTimeout(maxTimeout)
|
||||
rwUtils.setOutputQuality(imageOutputQuality)
|
||||
rwUtils.setOutputFormat(imageOutputFormat)
|
||||
rwUtils.setEnableImagesCaching(enableImagesCaching)
|
||||
rwUtils.setMinImageCacheSize(minImageCacheSize)
|
||||
|
||||
return ()
|
||||
|
||||
+173
-131
@@ -2,14 +2,16 @@ import json
|
||||
import uuid
|
||||
import requests
|
||||
import torch
|
||||
import torchaudio
|
||||
import numpy as np
|
||||
from io import BytesIO
|
||||
import librosa
|
||||
import tempfile
|
||||
import os
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareAudioInference:
|
||||
"""Runware Audio Inference node for generating audio using Runware API"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -17,22 +19,26 @@ class RunwareAudioInference:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"positivePrompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "classical piano piece, gentle and melodic",
|
||||
"tooltip": "Text description that guides the audio generation process"
|
||||
"model": ("RUNWAREAUDIOMODEL", {
|
||||
"tooltip": "AI model to use for audio generation"
|
||||
}),
|
||||
"usePositivePrompt": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Enable/disable positivePrompt parameter in API request"
|
||||
}),
|
||||
"positivePrompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "classical piano piece, gentle and melodic",
|
||||
"tooltip": "Text description that guides the audio generation process"
|
||||
}),
|
||||
"negativePrompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"tooltip": "Text description of what you don't want in the audio"
|
||||
}),
|
||||
"model": ("RUNWAREAUDIOMODEL", {
|
||||
"tooltip": "AI model to use for audio generation"
|
||||
"useDuration": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Enable/disable duration parameter in API request"
|
||||
}),
|
||||
"duration": ("INT", {
|
||||
"default": 10,
|
||||
@@ -41,10 +47,6 @@ class RunwareAudioInference:
|
||||
"step": 1,
|
||||
"tooltip": "Length of generated audio in seconds (10-300)"
|
||||
}),
|
||||
"useDuration": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Enable/disable duration parameter in API request"
|
||||
}),
|
||||
"sampleRate": ("INT", {
|
||||
"default": 22050,
|
||||
"min": 8000,
|
||||
@@ -87,146 +89,186 @@ class RunwareAudioInference:
|
||||
CATEGORY = "Runware/Audio"
|
||||
|
||||
def generateAudio(self, **kwargs):
|
||||
# Get required parameters
|
||||
positivePrompt = kwargs.get("positivePrompt", "")
|
||||
usePositivePrompt = kwargs.get("usePositivePrompt", True)
|
||||
model = kwargs.get("model", "")
|
||||
duration = kwargs.get("duration", 30)
|
||||
useDuration = kwargs.get("useDuration", True)
|
||||
sampleRate = kwargs.get("sampleRate", 44100)
|
||||
bitrate = kwargs.get("bitrate", 128)
|
||||
outputType = kwargs.get("outputType", "URL")
|
||||
outputFormat = kwargs.get("outputFormat", "MP3")
|
||||
negativePrompt = kwargs.get("negativePrompt", "")
|
||||
numberResults = kwargs.get("numberResults", 1)
|
||||
providerSettings = kwargs.get("providerSettings", None)
|
||||
"""Main function to generate audio using Runware API"""
|
||||
params = self._extractParameters(kwargs)
|
||||
genConfig = self._buildGenConfig(params)
|
||||
genResult = rwUtils.inferenecRequest(genConfig)
|
||||
|
||||
# Create task UUID
|
||||
taskUUID = str(uuid.uuid4())
|
||||
self._logResponse(genResult)
|
||||
self._validateResponse(genResult)
|
||||
|
||||
audioUrl = self._extractAudioUrl(genResult)
|
||||
audioObj = self._downloadAndProcessAudio(audioUrl, params["sampleRate"])
|
||||
|
||||
return (audioObj,)
|
||||
|
||||
def _extractParameters(self, kwargs):
|
||||
"""Extract and validate parameters from kwargs"""
|
||||
return {
|
||||
"positivePrompt": kwargs.get("positivePrompt", ""),
|
||||
"usePositivePrompt": kwargs.get("usePositivePrompt", True),
|
||||
"model": kwargs.get("model", ""),
|
||||
"duration": kwargs.get("duration", 30),
|
||||
"useDuration": kwargs.get("useDuration", True),
|
||||
"sampleRate": kwargs.get("sampleRate", 44100),
|
||||
"bitrate": kwargs.get("bitrate", 128),
|
||||
"outputType": kwargs.get("outputType", "URL"),
|
||||
"outputFormat": kwargs.get("outputFormat", "MP3"),
|
||||
"negativePrompt": kwargs.get("negativePrompt", ""),
|
||||
"numberResults": kwargs.get("numberResults", 1),
|
||||
"providerSettings": kwargs.get("providerSettings", None),
|
||||
}
|
||||
|
||||
def _hasSections(self, providerSettings):
|
||||
"""Check if provider settings contain sections"""
|
||||
if not providerSettings or not isinstance(providerSettings, dict):
|
||||
return False
|
||||
|
||||
elevenlabsSettings = providerSettings.get("elevenlabs", {})
|
||||
musicSettings = elevenlabsSettings.get("music", {})
|
||||
compositionPlan = musicSettings.get("compositionPlan", {})
|
||||
sections = compositionPlan.get("sections", [])
|
||||
|
||||
return len(sections) > 0
|
||||
|
||||
def _buildGenConfig(self, params):
|
||||
"""Build the generation configuration for API request"""
|
||||
taskUuid = str(uuid.uuid4())
|
||||
|
||||
# Build the generation config
|
||||
genConfig = [{
|
||||
"taskType": "audioInference",
|
||||
"taskUUID": taskUUID,
|
||||
"model": model,
|
||||
"outputType": outputType,
|
||||
"outputFormat": outputFormat,
|
||||
"taskUUID": taskUuid,
|
||||
"model": params["model"],
|
||||
"outputType": params["outputType"],
|
||||
"outputFormat": params["outputFormat"],
|
||||
"deliveryMethod": "sync",
|
||||
"includeCost": True,
|
||||
"numberResults": numberResults,
|
||||
"numberResults": params["numberResults"],
|
||||
"audioSettings": {
|
||||
"sampleRate": sampleRate,
|
||||
"bitrate": bitrate
|
||||
"sampleRate": params["sampleRate"],
|
||||
"bitrate": params["bitrate"]
|
||||
}
|
||||
}]
|
||||
|
||||
# Add positivePrompt only if usePositivePrompt is True AND no sections are provided
|
||||
has_sections = False
|
||||
if providerSettings is not None:
|
||||
# Check if there are sections in the provider settings
|
||||
if isinstance(providerSettings, dict):
|
||||
elevenlabs_settings = providerSettings.get("elevenlabs", {})
|
||||
music_settings = elevenlabs_settings.get("music", {})
|
||||
composition_plan = music_settings.get("compositionPlan", {})
|
||||
sections = composition_plan.get("sections", [])
|
||||
has_sections = len(sections) > 0
|
||||
# Handle sections - disable duration if sections are provided
|
||||
hasSections = self._hasSections(params["providerSettings"])
|
||||
if hasSections:
|
||||
params["useDuration"] = False
|
||||
print(f"[DEBUG] Disabled duration because sections are provided")
|
||||
|
||||
# When sections are provided, disable duration but add minimal positivePrompt (ElevenLabs requires it)
|
||||
if has_sections:
|
||||
useDuration = False
|
||||
# ElevenLabs requires positivePrompt even with composition plans
|
||||
#genConfig[0]["positivePrompt"] = "Music composition"
|
||||
print(f"[DEBUG] Disabled duration and added minimal positivePrompt because sections are provided")
|
||||
|
||||
if usePositivePrompt:
|
||||
genConfig[0]["positivePrompt"] = positivePrompt
|
||||
print(f"[DEBUG] Added positivePrompt: '{positivePrompt}' (usePositivePrompt={usePositivePrompt})")
|
||||
# Add prompts conditionally
|
||||
if params["usePositivePrompt"]:
|
||||
genConfig[0]["positivePrompt"] = params["positivePrompt"]
|
||||
print(f"[DEBUG] Added positivePrompt: '{params['positivePrompt']}'")
|
||||
else:
|
||||
print(f"[DEBUG] Skipped positivePrompt (usePositivePrompt={usePositivePrompt})")
|
||||
print(f"[DEBUG] Skipped positivePrompt")
|
||||
|
||||
# Add duration only if useDuration is True
|
||||
if useDuration:
|
||||
genConfig[0]["duration"] = duration
|
||||
print(f"[DEBUG] Added duration: {duration} (useDuration={useDuration})")
|
||||
if params["useDuration"]:
|
||||
genConfig[0]["duration"] = params["duration"]
|
||||
print(f"[DEBUG] Added duration: {params['duration']}")
|
||||
else:
|
||||
print(f"[DEBUG] Skipped duration (useDuration={useDuration})")
|
||||
print(f"[DEBUG] Skipped duration")
|
||||
|
||||
# Add optional parameters if provided
|
||||
if negativePrompt:
|
||||
genConfig[0]["negativePrompt"] = negativePrompt
|
||||
|
||||
if providerSettings is not None:
|
||||
genConfig[0]["providerSettings"] = providerSettings
|
||||
# Add optional parameters
|
||||
if params["negativePrompt"]:
|
||||
genConfig[0]["negativePrompt"] = params["negativePrompt"]
|
||||
|
||||
if params["providerSettings"] is not None:
|
||||
genConfig[0]["providerSettings"] = params["providerSettings"]
|
||||
|
||||
# Debug: Print the request being sent
|
||||
print(f"[DEBUG] Sending Audio Inference Request:")
|
||||
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
|
||||
|
||||
# Make the API request
|
||||
genResult = rwUtils.inferenecRequest(genConfig)
|
||||
|
||||
# Debug: Print the response received
|
||||
return genConfig
|
||||
|
||||
def _logResponse(self, genResult):
|
||||
"""Log the API response for debugging"""
|
||||
print(f"[DEBUG] Received Audio Inference Response:")
|
||||
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
|
||||
|
||||
# Check for errors
|
||||
|
||||
def _validateResponse(self, genResult):
|
||||
"""Validate API response and raise errors if needed"""
|
||||
if "errors" in genResult:
|
||||
error_message = genResult["errors"][0]["message"]
|
||||
raise Exception(f"Audio generation failed: {error_message}")
|
||||
errorMessage = genResult["errors"][0]["message"]
|
||||
raise Exception(f"Audio generation failed: {errorMessage}")
|
||||
|
||||
# Extract audio data from response
|
||||
if "data" in genResult and len(genResult["data"]) > 0:
|
||||
audioData = genResult["data"][0]
|
||||
|
||||
# Get the audio URL
|
||||
audioURL = audioData.get("audioURL", "")
|
||||
if not audioURL:
|
||||
# Fallback to other audio data formats
|
||||
audioURL = audioData.get("audioDataURI", "")
|
||||
if not audioURL:
|
||||
audioURL = audioData.get("audioBase64Data", "")
|
||||
|
||||
if audioURL:
|
||||
# Download the audio and create proper audio data for ComfyUI
|
||||
try:
|
||||
# Download the audio file
|
||||
response = requests.get(audioURL, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Save to temporary file and load with torchaudio
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
|
||||
temp_file.write(response.content)
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
try:
|
||||
# Load audio with torchaudio
|
||||
waveform, original_sample_rate = torchaudio.load(temp_file_path)
|
||||
|
||||
# Resample if necessary
|
||||
if original_sample_rate != sampleRate:
|
||||
resampler = torchaudio.transforms.Resample(original_sample_rate, sampleRate)
|
||||
waveform = resampler(waveform)
|
||||
|
||||
# Ensure correct format: [batch, channels, samples]
|
||||
if waveform.dim() == 2:
|
||||
waveform = waveform.unsqueeze(0) # Add batch dimension
|
||||
|
||||
# Create the audio object
|
||||
audio_obj = {
|
||||
"waveform": waveform,
|
||||
"sample_rate": sampleRate
|
||||
}
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if os.path.exists(temp_file_path):
|
||||
os.unlink(temp_file_path)
|
||||
|
||||
return (audio_obj,)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to download audio: {e}")
|
||||
else:
|
||||
raise Exception("No audio URL received from API")
|
||||
else:
|
||||
if "data" not in genResult or len(genResult["data"]) == 0:
|
||||
raise Exception("No audio data received from API")
|
||||
|
||||
def _extractAudioUrl(self, genResult):
|
||||
"""Extract audio URL from API response"""
|
||||
audioData = genResult["data"][0]
|
||||
|
||||
audioUrl = audioData.get("audioURL", "")
|
||||
if not audioUrl:
|
||||
audioUrl = audioData.get("audioDataURI", "")
|
||||
if not audioUrl:
|
||||
audioUrl = audioData.get("audioBase64Data", "")
|
||||
|
||||
if not audioUrl:
|
||||
raise Exception("No audio URL received from API")
|
||||
|
||||
return audioUrl
|
||||
|
||||
def _downloadAndProcessAudio(self, audioUrl, targetSampleRate):
|
||||
"""Download audio file and process it for ComfyUI"""
|
||||
try:
|
||||
response = requests.get(audioUrl, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tempFile:
|
||||
tempFile.write(response.content)
|
||||
tempFilePath = tempFile.name
|
||||
|
||||
try:
|
||||
waveformNp, originalSampleRate = self._loadAudioFile(tempFilePath)
|
||||
waveformNp = self._resampleAudio(waveformNp, originalSampleRate, targetSampleRate)
|
||||
waveformTensor = self._convertToTensor(waveformNp)
|
||||
|
||||
return {
|
||||
"waveform": waveformTensor,
|
||||
"sample_rate": targetSampleRate
|
||||
}
|
||||
finally:
|
||||
if os.path.exists(tempFilePath):
|
||||
os.unlink(tempFilePath)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to download audio: {e}")
|
||||
|
||||
def _loadAudioFile(self, filePath):
|
||||
"""Load audio file using librosa"""
|
||||
waveformNp, originalSampleRate = librosa.load(filePath, sr=None, mono=False)
|
||||
|
||||
# Convert to [samples, channels] format
|
||||
if waveformNp.ndim == 1:
|
||||
waveformNp = waveformNp.reshape(-1, 1)
|
||||
else:
|
||||
waveformNp = waveformNp.T # [channels, samples] -> [samples, channels]
|
||||
|
||||
return waveformNp, originalSampleRate
|
||||
|
||||
def _resampleAudio(self, waveformNp, originalSampleRate, targetSampleRate):
|
||||
"""Resample audio to target sample rate"""
|
||||
if originalSampleRate == targetSampleRate:
|
||||
return waveformNp
|
||||
|
||||
if waveformNp.ndim == 1:
|
||||
return librosa.resample(waveformNp, orig_sr=originalSampleRate, target_sr=targetSampleRate)
|
||||
else:
|
||||
# Multi-channel: resample each channel
|
||||
return librosa.resample(waveformNp.T, orig_sr=originalSampleRate, target_sr=targetSampleRate).T
|
||||
|
||||
def _convertToTensor(self, waveformNp):
|
||||
"""Convert numpy array to PyTorch tensor with correct format"""
|
||||
# Ensure 2D shape [samples, channels]
|
||||
if waveformNp.ndim == 1:
|
||||
waveformNp = waveformNp.reshape(-1, 1)
|
||||
|
||||
# Convert to [channels, samples] for ComfyUI
|
||||
waveformTensor = torch.from_numpy(waveformNp.T).float()
|
||||
|
||||
# Add batch dimension if needed: [batch, channels, samples]
|
||||
if waveformTensor.dim() == 2:
|
||||
waveformTensor = waveformTensor.unsqueeze(0)
|
||||
|
||||
return waveformTensor
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class RunwareAudioModelSearch:
|
||||
"""Audio Model Search node for searching audio models"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -19,13 +21,12 @@ class RunwareAudioModelSearch:
|
||||
|
||||
RETURN_TYPES = ("RUNWAREAUDIOMODEL",)
|
||||
RETURN_NAMES = ("model",)
|
||||
FUNCTION = "search_models"
|
||||
FUNCTION = "searchModels"
|
||||
CATEGORY = "Runware/Audio"
|
||||
|
||||
def search_models(self, **kwargs):
|
||||
def searchModels(self, **kwargs):
|
||||
"""Search for audio models"""
|
||||
searchQuery = kwargs.get("searchQuery", "")
|
||||
provider = kwargs.get("provider", "elevenlabs")
|
||||
|
||||
return (searchQuery,)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class RunwareAudioSections:
|
||||
"""Audio Sections node for creating audio composition sections"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -34,23 +36,31 @@ class RunwareAudioSections:
|
||||
|
||||
RETURN_TYPES = ("RUNWAREAUDIOSECTIONS",)
|
||||
RETURN_NAMES = ("sections",)
|
||||
FUNCTION = "create_sections"
|
||||
FUNCTION = "createSections"
|
||||
CATEGORY = "Runware/Audio"
|
||||
|
||||
def create_sections(self, **kwargs):
|
||||
def createSections(self, **kwargs):
|
||||
"""Create audio section object from provided parameters"""
|
||||
sectionName = kwargs.get("sectionName", "")
|
||||
positiveLocalStyles = kwargs.get("positiveLocalStyles", "")
|
||||
negativeLocalStyles = kwargs.get("negativeLocalStyles", "")
|
||||
duration = kwargs.get("duration", 0)
|
||||
lines = kwargs.get("lines", "")
|
||||
|
||||
# Create section object
|
||||
section = {
|
||||
"sectionName": sectionName,
|
||||
"positiveLocalStyles": [style.strip() for style in positiveLocalStyles.split(",") if style.strip()],
|
||||
"negativeLocalStyles": [style.strip() for style in negativeLocalStyles.split(",") if style.strip()],
|
||||
"positiveLocalStyles": self._parseCommaSeparated(positiveLocalStyles),
|
||||
"negativeLocalStyles": self._parseCommaSeparated(negativeLocalStyles),
|
||||
"duration": duration,
|
||||
"lines": [line.strip() for line in lines.split("\n") if line.strip()]
|
||||
"lines": self._parseLines(lines)
|
||||
}
|
||||
|
||||
return (section,)
|
||||
|
||||
def _parseCommaSeparated(self, text):
|
||||
"""Parse comma-separated string into list"""
|
||||
return [item.strip() for item in text.split(",") if item.strip()]
|
||||
|
||||
def _parseLines(self, text):
|
||||
"""Parse multiline text into list"""
|
||||
return [line.strip() for line in text.split("\n") if line.strip()]
|
||||
|
||||
+42
-53
@@ -2,75 +2,64 @@ from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareFrameImages:
|
||||
"""Frame Images node for defining keyframes that constrain video timeline"""
|
||||
|
||||
FRAME_POSITIONS = ["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"]
|
||||
MAX_FRAMES = 4
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optionalInputs = {}
|
||||
|
||||
for i in range(1, cls.MAX_FRAMES + 1):
|
||||
optionalInputs[f"image{i}"] = ("IMAGE", {
|
||||
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
|
||||
})
|
||||
optionalInputs[f"frame{i}_position"] = (cls.FRAME_POSITIONS, {
|
||||
"default": "auto",
|
||||
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
|
||||
})
|
||||
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"image1": ("IMAGE", {
|
||||
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
|
||||
}),
|
||||
"frame1_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
|
||||
"default": "auto",
|
||||
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
|
||||
}),
|
||||
"image2": ("IMAGE", {
|
||||
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
|
||||
}),
|
||||
"frame2_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
|
||||
"default": "auto",
|
||||
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
|
||||
}),
|
||||
"image3": ("IMAGE", {
|
||||
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
|
||||
}),
|
||||
"frame3_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
|
||||
"default": "auto",
|
||||
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
|
||||
}),
|
||||
"image4": ("IMAGE", {
|
||||
"tooltip": "Frame image that will constrain video content at a specific timeline position. Used for keyframe control.",
|
||||
}),
|
||||
"frame4_position": (["auto", "first", "last", "0", "12", "24", "36", "48", "60", "72", "84", "96", "108", "120"], {
|
||||
"default": "auto",
|
||||
"tooltip": "Frame position: 'auto' (automatic distribution), 'first' (beginning), 'last' (end), or specific frame number (0-120).",
|
||||
}),
|
||||
}
|
||||
"optional": optionalInputs
|
||||
}
|
||||
|
||||
DESCRIPTION = "Define keyframes that constrain specific frames within the video timeline. Different from reference images - these control WHEN specific visual content appears, not overall style consistency."
|
||||
FUNCTION = "create_frame_images"
|
||||
FUNCTION = "createFrameImages"
|
||||
RETURN_TYPES = ("RUNWAREFRAMEIMAGES",)
|
||||
RETURN_NAMES = ("Frame Images",)
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def create_frame_images(self, **kwargs):
|
||||
frame_images = []
|
||||
def createFrameImages(self, **kwargs):
|
||||
"""Create frame images list from provided parameters"""
|
||||
frameImages = []
|
||||
|
||||
# Process each image input
|
||||
for i in range(1, 5): # Support up to 4 images
|
||||
image_key = f"image{i}"
|
||||
position_key = f"frame{i}_position"
|
||||
for i in range(1, self.MAX_FRAMES + 1):
|
||||
imageKey = f"image{i}"
|
||||
positionKey = f"frame{i}_position"
|
||||
|
||||
image = kwargs.get(image_key)
|
||||
position = kwargs.get(position_key, "auto")
|
||||
image = kwargs.get(imageKey)
|
||||
position = kwargs.get(positionKey, "auto")
|
||||
|
||||
if image is not None:
|
||||
# Convert image to data URI or UUID
|
||||
image_data = rwUtils.convertTensor2IMG(image)
|
||||
|
||||
frame_data = {"inputImage": image_data}
|
||||
|
||||
# Only add frame position if not auto
|
||||
if position != "auto":
|
||||
if position.isdigit():
|
||||
frame_data["frame"] = int(position)
|
||||
else:
|
||||
frame_data["frame"] = position # "first" or "last"
|
||||
|
||||
frame_images.append(frame_data)
|
||||
frameData = self._createFrameData(image, position)
|
||||
frameImages.append(frameData)
|
||||
|
||||
return (frame_images,)
|
||||
return (frameImages,)
|
||||
|
||||
def _createFrameData(self, image, position):
|
||||
"""Create frame data object from image and position"""
|
||||
imageData = rwUtils.convertTensor2IMG(image)
|
||||
frameData = {"inputImage": imageData}
|
||||
|
||||
if position != "auto":
|
||||
if position.isdigit():
|
||||
frameData["frame"] = int(position)
|
||||
else:
|
||||
frameData["frame"] = position # "first" or "last"
|
||||
|
||||
return frameData
|
||||
|
||||
|
||||
# Node class mappings for ComfyUI registration
|
||||
|
||||
@@ -87,10 +87,10 @@ class txt2img:
|
||||
"default": True,
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Random seeds are generated as 32-bit values for platform compatibility.",
|
||||
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 9223372036854776000,
|
||||
"min": 0,
|
||||
"max": 4294967295,
|
||||
}),
|
||||
"useClipSkip": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include clipSkip parameter in API request. Disable if your model doesn't support clipSkip.",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class imageInferenceInputs:
|
||||
"""Image Inference Inputs node for configuring image generation inputs"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -31,32 +34,31 @@ class imageInferenceInputs:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def createInputs(self, **kwargs):
|
||||
"""Create image inference inputs from provided parameters"""
|
||||
image = kwargs.get("image", None)
|
||||
refImage1 = kwargs.get("Reference Image 1", None)
|
||||
refImage2 = kwargs.get("Reference Image 2", None)
|
||||
refImage3 = kwargs.get("Reference Image 3", None)
|
||||
refImage4 = kwargs.get("Reference Image 4", None)
|
||||
|
||||
# Build the inputs structure
|
||||
inputs = {}
|
||||
|
||||
# Handle image if provided
|
||||
if image is not None:
|
||||
inputs["image"] = rwUtils.convertTensor2IMG(image)
|
||||
|
||||
# Handle references if any are provided
|
||||
references = []
|
||||
if refImage1 is not None:
|
||||
references.append(rwUtils.convertTensor2IMG(refImage1))
|
||||
if refImage2 is not None:
|
||||
references.append(rwUtils.convertTensor2IMG(refImage2))
|
||||
if refImage3 is not None:
|
||||
references.append(rwUtils.convertTensor2IMG(refImage3))
|
||||
if refImage4 is not None:
|
||||
references.append(rwUtils.convertTensor2IMG(refImage4))
|
||||
|
||||
references = self._collectReferences(refImage1, refImage2, refImage3, refImage4)
|
||||
if len(references) > 0:
|
||||
inputs["references"] = references
|
||||
|
||||
return (inputs, )
|
||||
return (inputs,)
|
||||
|
||||
def _collectReferences(self, refImage1, refImage2, refImage3, refImage4):
|
||||
"""Collect and convert reference images to list"""
|
||||
references = []
|
||||
refImages = [refImage1, refImage2, refImage3, refImage4]
|
||||
|
||||
for refImage in refImages:
|
||||
if refImage is not None:
|
||||
references.append(rwUtils.convertTensor2IMG(refImage))
|
||||
|
||||
return references
|
||||
|
||||
+14
-9
@@ -1,7 +1,9 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from typing import List, Dict, Any
|
||||
from typing import List
|
||||
|
||||
|
||||
class inputAudios:
|
||||
"""Input Audios node for configuring multiple audio inputs"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -29,11 +31,14 @@ class inputAudios:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def createInputAudios(self, **kwargs) -> tuple[List[str]]:
|
||||
audio_list = []
|
||||
"""Create list of audio UUIDs from provided parameters"""
|
||||
audioList = []
|
||||
|
||||
for i in range(1, 5):
|
||||
audio_key = f"Audio{i}"
|
||||
audio_uuid = kwargs.get(audio_key)
|
||||
if audio_uuid and audio_uuid.strip() != "":
|
||||
audio_list.append(audio_uuid.strip())
|
||||
return (audio_list,)
|
||||
|
||||
audioKey = f"Audio{i}"
|
||||
audioUuid = kwargs.get(audioKey)
|
||||
|
||||
if audioUuid and audioUuid.strip() != "":
|
||||
audioList.append(audioUuid.strip())
|
||||
|
||||
return (audioList,)
|
||||
|
||||
+269
-276
@@ -1,10 +1,9 @@
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
import time
|
||||
import requests
|
||||
import torch
|
||||
import torchaudio
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
from io import BytesIO
|
||||
from .utils.runwareUtils import (
|
||||
@@ -12,11 +11,27 @@ from .utils.runwareUtils import (
|
||||
RUNWARE_API_BASE_URL,
|
||||
generalRequestWrapper,
|
||||
genRandUUID,
|
||||
genRandSeed,
|
||||
sendMediaUUID,
|
||||
)
|
||||
|
||||
|
||||
class RunwareMediaUpload:
|
||||
"""Runware Media Upload node for uploading audio/video to Runware storage"""
|
||||
|
||||
# MIME type mapping for video files
|
||||
VIDEO_MIME_TYPES = {
|
||||
'.mp4': 'video/mp4',
|
||||
'.avi': 'video/avi',
|
||||
'.mov': 'video/quicktime',
|
||||
'.webm': 'video/webm',
|
||||
}
|
||||
|
||||
DEFAULT_SAMPLE_RATE = 22050
|
||||
DEFAULT_VIDEO_MIME = 'video/mp4'
|
||||
MAX_POLL_ATTEMPTS = 100
|
||||
POLL_INTERVAL = 2
|
||||
REQUEST_TIMEOUT = 30
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -33,7 +48,7 @@ class RunwareMediaUpload:
|
||||
"tooltip": "This field will be automatically populated with the generated media UUID after upload.",
|
||||
}),
|
||||
},
|
||||
"hidden": { "node_id": "UNIQUE_ID" }
|
||||
"hidden": {"node_id": "UNIQUE_ID"}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
@@ -44,325 +59,303 @@ class RunwareMediaUpload:
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, **kwargs):
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return float("NAN")
|
||||
|
||||
def uploadMedia(self, media=None, mediaUUID=None, **kwargs):
|
||||
print(f"[Debug] uploadMedia called with media: {type(media)}, mediaUUID: {mediaUUID}")
|
||||
def uploadMedia(self, media=None, mediaUuid=None, **kwargs):
|
||||
"""Main function to upload media and return mediaUUID"""
|
||||
if media is None:
|
||||
raise ValueError("Media input is required")
|
||||
|
||||
print(f"[Debug] uploadMedia called with media: {type(media)}, mediaUuid: {mediaUuid}")
|
||||
|
||||
try:
|
||||
# Convert media tensor to base64
|
||||
print(f"[Debug] Converting media to base64...")
|
||||
media_base64 = self._convert_media_to_base64(media)
|
||||
print(f"[Debug] Media converted to base64, length: {len(media_base64)}")
|
||||
mediaBase64 = self._convertMediaToBase64(media)
|
||||
print(f"[Debug] Media converted to base64, length: {len(mediaBase64)}")
|
||||
|
||||
# Upload to Runware
|
||||
print(f"[Debug] Starting upload to Runware...")
|
||||
media_uuid = self._upload_to_runware(media_base64)
|
||||
print(f"[Debug] Upload completed! MediaUUID: {media_uuid}")
|
||||
print(f"[Debug] ===== RESULT: {media_uuid} =====")
|
||||
|
||||
# Send the mediaUUID back to the UI
|
||||
sendMediaUUID(media_uuid, kwargs.get("node_id"))
|
||||
|
||||
return (media_uuid,)
|
||||
mediaUuid = self._uploadToRunware(mediaBase64)
|
||||
print(f"[Debug] Upload completed! MediaUUID: {mediaUuid}")
|
||||
print(f"[Debug] ===== RESULT: {mediaUuid} =====")
|
||||
|
||||
sendMediaUUID(mediaUuid, kwargs.get("node_id"))
|
||||
return (mediaUuid,)
|
||||
except Exception as e:
|
||||
print(f"[Debug] Media upload failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
def _convert_media_to_base64(self, media_tensor):
|
||||
def _convertMediaToBase64(self, mediaTensor):
|
||||
"""Convert media tensor to base64 data URI"""
|
||||
try:
|
||||
# Handle ComfyUI VIDEO objects (VideoFromFile)
|
||||
print(f"[Debug] Media type: {type(media_tensor)}")
|
||||
print(f"[Debug] Media attributes: {dir(media_tensor)}")
|
||||
|
||||
# Check if this is a ComfyUI VideoFromFile object
|
||||
if hasattr(media_tensor, 'video_path') or str(type(media_tensor)).find('VideoFromFile') != -1:
|
||||
# This is a ComfyUI VideoFromFile object
|
||||
print(f"[Debug] VideoFromFile object detected")
|
||||
|
||||
# Try to get the file path from the private attribute
|
||||
video_file = getattr(media_tensor, '_VideoFromFile__file', None)
|
||||
if video_file is None:
|
||||
# Try other possible attributes
|
||||
for attr in ['video_path', 'path', 'file_path', 'filename', '_file']:
|
||||
if hasattr(media_tensor, attr):
|
||||
video_file = getattr(media_tensor, attr)
|
||||
print(f"[Debug] Found video file via {attr}: {video_file}")
|
||||
break
|
||||
|
||||
if video_file is None:
|
||||
raise ValueError("Could not find video file in VideoFromFile object")
|
||||
|
||||
# If it's a file object, get the path
|
||||
if hasattr(video_file, 'name'):
|
||||
video_path = video_file.name
|
||||
elif isinstance(video_file, str):
|
||||
video_path = video_file
|
||||
else:
|
||||
raise ValueError(f"Unexpected video file type: {type(video_file)}")
|
||||
|
||||
print(f"[Debug] Processing video file: {video_path}")
|
||||
|
||||
# Read the video file and convert to base64
|
||||
with open(video_path, 'rb') as f:
|
||||
video_bytes = f.read()
|
||||
|
||||
# Detect MIME type based on file extension
|
||||
if video_path.lower().endswith('.mp4'):
|
||||
mime_type = 'video/mp4'
|
||||
elif video_path.lower().endswith('.avi'):
|
||||
mime_type = 'video/avi'
|
||||
elif video_path.lower().endswith('.mov'):
|
||||
mime_type = 'video/quicktime'
|
||||
elif video_path.lower().endswith('.webm'):
|
||||
mime_type = 'video/webm'
|
||||
else:
|
||||
mime_type = 'video/mp4' # Default
|
||||
|
||||
# Encode to base64
|
||||
video_base64 = base64.b64encode(video_bytes).decode('utf-8')
|
||||
|
||||
# Return as data URI
|
||||
return f"data:{mime_type};base64,{video_base64}"
|
||||
|
||||
# ComfyUI AUDIO objects are dictionaries with 'waveform' and 'sample_rate'
|
||||
elif isinstance(media_tensor, dict):
|
||||
if 'waveform' in media_tensor:
|
||||
waveform = media_tensor['waveform']
|
||||
sample_rate = media_tensor.get('sample_rate', 22050)
|
||||
|
||||
# Convert tensor to proper format for torchaudio
|
||||
if isinstance(waveform, torch.Tensor):
|
||||
# Ensure waveform is in the correct format [channels, samples]
|
||||
if waveform.dim() == 3:
|
||||
waveform = waveform.squeeze(0) # Remove batch dimension
|
||||
elif waveform.dim() == 1:
|
||||
waveform = waveform.unsqueeze(0) # Add channel dimension
|
||||
else:
|
||||
waveform = torch.tensor(waveform)
|
||||
if waveform.dim() == 1:
|
||||
waveform = waveform.unsqueeze(0)
|
||||
|
||||
# Create WAV file in memory
|
||||
buffer = BytesIO()
|
||||
torchaudio.save(buffer, waveform, sample_rate, format="wav")
|
||||
buffer.seek(0)
|
||||
|
||||
# Get the WAV file bytes
|
||||
wav_bytes = buffer.getvalue()
|
||||
|
||||
# Encode to base64
|
||||
wav_base64 = base64.b64encode(wav_bytes).decode('utf-8')
|
||||
|
||||
# Return as data URI
|
||||
return f"data:audio/wav;base64,{wav_base64}"
|
||||
else:
|
||||
raise ValueError("Invalid audio object: missing 'waveform' key")
|
||||
else:
|
||||
# Fallback for direct tensor input
|
||||
if isinstance(media_tensor, torch.Tensor):
|
||||
waveform = media_tensor
|
||||
sample_rate = 22050 # Default sample rate
|
||||
|
||||
# Ensure waveform is in the correct format [channels, samples]
|
||||
if waveform.dim() == 3:
|
||||
waveform = waveform.squeeze(0) # Remove batch dimension
|
||||
elif waveform.dim() == 1:
|
||||
waveform = waveform.unsqueeze(0) # Add channel dimension
|
||||
else:
|
||||
waveform = torch.tensor(media_tensor)
|
||||
sample_rate = 22050
|
||||
if waveform.dim() == 1:
|
||||
waveform = waveform.unsqueeze(0)
|
||||
|
||||
# Create WAV file in memory
|
||||
buffer = BytesIO()
|
||||
torchaudio.save(buffer, waveform, sample_rate, format="wav")
|
||||
buffer.seek(0)
|
||||
|
||||
# Get the WAV file bytes
|
||||
wav_bytes = buffer.getvalue()
|
||||
|
||||
# Encode to base64
|
||||
wav_base64 = base64.b64encode(wav_bytes).decode('utf-8')
|
||||
|
||||
# Return as data URI
|
||||
return f"data:audio/wav;base64,{wav_base64}"
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Debug] Error converting media to base64: {str(e)}")
|
||||
raise e
|
||||
|
||||
def _upload_to_runware(self, media_data_uri):
|
||||
"""Upload media to Runware and return mediaUUID"""
|
||||
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL
|
||||
print(f"[Debug] Media type: {type(mediaTensor)}")
|
||||
|
||||
task_uuid = genRandUUID()
|
||||
print(f"[Debug] Generated task UUID: {task_uuid}")
|
||||
|
||||
# Strip data URI prefix for media storage API
|
||||
if media_data_uri.startswith("data:"):
|
||||
media_data = media_data_uri.split(",", 1)[1]
|
||||
print(f"[Debug] Stripped data URI prefix, media data length: {len(media_data)}")
|
||||
if self._isVideoFile(mediaTensor):
|
||||
return self._convertVideoToBase64(mediaTensor)
|
||||
elif isinstance(mediaTensor, dict) and 'waveform' in mediaTensor:
|
||||
return self._convertAudioDictToBase64(mediaTensor)
|
||||
elif isinstance(mediaTensor, torch.Tensor):
|
||||
return self._convertTensorToBase64(mediaTensor)
|
||||
else:
|
||||
media_data = media_data_uri
|
||||
raise ValueError(f"Unsupported media type: {type(mediaTensor)}")
|
||||
|
||||
def _isVideoFile(self, mediaTensor):
|
||||
"""Check if media tensor is a VideoFromFile object"""
|
||||
return (hasattr(mediaTensor, 'video_path') or
|
||||
str(type(mediaTensor)).find('VideoFromFile') != -1)
|
||||
|
||||
def _convertVideoToBase64(self, videoTensor):
|
||||
"""Convert video file to base64 data URI"""
|
||||
print(f"[Debug] VideoFromFile object detected")
|
||||
|
||||
upload_config = [
|
||||
{
|
||||
"taskType": "mediaStorage",
|
||||
"taskUUID": task_uuid,
|
||||
"operation": "upload",
|
||||
"media": media_data,
|
||||
}
|
||||
]
|
||||
videoFile = self._getVideoFilePath(videoTensor)
|
||||
videoPath = self._extractVideoPath(videoFile)
|
||||
|
||||
print(f"[Debug] Upload config: {upload_config[0]}")
|
||||
print(f"[Debug] Processing video file: {videoPath}")
|
||||
|
||||
with open(videoPath, 'rb') as f:
|
||||
videoBytes = f.read()
|
||||
|
||||
mimeType = self._getMimeType(videoPath)
|
||||
videoBase64 = base64.b64encode(videoBytes).decode('utf-8')
|
||||
|
||||
return f"data:{mimeType};base64,{videoBase64}"
|
||||
|
||||
def _getVideoFilePath(self, videoTensor):
|
||||
"""Extract video file path from VideoFromFile object"""
|
||||
videoFile = getattr(videoTensor, '_VideoFromFile__file', None)
|
||||
|
||||
if videoFile is None:
|
||||
for attr in ['video_path', 'path', 'file_path', 'filename', '_file']:
|
||||
if hasattr(videoTensor, attr):
|
||||
videoFile = getattr(videoTensor, attr)
|
||||
print(f"[Debug] Found video file via {attr}: {videoFile}")
|
||||
break
|
||||
|
||||
if videoFile is None:
|
||||
raise ValueError("Could not find video file in VideoFromFile object")
|
||||
|
||||
return videoFile
|
||||
|
||||
def _extractVideoPath(self, videoFile):
|
||||
"""Extract file path from file object or string"""
|
||||
if hasattr(videoFile, 'name'):
|
||||
return videoFile.name
|
||||
elif isinstance(videoFile, str):
|
||||
return videoFile
|
||||
else:
|
||||
raise ValueError(f"Unexpected video file type: {type(videoFile)}")
|
||||
|
||||
def _getMimeType(self, filePath):
|
||||
"""Get MIME type based on file extension"""
|
||||
ext = os.path.splitext(filePath.lower())[1]
|
||||
return self.VIDEO_MIME_TYPES.get(ext, self.DEFAULT_VIDEO_MIME)
|
||||
|
||||
def _convertAudioDictToBase64(self, audioDict):
|
||||
"""Convert audio dictionary to base64 WAV data URI"""
|
||||
waveform = audioDict['waveform']
|
||||
sampleRate = audioDict.get('sample_rate', self.DEFAULT_SAMPLE_RATE)
|
||||
|
||||
waveformNp = self._prepareWaveformNumpy(waveform)
|
||||
wavBytes = self._convertWaveformToWav(waveformNp, sampleRate)
|
||||
wavBase64 = base64.b64encode(wavBytes).decode('utf-8')
|
||||
|
||||
return f"data:audio/wav;base64,{wavBase64}"
|
||||
|
||||
def _convertTensorToBase64(self, mediaTensor):
|
||||
"""Convert direct tensor input to base64 WAV data URI"""
|
||||
waveformNp = self._prepareWaveformNumpy(mediaTensor)
|
||||
wavBytes = self._convertWaveformToWav(waveformNp, self.DEFAULT_SAMPLE_RATE)
|
||||
wavBase64 = base64.b64encode(wavBytes).decode('utf-8')
|
||||
|
||||
return f"data:audio/wav;base64,{wavBase64}"
|
||||
|
||||
def _prepareWaveformNumpy(self, waveform):
|
||||
"""Prepare waveform tensor/array for soundfile (returns [channels, samples])"""
|
||||
if isinstance(waveform, torch.Tensor):
|
||||
# Normalize dimensions: [batch, channels, samples] -> [channels, samples]
|
||||
if waveform.dim() == 3:
|
||||
waveform = waveform.squeeze(0)
|
||||
elif waveform.dim() == 1:
|
||||
waveform = waveform.unsqueeze(0)
|
||||
waveformNp = waveform.cpu().numpy()
|
||||
else:
|
||||
waveformNp = np.array(waveform)
|
||||
if waveformNp.ndim == 1:
|
||||
waveformNp = waveformNp.reshape(1, -1)
|
||||
|
||||
return waveformNp
|
||||
|
||||
def _convertWaveformToWav(self, waveformNp, sampleRate):
|
||||
"""Convert numpy waveform to WAV bytes using soundfile"""
|
||||
# soundfile expects [samples, channels]
|
||||
waveformNp = waveformNp.T
|
||||
|
||||
buffer = BytesIO()
|
||||
sf.write(buffer, waveformNp, sampleRate, format='WAV')
|
||||
buffer.seek(0)
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
def _uploadToRunware(self, mediaDataUri):
|
||||
"""Upload media to Runware and return mediaUUID"""
|
||||
taskUuid = genRandUUID()
|
||||
print(f"[Debug] Generated task UUID: {taskUuid}")
|
||||
|
||||
mediaData = self._stripDataUriPrefix(mediaDataUri)
|
||||
uploadConfig = self._buildUploadConfig(taskUuid, mediaData)
|
||||
|
||||
print(f"[Debug] Upload config: {uploadConfig[0]}")
|
||||
print(f"[Debug] API URL: {RUNWARE_API_BASE_URL}")
|
||||
|
||||
headers = {
|
||||
uploadResult = self._makeUploadRequest(uploadConfig)
|
||||
self._validateUploadResponse(uploadResult)
|
||||
|
||||
uploadData = uploadResult.get("data", [])
|
||||
if uploadData and len(uploadData) > 0:
|
||||
mediaUuid = uploadData[0].get("mediaUUID", "")
|
||||
if mediaUuid and mediaUuid != "1":
|
||||
print(f"[Debug] Got immediate mediaUUID: {mediaUuid}")
|
||||
return mediaUuid
|
||||
|
||||
print(f"[Debug] Starting polling for result...")
|
||||
return self._pollForResult(taskUuid)
|
||||
|
||||
def _stripDataUriPrefix(self, mediaDataUri):
|
||||
"""Strip data URI prefix if present"""
|
||||
if mediaDataUri.startswith("data:"):
|
||||
mediaData = mediaDataUri.split(",", 1)[1]
|
||||
print(f"[Debug] Stripped data URI prefix, media data length: {len(mediaData)}")
|
||||
return mediaData
|
||||
return mediaDataUri
|
||||
|
||||
def _buildUploadConfig(self, taskUuid, mediaData):
|
||||
"""Build upload configuration for API request"""
|
||||
return [{
|
||||
"taskType": "mediaStorage",
|
||||
"taskUUID": taskUuid,
|
||||
"operation": "upload",
|
||||
"media": mediaData,
|
||||
}]
|
||||
|
||||
def _getApiHeaders(self):
|
||||
"""Get API request headers"""
|
||||
return {
|
||||
"Authorization": f"Bearer {RUNWARE_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
}
|
||||
|
||||
def _makeUploadRequest(self, uploadConfig):
|
||||
"""Make upload request to Runware API"""
|
||||
def recaller():
|
||||
print(f"[Debug] Making POST request to Runware API...")
|
||||
return requests.post(
|
||||
RUNWARE_API_BASE_URL,
|
||||
headers=self._getApiHeaders(),
|
||||
json=uploadConfig,
|
||||
timeout=self.REQUEST_TIMEOUT,
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
response = generalRequestWrapper(recaller)
|
||||
return self._parseJsonResponse(response)
|
||||
|
||||
def _parseJsonResponse(self, response):
|
||||
"""Parse JSON response from API"""
|
||||
if response.status_code != 200:
|
||||
errorText = response.text[:500]
|
||||
print(f"[Debug] Request failed with status {response.status_code}")
|
||||
print(f"[Debug] Response text: {errorText}")
|
||||
raise Exception(f"Request failed with status {response.status_code}: {response.text[:200]}")
|
||||
|
||||
if not response.text or not response.text.strip():
|
||||
raise Exception("Request failed: Empty response from server")
|
||||
|
||||
try:
|
||||
def recaller():
|
||||
print(f"[Debug] Making POST request to Runware API...")
|
||||
return requests.post(
|
||||
RUNWARE_API_BASE_URL,
|
||||
headers=headers,
|
||||
json=upload_config,
|
||||
timeout=30,
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
upload_result = generalRequestWrapper(recaller)
|
||||
|
||||
# Check response status before trying to parse JSON
|
||||
if upload_result.status_code != 200:
|
||||
print(f"[Debug] Upload failed with status {upload_result.status_code}")
|
||||
print(f"[Debug] Response text: {upload_result.text[:500]}")
|
||||
raise Exception(f"Upload failed with status {upload_result.status_code}: {upload_result.text[:200]}")
|
||||
|
||||
# Check if response is empty
|
||||
if not upload_result.text or not upload_result.text.strip():
|
||||
raise Exception("Upload failed: Empty response from server")
|
||||
|
||||
try:
|
||||
upload_result = upload_result.json()
|
||||
except ValueError as e:
|
||||
print(f"[Debug] Failed to parse JSON response: {str(e)}")
|
||||
print(f"[Debug] Response text: {upload_result.text[:500]}")
|
||||
raise Exception(f"Upload failed: Invalid JSON response from server: {str(e)}")
|
||||
|
||||
print(f"[Debug] Upload response: {upload_result}")
|
||||
|
||||
if "errors" in upload_result:
|
||||
print(f"[Debug] Upload error: {upload_result}")
|
||||
raise Exception(f"Upload failed: {upload_result}")
|
||||
|
||||
# Check if we got immediate result or need to poll
|
||||
if upload_result.get("data") and len(upload_result["data"]) > 0:
|
||||
data = upload_result["data"][0]
|
||||
print(f"[Debug] Upload data: {data}")
|
||||
if "mediaUUID" in data and data["mediaUUID"] != "1":
|
||||
print(f"[Debug] Got immediate mediaUUID: {data['mediaUUID']}")
|
||||
return data["mediaUUID"]
|
||||
|
||||
# Poll for result
|
||||
print(f"[Debug] Starting polling for result...")
|
||||
return self._poll_for_result(task_uuid)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Debug] Upload request failed: {str(e)}")
|
||||
raise e
|
||||
return response.json()
|
||||
except ValueError as e:
|
||||
print(f"[Debug] Failed to parse JSON response: {str(e)}")
|
||||
print(f"[Debug] Response text: {response.text[:500]}")
|
||||
raise Exception(f"Request failed: Invalid JSON response from server: {str(e)}")
|
||||
|
||||
def _poll_for_result(self, task_uuid):
|
||||
def _validateUploadResponse(self, uploadResult):
|
||||
"""Validate upload response"""
|
||||
print(f"[Debug] Upload response: {uploadResult}")
|
||||
|
||||
if "errors" in uploadResult:
|
||||
print(f"[Debug] Upload error: {uploadResult}")
|
||||
raise Exception(f"Upload failed: {uploadResult}")
|
||||
|
||||
def _pollForResult(self, taskUuid):
|
||||
"""Poll for media upload result"""
|
||||
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL
|
||||
pollConfig = [{
|
||||
"taskType": "getResponse",
|
||||
"taskUUID": taskUuid,
|
||||
}]
|
||||
|
||||
poll_config = [
|
||||
{
|
||||
"taskType": "getResponse",
|
||||
"taskUUID": task_uuid,
|
||||
}
|
||||
]
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {RUNWARE_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
}
|
||||
|
||||
max_attempts = 100
|
||||
for attempt in range(max_attempts):
|
||||
for attempt in range(self.MAX_POLL_ATTEMPTS):
|
||||
try:
|
||||
print(f"[Debug] Poll attempt {attempt + 1}/{max_attempts}")
|
||||
print(f"[Debug] Poll attempt {attempt + 1}/{self.MAX_POLL_ATTEMPTS}")
|
||||
|
||||
def recaller():
|
||||
return requests.post(
|
||||
RUNWARE_API_BASE_URL,
|
||||
headers=headers,
|
||||
json=poll_config,
|
||||
timeout=30,
|
||||
headers=self._getApiHeaders(),
|
||||
json=pollConfig,
|
||||
timeout=self.REQUEST_TIMEOUT,
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
poll_result = generalRequestWrapper(recaller)
|
||||
pollResult = generalRequestWrapper(recaller)
|
||||
pollData = self._parsePollResponse(pollResult)
|
||||
|
||||
# Check response status
|
||||
if poll_result.status_code != 200:
|
||||
print(f"[Debug] Poll failed with status {poll_result.status_code}")
|
||||
continue
|
||||
|
||||
# Check if response is empty
|
||||
if not poll_result.text or not poll_result.text.strip():
|
||||
print(f"[Debug] Poll returned empty response")
|
||||
continue
|
||||
|
||||
try:
|
||||
poll_result = poll_result.json()
|
||||
except ValueError as e:
|
||||
print(f"[Debug] Failed to parse JSON in poll: {str(e)}")
|
||||
continue
|
||||
|
||||
print(f"[Debug] Poll response: {poll_result}")
|
||||
|
||||
if "errors" in poll_result:
|
||||
print(f"[Debug] Poll error: {poll_result}")
|
||||
continue
|
||||
|
||||
if "data" in poll_result and len(poll_result["data"]) > 0:
|
||||
data = poll_result["data"][0]
|
||||
print(f"[Debug] Poll data: {data}")
|
||||
|
||||
# Check if mediaUUID is still "1" (still processing)
|
||||
if "mediaUUID" in data and data["mediaUUID"] == "1":
|
||||
if pollData:
|
||||
mediaUuid = pollData.get("mediaUUID", "")
|
||||
if mediaUuid == "1":
|
||||
print(f"[Debug] Still processing... attempt {attempt + 1}")
|
||||
time.sleep(2)
|
||||
time.sleep(self.POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
# Check for completion - look for mediaUUID
|
||||
if "mediaUUID" in data and data["mediaUUID"] != "1":
|
||||
print(f"[Debug] Upload completed! MediaUUID: {data['mediaUUID']}")
|
||||
return data["mediaUUID"]
|
||||
elif mediaUuid and mediaUuid != "1":
|
||||
print(f"[Debug] Upload completed! MediaUUID: {mediaUuid}")
|
||||
return mediaUuid
|
||||
|
||||
print(f"[Debug] Attempt {attempt + 1}: No data in response, continuing...")
|
||||
time.sleep(2)
|
||||
|
||||
time.sleep(self.POLL_INTERVAL)
|
||||
except Exception as e:
|
||||
print(f"[Debug] Poll attempt {attempt + 1} failed: {str(e)}")
|
||||
time.sleep(2)
|
||||
time.sleep(self.POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
raise Exception("Polling timeout - upload did not complete")
|
||||
|
||||
def _parsePollResponse(self, pollResult):
|
||||
"""Parse polling response"""
|
||||
if pollResult.status_code != 200:
|
||||
print(f"[Debug] Poll failed with status {pollResult.status_code}")
|
||||
return None
|
||||
|
||||
if not pollResult.text or not pollResult.text.strip():
|
||||
print(f"[Debug] Poll returned empty response")
|
||||
return None
|
||||
|
||||
try:
|
||||
pollResultJson = pollResult.json()
|
||||
except ValueError as e:
|
||||
print(f"[Debug] Failed to parse JSON in poll: {str(e)}")
|
||||
return None
|
||||
|
||||
print(f"[Debug] Poll response: {pollResultJson}")
|
||||
|
||||
if "errors" in pollResultJson:
|
||||
print(f"[Debug] Poll error: {pollResultJson}")
|
||||
return None
|
||||
|
||||
if "data" in pollResultJson and len(pollResultJson["data"]) > 0:
|
||||
print(f"[Debug] Poll data: {pollResultJson['data'][0]}")
|
||||
return pollResultJson["data"][0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Export the class directly
|
||||
runwareMediaUpload = RunwareMediaUpload
|
||||
|
||||
+46
-37
@@ -1,4 +1,42 @@
|
||||
class modelSearch:
|
||||
"""Model Search node for searching and selecting image models"""
|
||||
|
||||
MODEL_ARCHITECTURES = [
|
||||
"All",
|
||||
"FLUX.1-Schnell",
|
||||
"FLUX.1-Dev",
|
||||
"FLUX.1-Krea",
|
||||
"Pony",
|
||||
"SD 1.5",
|
||||
"SD 1.5 Hyper",
|
||||
"SD 1.5 LCM",
|
||||
"SD 3",
|
||||
"SDXL 1.0",
|
||||
"SDXL 1.0 LCM",
|
||||
"SDXL Distilled",
|
||||
"SDXL Hyper",
|
||||
"SDXL Lightning",
|
||||
"SDXL Turbo",
|
||||
]
|
||||
|
||||
MODEL_TYPES = [
|
||||
"Base Model",
|
||||
"Inpainting Model",
|
||||
]
|
||||
|
||||
FEATURED_MODELS = [
|
||||
"rundiffusion:110@101 (Juggernaut Lightning Flux by RunDiffusion)",
|
||||
"rundiffusion:130@100 (Juggernaut Pro Flux by RunDiffusion)",
|
||||
"runware:100@1 (Flux Schnell)",
|
||||
"runware:101@1 (Flux Dev)",
|
||||
"runware:107@1 (FLUX.1 Krea)",
|
||||
"runware:5@1 (SD3)",
|
||||
"civitai:4384@128713 (SDXL 1.5 DreamShaper)",
|
||||
"civitai:43331@176425 (SD 1.5 majicMIX realistic 麦橘写实)",
|
||||
"civitai:101055@128078 (SDXL v1.0 VAE fix)",
|
||||
"civitai:133005@288982 (SDXL Juggernaut XL V8)",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -6,45 +44,15 @@ class modelSearch:
|
||||
"Model Search": ("STRING", {
|
||||
"tooltip": "Search For Specific Model By Name Or Civit AIR Code (eg: Juggernaut).",
|
||||
}),
|
||||
"Model Architecture": ([
|
||||
"All",
|
||||
"FLUX.1-Schnell",
|
||||
"FLUX.1-Dev",
|
||||
"FLUX.1-Krea",
|
||||
"Pony",
|
||||
"SD 1.5",
|
||||
"SD 1.5 Hyper",
|
||||
"SD 1.5 LCM",
|
||||
"SD 3",
|
||||
"SDXL 1.0",
|
||||
"SDXL 1.0 LCM",
|
||||
"SDXL Distilled",
|
||||
"SDXL Hyper",
|
||||
"SDXL Lightning",
|
||||
"SDXL Turbo",
|
||||
], {
|
||||
"Model Architecture": (cls.MODEL_ARCHITECTURES, {
|
||||
"tooltip": "Choose Model Architecture To Filter Results.",
|
||||
"default": "All",
|
||||
}),
|
||||
"ModelType": ([
|
||||
"Base Model",
|
||||
"Inpainting Model",
|
||||
], {
|
||||
"ModelType": (cls.MODEL_TYPES, {
|
||||
"tooltip": "Choose Model Type To Filter Results.",
|
||||
"default": "Base Model",
|
||||
}),
|
||||
"ModelList": ([
|
||||
"rundiffusion:110@101 (Juggernaut Lightning Flux by RunDiffusion)",
|
||||
"rundiffusion:130@100 (Juggernaut Pro Flux by RunDiffusion)",
|
||||
"runware:100@1 (Flux Schnell)",
|
||||
"runware:101@1 (Flux Dev)",
|
||||
"runware:107@1 (FLUX.1 Krea)",
|
||||
"runware:5@1 (SD3)",
|
||||
"civitai:4384@128713 (SDXL 1.5 DreamShaper)",
|
||||
"civitai:43331@176425 (SD 1.5 majicMIX realistic 麦橘写实)",
|
||||
"civitai:101055@128078 (SDXL v1.0 VAE fix)",
|
||||
"civitai:133005@288982 (SDXL Juggernaut XL V8)",
|
||||
], {
|
||||
"ModelList": (cls.FEATURED_MODELS, {
|
||||
"tooltip": "Model Results Will Show UP Here So You Could Choose From. If You didn't Search For Anything this will show featured Model List.",
|
||||
"default": "runware:100@1 (Flux Schnell)",
|
||||
}),
|
||||
@@ -68,13 +76,14 @@ class modelSearch:
|
||||
return True
|
||||
|
||||
def modelSearch(self, **kwargs):
|
||||
"""Search and return model AIR code"""
|
||||
enableSearchValue = kwargs.get("Use Search Value", False)
|
||||
searchInput = kwargs.get("Model Search")
|
||||
|
||||
if enableSearchValue:
|
||||
modelAIRCode = searchInput
|
||||
modelAirCode = searchInput
|
||||
else:
|
||||
crModel = kwargs.get("ModelList")
|
||||
modelAIRCode = crModel.split(" ")[0]
|
||||
currentModel = kwargs.get("ModelList")
|
||||
modelAirCode = currentModel.split(" ")[0]
|
||||
|
||||
return (modelAIRCode,)
|
||||
return (modelAirCode,)
|
||||
|
||||
@@ -85,10 +85,10 @@ class photoMaker:
|
||||
"max": 2,
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value.",
|
||||
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
|
||||
"default": 0,
|
||||
"min": 1,
|
||||
"max": 9223372036854776000,
|
||||
"min": 0,
|
||||
"max": 4294967295,
|
||||
}),
|
||||
"batchSize": ("INT", {
|
||||
"tooltip": "The number of images to generate in a single request.",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Runware Provider Settings Module
|
||||
Contains all provider-specific settings nodes for Runware ComfyUI integration.
|
||||
"""
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from ..utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareOpenAIProviderSettings:
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from ..utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwarePixverseProviderSettings:
|
||||
@@ -1,4 +1,4 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from ..utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareProviderSettings:
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Runware Runway Provider Settings Node
|
||||
Provides Runway-specific settings for video generation
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class RunwareRunwayProviderSettings:
|
||||
"""Runware Runway Provider Settings Node"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"publicFigureThreshold": ("STRING", {
|
||||
"tooltip": "Public figure threshold for content moderation (provider key: contentModeration.publicFigureThreshold).",
|
||||
"default": None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("RUNWAREPROVIDERSETTINGS",)
|
||||
RETURN_NAMES = ("Provider Settings",)
|
||||
FUNCTION = "createProviderSettings"
|
||||
CATEGORY = "Runware"
|
||||
DESCRIPTION = "Configure Runway-specific provider settings for video generation including content moderation options."
|
||||
|
||||
def createProviderSettings(self, **kwargs) -> tuple[Dict[str, Any]]:
|
||||
"""Create Runway provider settings"""
|
||||
|
||||
publicFigureThreshold = kwargs.get("publicFigureThreshold", None)
|
||||
|
||||
runwaySettings: Dict[str, Any] = {}
|
||||
|
||||
# Build contentModeration if publicFigureThreshold is provided
|
||||
if publicFigureThreshold is not None and publicFigureThreshold.strip():
|
||||
contentModeration: Dict[str, Any] = {}
|
||||
contentModeration["publicFigureThreshold"] = publicFigureThreshold.strip()
|
||||
runwaySettings["contentModeration"] = contentModeration
|
||||
|
||||
# Remove unset values
|
||||
runwaySettings = {k: v for k, v in runwaySettings.items() if v is not None}
|
||||
|
||||
return (runwaySettings,)
|
||||
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RunwareRunwayProviderSettings": RunwareRunwayProviderSettings,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareRunwayProviderSettings": "Runware Runway Provider Settings",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from ..utils import runwareUtils as rwUtils
|
||||
|
||||
class RunwareViduProviderSettings:
|
||||
@classmethod
|
||||
+13
-10
@@ -1,13 +1,16 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class referenceImages:
|
||||
"""Reference Images node for providing reference images for identity consistency"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"Image 1": ("IMAGE", {
|
||||
"tooltip": "Specifies Reference Image 1 that will be used as reference for the subject. These reference images help the AI to maintain identity consistency during the generation process."
|
||||
}),
|
||||
"Image 1": ("IMAGE", {
|
||||
"tooltip": "Specifies Reference Image 1 that will be used as reference for the subject. These reference images help the AI to maintain identity consistency during the generation process."
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"Image 2": ("IMAGE", {
|
||||
@@ -29,17 +32,17 @@ class referenceImages:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def referenceImages(self, **kwargs):
|
||||
"""Collect and convert reference images to list"""
|
||||
image1 = kwargs.get("Image 1")
|
||||
image2 = kwargs.get("Image 2", None)
|
||||
image3 = kwargs.get("Image 3", None)
|
||||
image4 = kwargs.get("Image 4", None)
|
||||
|
||||
imageList = [rwUtils.convertTensor2IMG(image1)]
|
||||
if (image2 is not None):
|
||||
imageList.append(rwUtils.convertTensor2IMG(image2))
|
||||
if (image3 is not None):
|
||||
imageList.append(rwUtils.convertTensor2IMG(image3))
|
||||
if (image4 is not None):
|
||||
imageList.append(rwUtils.convertTensor2IMG(image4))
|
||||
|
||||
optionalImages = [image2, image3, image4]
|
||||
for image in optionalImages:
|
||||
if image is not None:
|
||||
imageList.append(rwUtils.convertTensor2IMG(image))
|
||||
|
||||
return (imageList, )
|
||||
return (imageList,)
|
||||
|
||||
+26
-23
@@ -1,25 +1,25 @@
|
||||
from typing import List
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class referenceVideos:
|
||||
"""Reference Videos node for configuring reference video inputs"""
|
||||
|
||||
MAX_VIDEOS = 4
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optionalInputs = {}
|
||||
|
||||
for i in range(1, cls.MAX_VIDEOS + 1):
|
||||
ordinal = rwUtils.getOrdinal(i)
|
||||
optionalInputs[f"Video{i}"] = ("STRING", {
|
||||
"tooltip": f"MediaUUID for the {ordinal} reference video from Runware Media Upload node.",
|
||||
})
|
||||
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"Video1": ("STRING", {
|
||||
"tooltip": "MediaUUID for the first reference video from Runware Media Upload node.",
|
||||
}),
|
||||
"Video2": ("STRING", {
|
||||
"tooltip": "MediaUUID for the second reference video from Runware Media Upload node.",
|
||||
}),
|
||||
"Video3": ("STRING", {
|
||||
"tooltip": "MediaUUID for the third reference video from Runware Media Upload node.",
|
||||
}),
|
||||
"Video4": ("STRING", {
|
||||
"tooltip": "MediaUUID for the fourth reference video from Runware Media Upload node.",
|
||||
}),
|
||||
}
|
||||
"optional": optionalInputs
|
||||
}
|
||||
|
||||
DESCRIPTION = "Configure multiple reference video inputs (mediaUUIDs) for video generation."
|
||||
@@ -29,11 +29,14 @@ class referenceVideos:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def createReferenceVideos(self, **kwargs) -> tuple[List[str]]:
|
||||
video_list = []
|
||||
for i in range(1, 5):
|
||||
video_key = f"Video{i}"
|
||||
video_uuid = kwargs.get(video_key)
|
||||
if video_uuid and video_uuid.strip() != "":
|
||||
video_list.append(video_uuid.strip())
|
||||
return (video_list,)
|
||||
|
||||
"""Create list of reference video UUIDs"""
|
||||
videoList = []
|
||||
|
||||
for i in range(1, self.MAX_VIDEOS + 1):
|
||||
videoKey = f"Video{i}"
|
||||
videoUuid = kwargs.get(videoKey)
|
||||
|
||||
if videoUuid and videoUuid.strip() != "":
|
||||
videoList.append(videoUuid.strip())
|
||||
|
||||
return (videoList,)
|
||||
|
||||
+11
-4
@@ -1,4 +1,6 @@
|
||||
class refiner:
|
||||
"""Refiner node for enhancing image quality using refiner models"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -28,22 +30,27 @@ class refiner:
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, startStep, startStepPercentage):
|
||||
if(startStep != 0 and startStepPercentage != 0):
|
||||
"""Validate that exactly one of startStep or startStepPercentage is provided"""
|
||||
if startStep != 0 and startStepPercentage != 0:
|
||||
raise Exception("Please provide either startStep or startStepPercentage, not both.")
|
||||
elif(startStep == 0 and startStepPercentage == 0):
|
||||
elif startStep == 0 and startStepPercentage == 0:
|
||||
raise Exception("Please provide either startStep or startStepPercentage.")
|
||||
else:
|
||||
return True
|
||||
|
||||
def refiner(self, **kwargs):
|
||||
"""Create refiner configuration"""
|
||||
runwareModel = kwargs.get("Model")
|
||||
startStep = kwargs.get("startStep")
|
||||
startStepPercentage = kwargs.get("startStepPercentage")
|
||||
|
||||
refinerResult = {
|
||||
"model": runwareModel,
|
||||
}
|
||||
if(startStep != 0):
|
||||
|
||||
if startStep != 0:
|
||||
refinerResult["startStep"] = startStep
|
||||
else:
|
||||
refinerResult["startStepPercentage"] = startStepPercentage
|
||||
return (refinerResult,)
|
||||
|
||||
return (refinerResult,)
|
||||
|
||||
@@ -38,10 +38,10 @@ class runwareKontext:
|
||||
"tooltip": "Adjust the dimensions of the generated image. This setting allows you to choose from various aspect ratios, such as 16:9 for wide images or 1:1 for square images. The width and height will be automatically adjusted based on the selected aspect ratio.",
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value.",
|
||||
"tooltip": "A value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
|
||||
"default": 0,
|
||||
"min": 1,
|
||||
"max": 9223372036854776000,
|
||||
"min": 0,
|
||||
"max": 4294967295,
|
||||
}),
|
||||
"Multi Inference Mode": ("BOOLEAN", {
|
||||
"tooltip": "If Enabled the node will skip the image generation process and will only return the Runware Task Object to be used in the Multi Inference Node.",
|
||||
|
||||
+11
-13
@@ -1,7 +1,8 @@
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class safetyInputs:
|
||||
"""Runware Safety Inputs Node"""
|
||||
"""Runware Safety Inputs Node for configuring safety and content moderation settings"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -35,32 +36,30 @@ class safetyInputs:
|
||||
|
||||
RETURN_TYPES = ("RUNWARESAFETYINPUTS",)
|
||||
RETURN_NAMES = ("Safety Inputs",)
|
||||
FUNCTION = "create_safety_inputs"
|
||||
FUNCTION = "createSafetyInputs"
|
||||
CATEGORY = "Runware"
|
||||
DESCRIPTION = "Configure safety and content moderation settings for Runware tasks."
|
||||
|
||||
def create_safety_inputs(self, **kwargs) -> tuple[Dict[str, Any]]:
|
||||
def createSafetyInputs(self, **kwargs) -> tuple[Dict[str, Any]]:
|
||||
"""Create safety inputs dictionary"""
|
||||
|
||||
# Get parameters
|
||||
mode = kwargs.get("mode", None)
|
||||
tolerance = kwargs.get("tolerance", None)
|
||||
checkInputs = kwargs.get("checkInputs", None)
|
||||
checkContent = kwargs.get("checkContent", None)
|
||||
|
||||
# Build safety inputs dictionary - only include non-None values
|
||||
safety_inputs = {}
|
||||
safetyInputs = {}
|
||||
|
||||
if mode is not None:
|
||||
safety_inputs["mode"] = mode
|
||||
safetyInputs["mode"] = mode
|
||||
if tolerance is not None:
|
||||
safety_inputs["tolerance"] = tolerance
|
||||
safetyInputs["tolerance"] = tolerance
|
||||
if checkInputs is not None:
|
||||
safety_inputs["checkInputs"] = checkInputs
|
||||
safetyInputs["checkInputs"] = checkInputs
|
||||
if checkContent is not None:
|
||||
safety_inputs["checkContent"] = checkContent
|
||||
safetyInputs["checkContent"] = checkContent
|
||||
|
||||
return (safety_inputs,)
|
||||
return (safetyInputs,)
|
||||
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -70,4 +69,3 @@ NODE_CLASS_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareSafetyInputs": "Runware Safety Inputs",
|
||||
}
|
||||
|
||||
|
||||
@@ -192,12 +192,18 @@ def setMinImageCacheSize(size: int):
|
||||
os.environ["RUNWARE_MIN_IMAGE_CACHE_SIZE"] = str(size)
|
||||
return True
|
||||
|
||||
def genRandSeed(minSeed=1000, maxSeed=9223372036854776000):
|
||||
def genRandSeed(minSeed=1, maxSeed=4294967295):
|
||||
"""Generate a random seed value (32-bit unsigned integer range for compatibility)"""
|
||||
return random.randint(minSeed, maxSeed)
|
||||
|
||||
def genRandUUID():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def getOrdinal(num):
|
||||
"""Get ordinal string for number (e.g., 1 -> 'first', 2 -> 'second')"""
|
||||
ordinals = {1: "first", 2: "second", 3: "third", 4: "fourth"}
|
||||
return ordinals.get(num, f"{num}th")
|
||||
|
||||
def checkAPIKey(apiKey):
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
|
||||
+59
-61
@@ -3,14 +3,18 @@ import json
|
||||
import base64
|
||||
import io
|
||||
|
||||
|
||||
class SVGData:
|
||||
"""Wrapper class to match RecraftIO.SVG format for SaveSVGNode"""
|
||||
def __init__(self, svg_content):
|
||||
# Convert SVG string to BytesIO
|
||||
svg_bytes = io.BytesIO(svg_content.encode('utf-8'))
|
||||
self.data = [svg_bytes]
|
||||
|
||||
def __init__(self, svgContent):
|
||||
svgBytes = io.BytesIO(svgContent.encode('utf-8'))
|
||||
self.data = [svgBytes]
|
||||
|
||||
|
||||
class vectorize:
|
||||
"""Vectorize node for converting images to vector format"""
|
||||
|
||||
RUNWARE_VECTORIZE_MODELS = {
|
||||
"recraft:1@1": "recraft:1@1",
|
||||
"picsart:1@1": "picsart:1@1",
|
||||
@@ -37,69 +41,64 @@ class vectorize:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def vectorizeImage(self, **kwargs):
|
||||
"""Vectorize image and return SVG data"""
|
||||
image = kwargs.get("Image")
|
||||
modelName = kwargs.get("Model", "recraft:1@1")
|
||||
|
||||
# Get the model AIR code
|
||||
model = self.RUNWARE_VECTORIZE_MODELS.get(modelName, "recraft:1@1")
|
||||
imageUuid = rwUtils.convertTensor2IMG(image)
|
||||
|
||||
# Convert image to UUID
|
||||
image_uuid = rwUtils.convertTensor2IMG(image)
|
||||
genConfig = self._buildGenConfig(model, imageUuid)
|
||||
|
||||
# Build the API request
|
||||
genConfig = [
|
||||
{
|
||||
"taskType": "vectorize",
|
||||
"taskUUID": rwUtils.genRandUUID(),
|
||||
"model": model,
|
||||
"inputs": {
|
||||
"image": image_uuid
|
||||
},
|
||||
"outputType": "base64Data",
|
||||
"outputFormat": "svg",
|
||||
}
|
||||
]
|
||||
print(f"[DEBUG] Sending Vectorize Request:")
|
||||
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
|
||||
|
||||
try:
|
||||
# Debug: Print the request being sent
|
||||
print(f"[DEBUG] Sending Vectorize Request:")
|
||||
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
|
||||
|
||||
genResult = rwUtils.inferenecRequest(genConfig)
|
||||
|
||||
# Debug: Print the response received
|
||||
print(f"[DEBUG] Received Vectorize Response:")
|
||||
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
|
||||
|
||||
# Check for errors
|
||||
if "errors" in genResult:
|
||||
error_message = genResult["errors"][0]["message"]
|
||||
raise Exception(f"Vectorization failed: {error_message}")
|
||||
|
||||
# Extract the result
|
||||
if "data" in genResult and len(genResult["data"]) > 0:
|
||||
result_data = genResult["data"][0]
|
||||
|
||||
# Check for base64 SVG data
|
||||
if "imageBase64Data" in result_data:
|
||||
base64_svg = result_data["imageBase64Data"]
|
||||
|
||||
# Decode base64 SVG to get the actual SVG content for SaveSVGNode
|
||||
svg_content = base64.b64decode(base64_svg).decode('utf-8')
|
||||
|
||||
# Wrap SVG content in SVGData for SaveSVGNode compatibility
|
||||
svg_data = SVGData(svg_content)
|
||||
|
||||
# Return only SVG data
|
||||
return (svg_data,)
|
||||
else:
|
||||
raise Exception("imageBase64Data not found in response")
|
||||
else:
|
||||
raise Exception("No data returned from vectorization API")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Error] Vectorization failed: {str(e)}")
|
||||
raise e
|
||||
genResult = rwUtils.inferenecRequest(genConfig)
|
||||
|
||||
print(f"[DEBUG] Received Vectorize Response:")
|
||||
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
|
||||
|
||||
self._validateResponse(genResult)
|
||||
|
||||
svgData = self._extractSvgData(genResult)
|
||||
|
||||
return (svgData,)
|
||||
|
||||
def _buildGenConfig(self, model, imageUuid):
|
||||
"""Build generation configuration for API request"""
|
||||
return [{
|
||||
"taskType": "vectorize",
|
||||
"taskUUID": rwUtils.genRandUUID(),
|
||||
"model": model,
|
||||
"inputs": {
|
||||
"image": imageUuid
|
||||
},
|
||||
"outputType": "base64Data",
|
||||
"outputFormat": "svg",
|
||||
}]
|
||||
|
||||
def _validateResponse(self, genResult):
|
||||
"""Validate API response"""
|
||||
if "errors" in genResult:
|
||||
errorMessage = genResult["errors"][0]["message"]
|
||||
raise Exception(f"Vectorization failed: {errorMessage}")
|
||||
|
||||
if "data" not in genResult or len(genResult["data"]) == 0:
|
||||
raise Exception("No data returned from vectorization API")
|
||||
|
||||
def _extractSvgData(self, genResult):
|
||||
"""Extract SVG data from API response"""
|
||||
resultData = genResult["data"][0]
|
||||
|
||||
if "imageBase64Data" not in resultData:
|
||||
raise Exception("imageBase64Data not found in response")
|
||||
|
||||
base64Svg = resultData["imageBase64Data"]
|
||||
svgContent = base64.b64decode(base64Svg).decode('utf-8')
|
||||
svgData = SVGData(svgContent)
|
||||
|
||||
return svgData
|
||||
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -109,4 +108,3 @@ NODE_CLASS_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareVectorize": "Runware Vectorize",
|
||||
}
|
||||
|
||||
|
||||
+68
-77
@@ -2,7 +2,10 @@ from .utils import runwareUtils as rwUtils
|
||||
import json
|
||||
import comfy.model_management
|
||||
|
||||
|
||||
class videoBgRemoval:
|
||||
"""Video Background Removal node for removing video backgrounds"""
|
||||
|
||||
RUNWARE_VRMBG_MODELS = {
|
||||
"bria:51@1": "bria:51@1",
|
||||
}
|
||||
@@ -20,7 +23,6 @@ class videoBgRemoval:
|
||||
"Orange [255, 165, 0, 0]": [255, 165, 0, 0],
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -52,96 +54,86 @@ class videoBgRemoval:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def removeBackground(self, **kwargs):
|
||||
video_uuid = kwargs.get("Video")
|
||||
"""Remove background from video"""
|
||||
videoUuid = kwargs.get("Video")
|
||||
modelName = kwargs.get("Model", "Bria Video Background Removal")
|
||||
backgroundColor = kwargs.get("Background Color", "Transparent")
|
||||
outputFormat = kwargs.get("Output Format", "mp4")
|
||||
|
||||
# Validate mediaUUID
|
||||
if not video_uuid or video_uuid.strip() == "":
|
||||
if not videoUuid or videoUuid.strip() == "":
|
||||
raise ValueError("Video mediaUUID is required")
|
||||
|
||||
# Get the model AIR code
|
||||
modelAIR = self.RUNWARE_VRMBG_MODELS.get(modelName, "bria:51@1")
|
||||
modelAir = self.RUNWARE_VRMBG_MODELS.get(modelName, "bria:51@1")
|
||||
rgbaColor = self.BACKGROUND_COLORS.get(backgroundColor, [255, 255, 255, 0])
|
||||
|
||||
# Get background color RGBA
|
||||
rgba_color = self.BACKGROUND_COLORS.get(backgroundColor, [255, 255, 255, 0])
|
||||
genConfig = self._buildGenConfig(videoUuid, modelAir, outputFormat, rgbaColor)
|
||||
|
||||
# Build the API request
|
||||
genConfig = [
|
||||
{
|
||||
"taskType": "removeBackground",
|
||||
"taskUUID": rwUtils.genRandUUID(),
|
||||
"inputs": {
|
||||
"video": video_uuid
|
||||
},
|
||||
"model": modelAIR,
|
||||
"outputFormat": outputFormat,
|
||||
"settings": {
|
||||
"rgba": rgba_color
|
||||
}
|
||||
print(f"[DEBUG] Sending Video Background Removal Request:")
|
||||
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
|
||||
|
||||
genResult = rwUtils.inferenecRequest(genConfig)
|
||||
|
||||
print(f"[DEBUG] Received Video Background Removal Response:")
|
||||
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
|
||||
|
||||
self._validateResponse(genResult)
|
||||
|
||||
taskUuid = genConfig[0]["taskUUID"]
|
||||
videos = self._pollForVideoResult(taskUuid)
|
||||
|
||||
return videos
|
||||
|
||||
def _buildGenConfig(self, videoUuid, modelAir, outputFormat, rgbaColor):
|
||||
"""Build generation configuration for API request"""
|
||||
return [{
|
||||
"taskType": "removeBackground",
|
||||
"taskUUID": rwUtils.genRandUUID(),
|
||||
"inputs": {
|
||||
"video": videoUuid
|
||||
},
|
||||
"model": modelAir,
|
||||
"outputFormat": outputFormat,
|
||||
"settings": {
|
||||
"rgba": rgbaColor
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
# Debug: Print the request being sent
|
||||
print(f"[DEBUG] Sending Video Background Removal Request:")
|
||||
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
|
||||
}]
|
||||
|
||||
def _validateResponse(self, genResult):
|
||||
"""Validate API response"""
|
||||
if "errors" in genResult:
|
||||
errorMessage = genResult["errors"][0]["message"]
|
||||
raise Exception(f"Video background removal failed: {errorMessage}")
|
||||
|
||||
def _pollForVideoResult(self, taskUuid):
|
||||
"""Poll for video result"""
|
||||
while True:
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
genResult = rwUtils.inferenecRequest(genConfig)
|
||||
pollResult = rwUtils.pollVideoResult(taskUuid)
|
||||
print(f"[Debugging] Poll result: {pollResult}")
|
||||
|
||||
# Debug: Print the response received
|
||||
print(f"[DEBUG] Received Video Background Removal Response:")
|
||||
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
|
||||
if pollResult and "errors" in pollResult and len(pollResult["errors"]) > 0:
|
||||
errorInfo = pollResult["errors"][0]
|
||||
errorMessage = errorInfo.get("message", "Unknown error")
|
||||
raise Exception(f"Video background removal failed: {errorMessage}")
|
||||
|
||||
# Check for errors
|
||||
if "errors" in genResult:
|
||||
error_message = genResult["errors"][0]["message"]
|
||||
raise Exception(f"Video background removal failed: {error_message}")
|
||||
|
||||
# Extract task UUID for polling
|
||||
taskUUID = genConfig[0]["taskUUID"]
|
||||
|
||||
# Poll for video completion
|
||||
while True:
|
||||
# Check for interrupt before each poll
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
|
||||
videoData = pollResult["data"][0]
|
||||
|
||||
# Poll for video result
|
||||
pollResult = rwUtils.pollVideoResult(taskUUID)
|
||||
print(f"[Debugging] Poll result: {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")
|
||||
raise Exception(f"Video background removal failed: {error_message}")
|
||||
|
||||
if pollResult and "data" in pollResult and len(pollResult["data"]) > 0:
|
||||
video_data = pollResult["data"][0]
|
||||
if "status" in videoData:
|
||||
status = videoData["status"]
|
||||
|
||||
# Check status directly
|
||||
if "status" in video_data:
|
||||
status = video_data["status"]
|
||||
|
||||
if status == "success":
|
||||
if "videoURL" in video_data or "videoBase64Data" in video_data:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
|
||||
return videos
|
||||
|
||||
# If status is "processing", continue polling
|
||||
|
||||
# Check for interrupt before waiting
|
||||
if status == "success":
|
||||
if "videoURL" in videoData or "videoBase64Data" in videoData:
|
||||
videos = rwUtils.convertVideoB64List(pollResult, 1920, 1080)
|
||||
return videos
|
||||
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
for _ in range(10):
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
# Wait before next poll
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Error] Video background removal failed: {str(e)}")
|
||||
raise e
|
||||
rwUtils.time.sleep(0.1)
|
||||
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -151,4 +143,3 @@ NODE_CLASS_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareVideoBgRemoval": "Runware Video Background Removal",
|
||||
}
|
||||
|
||||
|
||||
+11
-13
@@ -71,10 +71,10 @@ class txt2vid:
|
||||
"default": True,
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"tooltip": "A value used to randomize the video generation. If you want to make videos reproducible (generate the same video multiple times), you can use the same seed value. Leave empty or 0 to auto-generate.",
|
||||
"tooltip": "A value used to randomize the video generation. If you want to make videos reproducible (generate the same video multiple times), you can use the same seed value. Set to 0 to auto-generate a random seed.",
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 9223372036854776000,
|
||||
"min": 0,
|
||||
"max": 4294967295,
|
||||
}),
|
||||
"useSteps": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include steps parameter in API request. Disable if your model doesn't support steps.",
|
||||
@@ -205,11 +205,9 @@ class txt2vid:
|
||||
width = customWidth
|
||||
height = customHeight
|
||||
else:
|
||||
|
||||
|
||||
model_dimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
|
||||
width = model_dimensions["width"]
|
||||
height = model_dimensions["height"]
|
||||
modelDimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
|
||||
width = modelDimensions["width"]
|
||||
height = modelDimensions["height"]
|
||||
|
||||
genConfig = [
|
||||
{
|
||||
@@ -221,7 +219,7 @@ class txt2vid:
|
||||
"includeCost": True,
|
||||
}
|
||||
]
|
||||
if height!=0 and width!=0:
|
||||
if height != 0 and width != 0:
|
||||
genConfig[0]['height'] = height
|
||||
genConfig[0]['width'] = width
|
||||
|
||||
@@ -359,11 +357,11 @@ class txt2vid:
|
||||
"invalid height" in error_msg.lower() or
|
||||
"dimension not supported" in error_msg.lower()):
|
||||
# Get model default dimensions for comparison
|
||||
model_dimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
|
||||
expected_width = model_dimensions["width"]
|
||||
expected_height = model_dimensions["height"]
|
||||
modelDimensions = videoModelSearch.MODEL_DIMENSIONS.get(model, {"width": 1024, "height": 576})
|
||||
expectedWidth = modelDimensions["width"]
|
||||
expectedHeight = modelDimensions["height"]
|
||||
|
||||
raise Exception(f"Error: Unsupported width/height combination for this model architecture. You used {width}x{height}, but {model} expects {expected_width}x{expected_height}. Please use 'Model Default' or set custom dimensions to {expected_width}x{expected_height}.")
|
||||
raise Exception(f"Error: Unsupported width/height combination for this model architecture. You used {width}x{height}, but {model} expects {expectedWidth}x{expectedHeight}. Please use 'Model Default' or set custom dimensions to {expectedWidth}x{expectedHeight}.")
|
||||
else:
|
||||
# Re-raise the original error
|
||||
raise e
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from typing import List
|
||||
|
||||
|
||||
class videoInferenceInputs:
|
||||
"""Video Inference Inputs node for configuring video generation inputs"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -34,6 +38,7 @@ class videoInferenceInputs:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def createInputs(self, **kwargs):
|
||||
"""Create video inference inputs from provided parameters"""
|
||||
image = kwargs.get("Image", None)
|
||||
audio = kwargs.get("Audio", None)
|
||||
video = kwargs.get("Video", None)
|
||||
@@ -44,8 +49,7 @@ class videoInferenceInputs:
|
||||
inputs = {}
|
||||
|
||||
if image is not None:
|
||||
image_uuid = rwUtils.convertTensor2IMG(image)
|
||||
inputs["image"] = image_uuid
|
||||
inputs["image"] = rwUtils.convertTensor2IMG(image)
|
||||
|
||||
if audio is not None and audio.strip() != "":
|
||||
inputs["audio"] = audio.strip()
|
||||
@@ -54,15 +58,12 @@ class videoInferenceInputs:
|
||||
inputs["video"] = video.strip()
|
||||
|
||||
if mask is not None:
|
||||
mask_uuid = rwUtils.convertTensor2IMG(mask)
|
||||
inputs["mask"] = mask_uuid
|
||||
inputs["mask"] = rwUtils.convertTensor2IMG(mask)
|
||||
|
||||
if frame is not None:
|
||||
frame_uuid = rwUtils.convertTensor2IMG(frame)
|
||||
inputs["frame"] = frame_uuid
|
||||
inputs["frame"] = rwUtils.convertTensor2IMG(frame)
|
||||
|
||||
if references is not None and len(references) > 0:
|
||||
inputs["references"] = references
|
||||
|
||||
return (inputs,)
|
||||
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
from .utils import runwareUtils as rwUtils
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class videoInputsReferences:
|
||||
"""Video Inputs References node for configuring reference images with types"""
|
||||
|
||||
MAX_REFERENCES = 4
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optionalInputs = {}
|
||||
|
||||
for i in range(1, cls.MAX_REFERENCES + 1):
|
||||
ordinal = rwUtils.getOrdinal(i)
|
||||
optionalInputs[f"Image{i}"] = ("IMAGE", {
|
||||
"tooltip": f"{ordinal.capitalize()} reference image.",
|
||||
})
|
||||
optionalInputs[f"Type{i}"] = ("STRING", {
|
||||
"tooltip": f"Type for the {ordinal} reference (e.g., 'asset'). Leave empty to omit.",
|
||||
"default": "",
|
||||
})
|
||||
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"Image1": ("IMAGE", {
|
||||
"tooltip": "First reference image.",
|
||||
}),
|
||||
"Type1": ("STRING", {
|
||||
"tooltip": "Type for the first reference (e.g., 'asset'). Leave empty to omit.",
|
||||
"default": "",
|
||||
}),
|
||||
"Image2": ("IMAGE", {
|
||||
"tooltip": "Second reference image.",
|
||||
}),
|
||||
"Type2": ("STRING", {
|
||||
"tooltip": "Type for the second reference (e.g., 'asset'). Leave empty to omit.",
|
||||
"default": "",
|
||||
}),
|
||||
"Image3": ("IMAGE", {
|
||||
"tooltip": "Third reference image.",
|
||||
}),
|
||||
"Type3": ("STRING", {
|
||||
"tooltip": "Type for the third reference (e.g., 'asset'). Leave empty to omit.",
|
||||
"default": "",
|
||||
}),
|
||||
"Image4": ("IMAGE", {
|
||||
"tooltip": "Fourth reference image.",
|
||||
}),
|
||||
"Type4": ("STRING", {
|
||||
"tooltip": "Type for the fourth reference (e.g., 'asset'). Leave empty to omit.",
|
||||
"default": "",
|
||||
}),
|
||||
}
|
||||
"optional": optionalInputs
|
||||
}
|
||||
|
||||
DESCRIPTION = "Configure multiple reference images with optional types for video inference inputs."
|
||||
@@ -45,29 +33,33 @@ class videoInputsReferences:
|
||||
CATEGORY = "Runware"
|
||||
|
||||
def createReferences(self, **kwargs) -> tuple[List[Dict[str, Any]]]:
|
||||
"""Create list of reference objects from provided parameters"""
|
||||
references = []
|
||||
|
||||
for i in range(1, 5):
|
||||
image_key = f"Image{i}"
|
||||
type_key = f"Type{i}"
|
||||
for i in range(1, self.MAX_REFERENCES + 1):
|
||||
imageKey = f"Image{i}"
|
||||
typeKey = f"Type{i}"
|
||||
|
||||
image = kwargs.get(image_key)
|
||||
ref_type = kwargs.get(type_key, "")
|
||||
image = kwargs.get(imageKey)
|
||||
refType = kwargs.get(typeKey, "")
|
||||
|
||||
if image is not None:
|
||||
# Convert image tensor to URL
|
||||
image_url = rwUtils.convertTensor2IMG(image)
|
||||
|
||||
reference = {"image": image_url}
|
||||
|
||||
# Only add type if it's not empty
|
||||
if ref_type and ref_type.strip() != "":
|
||||
reference["type"] = ref_type.strip()
|
||||
|
||||
reference = self._createReference(image, refType)
|
||||
references.append(reference)
|
||||
|
||||
return (references,)
|
||||
|
||||
def _createReference(self, image, refType):
|
||||
"""Create reference object from image and type"""
|
||||
imageUrl = rwUtils.convertTensor2IMG(image)
|
||||
reference = {"image": imageUrl}
|
||||
|
||||
if refType and refType.strip() != "":
|
||||
reference["type"] = refType.strip()
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RunwareVideoInputsReferences": videoInputsReferences,
|
||||
@@ -76,4 +68,3 @@ NODE_CLASS_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareVideoInputsReferences": "Runware Video Inputs References",
|
||||
}
|
||||
|
||||
|
||||
+106
-71
@@ -1,4 +1,6 @@
|
||||
class videoModelSearch:
|
||||
"""Video Model Search node for searching and selecting video models"""
|
||||
|
||||
# Define the video models by architecture with their default dimensions
|
||||
VIDEO_MODELS = {
|
||||
"KlingAI": [
|
||||
@@ -21,7 +23,7 @@ class videoModelSearch:
|
||||
"google:3@2 (Veo 3.1)",
|
||||
"google:3@3 (Veo 3.1 Fast)",
|
||||
],
|
||||
"Seedance": [
|
||||
"Bytedance": [
|
||||
"bytedance:2@1 (Seedance 1.0 Pro)",
|
||||
"bytedance:1@1 (Seedance 1.0 Lite)",
|
||||
"bytedance:5@1 (OmniHuman 1)",
|
||||
@@ -68,100 +70,103 @@ class videoModelSearch:
|
||||
# Model dimensions mapping
|
||||
MODEL_DIMENSIONS = {
|
||||
# KlingAI Models
|
||||
"klingai:1@2": {"width": 1280, "height": 720}, # KlingAI V1.0 Pro
|
||||
"klingai:1@1": {"width": 1280, "height": 720}, # KlingAI V1 Standard
|
||||
"klingai:2@2": {"width": 1920, "height": 1080}, # KlingAI V1.5 Pro
|
||||
"klingai:2@1": {"width": 1280, "height": 720}, # KlingAI V1.5 Standard
|
||||
"klingai:3@1": {"width": 1280, "height": 720}, # KlingAI V1.6 Standard
|
||||
"klingai:3@2": {"width": 1920, "height": 1080}, # KlingAI V1.6 Pro
|
||||
"klingai:4@3": {"width": 1280, "height": 720}, # KlingAI V2.1 Master
|
||||
"klingai:5@1": {"width": 1280, "height": 720}, # KlingAI V2.1 Standard (I2V)
|
||||
"klingai:5@2": {"width": 1920, "height": 1080}, # KlingAI V2.1 Pro (I2V)
|
||||
"klingai:5@3": {"width": 1920, "height": 1080}, # KlingAI V2.0 Master
|
||||
"klingai:7@1": {"width": 0, "height": 0}, # KlingAI V3.0 Standard
|
||||
"klingai:1@2": {"width": 1280, "height": 720},
|
||||
"klingai:1@1": {"width": 1280, "height": 720},
|
||||
"klingai:2@2": {"width": 1920, "height": 1080},
|
||||
"klingai:2@1": {"width": 1280, "height": 720},
|
||||
"klingai:3@1": {"width": 1280, "height": 720},
|
||||
"klingai:3@2": {"width": 1920, "height": 1080},
|
||||
"klingai:4@3": {"width": 1280, "height": 720},
|
||||
"klingai:5@1": {"width": 1280, "height": 720},
|
||||
"klingai:5@2": {"width": 1920, "height": 1080},
|
||||
"klingai:5@3": {"width": 1920, "height": 1080},
|
||||
"klingai:7@1": {"width": 0, "height": 0},
|
||||
|
||||
# Veo Models
|
||||
"google:2@0": {"width": 1280, "height": 720}, # Veo 2.0
|
||||
"google:3@0": {"width": 1280, "height": 720}, # Veo 3.0
|
||||
"google:3@1": {"width": 1280, "height": 720}, # Veo 3.0 Fast
|
||||
"google:3@2": {"width": 1280, "height": 720}, # Veo 3.1
|
||||
"google:3@3": {"width": 1280, "height": 720}, # Veo 3.1 Fast
|
||||
"google:2@0": {"width": 1280, "height": 720},
|
||||
"google:3@0": {"width": 1280, "height": 720},
|
||||
"google:3@1": {"width": 1280, "height": 720},
|
||||
"google:3@2": {"width": 1280, "height": 720},
|
||||
"google:3@3": {"width": 1280, "height": 720},
|
||||
|
||||
# Seedance Models
|
||||
"bytedance:2@1": {"width": 864, "height": 480}, # Seedance 1.0 Pro
|
||||
"bytedance:1@1": {"width": 864, "height": 480}, # Seedance 1.0 Lite
|
||||
"bytedance:5@1": {"width": 1024, "height": 1024}, # OmniHuman 1
|
||||
"bytedance:5@2": {"width": 1024, "height": 1024}, # OmniHuman 1.5
|
||||
# Bytedance Models
|
||||
"bytedance:2@1": {"width": 864, "height": 480},
|
||||
"bytedance:1@1": {"width": 864, "height": 480},
|
||||
"bytedance:5@1": {"width": 1024, "height": 1024},
|
||||
"bytedance:5@2": {"width": 1024, "height": 1024},
|
||||
|
||||
# MiniMax Models
|
||||
"minimax:1@1": {"width": 1366, "height": 768}, # MiniMax 01 Base
|
||||
"minimax:2@1": {"width": 1366, "height": 768}, # MiniMax 01 Director
|
||||
"minimax:2@3": {"width": 1366, "height": 768}, # MiniMax I2V 01 Live
|
||||
"minimax:3@1": {"width": 1366, "height": 768}, # MiniMax 02 Hailuo
|
||||
"minimax:4@1": {"width": 1366, "height": 768}, # MiniMax Hailuo 2.3
|
||||
"minimax:4@2": {"width": 1366, "height": 768}, # MiniMax Hailuo 2.3 Fast
|
||||
"minimax:1@1": {"width": 1366, "height": 768},
|
||||
"minimax:2@1": {"width": 1366, "height": 768},
|
||||
"minimax:2@3": {"width": 1366, "height": 768},
|
||||
"minimax:3@1": {"width": 1366, "height": 768},
|
||||
"minimax:4@1": {"width": 1366, "height": 768},
|
||||
"minimax:4@2": {"width": 1366, "height": 768},
|
||||
|
||||
# PixVerse Models
|
||||
"pixverse:1@1": {"width": 640, "height": 360}, # PixVerse v3.5
|
||||
"pixverse:1@2": {"width": 640, "height": 360}, # PixVerse v4
|
||||
"pixverse:1@3": {"width": 640, "height": 360}, # PixVerse v4.5
|
||||
"pixverse:lipsync@1": {"width": 640, "height": 360}, # PixVerse LipSync
|
||||
"pixverse:1@1": {"width": 640, "height": 360},
|
||||
"pixverse:1@2": {"width": 640, "height": 360},
|
||||
"pixverse:1@3": {"width": 640, "height": 360},
|
||||
"pixverse:lipsync@1": {"width": 640, "height": 360},
|
||||
|
||||
# Vidu Models
|
||||
"vidu:1@0": {"width": 1920, "height": 1080}, # Vidu Q1 Classic
|
||||
"vidu:1@1": {"width": 1920, "height": 1080}, # Vidu Q1
|
||||
"vidu:1@5": {"width": 1920, "height": 1080}, # Vidu 1.5
|
||||
"vidu:2@0": {"width": 1920, "height": 1080}, # Vidu 2.0
|
||||
"vidu:1@0": {"width": 1920, "height": 1080},
|
||||
"vidu:1@1": {"width": 1920, "height": 1080},
|
||||
"vidu:1@5": {"width": 1920, "height": 1080},
|
||||
"vidu:2@0": {"width": 1920, "height": 1080},
|
||||
|
||||
# Wan Models
|
||||
"runware:200@1": {"width": 853, "height": 480}, # Wan 2.1 1.3B
|
||||
"runware:200@2": {"width": 853, "height": 480}, # Wan 2.1 14B
|
||||
"runware:200@6": {"width": 1280, "height": 720}, # Wan 2.2
|
||||
"runware:200@1": {"width": 853, "height": 480},
|
||||
"runware:200@2": {"width": 853, "height": 480},
|
||||
"runware:200@6": {"width": 1280, "height": 720},
|
||||
|
||||
# OpenAI Models
|
||||
"openai:3@1": {"width": 1280, "height": 720}, # OpenAI Sora 3.1
|
||||
"openai:3@0": {"width": 1280, "height": 720}, # OpenAI Sora 3.0
|
||||
"openai:3@1": {"width": 1280, "height": 720},
|
||||
"openai:3@0": {"width": 1280, "height": 720},
|
||||
|
||||
# Lightricks Models
|
||||
"lightricks:2@0": {"width": 1920, "height": 1080}, # Lightricks v2.0 Fast
|
||||
"lightricks:2@1": {"width": 1920, "height": 1080}, # Lightricks v2.1 Pro
|
||||
"lightricks:2@0": {"width": 1920, "height": 1080},
|
||||
"lightricks:2@1": {"width": 1920, "height": 1080},
|
||||
|
||||
# Ovi Models
|
||||
"runware:190@1": {"width": 0, "height": 0}, # Ovi
|
||||
"runware:190@1": {"width": 0, "height": 0},
|
||||
}
|
||||
|
||||
MODEL_ARCHITECTURES = [
|
||||
"All",
|
||||
"KlingAI",
|
||||
"Veo",
|
||||
"Bytedance",
|
||||
"MiniMax",
|
||||
"PixVerse",
|
||||
"Vidu",
|
||||
"Wan",
|
||||
"OpenAI",
|
||||
"Lightricks",
|
||||
"Ovi"
|
||||
]
|
||||
|
||||
DEFAULT_DIMENSIONS = {"width": 1024, "height": 576}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# Get all models for "All" option
|
||||
all_models = []
|
||||
for models in cls.VIDEO_MODELS.values():
|
||||
all_models.extend(models)
|
||||
allModels = cls._getAllModels()
|
||||
defaultModel = "klingai:5@3 (KlingAI V2.0 Master)"
|
||||
defaultModelCode = defaultModel.split(" (")[0]
|
||||
defaultDims = cls.MODEL_DIMENSIONS.get(defaultModelCode, cls.DEFAULT_DIMENSIONS)
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"Model Search": ("STRING", {
|
||||
"tooltip": "Search For Specific Video Model By Name Or Civit AIR Code (eg: KlingAI, Veo).",
|
||||
}),
|
||||
"Model Architecture": ([
|
||||
"All",
|
||||
"KlingAI",
|
||||
"Veo",
|
||||
"Seedance",
|
||||
"MiniMax",
|
||||
"PixVerse",
|
||||
"Vidu",
|
||||
"Wan",
|
||||
"OpenAI",
|
||||
"Lightricks",
|
||||
"Ovi"
|
||||
], {
|
||||
"Model Architecture": (cls.MODEL_ARCHITECTURES, {
|
||||
"tooltip": "Choose Video Model Architecture To Filter Results.",
|
||||
"default": "KlingAI",
|
||||
}),
|
||||
"VideoList": (all_models, {
|
||||
"VideoList": (allModels, {
|
||||
"tooltip": "Video Model Results Will Show UP Here So You Could Choose From.",
|
||||
"default": "klingai:5@3 (KlingAI V2.0 Master)",
|
||||
"default": defaultModel,
|
||||
}),
|
||||
"Use Search Value": ("BOOLEAN", {
|
||||
"tooltip": "When Enabled, the value you've set in the search input will be used instead.\n\nThis is useful in case the model search API is down or you prefer to set the model manually.",
|
||||
@@ -169,9 +174,31 @@ class videoModelSearch:
|
||||
"label_on": "Enabled",
|
||||
"label_off": "Disabled",
|
||||
}),
|
||||
"Width": ("INT", {
|
||||
"default": defaultDims["width"],
|
||||
"min": 0,
|
||||
"max": 4096,
|
||||
"step": 1,
|
||||
"tooltip": "Custom Width For The Selected Model. Defaults To Model's Recommended Width.",
|
||||
}),
|
||||
"Height": ("INT", {
|
||||
"default": defaultDims["height"],
|
||||
"min": 0,
|
||||
"max": 4096,
|
||||
"step": 1,
|
||||
"tooltip": "Custom Height For The Selected Model. Defaults To Model's Recommended Height.",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _getAllModels(cls):
|
||||
"""Get all models from all architectures"""
|
||||
allModels = []
|
||||
for models in cls.VIDEO_MODELS.values():
|
||||
allModels.extend(models)
|
||||
return allModels
|
||||
|
||||
DESCRIPTION = "Directly Search and Connect Video Models to Runware Video Inference Nodes In ComfyUI."
|
||||
FUNCTION = "videoModelSearch"
|
||||
RETURN_TYPES = ("RUNWAREVIDEOMODEL", "INT", "INT")
|
||||
@@ -183,19 +210,27 @@ class videoModelSearch:
|
||||
return True
|
||||
|
||||
def videoModelSearch(self, **kwargs):
|
||||
"""Search and return video model with dimensions"""
|
||||
enableSearchValue = kwargs.get("Use Search Value", False)
|
||||
searchInput = kwargs.get("Model Search")
|
||||
customWidth = kwargs.get("Width", 0)
|
||||
customHeight = kwargs.get("Height", 0)
|
||||
|
||||
if enableSearchValue:
|
||||
modelAIRCode = searchInput
|
||||
modelAirCode = searchInput
|
||||
else:
|
||||
crModel = kwargs.get("VideoList")
|
||||
# Extract the full AIR identifier (everything before the first space)
|
||||
modelAIRCode = crModel.split(" (")[0]
|
||||
currentModel = kwargs.get("VideoList")
|
||||
modelAirCode = currentModel.split(" (")[0]
|
||||
|
||||
# Get dimensions for the selected model
|
||||
dimensions = self.MODEL_DIMENSIONS.get(modelAIRCode, {"width": 1024, "height": 576})
|
||||
dimensions = self.MODEL_DIMENSIONS.get(modelAirCode, self.DEFAULT_DIMENSIONS)
|
||||
|
||||
width = customWidth if customWidth > 0 else dimensions["width"]
|
||||
height = customHeight if customHeight > 0 else dimensions["height"]
|
||||
|
||||
if width == 0 and height == 0:
|
||||
width = None
|
||||
height = None
|
||||
|
||||
return ({
|
||||
"model": modelAIRCode,
|
||||
}, dimensions["width"], dimensions["height"])
|
||||
"model": modelAirCode,
|
||||
}, width, height)
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
python-dotenv
|
||||
brotli
|
||||
zstd
|
||||
zstd
|
||||
librosa
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"id":"5549ed01-3c63-4157-a500-1c576048515a","revision":0,"last_node_id":21,"last_link_id":18,"nodes":[{"id":16,"type":"SaveAudioMP3","pos":[1199.99267578125,-78.82829284667969],"size":[270,136],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":18},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"quality","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveAudioMP3"},"widgets_values":["audio/ComfyUI","V0"]},{"id":18,"type":"Runware Audio Model Search","pos":[222.23391723632812,-129.12527465820312],"size":[270,82],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"searchQuery","name":"searchQuery","type":"STRING","widget":{"name":"searchQuery"},"link":null},{"localized_name":"provider","name":"provider","type":"COMBO","widget":{"name":"provider"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","links":[17]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"5e5e2b3fac2c984692b057e4e607d778a8c73e40","Node name for S&R":"Runware Audio Model Search"},"widgets_values":["elevenlabs:1@1","elevenlabs"]},{"id":21,"type":"Runware Audio Inference","pos":[590.1148681640625,-162.9167938232422],"size":[400,354],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","link":17},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"usePositivePrompt","name":"usePositivePrompt","type":"BOOLEAN","widget":{"name":"usePositivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"sampleRate","name":"sampleRate","type":"INT","widget":{"name":"sampleRate"},"link":null},{"localized_name":"bitrate","name":"bitrate","type":"INT","widget":{"name":"bitrate"},"link":null},{"localized_name":"outputType","name":"outputType","type":"COMBO","widget":{"name":"outputType"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"numberResults","name":"numberResults","type":"INT","widget":{"name":"numberResults"},"link":null}],"outputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","links":[18]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"5583f01fabae66b237ccd315242dbc74f1d110d6","Node name for S&R":"Runware Audio Inference"},"widgets_values":["classical piano piece, gentle and melodic",true,"",10,true,22050,32,"URL","MP3",1]}],"links":[[17,18,0,21,0,"RUNWAREAUDIOMODEL"],[18,21,0,16,0,"AUDIO"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[857.6627752576644,624.9404459140089]}},"version":0.4}
|
||||
{"id":"5549ed01-3c63-4157-a500-1c576048515a","revision":0,"last_node_id":22,"last_link_id":20,"nodes":[{"id":16,"type":"SaveAudioMP3","pos":[1199.99267578125,-78.82829284667969],"size":[270,136],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":19},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"quality","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"SaveAudioMP3"},"widgets_values":["audio/ComfyUI","V0"]},{"id":22,"type":"Runware Audio Inference","pos":[626.7266845703125,-157.00523376464844],"size":[400,354],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","link":20},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"usePositivePrompt","name":"usePositivePrompt","type":"BOOLEAN","widget":{"name":"usePositivePrompt"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"sampleRate","name":"sampleRate","type":"INT","widget":{"name":"sampleRate"},"link":null},{"localized_name":"bitrate","name":"bitrate","type":"INT","widget":{"name":"bitrate"},"link":null},{"localized_name":"outputType","name":"outputType","type":"COMBO","widget":{"name":"outputType"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"numberResults","name":"numberResults","type":"INT","widget":{"name":"numberResults"},"link":null}],"outputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","links":[19]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"b75f0803ad2a5b5624f97d4025927744329e0c31","Node name for S&R":"Runware Audio Inference"},"widgets_values":[true,"classical piano piece, gentle and melodic","",true,10,22050,32,"URL","MP3",1],"bgcolor":"#5345bf"},{"id":18,"type":"Runware Audio Model Search","pos":[222.23391723632812,-129.12527465820312],"size":[270,82],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"searchQuery","name":"searchQuery","type":"STRING","widget":{"name":"searchQuery"},"link":null},{"localized_name":"provider","name":"provider","type":"COMBO","widget":{"name":"provider"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","links":[20]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"5e5e2b3fac2c984692b057e4e607d778a8c73e40","Node name for S&R":"Runware Audio Model Search"},"widgets_values":["elevenlabs:1@1","elevenlabs"],"bgcolor":"#5345bf"}],"links":[[19,22,0,16,0,"AUDIO"],[20,18,0,22,0,"RUNWAREAUDIOMODEL"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.2401314071374674,"offset":[-0.940790812174555,389.1820880323489]}},"version":0.4}
|
||||
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
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
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
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"id":"7493d64d-0c0c-40ca-a1df-816552f0526b","revision":0,"last_node_id":12,"last_link_id":9,"nodes":[{"id":11,"type":"Runware Media Upload","pos":[448.4990234375,623.826416015625],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"media","name":"media","shape":7,"type":"VIDEO,AUDIO","link":9},{"localized_name":"mediaUUID","name":"mediaUUID","shape":7,"type":"STRING","widget":{"name":"mediaUUID"},"link":null}],"outputs":[{"localized_name":"mediaUUID","name":"mediaUUID","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"1c7da18f11d51514140944e0f9f90e62ae99572c","Node name for S&R":"Runware Media Upload"},"widgets_values":["f61620ac-27aa-4bee-9795-0df76fb2b0ce"],"bgcolor":"#5345bf"},{"id":12,"type":"LoadAudio","pos":[133.54676818847656,597.8778686523438],"size":[274.080078125,136],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadAudio"},"widgets_values":["president_speech_6sec.mp3",null,""]}],"links":[[9,12,0,11,0,"VIDEO,AUDIO"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8264462809917354,"offset":[453.6411562800821,-175.4364195346344]}},"version":0.4}
|
||||
{"id":"7493d64d-0c0c-40ca-a1df-816552f0526b","revision":0,"last_node_id":12,"last_link_id":9,"nodes":[{"id":11,"type":"Runware Media Upload","pos":[448.4990234375,623.826416015625],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"media","name":"media","shape":7,"type":"VIDEO,AUDIO","link":9},{"localized_name":"mediaUUID","name":"mediaUUID","shape":7,"type":"STRING","widget":{"name":"mediaUUID"},"link":null}],"outputs":[{"localized_name":"mediaUUID","name":"mediaUUID","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"1c7da18f11d51514140944e0f9f90e62ae99572c","Node name for S&R":"Runware Media Upload"},"widgets_values":["f61620ac-27aa-4bee-9795-0df76fb2b0ce"],"bgcolor":"#5345bf"},{"id":12,"type":"LoadAudio","pos":[133.54676818847656,597.8778686523438],"size":[274.080078125,136],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[9]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadAudio"},"widgets_values":["president_speech_6sec.mp3",null,null]}],"links":[[9,12,0,11,0,"VIDEO,AUDIO"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8264462809917354,"offset":[453.6411562800821,-175.4364195346344]}},"version":0.4}
|
||||
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
@@ -0,0 +1 @@
|
||||
{"id":"df246f14-f1cb-4379-91af-5185b0c663d8","revision":0,"last_node_id":4,"last_link_id":2,"nodes":[{"id":4,"type":"Runware Media Upload","pos":[-151.37985229492188,276.1902160644531],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"media","name":"media","shape":7,"type":"VIDEO,AUDIO","link":1},{"localized_name":"mediaUUID","name":"mediaUUID","shape":7,"type":"STRING","widget":{"name":"mediaUUID"},"link":null}],"outputs":[{"localized_name":"mediaUUID","name":"mediaUUID","type":"STRING","links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"056c15a1b72b1555184d0447562c095797825d6e","Node name for S&R":"Runware Media Upload"},"widgets_values":["f4d54ced-fe68-45d5-9cb8-ba7b29729fe9"],"bgcolor":"#5345bf"},{"id":3,"type":"LoadVideo","pos":[-436.6539001464844,430.0327453613281],"size":[274.080078125,442.0882263183594],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[1]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.46","Node name for S&R":"LoadVideo"},"widgets_values":["b395d4bb-364c-44b5-91ba-f768e5ff3e98.mp4","image"]},{"id":1,"type":"Runware Video Transcription","pos":[-75.60359191894531,496.2238464355469],"size":[400,200],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"Always Retranscribe","name":"Always Retranscribe","type":"BOOLEAN","widget":{"name":"Always Retranscribe"},"link":null},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"video","name":"video","type":"STRING","widget":{"name":"video"},"link":2},{"localized_name":"model","name":"model","type":"STRING","widget":{"name":"model"},"link":null}],"outputs":[{"localized_name":"Video Transcription","name":"Video Transcription","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"056c15a1b72b1555184d0447562c095797825d6e","Node name for S&R":"Runware Video Transcription"},"widgets_values":[false,"[0] (0s-1s): A man with curly brown hair and closed eyes is shown against a plain gray background. He is wearing a dark blue t-shirt.\n[1] (1s-3s): The man opens his eyes and begins to speak, his mouth slightly open.\n[2] (3s-5s): He continues to speak, his expression animated.\n[3] (5s-7s): The man purses his lips as if about to speak or make a sound.\n[4] (7s-9s): He speaks again, his mouth open and teeth visible.\n[5] (9s-10s): The man's expression is neutral as he looks forward.\n[6] (10s-11s): He blinks slowly, his eyes partially closed.\n[7] (11s-13s): The man sticks out his tongue slightly and speaks.\n[8] (13s-14s): He speaks with his mouth open, showing his teeth.","","memories:2@1"]}],"links":[[1,3,0,4,0,"VIDEO,AUDIO"],[2,4,0,1,2,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.118153876706717,"offset":[993.4996947608385,-128.5350901285807]}},"version":0.4}
|
||||
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
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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user