Added initial progress on textInference

This commit is contained in:
Sirshendu Ganguly
2026-03-25 21:56:47 -04:00
parent 6d47a67b85
commit 61359b1cb7
12 changed files with 585 additions and 2 deletions
+13
View File
@@ -42,6 +42,11 @@ from .modules.providerSettings.briaProviderMask import RunwareBriaProviderMask
from .modules.audioInference import RunwareAudioInference
from .modules.audioInferenceSpeech import RunwareAudioInferenceSpeech
from .modules.audioModelSearch import RunwareAudioModelSearch
from .modules.textModelSearch import RunwareTextModelSearch
from .modules.textInferenceMessages import RunwareTextInferenceMessages
from .modules.textInferenceMessage import RunwareTextInferenceMessage
from .modules.textInference import RunwareTextInference
from .modules.textInferenceSettings import RunwareTextInferenceSettings
from .modules.audioSections import RunwareAudioSections
from .modules.audioInferenceInputs import audioInferenceInputs
from .modules.audioSettings import RunwareAudioSettings
@@ -96,6 +101,7 @@ from .modules.threeDInferenceSettingsSparseStructure import Runware3DInferenceSe
from .modules.threeDInferenceSettingsShapeSlat import Runware3DInferenceSettingsShapeSlat
from .modules.threeDInferenceSettingsTexSlat import Runware3DInferenceSettingsTexSlat
from .modules.save3D import RunwareSave3D
from .modules.saveText import RunwareSaveText
from .modules.loadMesh import RunwareLoadMesh
RUNWARE_COMFYUI_VERSION = "1.4.0 Beta"
@@ -151,6 +157,11 @@ NODE_CLASS_MAPPINGS = {
"Runware Audio Inference": RunwareAudioInference,
"Runware Audio Inference Speech": RunwareAudioInferenceSpeech,
"Runware Audio Model Search": RunwareAudioModelSearch,
"Runware Text Model Search": RunwareTextModelSearch,
"Runware Text Inference Messages": RunwareTextInferenceMessages,
"Runware Text Inference Message": RunwareTextInferenceMessage,
"Runware Text Inference": RunwareTextInference,
"Runware Text Inference Settings": RunwareTextInferenceSettings,
"Runware Audio Sections": RunwareAudioSections,
"Runware Audio Inference Inputs": audioInferenceInputs,
"Runware Audio Inference Settings": RunwareAudioSettings,
@@ -207,6 +218,7 @@ NODE_CLASS_MAPPINGS = {
"Runware 3D Inference Settings Shape Slat": Runware3DInferenceSettingsShapeSlat,
"Runware 3D Inference Settings Tex Slat": Runware3DInferenceSettingsTexSlat,
"Runware Save 3D": RunwareSave3D,
"Runware Save Text": RunwareSaveText,
"Runware Load Mesh": RunwareLoadMesh,
}
@@ -218,6 +230,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Runware Multi Inference": "Runware Multi Inference [BETA]",
"Runware Video Model Search": "Runware Video Model",
"Runware Audio Model Search": "Runware Audio Model",
"Runware Text Model Search": "Runware Text Model",
"Runware Video Transcription": "Runware Video Caption",
}
+6 -1
View File
@@ -1,6 +1,6 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, mediaUUIDHandler, save3DFilepathHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, vectorizeModelSearchFilterHandler, vectorizeToggleHandler, useParameterToggleHandler, imageInferenceToggleHandler, imageInferenceAdvancedFeaturesToggleHandler, watermarkAdvancedFeatureToggleHandler, videoInferenceSpeechInputToggleHandler, regionalPromptingRegionsToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, audioInferenceSpeechToggleHandler, audioSettingsToggleHandler, videoSettingsToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, xaiProviderSettingsToggleHandler, viduProviderSettingsToggleHandler, sourcefulProviderSettingsToggleHandler, sourcefulProviderSettingsFontsToggleHandler, threeDInferenceToggleHandler, threeDInferenceSettingsToggleHandler, threeDInferenceSettingsLatToggleHandler, ultralyticsProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js";
import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, saveTextHandler, mediaUUIDHandler, save3DFilepathHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, textModelSearchFilterHandler, vectorizeModelSearchFilterHandler, vectorizeToggleHandler, useParameterToggleHandler, imageInferenceToggleHandler, imageInferenceAdvancedFeaturesToggleHandler, watermarkAdvancedFeatureToggleHandler, videoInferenceSpeechInputToggleHandler, regionalPromptingRegionsToggleHandler, upscalerToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, audioInferenceSpeechToggleHandler, audioSettingsToggleHandler, textInferenceSettingsToggleHandler, videoSettingsToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, xaiProviderSettingsToggleHandler, viduProviderSettingsToggleHandler, sourcefulProviderSettingsToggleHandler, sourcefulProviderSettingsFontsToggleHandler, threeDInferenceToggleHandler, threeDInferenceSettingsToggleHandler, threeDInferenceSettingsLatToggleHandler, ultralyticsProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler } from "./utils.js";
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
const nodeInitList = [];
@@ -9,6 +9,7 @@ app.registerExtension({
async setup() {
api.addEventListener('runwareError', handleCustomErrors);
api.addEventListener('runwareImageCaption', captionNodeHandler);
api.addEventListener('runwareSaveText', saveTextHandler);
api.addEventListener('runwareMediaUUID', mediaUUIDHandler);
api.addEventListener('runwareSave3DFilepath', save3DFilepathHandler);
api.addEventListener('runwareVideoTranscription', videoTranscriptionHandler);
@@ -90,6 +91,8 @@ app.registerExtension({
audioInferenceSpeechToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOSETTINGS) {
audioSettingsToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.TEXTINFERENCESETTINGS) {
textInferenceSettingsToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOSETTINGS) {
videoSettingsToggleHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.ACCELERATOROPTIONS) {
@@ -159,6 +162,8 @@ app.registerExtension({
videoModelSearchFilterHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOMODELSEARCH) {
audioModelSearchFilterHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.TEXTMODELSEARCH) {
textModelSearchFilterHandler(node);
} else if(nodeClass === RUNWARE_NODE_TYPES.VECTORIZE) {
vectorizeModelSearchFilterHandler(node);
vectorizeToggleHandler(node);
+29
View File
@@ -82,6 +82,11 @@ const RUNWARE_NODE_TYPES = {
AUDIOINFERENCE: "Runware Audio Inference",
AUDIOINFERENCESPEECH: "Runware Audio Inference Speech",
AUDIOMODELSEARCH: "Runware Audio Model Search",
TEXTMODELSEARCH: "Runware Text Model Search",
TEXTINFERENCE: "Runware Text Inference",
TEXTINFERENCEMESSAGES: "Runware Text Inference Messages",
TEXTINFERENCEMESSAGE: "Runware Text Inference Message",
TEXTINFERENCESETTINGS: "Runware Text Inference Settings",
AUDIOSECTIONS: "Runware Audio Sections",
AUDIOINFERENCEINPUTS: "Runware Audio Inference Inputs",
AUDIOSETTINGS: "Runware Audio Inference Settings",
@@ -136,6 +141,7 @@ const RUNWARE_NODE_TYPES = {
VIDEOADVANCEDFEATUREINPUTS: "Runware Video Advanced Feature Inputs",
WANANIMATEADVANCEDFEATURESETTINGS: "Runware Wan Animate Advanced Feature Settings",
SAVEIMAGE: "Runware Save Image",
SAVETEXT: "Runware Save Text",
VIDEOINFERENCEOUTPUTS: "Runware Video Inference Outputs",
THREEDINFERENCE: "Runware 3D Inference",
THREEDINFERENCEINPUTS: "Runware 3D Inference Inputs",
@@ -285,6 +291,25 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
liveSearch: true,
},
[RUNWARE_NODE_TYPES.TEXTMODELSEARCH]: {
bgColor: DEFAULT_BGCOLOR,
liveSearch: false,
},
[RUNWARE_NODE_TYPES.TEXTINFERENCE]: {
bgColor: DEFAULT_BGCOLOR,
},
[RUNWARE_NODE_TYPES.TEXTINFERENCEMESSAGES]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.TEXTINFERENCEMESSAGE]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.TEXTINFERENCESETTINGS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.AUDIOSECTIONS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
@@ -513,6 +538,10 @@ const RUNWARE_NODE_PROPS = {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.SAVETEXT]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
},
[RUNWARE_NODE_TYPES.VIDEOINFERENCEOUTPUTS]: {
bgColor: DEFAULT_BGCOLOR,
colorModeOnly: true,
+102
View File
@@ -113,6 +113,28 @@ function captionNodeHandler(msgEvent) {
return false;
}
function saveTextHandler(msgEvent) {
const data = msgEvent.detail;
const displayText = data.text;
const nodeID = parseInt(data.nodeID, 10);
if (!data.success || Number.isNaN(nodeID)) return false;
const node = app.graph.getNodeById(nodeID);
if (node === null || node === undefined) return false;
const widgetName = data.widgetName || "text";
const textWidget = node.widgets.find((w) => w.name === widgetName);
if (textWidget) {
if (textWidget.value !== undefined) {
textWidget.value = displayText;
}
if (textWidget.inputEl) {
textWidget.inputEl.value = displayText;
textWidget.inputEl.dispatchEvent(new Event("input", { bubbles: true }));
textWidget.inputEl.dispatchEvent(new Event("change", { bubbles: true }));
}
}
return false;
}
function videoTranscriptionHandler(msgEvent) {
const transcriptionData = msgEvent.detail;
const transcriptionText = transcriptionData.transcriptionText;
@@ -1169,6 +1191,43 @@ function audioSettingsToggleHandler(settingsNode) {
if (useTextNormalizationWidget && textNormalizationWidget) toggleWidgetState(useTextNormalizationWidget, textNormalizationWidget, "textNormalization");
}
function textInferenceSettingsToggleHandler(settingsNode) {
if (!settingsNode?.widgets) return;
const useSystemPromptWidget = settingsNode.widgets.find(w => w && w.name === "useSystemPrompt");
const systemWidget = settingsNode.widgets.find(w => w && w.name === "system");
const useMaxTokensWidget = settingsNode.widgets.find(w => w && w.name === "useMaxTokens");
const maxTokensWidget = settingsNode.widgets.find(w => w && w.name === "maxTokens");
const useTemperatureWidget = settingsNode.widgets.find(w => w && w.name === "useTemperature");
const temperatureWidget = settingsNode.widgets.find(w => w && w.name === "temperature");
const useTopPWidget = settingsNode.widgets.find(w => w && w.name === "useTopP");
const topPWidget = settingsNode.widgets.find(w => w && w.name === "topP");
const useTopKWidget = settingsNode.widgets.find(w => w && w.name === "useTopK");
const topKWidget = settingsNode.widgets.find(w => w && w.name === "topK");
function toggleWidgetState(useWidget, paramWidget) {
if (!useWidget || !paramWidget) return;
function applyState() {
const enabled = useWidget.value === true;
toggleWidgetEnabled(paramWidget, enabled, settingsNode);
if (paramWidget.options && paramWidget.options.element) {
paramWidget.options.element.disabled = !enabled;
paramWidget.options.element.style.opacity = enabled ? "1" : "0.5";
paramWidget.options.element.style.pointerEvents = enabled ? "auto" : "none";
}
settingsNode.setDirtyCanvas(true);
}
setTimeout(applyState, 100);
appendWidgetCB(useWidget, () => setTimeout(applyState, 50));
}
if (useSystemPromptWidget && systemWidget) toggleWidgetState(useSystemPromptWidget, systemWidget);
if (useMaxTokensWidget && maxTokensWidget) toggleWidgetState(useMaxTokensWidget, maxTokensWidget);
if (useTemperatureWidget && temperatureWidget) toggleWidgetState(useTemperatureWidget, temperatureWidget);
if (useTopPWidget && topPWidget) toggleWidgetState(useTopPWidget, topPWidget);
if (useTopKWidget && topKWidget) toggleWidgetState(useTopKWidget, topKWidget);
}
function videoSettingsToggleHandler(settingsNode) {
if (!settingsNode?.widgets) return;
@@ -2748,6 +2807,46 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
updateDimensions();
}
function textModelSearchFilterHandler(textModelSearchNode) {
const modelProviderWidget = textModelSearchNode.widgets.find(w => w.name === "Model Provider");
const textListWidget = textModelSearchNode.widgets.find(w => w.name === "TextList");
if (!modelProviderWidget || !textListWidget) return;
const TEXT_MODELS = {
"MiniMax": [
"minimax:m2.7@0 (MiniMax-M2.7)",
"minimax:m2.7@highspeed (MiniMax-M2.7 Highspeed)",
"minimax:m2.5@0 (MiniMax-M2.5)",
],
};
function filterModelList() {
const selectedProvider = modelProviderWidget.value;
let filteredModels = [];
if (selectedProvider === "All") {
Object.values(TEXT_MODELS).forEach(models => filteredModels.push(...models));
} else if (TEXT_MODELS[selectedProvider]) {
filteredModels = TEXT_MODELS[selectedProvider];
}
if (filteredModels.length > 0) {
const currentValue = textListWidget.value;
textListWidget.options.values = filteredModels;
if (!filteredModels.includes(currentValue)) {
textListWidget.value = filteredModels[0];
}
textModelSearchNode.setDirtyCanvas(true);
}
}
appendWidgetCB(modelProviderWidget, filterModelList);
filterModelList();
}
function audioModelSearchFilterHandler(audioModelSearchNode) {
const modelProviderWidget = audioModelSearchNode.widgets.find(w => w.name === "Model Provider");
const audioListWidget = audioModelSearchNode.widgets.find(w => w.name === "AudioList");
@@ -4193,6 +4292,7 @@ export {
mediaUUIDHandler,
save3DFilepathHandler,
captionNodeHandler,
saveTextHandler,
videoTranscriptionHandler,
videoOutputsHandler,
handleCustomErrors,
@@ -4200,6 +4300,7 @@ export {
videoInferenceDimensionsHandler,
videoModelSearchFilterHandler,
audioModelSearchFilterHandler,
textModelSearchFilterHandler,
vectorizeModelSearchFilterHandler,
vectorizeToggleHandler,
useParameterToggleHandler,
@@ -4213,6 +4314,7 @@ export {
audioInferenceToggleHandler,
audioInferenceSpeechToggleHandler,
audioSettingsToggleHandler,
textInferenceSettingsToggleHandler,
videoSettingsToggleHandler,
acceleratorOptionsToggleHandler,
bytedanceProviderSettingsToggleHandler,
+37
View File
@@ -0,0 +1,37 @@
from .utils import runwareUtils as rwUtils
class RunwareSaveText:
"""Show text in the node (e.g. from Runware Text Inference) and forward it for downstream nodes."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Connect the 'text' output from Runware Text Inference. Text is mirrored here after run.",
}),
},
"hidden": {"node_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "forward_text"
OUTPUT_NODE = True
CATEGORY = "Runware"
DESCRIPTION = "Display text in the node and forward the same string to other nodes."
def forward_text(self, text, **kwargs):
if text is None:
text = ""
elif not isinstance(text, str):
text = str(text)
node_id = kwargs.get("node_id")
if node_id is not None:
rwUtils.sendSaveText(text, node_id)
return (text,)
+102
View File
@@ -0,0 +1,102 @@
import uuid
from typing import Any, Dict, List, Optional
from .utils import runwareUtils as rwUtils
class RunwareTextInference:
"""Runware textInference API: chat completion via selected text model and messages."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("RUNWARETEXTMODEL", {
"tooltip": "Connect Runware Text Model Search output.",
}),
"messages": ("RUNWARETEXTINFERENCEMESSAGES", {
"tooltip": "Connect Runware Text Inference Messages output.",
}),
},
"optional": {
"settings": ("RUNWARETEXTINFERENCESETTINGS", {
"tooltip": "Connect Runware Text Inference Settings (setting + thinking).",
}),
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
"tooltip": "Optional provider-specific settings (wrapped by provider prefix from the model AIR).",
}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "infer_text"
CATEGORY = "Runware/Text"
DESCRIPTION = "Run text/chat inference using Runware textInference. Connect Text Model and Text Inference Messages."
@staticmethod
def _extract_text_from_data_item(item: Dict[str, Any]) -> Optional[str]:
if not isinstance(item, dict):
return None
if "text" in item and item["text"] is not None:
return str(item["text"])
if "content" in item and isinstance(item["content"], str):
return item["content"]
choices = item.get("choices")
if isinstance(choices, list) and len(choices) > 0:
ch0 = choices[0]
if isinstance(ch0, dict):
msg = ch0.get("message")
if isinstance(msg, dict) and "content" in msg:
return str(msg["content"])
if "text" in ch0:
return str(ch0["text"])
return None
def _finalize_text(self, gen_result: Dict[str, Any]) -> str:
if "data" not in gen_result or not gen_result["data"]:
raise Exception("No data in text inference response.")
item = gen_result["data"][0]
text = self._extract_text_from_data_item(item)
if text is None:
raise Exception(
"Could not read assistant text from response. "
f"Keys on data[0]: {list(item.keys()) if isinstance(item, dict) else type(item)}"
)
return text
def infer_text(self, **kwargs):
model = (kwargs.get("model") or "").strip()
messages: List[Dict[str, str]] = kwargs.get("messages") or []
text_settings = kwargs.get("settings")
provider_settings = kwargs.get("providerSettings")
if not model:
raise Exception("model is empty.")
if not messages:
raise Exception("messages are empty.")
task_uuid = str(uuid.uuid4())
task: Dict[str, Any] = {
"taskType": "textInference",
"taskUUID": task_uuid,
"model": model,
"deliveryMethod": "sync",
"messages": messages,
"includeCost": True,
}
if text_settings is not None and isinstance(text_settings, dict) and len(text_settings) > 0:
task["settings"] = text_settings
if provider_settings is not None and isinstance(provider_settings, dict) and len(provider_settings) > 0:
provider_name = model.split(":")[0] if ":" in model else model
task["providerSettings"] = {provider_name: provider_settings}
gen_config = [task]
print(f"[Runware Text Inference] Request: {rwUtils.safe_json_dumps(gen_config, indent=2)}")
gen_result = rwUtils.inferenecRequest(gen_config)
print(f"[Runware Text Inference] Response: {rwUtils.safe_json_dumps(gen_result, indent=2)}")
return (self._finalize_text(gen_result),)
+33
View File
@@ -0,0 +1,33 @@
class RunwareTextInferenceMessage:
"""One chat message (role + content) for Runware Text Inference Messages combiner."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"role": (["user", "assistant"], {
"default": "user",
"tooltip": "Speaker role for this message.",
}),
"content": ("STRING", {
"multiline": True,
"default": "What's Photosynthesis?",
"tooltip": "Message text sent to the model.",
}),
}
}
RETURN_TYPES = ("RUNWARETEXTINFERENCEMESSAGE",)
RETURN_NAMES = ("message",)
FUNCTION = "create_message"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"One chat message (role + content). Connect to Runware Text Inference Messages."
)
def create_message(self, role: str, content: str):
r = (role or "user").strip().lower()
if r not in ("user", "assistant"):
r = "user"
body = (content or "").strip()
return ({"role": r, "content": body},)
+58
View File
@@ -0,0 +1,58 @@
from typing import Dict, List
class RunwareTextInferenceMessages:
"""Merge Runware Text Inference Message segments into the API `messages` array (order = segment 1 … N)."""
_SEGMENT_COUNT = 8
@classmethod
def INPUT_TYPES(cls):
optional = {}
for i in range(2, cls._SEGMENT_COUNT + 1):
optional[f"Message Segment {i}"] = (
"RUNWARETEXTINFERENCEMESSAGE",
{"tooltip": f"Optional message {i}. Connect Runware Text Inference Message."},
)
return {
"required": {
"Message Segment 1": ("RUNWARETEXTINFERENCEMESSAGE", {
"tooltip": "First message. Connect Runware Text Inference Message.",
}),
},
"optional": optional,
}
RETURN_TYPES = ("RUNWARETEXTINFERENCEMESSAGES",)
RETURN_NAMES = ("messages",)
FUNCTION = "merge_messages"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Combine one or more Runware Text Inference Message nodes into a messages list for Runware Text Inference "
"(same idea as Kling MultiPrompt Segment → Kling Provider Settings MultiPrompt)."
)
def merge_messages(self, **kwargs) -> tuple:
messages: List[Dict[str, str]] = []
for i in range(1, self._SEGMENT_COUNT + 1):
key = f"Message Segment {i}"
seg = kwargs.get(key)
if seg is None:
continue
if not isinstance(seg, dict):
continue
content = (seg.get("content") or "").strip()
if not content:
continue
role = seg.get("role", "user")
if role not in ("user", "assistant"):
role = "user"
messages.append({"role": role, "content": content})
if not messages:
raise Exception(
"No messages: connect Runware Text Inference Message to Message Segment 1 with non-empty content."
)
return (messages,)
+113
View File
@@ -0,0 +1,113 @@
from typing import Any, Dict, Tuple
class RunwareTextInferenceSettings:
"""Optional generation controls for Runware Text Inference (flat task.settings per API)."""
_MAX_SYSTEM_CHARS = 200000
_MAX_TOKENS_CAP = 196608
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"useSystemPrompt": ("BOOLEAN", {
"default": False,
"tooltip": "Include systemPrompt in settings.",
}),
"system": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "System instruction (max 200,000 characters). Sent as systemPrompt.",
}),
"useMaxTokens": ("BOOLEAN", {
"default": False,
"tooltip": "Include maxTokens in settings.",
}),
"maxTokens": ("INT", {
"default": 32768,
"min": 1,
"max": cls._MAX_TOKENS_CAP,
"step": 1,
"tooltip": f"maxTokens (1–{cls._MAX_TOKENS_CAP}).",
}),
"useTemperature": ("BOOLEAN", {
"default": False,
"tooltip": "Include temperature in settings.",
}),
"temperature": ("FLOAT", {
"default": 1.0,
"min": 0.01,
"max": 1.0,
"step": 0.01,
"tooltip": "Sampling temperature on (0, 1].",
}),
"useTopP": ("BOOLEAN", {
"default": False,
"tooltip": "Include topP in settings.",
}),
"topP": ("FLOAT", {
"default": 0.95,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Nucleus sampling (0.0–1.0).",
}),
"useTopK": ("BOOLEAN", {
"default": False,
"tooltip": "Include topK in settings.",
}),
"topK": ("INT", {
"default": 40,
"min": 1,
"max": 128,
"step": 1,
"tooltip": "Top-k sampling (1–128).",
}),
"thinkingLevel": (["off", "minimal", "low", "medium", "high"], {
"default": "off",
"tooltip": "thinkingLevel in settings (omit when off).",
}),
},
}
RETURN_TYPES = ("RUNWARETEXTINFERENCESETTINGS",)
RETURN_NAMES = ("settings",)
FUNCTION = "create_settings"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Optional text inference settings (flat: systemPrompt, maxTokens, temperature, topP, topK, thinkingLevel). "
"Connect to Runware Text Inference."
)
def create_settings(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
out: Dict[str, Any] = {}
if kwargs.get("useSystemPrompt", False):
sys_text = (kwargs.get("system") or "").strip()
if sys_text:
if len(sys_text) > self._MAX_SYSTEM_CHARS:
sys_text = sys_text[: self._MAX_SYSTEM_CHARS]
out["systemPrompt"] = sys_text
if kwargs.get("useMaxTokens", False):
out["maxTokens"] = int(kwargs.get("maxTokens", 32768))
if kwargs.get("useTemperature", False):
out["temperature"] = float(kwargs.get("temperature", 1.0))
if kwargs.get("useTopP", False):
out["topP"] = float(kwargs.get("topP", 0.95))
if kwargs.get("useTopK", False):
out["topK"] = int(kwargs.get("topK", 40))
tl = kwargs.get("thinkingLevel", "off")
if tl != "off":
out["thinkingLevel"] = tl
if not out:
return ({},)
return (out,)
+73
View File
@@ -0,0 +1,73 @@
class RunwareTextModelSearch:
"""Text model search node: pick or type a text/chat AIR model for Runware Text Inference."""
TEXT_MODELS = {
"MiniMax": [
"minimax:m2.7@0 (MiniMax-M2.7)",
"minimax:m2.7@highspeed (MiniMax-M2.7 Highspeed)",
"minimax:m2.5@0 (MiniMax-M2.5)",
],
}
MODEL_PROVIDERS = [
"All",
"MiniMax",
]
@classmethod
def INPUT_TYPES(cls):
all_models = cls._get_all_models()
default_model = "minimax:m2.7@0 (MiniMax-M2.7)"
return {
"required": {
"Model Search": ("STRING", {
"tooltip": "Search or type a text model AIR code (e.g. minimax:m2.7@0). Used when 'Use Search Value' is enabled.",
}),
"Model Provider": (cls.MODEL_PROVIDERS, {
"tooltip": "Filter the model list by provider.",
"default": "MiniMax",
}),
"TextList": (all_models, {
"tooltip": "Choose a model from the filtered list (AIR code is taken before the first space).",
"default": default_model,
}),
"Use Search Value": ("BOOLEAN", {
"tooltip": "When enabled, 'Model Search' is used as the model AIR instead of the selection in TextList.",
"default": False,
"label_on": "Enabled",
"label_off": "Disabled",
}),
}
}
@classmethod
def _get_all_models(cls):
all_models = []
for models in cls.TEXT_MODELS.values():
all_models.extend(models)
return all_models
RETURN_TYPES = ("RUNWARETEXTMODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "searchModels"
CATEGORY = "Runware"
DESCRIPTION = "Search and select text/chat models for Runware Text Inference."
@classmethod
def VALIDATE_INPUTS(cls, TextList):
return True
def searchModels(self, **kwargs):
use_search = kwargs.get("Use Search Value", False)
search_input = (kwargs.get("Model Search") or "").strip()
if use_search:
if not search_input:
raise Exception("Use Search Value is enabled but Model Search is empty.")
model_air = search_input
else:
current = kwargs.get("TextList")
model_air = current.split(" (")[0] if current else ""
return (model_air,)
+18 -1
View File
@@ -318,6 +318,19 @@ def sendImageCaption(captionText, nodeID):
},
)
def sendSaveText(text, nodeID):
"""Push text into Runware Save Text node's textarea (ComfyUI does not mirror linked STRING inputs visually)."""
PromptServer.instance.send_sync(
"runwareSaveText",
{
"success": True,
"text": text if text is not None else "",
"nodeID": nodeID,
"widgetName": "text",
},
)
def sendVideoTranscription(transcriptionText, nodeID):
PromptServer.instance.send_sync(
"runwareVideoTranscription",
@@ -698,7 +711,7 @@ def convertVideoB64List(videoDataObject, width=None, height=None):
return videos
def pollVideoResult(taskUUID):
"""Poll for video generation result using task UUID with getResponse"""
"""Poll async task result with taskType getResponse (video, audio, text inference, etc.)."""
global RUNWARE_API_KEY, RUNWARE_API_BASE_URL, SESSION_TIMEOUT
pollConfig = [
@@ -740,3 +753,7 @@ def pollVideoResult(taskUUID):
except Exception as e:
print(f"[Debugging] Poll request failed: {str(e)}")
return None
# Alias: getResponse is the generic async poll for any task type.
pollGetResponse = pollVideoResult
File diff suppressed because one or more lines are too long