Files

937 lines
44 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Workflow converter for ComfyUI
Converts non-API workflow format to API format for execution
Created by Seth A. Robinson - https://github.com/SethRobinson/comfyui-workflow-to-api-converter-endpoint
"""
import logging
from typing import Dict, Any, List, Tuple, Optional
# Set up logging
logger = logging.getLogger(__name__)
# Import ComfyUI node information - this is required
try:
import nodes
except ImportError as e:
raise ImportError(
"Cannot import ComfyUI nodes module. "
"This converter must be run within the ComfyUI environment. "
"Make sure ComfyUI is properly initialized before using the converter."
) from e
# Cache for node definitions
_node_info_cache = {}
def get_node_info_for_type(node_type: str) -> Dict[str, Any]:
"""Get node information for a specific node type"""
global _node_info_cache
if node_type not in _node_info_cache:
# Try to get the node info
if node_type in nodes.NODE_CLASS_MAPPINGS:
try:
obj_class = nodes.NODE_CLASS_MAPPINGS[node_type]
info = {}
info['input'] = obj_class.INPUT_TYPES()
info['input_order'] = {key: list(value.keys()) for (key, value) in obj_class.INPUT_TYPES().items()}
_node_info_cache[node_type] = info
except Exception as e:
logger.debug(f"Could not get node info for {node_type}: {e}")
_node_info_cache[node_type] = None
else:
_node_info_cache[node_type] = None
return _node_info_cache.get(node_type)
class WorkflowConverter:
"""Converts non-API workflow format to API prompt format"""
@staticmethod
def is_subgraph_uuid(node_type: str) -> bool:
"""
Check if a node type is a subgraph UUID.
Subgraphs are identified by UUID format (e.g., "b43bb7e6-178c-4f1a-b014-ac4d6a50fca2")
"""
if not node_type or not isinstance(node_type, str):
return False
# UUIDs are 36 characters with dashes at positions 8, 13, 18, 23
if len(node_type) == 36 and node_type.count('-') == 4:
parts = node_type.split('-')
if len(parts) == 5 and all(len(p) in [8, 4, 4, 4, 12] for i, p in enumerate(parts) if i == 0 or i == 4 or len(p) == 4):
return True
return False
@staticmethod
def expand_subgraph(subgraph_node_id: int, subgraph_def: Dict[str, Any], workflow_links: List) -> Tuple[List[Dict], List]:
"""
Expand a subgraph into individual nodes.
Args:
subgraph_node_id: The ID of the subgraph node in the main workflow
subgraph_def: The subgraph definition from definitions.subgraphs
workflow_links: The links from the main workflow
Returns:
Tuple of (expanded_nodes, expanded_links)
"""
expanded_nodes = []
expanded_links = []
# Get subgraph internal nodes and links
internal_nodes = subgraph_def.get('nodes', [])
internal_links = subgraph_def.get('links', [])
# Build a mapping of internal link IDs to link data
internal_link_map = {}
for link in internal_links:
if isinstance(link, dict):
link_id = link.get('id')
internal_link_map[link_id] = link
# Build input/output mappings for the subgraph
# Inputs: map input slot index to (internal_node_id, internal_slot)
# Outputs: map (internal_node_id, internal_slot) to output slot index
subgraph_inputs = subgraph_def.get('inputs', [])
subgraph_outputs = subgraph_def.get('outputs', [])
input_slot_to_internal = {} # slot_index -> (target_node_id, target_slot)
internal_to_output_slot = {} # (source_node_id, source_slot) -> slot_index
# Map inputs from the inputNode (-10) to actual internal nodes
for idx, input_def in enumerate(subgraph_inputs):
# Find links from inputNode to internal nodes
input_link_ids = input_def.get('linkIds', [])
for link_id in input_link_ids:
if link_id in internal_link_map:
link = internal_link_map[link_id]
target_id = link.get('target_id')
target_slot = link.get('target_slot')
input_slot_to_internal[idx] = (target_id, target_slot)
# Map outputs from internal nodes to the outputNode (-20)
for idx, output_def in enumerate(subgraph_outputs):
# Find links from internal nodes to outputNode
output_link_ids = output_def.get('linkIds', [])
for link_id in output_link_ids:
if link_id in internal_link_map:
link = internal_link_map[link_id]
origin_id = link.get('origin_id')
origin_slot = link.get('origin_slot')
internal_to_output_slot[(origin_id, origin_slot)] = idx
# Create expanded nodes with prefixed IDs
for node in internal_nodes:
internal_id = node.get('id')
expanded_node = node.copy()
# Prefix the node ID with the subgraph node ID
expanded_node['id'] = f"{subgraph_node_id}:{internal_id}"
# Update the node's inputs to remove links from the inputNode (-10)
# These will be replaced by external connections
if 'inputs' in expanded_node:
updated_inputs = []
for input_info in expanded_node['inputs']:
input_link = input_info.get('link')
# Check if this link comes from the inputNode
if input_link in internal_link_map:
link_data = internal_link_map[input_link]
if link_data.get('origin_id') == -10:
# This input comes from the subgraph's inputNode
# Remove the link - it will be replaced by an external connection
input_copy = input_info.copy()
input_copy['link'] = None
updated_inputs.append(input_copy)
else:
# Internal connection, keep as-is
updated_inputs.append(input_info)
else:
# No link or unknown link, keep as-is
updated_inputs.append(input_info)
expanded_node['inputs'] = updated_inputs
expanded_nodes.append(expanded_node)
# Create expanded links
# First, handle internal links (between nodes inside the subgraph)
for link in internal_links:
if isinstance(link, dict):
origin_id = link.get('origin_id')
target_id = link.get('target_id')
# Skip links from/to input/output nodes (-10, -20)
if origin_id in [-10, -20] or target_id in [-10, -20]:
continue
# Create new link with prefixed node IDs
expanded_link = [
link.get('id'), # link_id - we'll need to ensure these don't conflict
f"{subgraph_node_id}:{origin_id}", # source with prefix
link.get('origin_slot'),
f"{subgraph_node_id}:{target_id}", # target with prefix
link.get('target_slot'),
link.get('type')
]
expanded_links.append(expanded_link)
return expanded_nodes, expanded_links, input_slot_to_internal, internal_to_output_slot
@staticmethod
def is_api_format(workflow: Dict[str, Any]) -> bool:
"""
Check if a workflow is already in API format.
API format has node IDs as keys with 'class_type' and 'inputs'.
Non-API format has 'nodes', 'links', etc.
"""
# Check for non-API format indicators
if 'nodes' in workflow and 'links' in workflow:
return False
# Check if it looks like API format
# API format should have numeric string keys with class_type
for key, value in workflow.items():
if key in ['prompt', 'extra_data', 'client_id']:
continue
if isinstance(value, dict) and 'class_type' in value:
return True
return False
@staticmethod
def convert_to_api(workflow: Dict[str, Any]) -> Dict[str, Any]:
"""
Convert a non-API workflow to API format.
Args:
workflow: Non-API format workflow with nodes and links
Returns:
API format workflow ready for execution
"""
if WorkflowConverter.is_api_format(workflow):
# Already in API format
return workflow
# Extract nodes and links
workflow_nodes = workflow.get('nodes', [])
links = workflow.get('links', [])
# Extract subgraph definitions
subgraph_defs = {}
definitions = workflow.get('definitions', {})
for subgraph in definitions.get('subgraphs', []):
subgraph_id = subgraph.get('id')
if subgraph_id:
subgraph_defs[subgraph_id] = subgraph
# Expand subgraphs into individual nodes
# We need to do this recursively to handle nested subgraphs
# Keep expanding until no more subgraphs are found
subgraph_input_mappings = {} # subgraph_node_id -> {slot_idx: (internal_node_id, internal_slot)}
subgraph_output_mappings = {} # subgraph_node_id -> {(internal_node_id, internal_slot): slot_idx}
max_iterations = 10 # Prevent infinite loops
iteration = 0
while iteration < max_iterations:
iteration += 1
expanded_nodes = []
found_subgraph = False
for node in workflow_nodes:
node_type = node.get('type')
node_id = node.get('id')
# Check if this is a subgraph node
if WorkflowConverter.is_subgraph_uuid(node_type) and node_type in subgraph_defs:
found_subgraph = True
logger.debug(f"Expanding subgraph node {node_id} (type: {node_type}) - iteration {iteration}")
# Expand the subgraph
subgraph_def = subgraph_defs[node_type]
sg_nodes, sg_links, input_map, output_map = WorkflowConverter.expand_subgraph(
node_id, subgraph_def, links
)
# Add expanded nodes to our list
expanded_nodes.extend(sg_nodes)
# Add internal links from the subgraph
links.extend(sg_links)
# Store input/output mappings for later link remapping
# Use string representation of node_id for consistency
subgraph_input_mappings[str(node_id)] = input_map
subgraph_output_mappings[str(node_id)] = output_map
else:
# Regular node, keep as-is
expanded_nodes.append(node)
# Replace workflow_nodes with expanded version
workflow_nodes = expanded_nodes
# If we didn't find any subgraphs in this iteration, we're done
if not found_subgraph:
logger.debug(f"Subgraph expansion complete after {iteration} iteration(s)")
break
if iteration >= max_iterations:
logger.warning(f"Reached maximum subgraph expansion iterations ({max_iterations}). There may be circular subgraph references.")
# Helper function to recursively resolve subgraph outputs
def resolve_subgraph_output(node_id_str, slot):
"""
Recursively resolve a subgraph output to the actual internal node.
Returns (resolved_node_id, resolved_slot)
"""
if node_id_str in subgraph_output_mappings:
output_map = subgraph_output_mappings[node_id_str]
for (internal_node, internal_slot), out_slot in output_map.items():
if out_slot == slot:
# Found the internal node for this output
# Construct the new node ID
new_node_id = f"{node_id_str}:{internal_node}"
# Recursively resolve in case this is also a subgraph
return resolve_subgraph_output(new_node_id, internal_slot)
# Not a subgraph or not found in mappings, return as-is
return (node_id_str, slot)
# Helper function to recursively resolve subgraph inputs
def resolve_subgraph_input(node_id_str, slot):
"""
Recursively resolve a subgraph input to the actual internal node.
Returns (resolved_node_id, resolved_slot)
"""
if node_id_str in subgraph_input_mappings:
input_map = subgraph_input_mappings[node_id_str]
if slot in input_map:
internal_node, internal_slot = input_map[slot]
# Construct the new node ID
new_node_id = f"{node_id_str}:{internal_node}"
# Recursively resolve in case this is also a subgraph
return resolve_subgraph_input(new_node_id, internal_slot)
# Not a subgraph or not found in mappings, return as-is
return (node_id_str, slot)
# Update links to handle subgraph inputs and outputs
# Also track which expanded node inputs need to be updated
node_input_updates = {} # expanded_node_id -> {input_name: link_id}
updated_links = []
for link in links:
if len(link) >= 6:
link_id = link[0]
source_id = link[1]
source_slot = link[2]
target_id = link[3]
target_slot = link[4]
link_type = link[5] if len(link) > 5 else None
# Recursively resolve subgraph source
source_id_str = str(source_id)
source_id, source_slot = resolve_subgraph_output(source_id_str, source_slot)
# Recursively resolve subgraph target
target_id_str = str(target_id)
resolved_target_id, resolved_target_slot = resolve_subgraph_input(target_id_str, target_slot)
# Track node input updates if target was remapped
if resolved_target_id != target_id_str:
if resolved_target_id not in node_input_updates:
node_input_updates[resolved_target_id] = {}
node_input_updates[resolved_target_id][resolved_target_slot] = link_id
target_id = resolved_target_id
target_slot = resolved_target_slot
updated_links.append([link_id, source_id, source_slot, target_id, target_slot, link_type])
# Replace links with updated version
links = updated_links
# Update the expanded nodes' inputs to reference the external link IDs
for node in workflow_nodes:
node_id_str = str(node.get('id'))
if node_id_str in node_input_updates and 'inputs' in node:
slot_to_link = node_input_updates[node_id_str]
inputs = node.get('inputs', [])
# Update the input at each slot to reference the external link ID
for i, input_info in enumerate(inputs):
# The slot corresponds to the index in the inputs list
if i in slot_to_link:
input_info['link'] = slot_to_link[i]
# Build link map for quick lookup
# link_id -> (source_node_id, source_slot, target_node_id, target_slot, type)
link_map = {}
# Also track which nodes are connected to others (have outputs that go somewhere)
nodes_with_connected_outputs = set()
for link in links:
if len(link) >= 6:
link_id = link[0]
source_id = link[1]
source_slot = link[2]
target_id = link[3]
target_slot = link[4]
link_type = link[5] if len(link) > 5 else None
link_map[link_id] = {
'source_id': source_id,
'source_slot': source_slot,
'target_id': target_id,
'target_slot': target_slot,
'type': link_type
}
# Track that this source node has connected outputs
nodes_with_connected_outputs.add(source_id)
# First pass: identify PrimitiveNodes and their values
# Also identify nodes that should be excluded from API format
primitive_values = {}
nodes_to_exclude = set()
bypassed_nodes = set() # Track bypassed/disabled nodes
for node in workflow_nodes:
node_id = node.get('id')
node_type = node.get('type')
node_mode = node.get('mode', 0)
# Track bypassed/disabled nodes
# Store as string to match link_data['source_id'] type
if node_mode == 4:
bypassed_nodes.add(str(node_id))
logger.debug(f"Tracking bypassed node {node_id} ({node_type})")
# Track primitive nodes
if node_type == 'PrimitiveNode':
node_id_str = str(node_id)
widget_values = node.get('widgets_values')
if widget_values and len(widget_values) > 0:
primitive_values[node_id_str] = widget_values[0]
# Check if this node should be excluded from API format
# Exclude nodes that have no connected outputs (UI-only nodes)
outputs = node.get('outputs', [])
has_connected_output = False
for output in outputs:
output_links = output.get('links', [])
if output_links and len(output_links) > 0:
has_connected_output = True
break
# Check if this is a special UI-only node type that should be excluded
# LoadImageOutput is a special case - it's for loading from the output folder
# which is a UI convenience that shouldn't be in the API format
if node_type == 'LoadImageOutput':
nodes_to_exclude.add(str(node_id))
logger.debug(f"Marking node {node_id} ({node_type}) for exclusion - UI-only node type")
# If node has no outputs or no connected outputs, it should be excluded
# unless it's an OUTPUT_NODE (like SaveImage, PreviewImage)
elif not outputs or not has_connected_output:
# Check if this is an OUTPUT_NODE that should be kept
node_class = nodes.NODE_CLASS_MAPPINGS.get(node_type) if hasattr(nodes, 'NODE_CLASS_MAPPINGS') else None
is_output_node = node_class and hasattr(node_class, 'OUTPUT_NODE') and node_class.OUTPUT_NODE
if not is_output_node:
nodes_to_exclude.add(str(node_id))
logger.debug(f"Marking node {node_id} ({node_type}) for exclusion - no connected outputs")
else:
logger.debug(f"Keeping node {node_id} ({node_type}) - OUTPUT_NODE=True despite no connected outputs")
# Helper function to trace through bypassed nodes
def trace_through_bypassed(source_node_id, source_slot, visited=None):
"""
Trace through bypassed nodes to find the actual source.
Returns (actual_source_id, actual_source_slot) tuple.
Now handles ALL input types (widgets, strings, etc), not just image/latent.
This fixes kjnodes (WidgetToString) and other widget-based bypassed nodes.
"""
if visited is None:
visited = set()
# Avoid infinite loops
if source_node_id in visited:
return (source_node_id, source_slot)
visited.add(source_node_id)
# If source is not bypassed, return it as-is
if source_node_id not in bypassed_nodes:
return (source_node_id, source_slot)
# Find the input to this bypassed node
for node in workflow_nodes:
# Convert to string for comparison (source_node_id is string, node.get('id') is int)
if str(node.get('id')) == str(source_node_id):
node_type = node.get('type', 'unknown')
# Look for the input that should be passed through
node_inputs = node.get('inputs', [])
if node_inputs:
# Pass through ANY linked input, not just image/latent
# This is critical for kjnodes (WidgetToString) which use widget inputs
# Try to find a linked input (any type)
linked_input = None
for idx, input_info in enumerate(node_inputs):
input_link = input_info.get('link')
input_name = input_info.get('name', f'input_{idx}')
if input_link is not None and input_link in link_map:
linked_input = input_link
break
if linked_input is not None:
link_data = link_map[linked_input]
# Recursively trace through this source
return trace_through_bypassed(
link_data['source_id'],
link_data['source_slot'],
visited
)
break
# If we couldn't trace further, return original
return (source_node_id, source_slot)
# Build API format prompt
api_prompt = {}
for node in workflow_nodes:
node_id = str(node.get('id'))
node_type = node.get('type')
if not node_type:
continue
# Skip muted and bypassed/disabled nodes
node_mode = node.get('mode', 0)
if node_mode == 2: # Mode 2 is muted
logger.debug(f"Skipping muted node {node_id} ({node_type})")
continue
elif node_mode == 4: # Mode 4 is bypassed/disabled
logger.debug(f"Skipping bypassed/disabled node {node_id} ({node_type})")
continue
# Skip non-executable nodes
# These include UI-only nodes and nodes with no connected outputs
if node_type in ['Note', 'PrimitiveNode']:
logger.debug(f"Skipping {node_type} node {node_id}")
continue
# Skip nodes that were identified as having no connected outputs
# Use node_id (string) instead of node.get('id') (int) to match nodes_to_exclude type
if node_id in nodes_to_exclude:
logger.debug(f"Skipping {node_type} node {node_id} - no connected outputs")
continue
# Build node entry
api_node = {
'inputs': {},
'class_type': node_type
}
# Add _meta field with title if available
if 'title' in node:
api_node['_meta'] = {'title': node['title']}
elif hasattr(nodes, 'NODE_DISPLAY_NAME_MAPPINGS') and node_type in nodes.NODE_DISPLAY_NAME_MAPPINGS:
# Use ComfyUI's node display name mappings
api_node['_meta'] = {'title': nodes.NODE_DISPLAY_NAME_MAPPINGS[node_type]}
else:
# Use the node type as-is for the title
api_node['_meta'] = {'title': node_type}
# Process inputs (connections via links)
link_inputs = {}
primitive_inputs = {} # Separate tracking for resolved primitive values
node_inputs = node.get('inputs', [])
if node_inputs:
for input_info in node_inputs:
input_name = input_info.get('name')
input_link = input_info.get('link')
if input_link is not None and input_link in link_map:
# This input is connected via a link
link_data = link_map[input_link]
source_node_id = link_data['source_id']
source_slot = link_data['source_slot']
# If source is bypassed, SKIP this connection entirely
# Bypassed nodes are excluded from API format, so connections to them are invalid
# The target node will fall back to using widget values
if source_node_id in bypassed_nodes:
continue # Skip this connection, let widget value be used instead
# No need to trace through - source is not bypassed
actual_source_id = source_node_id
actual_source_slot = source_slot
source_node_id_str = str(actual_source_id)
# Check if the source is a PrimitiveNode or excluded node
if source_node_id_str in primitive_values:
# This is a resolved primitive value - treat as widget for ordering
primitive_inputs[input_name] = primitive_values[source_node_id_str]
elif actual_source_id in nodes_to_exclude:
# Source node is excluded (not an executable node), skip this input connection
logger.debug(f"Skipping input {input_name} from excluded node {source_node_id_str}")
elif actual_source_id in bypassed_nodes:
# If we still ended up with a bypassed node after tracing, it means
# the trace failed (couldn't find a non-bypassed source). Skip this connection.
logger.warning(f"Could not resolve bypassed node {source_node_id} for input {input_name}, skipping connection")
else:
# Keep as link with the actual source (after bypassing any disabled nodes)
if actual_source_id != source_node_id:
logger.info(f"Bypassed disabled node {source_node_id}, connecting {input_name} to {actual_source_id} (slot {actual_source_slot}) instead")
# Use actual_source_id (the traced node) not source_node_id (the bypassed node)
link_inputs[input_name] = [str(actual_source_id), actual_source_slot]
# Get the correct input order from the node class
ordered_inputs = WorkflowConverter._get_ordered_inputs(node_type, node)
# Process widget values
widget_values = node.get('widgets_values')
widget_inputs = {}
if widget_values is not None:
# Handle both list and dict widget values
if isinstance(widget_values, dict):
# Direct dictionary mapping - use as-is
for key, value in widget_values.items():
# Skip special keys that aren't actual inputs
if key in ['videopreview', 'preview']:
continue
# Only add if not connected via link
if key not in link_inputs:
widget_inputs[key] = value
elif isinstance(widget_values, list):
# List of values - need to map to widget names
# First check if widget values contain dictionaries with self-describing names
has_dict_widgets = any(isinstance(v, dict) for v in widget_values)
if has_dict_widgets:
# Handle widget values that are dictionaries
# These often self-describe their input names
WorkflowConverter._process_dict_widget_values(widget_values, widget_inputs, link_inputs)
else:
# Regular widget values - need to map to widget names
widget_mappings = WorkflowConverter._get_widget_mappings(node_type, node)
# Special handling for control_after_generate values
filtered_values = WorkflowConverter._filter_control_values(widget_values)
# Map values to widget names
if widget_mappings:
for i, value in enumerate(filtered_values):
if i < len(widget_mappings):
widget_name = widget_mappings[i]
# Only add if we have a valid name and it's not connected via link
if widget_name and widget_name not in link_inputs:
widget_inputs[widget_name] = value
else:
# If we couldn't determine mappings for an unknown node,
# we'll have to skip the widget values
if filtered_values:
logger.warning(f"Could not map widget values for unknown node type '{node_type}' (node {node_id})")
# Build inputs in the correct order
# The official "Save (API)" format follows this pattern:
# ALL widget values first (in node class order), then ALL link inputs (in node class order)
# Note: Resolved primitive values are treated as widgets for ordering
if ordered_inputs:
# First pass: add all widget values in order (including resolved primitives)
for input_name in ordered_inputs:
if input_name in widget_inputs:
api_node['inputs'][input_name] = widget_inputs[input_name]
elif input_name in primitive_inputs:
api_node['inputs'][input_name] = primitive_inputs[input_name]
# Second pass: add all link inputs in order
for input_name in ordered_inputs:
if input_name in link_inputs and input_name not in api_node['inputs']:
api_node['inputs'][input_name] = link_inputs[input_name]
# Add any remaining inputs that weren't in the ordered list
for key, value in widget_inputs.items():
if key not in api_node['inputs']:
api_node['inputs'][key] = value
for key, value in primitive_inputs.items():
if key not in api_node['inputs']:
api_node['inputs'][key] = value
for key, value in link_inputs.items():
if key not in api_node['inputs']:
api_node['inputs'][key] = value
else:
# Fallback when we don't have the node class: add all inputs in order they appear
# First add ALL widget inputs and primitives, then ALL link inputs
for key, value in widget_inputs.items():
api_node['inputs'][key] = value
for key, value in primitive_inputs.items():
if key not in api_node['inputs']:
api_node['inputs'][key] = value
for key, value in link_inputs.items():
if key not in api_node['inputs']:
api_node['inputs'][key] = value
api_prompt[node_id] = api_node
return api_prompt
@staticmethod
def _process_dict_widget_values(widget_values: List[Any], widget_inputs: Dict[str, Any], link_inputs: Dict[str, Any]) -> None:
"""
Process widget values that contain dictionaries.
These are self-describing widgets that contain their configuration as dicts.
"""
lora_counter = 0
for value in widget_values:
if isinstance(value, dict):
if not value:
# Empty dict - skip
continue
elif 'type' in value:
# Widget with a type field - use the type as the input name
widget_name = value.get('type')
if widget_name and widget_name not in link_inputs:
widget_inputs[widget_name] = value
elif 'lora' in value:
# This is a lora configuration entry
lora_counter += 1
widget_name = f'lora_{lora_counter}'
if widget_name not in link_inputs:
# Remove 'strengthTwo' if it's None (not used in API format)
clean_value = {k: v for k, v in value.items() if k != 'strengthTwo' or v is not None}
widget_inputs[widget_name] = clean_value
else:
# Unknown dict structure - include it as-is with a generic name
# This ensures we don't lose data even for unknown structures
logger.debug(f"Unknown dict widget value structure: {value}")
elif isinstance(value, str):
# String values at the end often represent buttons or special controls
# The "➕ Add Lora" button is a common example
if value == '':
# Empty string often represents the "Add" button
widget_inputs['➕ Add Lora'] = value
# Skip None values and other types that don't map to widgets
@staticmethod
def _filter_control_values(widget_values: List[Any]) -> List[Any]:
"""Filter out control_after_generate values from widget list"""
filtered = []
skip_next = False
for i, value in enumerate(widget_values):
if skip_next:
skip_next = False
continue
# Check if this is a control value that should be skipped
if value in ['fixed', 'increment', 'decrement', 'randomize']:
# This is a control_after_generate value, skip it
continue
# Check if this value is followed by a control value
if i + 1 < len(widget_values):
next_val = widget_values[i + 1]
if next_val in ['fixed', 'increment', 'decrement', 'randomize']:
# This widget has a control value after it
filtered.append(value)
# The control value will be skipped in the next iteration
continue
filtered.append(value)
return filtered
@staticmethod
def _get_ordered_inputs(node_type: str, node: Dict[str, Any]) -> List[str]:
"""
Get the ordered list of all inputs (both widgets and connections) for a node type.
Returns the inputs in the order they should appear in the API format.
"""
# Try to get input order from node properties first
properties = node.get('properties', {})
if 'Node name for S&R' in properties:
# Sometimes the actual node class name is stored here
node_type = properties['Node name for S&R']
# Try to get from cached node info first
node_info = get_node_info_for_type(node_type)
if node_info and 'input_order' in node_info:
input_order = node_info['input_order']
input_names = []
for section in ['required', 'optional']:
if section in input_order:
input_names.extend(input_order[section])
if input_names:
return input_names
# Fallback: Get input order from actual node class if it's loaded
if hasattr(nodes, 'NODE_CLASS_MAPPINGS') and node_type in nodes.NODE_CLASS_MAPPINGS:
try:
node_class = nodes.NODE_CLASS_MAPPINGS[node_type]
input_types = node_class.INPUT_TYPES()
# Build ordered list of all input names
input_names = []
# Process required inputs first, then optional
for section in ['required', 'optional']:
if section in input_types:
for input_name in input_types[section].keys():
input_names.append(input_name)
if input_names:
return input_names
except Exception as e:
logger.debug(f"Could not get input order from node class for {node_type}: {e}")
# For unknown nodes, return empty list (will use fallback ordering)
return []
@staticmethod
def _get_widget_mappings(node_type: str, node: Dict[str, Any]) -> List[Optional[str]]:
"""
Get widget name mappings for a given node type.
Returns a list of widget names in the order they appear.
This is used to map the widget_values list to actual input names.
"""
# Try to get widget order from node properties first
properties = node.get('properties', {})
if 'Node name for S&R' in properties:
# Sometimes the actual node class name is stored here
node_type = properties['Node name for S&R']
# Try to get from cached node info first
node_info = get_node_info_for_type(node_type)
if node_info and 'input' in node_info:
try:
input_def = node_info['input']
widget_names = []
# Process required inputs first, then optional
for section in ['required', 'optional']:
if section in input_def:
for input_name, input_spec in input_def[section].items():
# Check if this is a widget input (not a node connection)
if isinstance(input_spec, (list, tuple)) and len(input_spec) >= 1:
input_type = input_spec[0]
# Check if it's a widget type
if isinstance(input_type, list):
# This is a combo box widget (list of choices)
widget_names.append(input_name)
elif input_type in ['INT', 'FLOAT', 'STRING', 'BOOLEAN', 'COMBO']:
# Basic widget types (COMBO is also a widget type, not a connection)
widget_names.append(input_name)
elif isinstance(input_type, str) and not input_type.isupper():
# Custom widget types (lowercase)
widget_names.append(input_name)
# Skip connection types (MODEL, LATENT, CONDITIONING, etc.)
if widget_names:
return widget_names
except Exception as e:
logger.debug(f"Could not get widget mappings from node info for {node_type}: {e}")
# Fallback: Get widget order from actual node class if it's loaded
if hasattr(nodes, 'NODE_CLASS_MAPPINGS') and node_type in nodes.NODE_CLASS_MAPPINGS:
try:
node_class = nodes.NODE_CLASS_MAPPINGS[node_type]
input_types = node_class.INPUT_TYPES()
# Build ordered list of widget names (non-connection inputs)
widget_names = []
# Process required inputs first, then optional
for section in ['required', 'optional']:
if section in input_types:
for input_name, input_spec in input_types[section].items():
# Check if this is a widget input (not a node connection)
if isinstance(input_spec, tuple) and len(input_spec) >= 1:
input_type = input_spec[0]
# Check if it's a widget type
if isinstance(input_type, list):
# This is a combo box widget (list of choices)
widget_names.append(input_name)
elif input_type in ['INT', 'FLOAT', 'STRING', 'BOOLEAN', 'COMBO']:
# Basic widget types (COMBO is also a widget type, not a connection)
widget_names.append(input_name)
elif isinstance(input_type, str) and not input_type.isupper():
# Custom widget types (lowercase)
widget_names.append(input_name)
# Skip connection types (MODEL, LATENT, CONDITIONING, etc.)
if widget_names:
return widget_names
except Exception as e:
logger.debug(f"Could not get widget mappings from node class for {node_type}: {e}")
# Fallback: Try to infer widget mappings from the workflow and widget values
widget_values = node.get('widgets_values', [])
if not isinstance(widget_values, list) or len(widget_values) == 0:
return []
# Try to get widget names from ue_properties.widget_ue_connectable
# This property lists all widgets that can be converted to connectable inputs
properties = node.get('properties', {})
ue_properties = properties.get('ue_properties', {})
widget_ue_connectable = ue_properties.get('widget_ue_connectable', {})
if widget_ue_connectable and isinstance(widget_ue_connectable, dict):
# Get the keys which are the widget names
# These are in dictionary order, which should match the widget_values order
widget_names = list(widget_ue_connectable.keys())
if widget_names and len(widget_names) >= len(widget_values):
return widget_names[:len(widget_values)]
# Get all inputs from the node
all_inputs = []
connected_inputs = set()
widget_flagged_inputs = []
for input_info in node.get('inputs', []):
input_name = input_info.get('name')
if input_name:
all_inputs.append(input_name)
if input_info.get('link') is not None:
connected_inputs.add(input_name)
if input_info.get('widget'):
widget_flagged_inputs.append(input_name)
# If we have widget-flagged inputs, start with those
if widget_flagged_inputs:
# But we might have more widget values than flagged inputs
# This happens with nodes like WanImageToVideo where not all widgets are flagged
if len(widget_values) > len(widget_flagged_inputs):
# We need to find the additional widget inputs
# They should be the non-connected inputs that aren't flagged
potential_widgets = [inp for inp in all_inputs
if inp not in connected_inputs and inp not in widget_flagged_inputs]
# Combine them in order
return widget_flagged_inputs + potential_widgets[:len(widget_values) - len(widget_flagged_inputs)]
return widget_flagged_inputs
# No flagged widgets, try to guess based on which inputs aren't connected
unconnected = [inp for inp in all_inputs if inp not in connected_inputs]
if unconnected and len(unconnected) >= len(widget_values):
return unconnected[:len(widget_values)]
# If we still can't determine mappings, return empty
return []