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:
+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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user