Merge pull request #112 from Runware/feature-textInference
Added initial progress on textInference
This commit is contained in:
+13
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -116,6 +116,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;
|
||||
@@ -1222,6 +1244,43 @@ function audioSettingsToggleHandler(settingsNode) {
|
||||
if (useRepaintingEndWidget && repaintingEndWidget) toggleWidgetState(useRepaintingEndWidget, repaintingEndWidget, "repaintingEnd");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -2801,6 +2860,55 @@ 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)",
|
||||
],
|
||||
"OpenAI": [
|
||||
"openai:gpt@5.4 (GPT-5.4)",
|
||||
"openai:gpt@5.4-pro (GPT-5.4 Pro)",
|
||||
"openai:gpt@5.4-mini (GPT-5.4 Mini)",
|
||||
"openai:gpt@5.4-nano (GPT-5.4 Nano)",
|
||||
],
|
||||
"Google": [
|
||||
"google:gemini@3-flash (Gemini 3 Flash)",
|
||||
],
|
||||
};
|
||||
|
||||
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");
|
||||
@@ -4268,6 +4376,7 @@ export {
|
||||
mediaUUIDHandler,
|
||||
save3DFilepathHandler,
|
||||
captionNodeHandler,
|
||||
saveTextHandler,
|
||||
videoTranscriptionHandler,
|
||||
videoOutputsHandler,
|
||||
handleCustomErrors,
|
||||
@@ -4275,6 +4384,7 @@ export {
|
||||
videoInferenceDimensionsHandler,
|
||||
videoModelSearchFilterHandler,
|
||||
audioModelSearchFilterHandler,
|
||||
textModelSearchFilterHandler,
|
||||
vectorizeModelSearchFilterHandler,
|
||||
vectorizeToggleHandler,
|
||||
useParameterToggleHandler,
|
||||
@@ -4288,6 +4398,7 @@ export {
|
||||
audioInferenceToggleHandler,
|
||||
audioInferenceSpeechToggleHandler,
|
||||
audioSettingsToggleHandler,
|
||||
textInferenceSettingsToggleHandler,
|
||||
videoSettingsToggleHandler,
|
||||
acceleratorOptionsToggleHandler,
|
||||
bytedanceProviderSettingsToggleHandler,
|
||||
|
||||
@@ -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,)
|
||||
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
"includeUsage": 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),)
|
||||
@@ -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},)
|
||||
@@ -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,)
|
||||
@@ -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,)
|
||||
@@ -0,0 +1,84 @@
|
||||
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)",
|
||||
],
|
||||
"OpenAI": [
|
||||
"openai:gpt@5.4 (GPT-5.4)",
|
||||
"openai:gpt@5.4-pro (GPT-5.4 Pro)",
|
||||
"openai:gpt@5.4-mini (GPT-5.4 Mini)",
|
||||
"openai:gpt@5.4-nano (GPT-5.4 Nano)",
|
||||
],
|
||||
"Google": [
|
||||
"google:gemini@3-flash (Gemini 3 Flash)",
|
||||
],
|
||||
}
|
||||
|
||||
MODEL_PROVIDERS = [
|
||||
"All",
|
||||
"Google",
|
||||
"MiniMax",
|
||||
"OpenAI",
|
||||
]
|
||||
|
||||
@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 from text before ' (').",
|
||||
"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,)
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user