Merge branch 'main' into feature-ACE-1-5-Step
Resolve conflicts in audioInference.py (keep 8/16 kbps bitrate with main's OGG tooltip) and settings.py (restore renderingSpeed/magicPrompt handling from main).
This commit is contained in:
+10
@@ -53,6 +53,11 @@ from .modules.textInferenceMessages import RunwareTextInferenceMessages
|
||||
from .modules.textInferenceMessage import RunwareTextInferenceMessage
|
||||
from .modules.textInference import RunwareTextInference
|
||||
from .modules.textInferenceSettings import RunwareTextInferenceSettings
|
||||
from .modules.textInferenceToolChoice import RunwareTextInferenceToolChoice
|
||||
from .modules.textInferenceSettingsCache import RunwareTextInferenceSettingsCache
|
||||
from .modules.textInferenceInputs import RunwareTextInferenceInputs
|
||||
from .modules.textInferenceInputsImages import RunwareTextInferenceInputsImages
|
||||
from .modules.textInferenceInputsVideos import RunwareTextInferenceInputsVideos
|
||||
from .modules.audioSections import RunwareAudioSections
|
||||
from .modules.audioInferenceInputs import audioInferenceInputs
|
||||
from .modules.audioSettings import RunwareAudioSettings
|
||||
@@ -177,6 +182,11 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Runware Text Inference Message": RunwareTextInferenceMessage,
|
||||
"Runware Text Inference": RunwareTextInference,
|
||||
"Runware Text Inference Settings": RunwareTextInferenceSettings,
|
||||
"Runware Text Inference Tool Choice": RunwareTextInferenceToolChoice,
|
||||
"Runware Text Inference Settings Cache": RunwareTextInferenceSettingsCache,
|
||||
"Runware Text Inference Inputs": RunwareTextInferenceInputs,
|
||||
"Runware Text Inference Inputs Images": RunwareTextInferenceInputsImages,
|
||||
"Runware Text Inference Inputs Videos": RunwareTextInferenceInputsVideos,
|
||||
"Runware Audio Sections": RunwareAudioSections,
|
||||
"Runware Audio Inference Inputs": audioInferenceInputs,
|
||||
"Runware Audio Inference Settings": RunwareAudioSettings,
|
||||
|
||||
@@ -89,6 +89,11 @@ const RUNWARE_NODE_TYPES = {
|
||||
TEXTINFERENCEMESSAGES: "Runware Text Inference Messages",
|
||||
TEXTINFERENCEMESSAGE: "Runware Text Inference Message",
|
||||
TEXTINFERENCESETTINGS: "Runware Text Inference Settings",
|
||||
TEXTINFERENCETOOLCHOICE: "Runware Text Inference Tool Choice",
|
||||
TEXTINFERENCESETTINGSCACHE: "Runware Text Inference Settings Cache",
|
||||
TEXTINFERENCEINPUTS: "Runware Text Inference Inputs",
|
||||
TEXTINFERENCEINPUTSIMAGES: "Runware Text Inference Inputs Images",
|
||||
TEXTINFERENCEINPUTSVIDEOS: "Runware Text Inference Inputs Videos",
|
||||
AUDIOSECTIONS: "Runware Audio Sections",
|
||||
AUDIOINFERENCEINPUTS: "Runware Audio Inference Inputs",
|
||||
AUDIOSETTINGS: "Runware Audio Inference Settings",
|
||||
@@ -328,6 +333,26 @@ const RUNWARE_NODE_PROPS = {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.TEXTINFERENCETOOLCHOICE]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.TEXTINFERENCESETTINGSCACHE]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.TEXTINFERENCEINPUTSIMAGES]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.TEXTINFERENCEINPUTSVIDEOS]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.TEXTINFERENCEINPUTS]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
},
|
||||
[RUNWARE_NODE_TYPES.TEXTINFERENCESETTINGS]: {
|
||||
bgColor: DEFAULT_BGCOLOR,
|
||||
colorModeOnly: true,
|
||||
|
||||
+19
-3
@@ -1360,8 +1360,6 @@ function textInferenceSettingsToggleHandler(settingsNode) {
|
||||
const frequencyPenaltyWidget = settingsNode.widgets.find(w => w && w.name === "frequencyPenalty");
|
||||
const useToolsWidget = settingsNode.widgets.find(w => w && w.name === "useTools");
|
||||
const toolsWidget = settingsNode.widgets.find(w => w && w.name === "tools");
|
||||
const useToolChoiceWidget = settingsNode.widgets.find(w => w && w.name === "useToolChoice");
|
||||
const toolChoiceWidget = settingsNode.widgets.find(w => w && w.name === "toolChoice");
|
||||
|
||||
function toggleWidgetState(useWidget, paramWidget) {
|
||||
if (!useWidget || !paramWidget) return;
|
||||
@@ -1388,7 +1386,6 @@ function textInferenceSettingsToggleHandler(settingsNode) {
|
||||
if (usePresencePenaltyWidget && presencePenaltyWidget) toggleWidgetState(usePresencePenaltyWidget, presencePenaltyWidget);
|
||||
if (useFrequencyPenaltyWidget && frequencyPenaltyWidget) toggleWidgetState(useFrequencyPenaltyWidget, frequencyPenaltyWidget);
|
||||
if (useToolsWidget && toolsWidget) toggleWidgetState(useToolsWidget, toolsWidget);
|
||||
if (useToolChoiceWidget && toolChoiceWidget) toggleWidgetState(useToolChoiceWidget, toolChoiceWidget);
|
||||
}
|
||||
|
||||
function videoSettingsToggleHandler(settingsNode) {
|
||||
@@ -3281,9 +3278,17 @@ function textModelSearchFilterHandler(textModelSearchNode) {
|
||||
"openai:gpt@5.4-mini (GPT-5.4 Mini)",
|
||||
"openai:gpt@5.4-nano (GPT-5.4 Nano)",
|
||||
],
|
||||
"Anthropic": [
|
||||
"anthropic:claude@opus-4.7 (Claude Opus 4.7)",
|
||||
"anthropic:claude@sonnet-4.6 (Claude Sonnet 4.6)",
|
||||
"anthropic:claude@haiku-4.5 (Claude Haiku 4.5)",
|
||||
],
|
||||
"Google": [
|
||||
"google:gemini@3-flash (Gemini 3 Flash)",
|
||||
],
|
||||
"MoonshotAI": [
|
||||
"moonshotai:kimi@k2.6 (Kimi K2.6)",
|
||||
],
|
||||
"Zai": [
|
||||
"zai-glm-5-1 (ZAI GLM 5.1)",
|
||||
],
|
||||
@@ -3359,6 +3364,7 @@ function audioModelSearchFilterHandler(audioModelSearchNode) {
|
||||
"minimax:music@2.6 (MiniMax Music 2.6)",
|
||||
],
|
||||
"Inworld": [
|
||||
"inworld:tts@2 (Inworld Realtime TTS 2)",
|
||||
"inworld:tts@1.5-mini (Inworld TTS-1.5 Mini)",
|
||||
"inworld:tts@1.5-max (Inworld TTS-1.5 Max)",
|
||||
],
|
||||
@@ -5137,6 +5143,10 @@ function settingsToggleHandler(settingsNode) {
|
||||
const thinkingLevelWidget = settingsNode.widgets.find(w => w.name === "thinkingLevel");
|
||||
const useSequentialWidget = settingsNode.widgets.find(w => w.name === "useSequential");
|
||||
const sequentialWidget = settingsNode.widgets.find(w => w.name === "sequential");
|
||||
const useRenderingSpeedWidget = settingsNode.widgets.find(w => w.name === "useRenderingSpeed");
|
||||
const renderingSpeedWidget = settingsNode.widgets.find(w => w.name === "renderingSpeed");
|
||||
const useMagicPromptWidget = settingsNode.widgets.find(w => w.name === "useMagicPrompt");
|
||||
const magicPromptWidget = settingsNode.widgets.find(w => w.name === "magicPrompt");
|
||||
|
||||
// Helper function to toggle widget enabled state
|
||||
function toggleWidgetState(useWidget, paramWidget, paramName) {
|
||||
@@ -5223,6 +5233,12 @@ function settingsToggleHandler(settingsNode) {
|
||||
if (useSequentialWidget && sequentialWidget) {
|
||||
toggleWidgetState(useSequentialWidget, sequentialWidget, "sequential");
|
||||
}
|
||||
if (useRenderingSpeedWidget && renderingSpeedWidget) {
|
||||
toggleWidgetState(useRenderingSpeedWidget, renderingSpeedWidget, "renderingSpeed");
|
||||
}
|
||||
if (useMagicPromptWidget && magicPromptWidget) {
|
||||
toggleWidgetState(useMagicPromptWidget, magicPromptWidget, "magicPrompt");
|
||||
}
|
||||
}
|
||||
|
||||
function safetyInputsToggleHandler(safetyNode) {
|
||||
|
||||
+90
-36
@@ -5,6 +5,7 @@ import numpy as np
|
||||
import librosa
|
||||
import tempfile
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
import comfy.model_management
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
@@ -22,10 +23,6 @@ class RunwareAudioInference:
|
||||
"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",
|
||||
@@ -51,17 +48,17 @@ class RunwareAudioInference:
|
||||
"default": False,
|
||||
"tooltip": "Enable/disable sampleRate parameter in API request"
|
||||
}),
|
||||
"sampleRate": ([8000, 16000, 22050, 24000, 32000, 44100], {
|
||||
"sampleRate": ([8000, 16000, 22050, 24000, 32000, 44100, 48000], {
|
||||
"default": 32000,
|
||||
"tooltip": "Audio sample rate in Hz. Supported: 8000, 16000, 22050, 24000, 32000, 44100."
|
||||
"tooltip": "Audio sample rate in Hz. Supported: 8000, 16000, 22050, 24000, 32000, 44100, 48000.",
|
||||
}),
|
||||
"useBitrate": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Enable/disable bitrate parameter in API request (MP3 only)"
|
||||
"tooltip": "Enable/disable bitrate parameter in API request"
|
||||
}),
|
||||
"bitrate": ([8, 16, 32, 64, 128, 256], {
|
||||
"default": 128,
|
||||
"tooltip": "Audio bitrate in kbps. Allowed: 8, 16, 32, 64, 128, 256. Only applies to MP3 format."
|
||||
"tooltip": "Audio bitrate in kbps. Allowed: 8, 16, 32, 64, 128, 256. Applies to compressed formats (e.g. MP3, OGG).",
|
||||
}),
|
||||
"useChannels": ("BOOLEAN", {
|
||||
"default": False,
|
||||
@@ -74,10 +71,6 @@ class RunwareAudioInference:
|
||||
"step": 1,
|
||||
"tooltip": "Number of audio channels (1=mono, 2=stereo)"
|
||||
}),
|
||||
"outputType": (["URL", "dataURI", "base64Data"], {
|
||||
"default": "URL",
|
||||
"tooltip": "Output type for the generated audio"
|
||||
}),
|
||||
"outputFormat": (["MP3", "MP4", "WAV", "FLAC", "OGG"], {
|
||||
"default": "MP3",
|
||||
"tooltip": "Format of the output audio"
|
||||
@@ -219,13 +212,16 @@ class RunwareAudioInference:
|
||||
status = audioData["status"]
|
||||
|
||||
if status == "success":
|
||||
audioUrl = self._extractAudioUrl(pollResult)
|
||||
hasAudio = audioUrl is not None
|
||||
audioUrls = self._extractAudioUrls(pollResult)
|
||||
hasAudio = len(audioUrls) > 0
|
||||
hasVideo = bool(audioData.get("videoURL") or audioData.get("videoBase64Data", False))
|
||||
|
||||
if hasAudio:
|
||||
audioObj = self._downloadAndProcessAudio(audioUrl, params["sampleRate"])
|
||||
print(f"[DEBUG] Audio URL found, returning audio. Audio URL: {audioUrl}")
|
||||
audioObjects = []
|
||||
for audioUrl in audioUrls:
|
||||
audioObjects.append(self._downloadAndProcessAudio(audioUrl, params["sampleRate"], params["outputFormat"]))
|
||||
audioObj = self._mergeAudioObjects(audioObjects, params["sampleRate"])
|
||||
print(f"[DEBUG] Audio URL(s) found, returning batched audio. Count: {len(audioUrls)}")
|
||||
# Return empty video object when only audio is present (prevents errors in downstream nodes)
|
||||
emptyVideoObj = rwUtils.VideoObject("", width=0, height=0)
|
||||
return (audioObj, emptyVideoObj)
|
||||
@@ -260,7 +256,6 @@ class RunwareAudioInference:
|
||||
"""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),
|
||||
@@ -270,7 +265,6 @@ class RunwareAudioInference:
|
||||
"useBitrate": kwargs.get("useBitrate", False),
|
||||
"channels": kwargs.get("channels", 2),
|
||||
"useChannels": kwargs.get("useChannels", False),
|
||||
"outputType": kwargs.get("outputType", "URL"),
|
||||
"outputFormat": kwargs.get("outputFormat", "MP3"),
|
||||
"negativePrompt": kwargs.get("negativePrompt", ""),
|
||||
"numberResults": kwargs.get("numberResults", 1),
|
||||
@@ -310,7 +304,7 @@ class RunwareAudioInference:
|
||||
"taskType": "audioInference",
|
||||
"taskUUID": taskUuid,
|
||||
"model": params["model"],
|
||||
"outputType": params["outputType"],
|
||||
"outputType": "URL",
|
||||
"outputFormat": params["outputFormat"],
|
||||
"deliveryMethod": "async",
|
||||
"includeCost": True,
|
||||
@@ -338,8 +332,8 @@ class RunwareAudioInference:
|
||||
params["useDuration"] = False
|
||||
print(f"[DEBUG] Disabled duration because sections are provided")
|
||||
|
||||
# Add prompts conditionally
|
||||
if params["usePositivePrompt"]:
|
||||
# Add positivePrompt only when non-empty
|
||||
if len(params["positivePrompt"]) > 0:
|
||||
genConfig[0]["positivePrompt"] = params["positivePrompt"]
|
||||
print(f"[DEBUG] Added positivePrompt: '{params['positivePrompt']}'")
|
||||
else:
|
||||
@@ -447,26 +441,68 @@ class RunwareAudioInference:
|
||||
if not hasAudio and not hasVideo:
|
||||
raise Exception("No audio or video data received from API")
|
||||
|
||||
def _extractAudioUrl(self, genResult):
|
||||
"""Extract audio URL from API response. Returns None if no audio URL is present (e.g., when only video is returned)."""
|
||||
audioData = genResult["data"][0]
|
||||
|
||||
audioUrl = audioData.get("audioURL", "")
|
||||
if not audioUrl:
|
||||
audioUrl = audioData.get("audioDataURI", "")
|
||||
if not audioUrl:
|
||||
audioUrl = audioData.get("audioBase64Data", "")
|
||||
|
||||
def _extractAudioUrls(self, genResult):
|
||||
"""Extract all available audio URLs from API response."""
|
||||
audioUrls = []
|
||||
for item in genResult.get("data", []):
|
||||
audioUrl = item.get("audioURL", "") or item.get("audioDataURI", "") or item.get("audioBase64Data", "")
|
||||
if audioUrl:
|
||||
audioUrls.append(audioUrl)
|
||||
return audioUrls
|
||||
|
||||
return audioUrl if audioUrl else None
|
||||
def _mergeAudioObjects(self, audioObjects, targetSampleRate):
|
||||
"""Merge multiple audio objects into a single batched audio object."""
|
||||
if not audioObjects:
|
||||
return {
|
||||
"waveform": torch.zeros((1, 1, 1)),
|
||||
"sample_rate": targetSampleRate,
|
||||
"format": "wav"
|
||||
}
|
||||
|
||||
def _downloadAndProcessAudio(self, audioUrl, targetSampleRate):
|
||||
waveforms = []
|
||||
maxChannels = 0
|
||||
maxSamples = 0
|
||||
|
||||
for audioObj in audioObjects:
|
||||
waveform = audioObj["waveform"]
|
||||
if waveform.dim() == 2:
|
||||
waveform = waveform.unsqueeze(0)
|
||||
waveforms.append(waveform)
|
||||
maxChannels = max(maxChannels, waveform.shape[1])
|
||||
maxSamples = max(maxSamples, waveform.shape[2])
|
||||
|
||||
paddedWaveforms = []
|
||||
for waveform in waveforms:
|
||||
batchSize, channels, samples = waveform.shape
|
||||
|
||||
if channels < maxChannels:
|
||||
channelPadding = torch.zeros(
|
||||
(batchSize, maxChannels - channels, samples),
|
||||
dtype=waveform.dtype,
|
||||
device=waveform.device
|
||||
)
|
||||
waveform = torch.cat([waveform, channelPadding], dim=1)
|
||||
|
||||
if samples < maxSamples:
|
||||
waveform = torch.nn.functional.pad(waveform, (0, maxSamples - samples))
|
||||
|
||||
paddedWaveforms.append(waveform)
|
||||
|
||||
outputFormat = audioObjects[0].get("format", "wav")
|
||||
return {
|
||||
"waveform": torch.cat(paddedWaveforms, dim=0),
|
||||
"sample_rate": targetSampleRate,
|
||||
"format": outputFormat
|
||||
}
|
||||
|
||||
def _downloadAndProcessAudio(self, audioUrl, targetSampleRate, outputFormat):
|
||||
"""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:
|
||||
|
||||
tempSuffix = self._inferAudioSuffix(audioUrl, outputFormat)
|
||||
with tempfile.NamedTemporaryFile(suffix=tempSuffix, delete=False) as tempFile:
|
||||
tempFile.write(response.content)
|
||||
tempFilePath = tempFile.name
|
||||
|
||||
@@ -477,7 +513,8 @@ class RunwareAudioInference:
|
||||
|
||||
return {
|
||||
"waveform": waveformTensor,
|
||||
"sample_rate": targetSampleRate
|
||||
"sample_rate": targetSampleRate,
|
||||
"format": tempSuffix.lstrip(".")
|
||||
}
|
||||
finally:
|
||||
if os.path.exists(tempFilePath):
|
||||
@@ -485,6 +522,23 @@ class RunwareAudioInference:
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to download audio: {e}")
|
||||
|
||||
def _inferAudioSuffix(self, audioUrl, outputFormat):
|
||||
"""Infer temporary audio file extension from URL or requested format."""
|
||||
parsedPath = urlparse(audioUrl).path if audioUrl else ""
|
||||
ext = os.path.splitext(parsedPath)[1].lower()
|
||||
allowed = {".mp3", ".mp4", ".wav", ".flac", ".ogg"}
|
||||
if ext in allowed:
|
||||
return ext
|
||||
|
||||
formatMap = {
|
||||
"MP3": ".mp3",
|
||||
"MP4": ".mp4",
|
||||
"WAV": ".wav",
|
||||
"FLAC": ".flac",
|
||||
"OGG": ".ogg",
|
||||
}
|
||||
return formatMap.get(str(outputFormat).upper(), ".wav")
|
||||
|
||||
def _loadAudioFile(self, filePath):
|
||||
"""Load audio file using librosa"""
|
||||
waveformNp, originalSampleRate = librosa.load(filePath, sr=None, mono=False)
|
||||
|
||||
@@ -40,6 +40,7 @@ class RunwareAudioModelSearch:
|
||||
"minimax:music@2.6 (MiniMax Music 2.6)",
|
||||
],
|
||||
"Inworld": [
|
||||
"inworld:tts@2 (Inworld Realtime TTS 2)",
|
||||
"inworld:tts@1.5-mini (Inworld TTS-1.5 Mini)",
|
||||
"inworld:tts@1.5-max (Inworld TTS-1.5 Max)",
|
||||
],
|
||||
|
||||
+28
-3
@@ -2,7 +2,8 @@
|
||||
Runware Image Inference Settings (registered as Runware Settings for workflow compatibility).
|
||||
Provides settings for image generation including temperature, systemPrompt, topP, layers,
|
||||
quality, background, style, search, promptExtend, editRegions, thinking (boolean),
|
||||
thinkingLevel (high/medium/low), sequential, and colorPalette
|
||||
thinkingLevel (high/medium/low), sequential, renderingSpeed (TURBO/DEFAULT/QUALITY),
|
||||
magicPrompt (AUTO/ON/OFF), and colorPalette
|
||||
(from Runware Image Inference Settings Color Palette).
|
||||
"""
|
||||
|
||||
@@ -155,6 +156,22 @@ class RunwareSettings:
|
||||
"label_on": "true",
|
||||
"label_off": "false",
|
||||
}),
|
||||
"useRenderingSpeed": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include renderingSpeed in settings.",
|
||||
"default": False,
|
||||
}),
|
||||
"renderingSpeed": (["TURBO", "DEFAULT", "QUALITY"], {
|
||||
"default": "DEFAULT",
|
||||
"tooltip": "The rendering speed to use. Only used when 'Use Rendering Speed' is enabled.",
|
||||
}),
|
||||
"useMagicPrompt": ("BOOLEAN", {
|
||||
"tooltip": "Enable to include magicPrompt in settings.",
|
||||
"default": False,
|
||||
}),
|
||||
"magicPrompt": (["AUTO", "ON", "OFF"], {
|
||||
"default": "AUTO",
|
||||
"tooltip": "Determine if MagicPrompt should be used. Only used when 'Use Magic Prompt' is enabled.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +181,8 @@ class RunwareSettings:
|
||||
CATEGORY = "Runware"
|
||||
DESCRIPTION = (
|
||||
"Configure general settings for image generation: temperature, system prompt, top-p, layers, quality, background, style, search, "
|
||||
"promptExtend, editRegions (JSON), thinking (boolean), thinkingLevel (high/medium/low), sequential, and optional colorPalette from the Color Palette node."
|
||||
"promptExtend, editRegions (JSON), thinking (boolean), thinkingLevel (high/medium/low), sequential, "
|
||||
"renderingSpeed (TURBO/DEFAULT/QUALITY), magicPrompt (AUTO/ON/OFF), and optional colorPalette from the Color Palette node."
|
||||
)
|
||||
|
||||
def createSettings(self, **kwargs) -> tuple[Dict[str, Any]]:
|
||||
@@ -186,6 +204,8 @@ class RunwareSettings:
|
||||
useThinking = kwargs.get("useThinking", False)
|
||||
useThinkingLevel = kwargs.get("useThinkingLevel", False)
|
||||
useSequential = kwargs.get("useSequential", False)
|
||||
useRenderingSpeed = kwargs.get("useRenderingSpeed", False)
|
||||
useMagicPrompt = kwargs.get("useMagicPrompt", False)
|
||||
|
||||
# Get value parameters
|
||||
temperature = kwargs.get("temperature", 1.0)
|
||||
@@ -196,6 +216,8 @@ class RunwareSettings:
|
||||
quality = kwargs.get("quality", "medium")
|
||||
background = kwargs.get("background", "auto")
|
||||
style = kwargs.get("style", "auto")
|
||||
renderingSpeed = kwargs.get("renderingSpeed", "DEFAULT")
|
||||
magicPrompt = kwargs.get("magicPrompt", "AUTO")
|
||||
|
||||
# Build settings dictionary - only include what is enabled
|
||||
settings: Dict[str, Any] = {}
|
||||
@@ -246,7 +268,10 @@ class RunwareSettings:
|
||||
if palette is not None and isinstance(palette, list) and len(palette) > 0:
|
||||
settings["colorPalette"] = palette
|
||||
|
||||
|
||||
if useRenderingSpeed:
|
||||
settings["renderingSpeed"] = str(renderingSpeed)
|
||||
if useMagicPrompt:
|
||||
settings["magicPrompt"] = str(magicPrompt)
|
||||
|
||||
# Clean up None values
|
||||
settings = {k: v for k, v in settings.items() if v is not None}
|
||||
|
||||
@@ -25,6 +25,12 @@ class RunwareTextInference:
|
||||
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
|
||||
"tooltip": "Optional provider-specific settings (wrapped by provider prefix from the model AIR).",
|
||||
}),
|
||||
"toolChoice": ("RUNWARETEXTINFERENCETOOLCHOICE", {
|
||||
"tooltip": "Connect Runware Text Inference Tool Choice for tool-calling strategy (auto / any / tool / none).",
|
||||
}),
|
||||
"inputs": ("RUNWARETEXTINFERENCEINPUTS", {
|
||||
"tooltip": "Connect Runware Text Inference Inputs for inputs.images / inputs.videos (URLs or mediaUUIDs).",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -32,7 +38,7 @@ class RunwareTextInference:
|
||||
RETURN_NAMES = ("text",)
|
||||
FUNCTION = "infer_text"
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = "Run text/chat inference using Runware textInference. Connect Text Model and Text Inference Messages."
|
||||
DESCRIPTION = "Run text/chat inference using Runware textInference. Connect Text Model and Text Inference Messages. Optional: Settings, provider settings, tool choice, inputs (images/videos)."
|
||||
|
||||
@staticmethod
|
||||
def _extract_text_from_data_item(item: Dict[str, Any]) -> Optional[str]:
|
||||
@@ -70,6 +76,8 @@ class RunwareTextInference:
|
||||
messages: List[Dict[str, str]] = kwargs.get("messages") or []
|
||||
text_settings = kwargs.get("settings")
|
||||
provider_settings = kwargs.get("providerSettings")
|
||||
tool_choice = kwargs.get("toolChoice")
|
||||
text_inputs = kwargs.get("inputs")
|
||||
|
||||
if not model:
|
||||
raise Exception("model is empty.")
|
||||
@@ -94,6 +102,12 @@ class RunwareTextInference:
|
||||
provider_name = model.split(":")[0] if ":" in model else model
|
||||
task["providerSettings"] = {provider_name: provider_settings}
|
||||
|
||||
if tool_choice is not None and isinstance(tool_choice, dict) and len(tool_choice) > 0:
|
||||
task["toolChoice"] = tool_choice
|
||||
|
||||
if text_inputs is not None and isinstance(text_inputs, dict) and len(text_inputs) > 0:
|
||||
task["inputs"] = dict(text_inputs)
|
||||
|
||||
gen_config = [task]
|
||||
print(f"[Runware Text Inference] Request: {rwUtils.safe_json_dumps(gen_config, indent=2)}")
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Runware Text Inference Inputs — merge Images + Videos blocks into task.inputs for textInference."""
|
||||
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
|
||||
class RunwareTextInferenceInputs:
|
||||
"""Merge Runware Text Inference Inputs Images and/or Videos into a single inputs dict."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"images": ("RUNWARETEXTINFERENCEINPUTSIMAGES", {
|
||||
"tooltip": "Connect Runware Text Inference Inputs Images for inputs.images[].",
|
||||
}),
|
||||
"videos": ("RUNWARETEXTINFERENCEINPUTSVIDEOS", {
|
||||
"tooltip": "Connect Runware Text Inference Inputs Videos for inputs.videos[].",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("RUNWARETEXTINFERENCEINPUTS",)
|
||||
RETURN_NAMES = ("inputs",)
|
||||
FUNCTION = "merge_inputs"
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = (
|
||||
"Combine Runware Text Inference Inputs Images and/or Videos into inputs for Runware Text Inference "
|
||||
"(inputs.images, inputs.videos)."
|
||||
)
|
||||
|
||||
def merge_inputs(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
|
||||
images_block = kwargs.get("images")
|
||||
videos_block = kwargs.get("videos")
|
||||
|
||||
out: Dict[str, Any] = {}
|
||||
|
||||
if isinstance(images_block, dict) and images_block.get("images"):
|
||||
imgs = images_block["images"]
|
||||
if isinstance(imgs, list) and len(imgs) > 0:
|
||||
filtered_imgs = [str(x).strip() for x in imgs if x is not None and str(x).strip()]
|
||||
if filtered_imgs:
|
||||
out["images"] = filtered_imgs
|
||||
|
||||
if isinstance(videos_block, dict) and videos_block.get("videos"):
|
||||
vids = videos_block["videos"]
|
||||
if isinstance(vids, list) and len(vids) > 0:
|
||||
filtered_vids = [str(x).strip() for x in vids if x is not None and str(x).strip()]
|
||||
if filtered_vids:
|
||||
out["videos"] = filtered_vids
|
||||
|
||||
return (out,)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Runware Text Inference Inputs Images — builds inputs.images from ComfyUI IMAGE tensors."""
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareTextInferenceInputsImages:
|
||||
"""Collect IMAGE tensors and convert them for textInference task.inputs.images.
|
||||
|
||||
Each connected IMAGE is converted via runwareUtils.convertTensor2IMG, which returns
|
||||
either a cached mediaUUID (when caching is enabled) or a base64 data URI.
|
||||
"""
|
||||
|
||||
_MAX_IMAGES = 8
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optional: Dict[str, tuple] = {}
|
||||
for i in range(1, cls._MAX_IMAGES + 1):
|
||||
ordinal = rwUtils.getOrdinal(i)
|
||||
optional[f"Image {i}"] = ("IMAGE", {
|
||||
"tooltip": (
|
||||
f"{ordinal.capitalize()} image for the model. Connect a Load Image (or any IMAGE source) node. "
|
||||
"Tensor is converted to base64 / mediaUUID before being sent to the API."
|
||||
),
|
||||
})
|
||||
return {"required": {}, "optional": optional}
|
||||
|
||||
RETURN_TYPES = ("RUNWARETEXTINFERENCEINPUTSIMAGES",)
|
||||
RETURN_NAMES = ("images",)
|
||||
FUNCTION = "createImages"
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = (
|
||||
"Collect one or more IMAGE tensors (Load Image, generated images, etc.) for Runware Text Inference "
|
||||
"inputs.images. Connect the output to Runware Text Inference Inputs."
|
||||
)
|
||||
|
||||
def createImages(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
|
||||
urls: List[str] = []
|
||||
for i in range(1, self._MAX_IMAGES + 1):
|
||||
tensor = kwargs.get(f"Image {i}", None)
|
||||
if tensor is None:
|
||||
continue
|
||||
urls.append(rwUtils.convertTensor2IMG(tensor))
|
||||
|
||||
if not urls:
|
||||
return ({},)
|
||||
|
||||
return ({"images": urls},)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Runware Text Inference Inputs Videos — builds inputs.videos (URL or mediaUUID per item)."""
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from .utils import runwareUtils as rwUtils
|
||||
|
||||
|
||||
class RunwareTextInferenceInputsVideos:
|
||||
"""Collect video references for textInference task.inputs.videos."""
|
||||
|
||||
_MAX_VIDEOS = 4
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optional: Dict[str, tuple] = {
|
||||
"useVideos": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Enable to include inputs.videos in the text inference request.",
|
||||
}),
|
||||
}
|
||||
for i in range(1, cls._MAX_VIDEOS + 1):
|
||||
ordinal = rwUtils.getOrdinal(i)
|
||||
optional[f"Video{i}"] = ("STRING", {
|
||||
"default": "",
|
||||
"tooltip": f"Video URL or mediaUUID for the {ordinal} video. Only used when 'Use Videos' is enabled.",
|
||||
})
|
||||
return {"required": {}, "optional": optional}
|
||||
|
||||
RETURN_TYPES = ("RUNWARETEXTINFERENCEINPUTSVIDEOS",)
|
||||
RETURN_NAMES = ("videos",)
|
||||
FUNCTION = "createVideos"
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = (
|
||||
"Collect one or more video references (URL or mediaUUID) for Runware Text Inference "
|
||||
"inputs.videos. Connect the output to Runware Text Inference Inputs."
|
||||
)
|
||||
|
||||
def createVideos(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
|
||||
if not kwargs.get("useVideos", False):
|
||||
return ({},)
|
||||
|
||||
urls: List[str] = []
|
||||
for i in range(1, self._MAX_VIDEOS + 1):
|
||||
raw = kwargs.get(f"Video{i}", "")
|
||||
if raw is not None and str(raw).strip():
|
||||
urls.append(str(raw).strip())
|
||||
|
||||
if not urls:
|
||||
return ({},)
|
||||
|
||||
return ({"videos": urls},)
|
||||
@@ -70,9 +70,13 @@ class RunwareTextInferenceSettings:
|
||||
"tooltip": f"Top-k sampling (1–{cls._MAX_TOP_K}).",
|
||||
}),
|
||||
# Keep this in the original slot to avoid shifting legacy widgets_values.
|
||||
"thinkingLevel": (["none", "low", "medium", "high"], {
|
||||
"thinkingLevel": (["none", "low", "medium", "high", "max", "adaptive"], {
|
||||
"default": "none",
|
||||
"tooltip": "Extended reasoning level: none, low, medium, high.",
|
||||
"tooltip": (
|
||||
"Extended reasoning level: none, low, medium, high, max, adaptive. "
|
||||
"none -> disabled. low/medium/high/max -> adaptive thinking with the chosen effort. "
|
||||
"adaptive -> model picks budget. Legacy 'off' is mapped to 'none'."
|
||||
),
|
||||
}),
|
||||
"useStopSequences": ("BOOLEAN", {
|
||||
"default": False,
|
||||
@@ -114,14 +118,8 @@ class RunwareTextInferenceSettings:
|
||||
"default": "[]",
|
||||
"tooltip": "JSON array for tools.",
|
||||
}),
|
||||
"useToolChoice": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Include toolChoice in settings.",
|
||||
}),
|
||||
"toolChoice": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "{}",
|
||||
"tooltip": "JSON object for toolChoice.",
|
||||
"cache": ("RUNWARETEXTINFERENCESETTINGSCACHE", {
|
||||
"tooltip": "Connect Runware Text Inference Settings Cache for settings.cache.{scope, ttl}.",
|
||||
}),
|
||||
},
|
||||
}
|
||||
@@ -132,8 +130,8 @@ class RunwareTextInferenceSettings:
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = (
|
||||
"Optional text inference settings (systemPrompt, maxTokens, temperature, topP, topK, "
|
||||
"stopSequences, presencePenalty, frequencyPenalty, tools, toolChoice, thinkingLevel). "
|
||||
"Connect to Runware Text Inference."
|
||||
"stopSequences, presencePenalty, frequencyPenalty, tools, thinkingLevel, cache). "
|
||||
"Connect to Runware Text Inference. For tool calling, use Runware Text Inference Tool Choice on the inference node."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -191,7 +189,7 @@ class RunwareTextInferenceSettings:
|
||||
"minimal": "low",
|
||||
}
|
||||
value = aliases.get(value, value)
|
||||
if value not in ("none", "low", "medium", "high"):
|
||||
if value not in ("none", "low", "medium", "high", "max", "adaptive"):
|
||||
return "none"
|
||||
return value
|
||||
|
||||
@@ -233,15 +231,14 @@ class RunwareTextInferenceSettings:
|
||||
if parsed_tools:
|
||||
out["tools"] = parsed_tools
|
||||
|
||||
if kwargs.get("useToolChoice", False):
|
||||
parsed_choice = self._parse_json_setting(kwargs.get("toolChoice"), dict, "toolChoice")
|
||||
if parsed_choice:
|
||||
out["toolChoice"] = parsed_choice
|
||||
|
||||
tl = self._normalize_thinking_level(kwargs.get("thinkingLevel", "none"))
|
||||
if tl != "none":
|
||||
out["thinkingLevel"] = tl
|
||||
|
||||
cache_cfg = kwargs.get("cache")
|
||||
if cache_cfg is not None and isinstance(cache_cfg, dict) and len(cache_cfg) > 0:
|
||||
out["cache"] = cache_cfg
|
||||
|
||||
if not out:
|
||||
return ({},)
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Runware Text Inference Settings Cache Node
|
||||
Builds the cache object for Runware Text Inference (settings.cache.{scope, ttl}).
|
||||
|
||||
Maps cache_control placement (Anthropic):
|
||||
- scope: where cache_control is attached
|
||||
* system -> system block only
|
||||
* system+history -> system block + last user message block (default)
|
||||
- ttl: cache lifetime (5m or 1h)
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class RunwareTextInferenceSettingsCache:
|
||||
"""Runware Text Inference Settings Cache node."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"scope": (["system", "system+history"], {
|
||||
"default": "system+history",
|
||||
"tooltip": (
|
||||
"Where cache_control is attached.\n"
|
||||
"- system: cache the system block only.\n"
|
||||
"- system+history: cache the system block and the last user message block."
|
||||
),
|
||||
}),
|
||||
"ttl": (["5m", "1h"], {
|
||||
"default": "5m",
|
||||
"tooltip": "Cache lifetime: 5m (5 minutes) or 1h (1 hour).",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("RUNWARETEXTINFERENCESETTINGSCACHE",)
|
||||
RETURN_NAMES = ("cache",)
|
||||
FUNCTION = "createCache"
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = (
|
||||
"Build a settings.cache object (scope + ttl) and connect it to Runware Text Inference. "
|
||||
"Merged into the request as settings.cache.{scope, ttl}."
|
||||
)
|
||||
|
||||
def createCache(self, **kwargs) -> tuple[Dict[str, Any]]:
|
||||
scope = (kwargs.get("scope") or "system+history").strip().lower()
|
||||
if scope not in ("system", "system+history"):
|
||||
scope = "system+history"
|
||||
|
||||
ttl = (kwargs.get("ttl") or "5m").strip().lower()
|
||||
if ttl not in ("5m", "1h"):
|
||||
ttl = "5m"
|
||||
|
||||
return ({"scope": scope, "ttl": ttl},)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RunwareTextInferenceSettingsCache": RunwareTextInferenceSettingsCache,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareTextInferenceSettingsCache": "Runware Text Inference Settings Cache",
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Runware Text Inference Tool Choice Node
|
||||
Builds the toolChoice object for Runware Text Inference (textInference).
|
||||
|
||||
Strategies (type):
|
||||
- auto: model decides whether to call a tool (recommended default)
|
||||
- any: model must call at least one tool but chooses which one
|
||||
- tool: model must call the specific tool identified by `name`
|
||||
- none: model will not call any tool
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class RunwareTextInferenceToolChoice:
|
||||
"""Runware Text Inference Tool Choice node."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"type": (["auto", "any", "tool", "none"], {
|
||||
"default": "auto",
|
||||
"tooltip": (
|
||||
"Strategy the model uses to decide when and which tools to call.\n"
|
||||
"- auto: model decides (recommended default).\n"
|
||||
"- any: model must call at least one tool, of its choosing.\n"
|
||||
"- tool: model must call the specific tool named below.\n"
|
||||
"- none: model will not call any tool."
|
||||
),
|
||||
}),
|
||||
"name": ("STRING", {
|
||||
"default": "",
|
||||
"tooltip": "Name of the specific tool the model must call. Required when type is 'tool'; ignored otherwise.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("RUNWARETEXTINFERENCETOOLCHOICE",)
|
||||
RETURN_NAMES = ("toolChoice",)
|
||||
FUNCTION = "createToolChoice"
|
||||
CATEGORY = "Runware/Text"
|
||||
DESCRIPTION = (
|
||||
"Build a toolChoice object (type + optional tool name) and connect it to "
|
||||
"Runware Text Inference. Required when type is 'tool'."
|
||||
)
|
||||
|
||||
def createToolChoice(self, **kwargs) -> tuple[Dict[str, Any]]:
|
||||
choice_type = (kwargs.get("type") or "auto").strip().lower()
|
||||
if choice_type not in ("auto", "any", "tool", "none"):
|
||||
choice_type = "auto"
|
||||
|
||||
name = (kwargs.get("name") or "").strip()
|
||||
|
||||
result: Dict[str, Any] = {"type": choice_type}
|
||||
if choice_type == "tool":
|
||||
if not name:
|
||||
raise Exception(
|
||||
"Tool Choice: 'name' is required when type is 'tool'. "
|
||||
"Provide the exact tool name the model must call."
|
||||
)
|
||||
result["name"] = name
|
||||
|
||||
return (result,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RunwareTextInferenceToolChoice": RunwareTextInferenceToolChoice,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RunwareTextInferenceToolChoice": "Runware Text Inference Tool Choice",
|
||||
}
|
||||
@@ -13,9 +13,17 @@ class RunwareTextModelSearch:
|
||||
"openai:gpt@5.4-mini (GPT-5.4 Mini)",
|
||||
"openai:gpt@5.4-nano (GPT-5.4 Nano)",
|
||||
],
|
||||
"Anthropic": [
|
||||
"anthropic:claude@opus-4.7 (Claude Opus 4.7)",
|
||||
"anthropic:claude@sonnet-4.6 (Claude Sonnet 4.6)",
|
||||
"anthropic:claude@haiku-4.5 (Claude Haiku 4.5)",
|
||||
],
|
||||
"Google": [
|
||||
"google:gemini@3-flash (Gemini 3 Flash)",
|
||||
],
|
||||
"MoonshotAI": [
|
||||
"moonshotai:kimi@k2.6 (Kimi K2.6)",
|
||||
],
|
||||
"Zai": [
|
||||
"zai-glm-5-1 (ZAI GLM 5.1)",
|
||||
],
|
||||
@@ -23,8 +31,10 @@ class RunwareTextModelSearch:
|
||||
|
||||
MODEL_PROVIDERS = [
|
||||
"All",
|
||||
"Anthropic",
|
||||
"Google",
|
||||
"MiniMax",
|
||||
"MoonshotAI",
|
||||
"OpenAI",
|
||||
"Zai",
|
||||
]
|
||||
|
||||
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