From 61359b1cb7a0bb8d833768ef68f1c2a1230f0025 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 25 Mar 2026 21:56:47 -0400 Subject: [PATCH] Added initial progress on textInference --- __init__.py | 13 ++ clientlibs/main.js | 7 +- clientlibs/types.js | 29 +++++ clientlibs/utils.js | 102 ++++++++++++++++ modules/saveText.py | 37 ++++++ modules/textInference.py | 102 ++++++++++++++++ modules/textInferenceMessage.py | 33 +++++ modules/textInferenceMessages.py | 58 +++++++++ modules/textInferenceSettings.py | 113 ++++++++++++++++++ modules/textModelSearch.py | 73 +++++++++++ modules/utils/runwareUtils.py | 19 ++- .../Runware_Text_Inference_Minimax_2_7.json | 1 + 12 files changed, 585 insertions(+), 2 deletions(-) create mode 100644 modules/saveText.py create mode 100644 modules/textInference.py create mode 100644 modules/textInferenceMessage.py create mode 100644 modules/textInferenceMessages.py create mode 100644 modules/textInferenceSettings.py create mode 100644 modules/textModelSearch.py create mode 100644 workflows/Runware_Text_Inference_Minimax_2_7.json diff --git a/__init__.py b/__init__.py index beaf3fc..6141e5f 100644 --- a/__init__.py +++ b/__init__.py @@ -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", } diff --git a/clientlibs/main.js b/clientlibs/main.js index e12b099..2e8e998 100644 --- a/clientlibs/main.js +++ b/clientlibs/main.js @@ -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); diff --git a/clientlibs/types.js b/clientlibs/types.js index 1f81aa4..28e309b 100644 --- a/clientlibs/types.js +++ b/clientlibs/types.js @@ -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, diff --git a/clientlibs/utils.js b/clientlibs/utils.js index bde3407..8deb216 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -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, diff --git a/modules/saveText.py b/modules/saveText.py new file mode 100644 index 0000000..708507a --- /dev/null +++ b/modules/saveText.py @@ -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,) diff --git a/modules/textInference.py b/modules/textInference.py new file mode 100644 index 0000000..328b5f9 --- /dev/null +++ b/modules/textInference.py @@ -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),) diff --git a/modules/textInferenceMessage.py b/modules/textInferenceMessage.py new file mode 100644 index 0000000..3c6770a --- /dev/null +++ b/modules/textInferenceMessage.py @@ -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},) diff --git a/modules/textInferenceMessages.py b/modules/textInferenceMessages.py new file mode 100644 index 0000000..2cbba57 --- /dev/null +++ b/modules/textInferenceMessages.py @@ -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,) diff --git a/modules/textInferenceSettings.py b/modules/textInferenceSettings.py new file mode 100644 index 0000000..4725e5b --- /dev/null +++ b/modules/textInferenceSettings.py @@ -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,) diff --git a/modules/textModelSearch.py b/modules/textModelSearch.py new file mode 100644 index 0000000..4442ba2 --- /dev/null +++ b/modules/textModelSearch.py @@ -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,) diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index 97000e7..dcd2743 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -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 diff --git a/workflows/Runware_Text_Inference_Minimax_2_7.json b/workflows/Runware_Text_Inference_Minimax_2_7.json new file mode 100644 index 0000000..9ff8ba6 --- /dev/null +++ b/workflows/Runware_Text_Inference_Minimax_2_7.json @@ -0,0 +1 @@ +{"id":"f1dab8a9-11a7-4fa3-9a3b-35351c84a964","revision":0,"last_node_id":9,"last_link_id":6,"nodes":[{"id":3,"type":"Runware Text Model Search","pos":[81.07406616210938,741.3767700195312],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"Model Search","name":"Model Search","type":"STRING","widget":{"name":"Model Search"},"link":null},{"localized_name":"Model Provider","name":"Model Provider","type":"COMBO","widget":{"name":"Model Provider"},"link":null},{"localized_name":"TextList","name":"TextList","type":"COMBO","widget":{"name":"TextList"},"link":null},{"localized_name":"Use Search Value","name":"Use Search Value","type":"BOOLEAN","widget":{"name":"Use Search Value"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"RUNWARETEXTMODEL","links":[1]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"6d47a67b859745d5a86f463d8edfd7da0d1cfa6b","Node name for S&R":"Runware Text Model Search"},"widgets_values":["","MiniMax","minimax:m2.7@0 (MiniMax M2.7)",false],"bgcolor":"#5345bf"},{"id":1,"type":"Runware Text Inference","pos":[497.6562194824219,734.759521484375],"size":[270,122],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"RUNWARETEXTMODEL","link":1},{"localized_name":"messages","name":"messages","type":"RUNWARETEXTINFERENCEMESSAGES","link":4},{"localized_name":"settings","name":"settings","shape":7,"type":"RUNWARETEXTINFERENCESETTINGS","link":6},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null}],"outputs":[{"localized_name":"text","name":"text","type":"STRING","links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"6d47a67b859745d5a86f463d8edfd7da0d1cfa6b","Node name for S&R":"Runware Text Inference"},"widgets_values":[],"bgcolor":"#5345bf"},{"id":5,"type":"Runware Text Inference Messages","pos":[503.0782165527344,919.4880981445312],"size":[255.45077514648438,166],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"Message Segment 1","name":"Message Segment 1","type":"RUNWARETEXTINFERENCEMESSAGE","link":5},{"localized_name":"Message Segment 2","name":"Message Segment 2","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null},{"localized_name":"Message Segment 3","name":"Message Segment 3","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null},{"localized_name":"Message Segment 4","name":"Message Segment 4","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null},{"localized_name":"Message Segment 5","name":"Message Segment 5","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null},{"localized_name":"Message Segment 6","name":"Message Segment 6","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null},{"localized_name":"Message Segment 7","name":"Message Segment 7","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null},{"localized_name":"Message Segment 8","name":"Message Segment 8","shape":7,"type":"RUNWARETEXTINFERENCEMESSAGE","link":null}],"outputs":[{"localized_name":"messages","name":"messages","type":"RUNWARETEXTINFERENCEMESSAGES","links":[4]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"6d47a67b859745d5a86f463d8edfd7da0d1cfa6b","Node name for S&R":"Runware Text Inference Messages"},"widgets_values":[],"bgcolor":"#5345bf"},{"id":4,"type":"Runware Save Text","pos":[950.5037841796875,745.9119262695312],"size":[459.2789001464844,429.43475341796875],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":2}],"outputs":[{"localized_name":"text","name":"text","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"6d47a67b859745d5a86f463d8edfd7da0d1cfa6b","Node name for S&R":"Runware Save Text"},"widgets_values":["During photosynthesis, chlorophyll absorbs light energy, converting carbon dioxide and water into glucose, releasing oxygen as a byproduct for growth."],"bgcolor":"#5345bf"},{"id":8,"type":"Runware Text Inference Message","pos":[457.5366516113281,1134.87060546875],"size":[400,200],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"role","name":"role","type":"COMBO","widget":{"name":"role"},"link":null},{"localized_name":"content","name":"content","type":"STRING","widget":{"name":"content"},"link":null}],"outputs":[{"localized_name":"message","name":"message","type":"RUNWARETEXTINFERENCEMESSAGE","links":[5]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"6d47a67b859745d5a86f463d8edfd7da0d1cfa6b","Node name for S&R":"Runware Text Inference Message"},"widgets_values":["user","Explain Photosynthesis in 20 words. "],"bgcolor":"#5345bf"},{"id":9,"type":"Runware Text Inference Settings","pos":[-6.560511112213135,948.0151977539062],"size":[400,328],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"useSystemPrompt","name":"useSystemPrompt","shape":7,"type":"BOOLEAN","widget":{"name":"useSystemPrompt"},"link":null},{"localized_name":"system","name":"system","shape":7,"type":"STRING","widget":{"name":"system"},"link":null},{"localized_name":"useMaxTokens","name":"useMaxTokens","shape":7,"type":"BOOLEAN","widget":{"name":"useMaxTokens"},"link":null},{"localized_name":"maxTokens","name":"maxTokens","shape":7,"type":"INT","widget":{"name":"maxTokens"},"link":null},{"localized_name":"useTemperature","name":"useTemperature","shape":7,"type":"BOOLEAN","widget":{"name":"useTemperature"},"link":null},{"localized_name":"temperature","name":"temperature","shape":7,"type":"FLOAT","widget":{"name":"temperature"},"link":null},{"localized_name":"useTopP","name":"useTopP","shape":7,"type":"BOOLEAN","widget":{"name":"useTopP"},"link":null},{"localized_name":"topP","name":"topP","shape":7,"type":"FLOAT","widget":{"name":"topP"},"link":null},{"localized_name":"useTopK","name":"useTopK","shape":7,"type":"BOOLEAN","widget":{"name":"useTopK"},"link":null},{"localized_name":"topK","name":"topK","shape":7,"type":"INT","widget":{"name":"topK"},"link":null},{"localized_name":"thinkingLevel","name":"thinkingLevel","shape":7,"type":"COMBO","widget":{"name":"thinkingLevel"},"link":null}],"outputs":[{"localized_name":"settings","name":"settings","type":"RUNWARETEXTINFERENCESETTINGS","links":[6]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"6d47a67b859745d5a86f463d8edfd7da0d1cfa6b","Node name for S&R":"Runware Text Inference Settings"},"widgets_values":[false,"",false,32768,false,1,true,0.95,false,40,"off"],"bgcolor":"#5345bf"}],"links":[[1,3,0,1,0,"RUNWARETEXTMODEL"],[2,1,0,4,0,"STRING"],[4,5,0,1,1,"RUNWARETEXTINFERENCEMESSAGES"],[5,8,0,5,0,"RUNWARETEXTINFERENCEMESSAGE"],[6,9,0,1,2,"RUNWARETEXTINFERENCESETTINGS"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6830134553650705,"offset":[306.87239836992575,-512.9827815463534]}},"version":0.4} \ No newline at end of file