Added initial progress on textInference
This commit is contained in:
@@ -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,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),)
|
||||
@@ -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,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,)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user