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
+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