mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-28 08:28:53 -03:00
checkpoint
This commit is contained in:
@@ -5,72 +5,102 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Dict, List, Any, Optional, Union, Type, Callable
|
from typing import Dict, List, Any, Optional, Union, Type, Callable, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Global mapper registry
|
# Global mapper registry
|
||||||
_MAPPER_REGISTRY: Dict[str, 'NodeMapper'] = {}
|
_MAPPER_REGISTRY: Dict[str, Dict] = {}
|
||||||
|
|
||||||
class NodeMapper:
|
# =============================================================================
|
||||||
"""Base class for node mappers that define how to extract information from a specific node type"""
|
# Mapper Definition Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def create_mapper(
|
||||||
|
node_type: str,
|
||||||
|
inputs_to_track: List[str],
|
||||||
|
transform_func: Callable[[Dict], Any] = None
|
||||||
|
) -> Dict:
|
||||||
|
"""Create a mapper definition for a node type"""
|
||||||
|
mapper = {
|
||||||
|
"node_type": node_type,
|
||||||
|
"inputs_to_track": inputs_to_track,
|
||||||
|
"transform": transform_func or (lambda inputs: inputs)
|
||||||
|
}
|
||||||
|
return mapper
|
||||||
|
|
||||||
|
def register_mapper(mapper: Dict) -> None:
|
||||||
|
"""Register a node mapper in the global registry"""
|
||||||
|
_MAPPER_REGISTRY[mapper["node_type"]] = mapper
|
||||||
|
logger.debug(f"Registered mapper for node type: {mapper['node_type']}")
|
||||||
|
|
||||||
|
def get_mapper(node_type: str) -> Optional[Dict]:
|
||||||
|
"""Get a mapper for the specified node type"""
|
||||||
|
return _MAPPER_REGISTRY.get(node_type)
|
||||||
|
|
||||||
|
def get_all_mappers() -> Dict[str, Dict]:
|
||||||
|
"""Get all registered mappers"""
|
||||||
|
return _MAPPER_REGISTRY.copy()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Node Processing Function
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def process_node(node_id: str, node_data: Dict, workflow: Dict, parser: 'WorkflowParser') -> Any:
|
||||||
|
"""Process a node using its mapper and extract relevant information"""
|
||||||
|
node_type = node_data.get("class_type")
|
||||||
|
mapper = get_mapper(node_type)
|
||||||
|
|
||||||
def __init__(self, node_type: str, inputs_to_track: List[str]):
|
if not mapper:
|
||||||
self.node_type = node_type
|
return None
|
||||||
self.inputs_to_track = inputs_to_track
|
|
||||||
|
|
||||||
def process(self, node_id: str, node_data: Dict, workflow: Dict, parser: 'WorkflowParser') -> Any: # type: ignore
|
|
||||||
"""Process the node and extract relevant information"""
|
|
||||||
result = {}
|
|
||||||
for input_name in self.inputs_to_track:
|
|
||||||
if input_name in node_data.get("inputs", {}):
|
|
||||||
input_value = node_data["inputs"][input_name]
|
|
||||||
# Check if input is a reference to another node's output
|
|
||||||
if isinstance(input_value, list) and len(input_value) == 2:
|
|
||||||
# Format is [node_id, output_slot]
|
|
||||||
try:
|
|
||||||
ref_node_id, output_slot = input_value
|
|
||||||
# Convert node_id to string if it's an integer
|
|
||||||
if isinstance(ref_node_id, int):
|
|
||||||
ref_node_id = str(ref_node_id)
|
|
||||||
|
|
||||||
# Recursively process the referenced node
|
|
||||||
ref_value = parser.process_node(ref_node_id, workflow)
|
|
||||||
|
|
||||||
# Store the processed value
|
|
||||||
if ref_value is not None:
|
|
||||||
result[input_name] = ref_value
|
|
||||||
else:
|
|
||||||
# If we couldn't get a value from the reference, store the raw value
|
|
||||||
result[input_name] = input_value
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing reference in node {node_id}, input {input_name}: {e}")
|
|
||||||
# If we couldn't process the reference, store the raw value
|
|
||||||
result[input_name] = input_value
|
|
||||||
else:
|
|
||||||
# Direct value
|
|
||||||
result[input_name] = input_value
|
|
||||||
|
|
||||||
# Apply any transformations
|
result = {}
|
||||||
return self.transform(result)
|
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Any:
|
# Extract inputs based on the mapper's tracked inputs
|
||||||
"""Transform the extracted inputs - override in subclasses"""
|
for input_name in mapper["inputs_to_track"]:
|
||||||
return inputs
|
if input_name in node_data.get("inputs", {}):
|
||||||
|
input_value = node_data["inputs"][input_name]
|
||||||
|
|
||||||
|
# Check if input is a reference to another node's output
|
||||||
|
if isinstance(input_value, list) and len(input_value) == 2:
|
||||||
|
try:
|
||||||
|
# Format is [node_id, output_slot]
|
||||||
|
ref_node_id, output_slot = input_value
|
||||||
|
# Convert node_id to string if it's an integer
|
||||||
|
if isinstance(ref_node_id, int):
|
||||||
|
ref_node_id = str(ref_node_id)
|
||||||
|
|
||||||
|
# Recursively process the referenced node
|
||||||
|
ref_value = parser.process_node(ref_node_id, workflow)
|
||||||
|
|
||||||
|
if ref_value is not None:
|
||||||
|
result[input_name] = ref_value
|
||||||
|
else:
|
||||||
|
# If we couldn't get a value from the reference, store the raw value
|
||||||
|
result[input_name] = input_value
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing reference in node {node_id}, input {input_name}: {e}")
|
||||||
|
result[input_name] = input_value
|
||||||
|
else:
|
||||||
|
# Direct value
|
||||||
|
result[input_name] = input_value
|
||||||
|
|
||||||
|
# Apply the transform function
|
||||||
|
try:
|
||||||
|
return mapper["transform"](result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in transform function for node {node_id} of type {node_type}: {e}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Default Mapper Definitions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
class KSamplerMapper(NodeMapper):
|
def register_default_mappers() -> None:
|
||||||
"""Mapper for KSampler nodes"""
|
"""Register all default mappers"""
|
||||||
|
|
||||||
def __init__(self):
|
# KSampler mapper
|
||||||
super().__init__(
|
def transform_ksampler(inputs: Dict) -> Dict:
|
||||||
node_type="KSampler",
|
|
||||||
inputs_to_track=["seed", "steps", "cfg", "sampler_name", "scheduler",
|
|
||||||
"denoise", "positive", "negative", "latent_image",
|
|
||||||
"model", "clip_skip"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Dict:
|
|
||||||
result = {
|
result = {
|
||||||
"seed": str(inputs.get("seed", "")),
|
"seed": str(inputs.get("seed", "")),
|
||||||
"steps": str(inputs.get("steps", "")),
|
"steps": str(inputs.get("steps", "")),
|
||||||
@@ -99,70 +129,52 @@ class KSamplerMapper(NodeMapper):
|
|||||||
result["clip_skip"] = str(inputs.get("clip_skip", ""))
|
result["clip_skip"] = str(inputs.get("clip_skip", ""))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class EmptyLatentImageMapper(NodeMapper):
|
|
||||||
"""Mapper for EmptyLatentImage nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="KSampler",
|
||||||
node_type="EmptyLatentImage",
|
inputs_to_track=["seed", "steps", "cfg", "sampler_name", "scheduler",
|
||||||
inputs_to_track=["width", "height", "batch_size"]
|
"denoise", "positive", "negative", "latent_image",
|
||||||
)
|
"model", "clip_skip"],
|
||||||
|
transform_func=transform_ksampler
|
||||||
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Dict:
|
# EmptyLatentImage mapper
|
||||||
|
def transform_empty_latent(inputs: Dict) -> Dict:
|
||||||
width = inputs.get("width", 0)
|
width = inputs.get("width", 0)
|
||||||
height = inputs.get("height", 0)
|
height = inputs.get("height", 0)
|
||||||
return {"width": width, "height": height, "size": f"{width}x{height}"}
|
return {"width": width, "height": height, "size": f"{width}x{height}"}
|
||||||
|
|
||||||
|
|
||||||
class EmptySD3LatentImageMapper(NodeMapper):
|
|
||||||
"""Mapper for EmptySD3LatentImage nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="EmptyLatentImage",
|
||||||
node_type="EmptySD3LatentImage",
|
inputs_to_track=["width", "height", "batch_size"],
|
||||||
inputs_to_track=["width", "height", "batch_size"]
|
transform_func=transform_empty_latent
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Dict:
|
# SD3LatentImage mapper - reuses same transform function as EmptyLatentImage
|
||||||
width = inputs.get("width", 0)
|
register_mapper(create_mapper(
|
||||||
height = inputs.get("height", 0)
|
node_type="EmptySD3LatentImage",
|
||||||
return {"width": width, "height": height, "size": f"{width}x{height}"}
|
inputs_to_track=["width", "height", "batch_size"],
|
||||||
|
transform_func=transform_empty_latent
|
||||||
|
))
|
||||||
class CLIPTextEncodeMapper(NodeMapper):
|
|
||||||
"""Mapper for CLIPTextEncode nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
# CLIPTextEncode mapper
|
||||||
super().__init__(
|
def transform_clip_text(inputs: Dict) -> Any:
|
||||||
node_type="CLIPTextEncode",
|
|
||||||
inputs_to_track=["text", "clip"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Any:
|
|
||||||
# Simply return the text
|
|
||||||
return inputs.get("text", "")
|
return inputs.get("text", "")
|
||||||
|
|
||||||
|
|
||||||
class LoraLoaderMapper(NodeMapper):
|
|
||||||
"""Mapper for LoraLoader nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="CLIPTextEncode",
|
||||||
node_type="Lora Loader (LoraManager)",
|
inputs_to_track=["text", "clip"],
|
||||||
inputs_to_track=["loras", "lora_stack"]
|
transform_func=transform_clip_text
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Dict:
|
# LoraLoader mapper
|
||||||
# Fallback to loras array if text field doesn't exist or is invalid
|
def transform_lora_loader(inputs: Dict) -> Dict:
|
||||||
loras_data = inputs.get("loras", [])
|
loras_data = inputs.get("loras", [])
|
||||||
lora_stack = inputs.get("lora_stack", {}).get("lora_stack", [])
|
lora_stack = inputs.get("lora_stack", {}).get("lora_stack", [])
|
||||||
|
|
||||||
# Process loras array - filter active entries
|
|
||||||
lora_texts = []
|
lora_texts = []
|
||||||
|
|
||||||
# Check if loras_data is a list or a dict with __value__ key (new format)
|
# Process loras array
|
||||||
if isinstance(loras_data, dict) and "__value__" in loras_data:
|
if isinstance(loras_data, dict) and "__value__" in loras_data:
|
||||||
loras_list = loras_data["__value__"]
|
loras_list = loras_data["__value__"]
|
||||||
elif isinstance(loras_data, list):
|
elif isinstance(loras_data, list):
|
||||||
@@ -172,42 +184,29 @@ class LoraLoaderMapper(NodeMapper):
|
|||||||
|
|
||||||
# Process each active lora entry
|
# Process each active lora entry
|
||||||
for lora in loras_list:
|
for lora in loras_list:
|
||||||
logger.info(f"Lora: {lora}, active: {lora.get('active')}")
|
|
||||||
if isinstance(lora, dict) and lora.get("active", False):
|
if isinstance(lora, dict) and lora.get("active", False):
|
||||||
lora_name = lora.get("name", "")
|
lora_name = lora.get("name", "")
|
||||||
strength = lora.get("strength", 1.0)
|
strength = lora.get("strength", 1.0)
|
||||||
lora_texts.append(f"<lora:{lora_name}:{strength}>")
|
lora_texts.append(f"<lora:{lora_name}:{strength}>")
|
||||||
|
|
||||||
# Process lora_stack if it exists and is a valid format (list of tuples)
|
# Process lora_stack if valid
|
||||||
if lora_stack and isinstance(lora_stack, list):
|
if lora_stack and isinstance(lora_stack, list):
|
||||||
# If lora_stack is a reference to another node ([node_id, output_slot]),
|
if not (len(lora_stack) == 2 and isinstance(lora_stack[0], (str, int)) and isinstance(lora_stack[1], int)):
|
||||||
# we don't process it here as it's already been processed recursively
|
|
||||||
if len(lora_stack) == 2 and isinstance(lora_stack[0], (str, int)) and isinstance(lora_stack[1], int):
|
|
||||||
# This is a reference to another node, already processed
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Format each entry from the stack (assuming it's a list of tuples)
|
|
||||||
for stack_entry in lora_stack:
|
for stack_entry in lora_stack:
|
||||||
lora_name = stack_entry[0]
|
lora_name = stack_entry[0]
|
||||||
strength = stack_entry[1]
|
strength = stack_entry[1]
|
||||||
lora_texts.append(f"<lora:{lora_name}:{strength}>")
|
lora_texts.append(f"<lora:{lora_name}:{strength}>")
|
||||||
|
|
||||||
# Join with spaces
|
return {"loras": " ".join(lora_texts)}
|
||||||
combined_text = " ".join(lora_texts)
|
|
||||||
|
|
||||||
return {"loras": combined_text}
|
|
||||||
|
|
||||||
|
|
||||||
class LoraStackerMapper(NodeMapper):
|
|
||||||
"""Mapper for LoraStacker nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="Lora Loader (LoraManager)",
|
||||||
node_type="Lora Stacker (LoraManager)",
|
inputs_to_track=["loras", "lora_stack"],
|
||||||
inputs_to_track=["loras", "lora_stack"]
|
transform_func=transform_lora_loader
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Dict:
|
# LoraStacker mapper
|
||||||
|
def transform_lora_stacker(inputs: Dict) -> Dict:
|
||||||
loras_data = inputs.get("loras", [])
|
loras_data = inputs.get("loras", [])
|
||||||
result_stack = []
|
result_stack = []
|
||||||
|
|
||||||
@@ -215,25 +214,18 @@ class LoraStackerMapper(NodeMapper):
|
|||||||
existing_stack = []
|
existing_stack = []
|
||||||
lora_stack_input = inputs.get("lora_stack", [])
|
lora_stack_input = inputs.get("lora_stack", [])
|
||||||
|
|
||||||
# Handle different formats of lora_stack
|
|
||||||
if isinstance(lora_stack_input, dict) and "lora_stack" in lora_stack_input:
|
if isinstance(lora_stack_input, dict) and "lora_stack" in lora_stack_input:
|
||||||
# Format from another LoraStacker node
|
|
||||||
existing_stack = lora_stack_input["lora_stack"]
|
existing_stack = lora_stack_input["lora_stack"]
|
||||||
elif isinstance(lora_stack_input, list):
|
elif isinstance(lora_stack_input, list):
|
||||||
# Direct list format or reference format [node_id, output_slot]
|
if not (len(lora_stack_input) == 2 and isinstance(lora_stack_input[0], (str, int)) and
|
||||||
if len(lora_stack_input) == 2 and isinstance(lora_stack_input[0], (str, int)) and isinstance(lora_stack_input[1], int):
|
isinstance(lora_stack_input[1], int)):
|
||||||
# This is likely a reference that was already processed
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Regular list of tuples/entries
|
|
||||||
existing_stack = lora_stack_input
|
existing_stack = lora_stack_input
|
||||||
|
|
||||||
# Add existing entries first
|
# Add existing entries
|
||||||
if existing_stack:
|
if existing_stack:
|
||||||
result_stack.extend(existing_stack)
|
result_stack.extend(existing_stack)
|
||||||
|
|
||||||
# Process loras array - filter active entries
|
# Process new loras
|
||||||
# Check if loras_data is a list or a dict with __value__ key (new format)
|
|
||||||
if isinstance(loras_data, dict) and "__value__" in loras_data:
|
if isinstance(loras_data, dict) and "__value__" in loras_data:
|
||||||
loras_list = loras_data["__value__"]
|
loras_list = loras_data["__value__"]
|
||||||
elif isinstance(loras_data, list):
|
elif isinstance(loras_data, list):
|
||||||
@@ -241,7 +233,6 @@ class LoraStackerMapper(NodeMapper):
|
|||||||
else:
|
else:
|
||||||
loras_list = []
|
loras_list = []
|
||||||
|
|
||||||
# Process each active lora entry
|
|
||||||
for lora in loras_list:
|
for lora in loras_list:
|
||||||
if isinstance(lora, dict) and lora.get("active", False):
|
if isinstance(lora, dict) and lora.get("active", False):
|
||||||
lora_name = lora.get("name", "")
|
lora_name = lora.get("name", "")
|
||||||
@@ -249,50 +240,40 @@ class LoraStackerMapper(NodeMapper):
|
|||||||
result_stack.append((lora_name, strength))
|
result_stack.append((lora_name, strength))
|
||||||
|
|
||||||
return {"lora_stack": result_stack}
|
return {"lora_stack": result_stack}
|
||||||
|
|
||||||
|
|
||||||
class JoinStringsMapper(NodeMapper):
|
|
||||||
"""Mapper for JoinStrings nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="Lora Stacker (LoraManager)",
|
||||||
node_type="JoinStrings",
|
inputs_to_track=["loras", "lora_stack"],
|
||||||
inputs_to_track=["string1", "string2", "delimiter"]
|
transform_func=transform_lora_stacker
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> str:
|
# JoinStrings mapper
|
||||||
|
def transform_join_strings(inputs: Dict) -> str:
|
||||||
string1 = inputs.get("string1", "")
|
string1 = inputs.get("string1", "")
|
||||||
string2 = inputs.get("string2", "")
|
string2 = inputs.get("string2", "")
|
||||||
delimiter = inputs.get("delimiter", "")
|
delimiter = inputs.get("delimiter", "")
|
||||||
return f"{string1}{delimiter}{string2}"
|
return f"{string1}{delimiter}{string2}"
|
||||||
|
|
||||||
|
|
||||||
class StringConstantMapper(NodeMapper):
|
|
||||||
"""Mapper for StringConstant and StringConstantMultiline nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="JoinStrings",
|
||||||
node_type="StringConstantMultiline",
|
inputs_to_track=["string1", "string2", "delimiter"],
|
||||||
inputs_to_track=["string"]
|
transform_func=transform_join_strings
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> str:
|
# StringConstant mapper
|
||||||
|
def transform_string_constant(inputs: Dict) -> str:
|
||||||
return inputs.get("string", "")
|
return inputs.get("string", "")
|
||||||
|
|
||||||
|
|
||||||
class TriggerWordToggleMapper(NodeMapper):
|
|
||||||
"""Mapper for TriggerWordToggle nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="StringConstantMultiline",
|
||||||
node_type="TriggerWord Toggle (LoraManager)",
|
inputs_to_track=["string"],
|
||||||
inputs_to_track=["toggle_trigger_words"]
|
transform_func=transform_string_constant
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> str:
|
# TriggerWordToggle mapper
|
||||||
|
def transform_trigger_word_toggle(inputs: Dict) -> str:
|
||||||
toggle_data = inputs.get("toggle_trigger_words", [])
|
toggle_data = inputs.get("toggle_trigger_words", [])
|
||||||
|
|
||||||
# check if toggle_words is a list or a dict with __value__ key (new format)
|
|
||||||
if isinstance(toggle_data, dict) and "__value__" in toggle_data:
|
if isinstance(toggle_data, dict) and "__value__" in toggle_data:
|
||||||
toggle_words = toggle_data["__value__"]
|
toggle_words = toggle_data["__value__"]
|
||||||
elif isinstance(toggle_data, list):
|
elif isinstance(toggle_data, list):
|
||||||
@@ -308,28 +289,21 @@ class TriggerWordToggleMapper(NodeMapper):
|
|||||||
if word and not word.startswith("__dummy"):
|
if word and not word.startswith("__dummy"):
|
||||||
active_words.append(word)
|
active_words.append(word)
|
||||||
|
|
||||||
# Join with commas
|
return ", ".join(active_words)
|
||||||
result = ", ".join(active_words)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class FluxGuidanceMapper(NodeMapper):
|
|
||||||
"""Mapper for FluxGuidance nodes"""
|
|
||||||
|
|
||||||
def __init__(self):
|
register_mapper(create_mapper(
|
||||||
super().__init__(
|
node_type="TriggerWord Toggle (LoraManager)",
|
||||||
node_type="FluxGuidance",
|
inputs_to_track=["toggle_trigger_words"],
|
||||||
inputs_to_track=["guidance", "conditioning"]
|
transform_func=transform_trigger_word_toggle
|
||||||
)
|
))
|
||||||
|
|
||||||
def transform(self, inputs: Dict) -> Dict:
|
# FluxGuidance mapper
|
||||||
|
def transform_flux_guidance(inputs: Dict) -> Dict:
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
# Handle guidance parameter
|
|
||||||
if "guidance" in inputs:
|
if "guidance" in inputs:
|
||||||
result["guidance"] = inputs["guidance"]
|
result["guidance"] = inputs["guidance"]
|
||||||
|
|
||||||
# Handle conditioning (the prompt text)
|
|
||||||
if "conditioning" in inputs:
|
if "conditioning" in inputs:
|
||||||
conditioning = inputs["conditioning"]
|
conditioning = inputs["conditioning"]
|
||||||
if isinstance(conditioning, str):
|
if isinstance(conditioning, str):
|
||||||
@@ -338,42 +312,12 @@ class FluxGuidanceMapper(NodeMapper):
|
|||||||
result["prompt"] = "Unknown prompt"
|
result["prompt"] = "Unknown prompt"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Mapper Registry Functions
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
def register_mapper(mapper: NodeMapper) -> None:
|
|
||||||
"""Register a node mapper in the global registry"""
|
|
||||||
_MAPPER_REGISTRY[mapper.node_type] = mapper
|
|
||||||
logger.debug(f"Registered mapper for node type: {mapper.node_type}")
|
|
||||||
|
|
||||||
def get_mapper(node_type: str) -> Optional[NodeMapper]:
|
|
||||||
"""Get a mapper for the specified node type"""
|
|
||||||
return _MAPPER_REGISTRY.get(node_type)
|
|
||||||
|
|
||||||
def get_all_mappers() -> Dict[str, NodeMapper]:
|
|
||||||
"""Get all registered mappers"""
|
|
||||||
return _MAPPER_REGISTRY.copy()
|
|
||||||
|
|
||||||
def register_default_mappers() -> None:
|
|
||||||
"""Register all default mappers"""
|
|
||||||
default_mappers = [
|
|
||||||
KSamplerMapper(),
|
|
||||||
EmptyLatentImageMapper(),
|
|
||||||
EmptySD3LatentImageMapper(),
|
|
||||||
CLIPTextEncodeMapper(),
|
|
||||||
LoraLoaderMapper(),
|
|
||||||
LoraStackerMapper(),
|
|
||||||
JoinStringsMapper(),
|
|
||||||
StringConstantMapper(),
|
|
||||||
TriggerWordToggleMapper(),
|
|
||||||
FluxGuidanceMapper()
|
|
||||||
]
|
|
||||||
|
|
||||||
for mapper in default_mappers:
|
register_mapper(create_mapper(
|
||||||
register_mapper(mapper)
|
node_type="FluxGuidance",
|
||||||
|
inputs_to_track=["guidance", "conditioning"],
|
||||||
|
transform_func=transform_flux_guidance
|
||||||
|
))
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Extension Loading
|
# Extension Loading
|
||||||
@@ -383,8 +327,8 @@ def load_extensions(ext_dir: str = None) -> None:
|
|||||||
"""
|
"""
|
||||||
Load mapper extensions from the specified directory
|
Load mapper extensions from the specified directory
|
||||||
|
|
||||||
Each Python file in the directory will be loaded, and any NodeMapper subclasses
|
Extension files should define mappers using the create_mapper function
|
||||||
defined in those files will be automatically registered.
|
and then call register_mapper to add them to the registry.
|
||||||
"""
|
"""
|
||||||
# Use default path if none provided
|
# Use default path if none provided
|
||||||
if ext_dir is None:
|
if ext_dir is None:
|
||||||
@@ -410,19 +354,9 @@ def load_extensions(ext_dir: str = None) -> None:
|
|||||||
if spec and spec.loader:
|
if spec and spec.loader:
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
|
logger.info(f"Loaded extension module: {filename}")
|
||||||
# Find all NodeMapper subclasses in the module
|
|
||||||
for name, obj in inspect.getmembers(module):
|
|
||||||
if (inspect.isclass(obj) and issubclass(obj, NodeMapper)
|
|
||||||
and obj != NodeMapper and hasattr(obj, 'node_type')):
|
|
||||||
# Instantiate and register the mapper
|
|
||||||
mapper = obj()
|
|
||||||
register_mapper(mapper)
|
|
||||||
logger.info(f"Loaded extension mapper: {mapper.node_type} from {filename}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error loading extension {filename}: {e}")
|
logger.warning(f"Error loading extension {filename}: {e}")
|
||||||
|
|
||||||
|
|
||||||
# Initialize the registry with default mappers
|
# Initialize the registry with default mappers
|
||||||
register_default_mappers()
|
register_default_mappers()
|
||||||
@@ -4,7 +4,7 @@ Main workflow parser implementation for ComfyUI
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Any, Optional, Union, Set
|
from typing import Dict, List, Any, Optional, Union, Set
|
||||||
from .mappers import get_mapper, get_all_mappers, load_extensions
|
from .mappers import get_mapper, get_all_mappers, load_extensions, process_node
|
||||||
from .utils import (
|
from .utils import (
|
||||||
load_workflow, save_output, find_node_by_type,
|
load_workflow, save_output, find_node_by_type,
|
||||||
trace_model_path
|
trace_model_path
|
||||||
@@ -45,10 +45,9 @@ class WorkflowParser:
|
|||||||
node_type = node_data.get("class_type")
|
node_type = node_data.get("class_type")
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
mapper = get_mapper(node_type)
|
if get_mapper(node_type):
|
||||||
if mapper:
|
|
||||||
try:
|
try:
|
||||||
result = mapper.process(node_id, node_data, workflow, self)
|
result = process_node(node_id, node_data, workflow, self)
|
||||||
# Cache the result
|
# Cache the result
|
||||||
self.node_results_cache[node_id] = result
|
self.node_results_cache[node_id] = result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
{
|
{
|
||||||
"loras": "<lora:ck-neon-retrowave-IL-000012:0.8> <lora:aorunIllstrious:1> <lora:ck-shadow-circuit-IL-000012:0.78> <lora:MoriiMee_Gothic_Niji_Style_Illustrious_r1:0.45> <lora:ck-nc-cyberpunk-IL-000011:0.4>",
|
"loras": "<lora:ck-neon-retrowave-IL-000012:0.8> <lora:aorunIllstrious:1> <lora:ck-shadow-circuit-IL-000012:0.78> <lora:MoriiMee_Gothic_Niji_Style_Illustrious_r1:0.45> <lora:ck-nc-cyberpunk-IL-000011:0.4>",
|
||||||
"gen_params": {
|
"prompt": "in the style of ck-rw, aorun, scales, makeup, bare shoulders, pointy ears, dress, claws, in the style of cksc, artist:moriimee, in the style of cknc, masterpiece, best quality, good quality, very aesthetic, absurdres, newest, 8K, depth of field, focused subject, close up, stylized, in gold and neon shades, wabi sabi, 1girl, rainbow angel wings, looking at viewer, dynamic angle, from below, from side, relaxing",
|
||||||
"prompt": "in the style of ck-rw, aorun, scales, makeup, bare shoulders, pointy ears, dress, claws, in the style of cksc, artist:moriimee, in the style of cknc, masterpiece, best quality, good quality, very aesthetic, absurdres, newest, 8K, depth of field, focused subject, close up, stylized, in gold and neon shades, wabi sabi, 1girl, rainbow angel wings, looking at viewer, dynamic angle, from below, from side, relaxing",
|
"negative_prompt": "bad quality, worst quality, worst detail, sketch ,signature, watermark, patreon logo, nsfw",
|
||||||
"negative_prompt": "bad quality, worst quality, worst detail, sketch ,signature, watermark, patreon logo, nsfw",
|
"steps": "20",
|
||||||
"steps": "20",
|
"sampler": "euler_ancestral",
|
||||||
"sampler": "euler_ancestral",
|
"cfg_scale": "8",
|
||||||
"cfg_scale": "8",
|
"seed": "241",
|
||||||
"seed": "241",
|
"size": "832x1216",
|
||||||
"size": "832x1216",
|
"clip_skip": "2"
|
||||||
"clip_skip": "2"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"3": {
|
"3": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"seed": 241,
|
"seed": 42,
|
||||||
"steps": 20,
|
"steps": 20,
|
||||||
"cfg": 8,
|
"cfg": 8,
|
||||||
"sampler_name": "euler_ancestral",
|
"sampler_name": "euler_ancestral",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
},
|
},
|
||||||
"21": {
|
"21": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"string": "masterpiece, best quality, good quality, very aesthetic, absurdres, newest, 8K, depth of field, focused subject, close up, stylized, in gold and neon shades, wabi sabi, 1girl, rainbow angel wings, looking at viewer, dynamic angle, from below, from side, relaxing",
|
"string": "masterpiece, best quality, good quality, very awa, newest, highres, absurdres, 1girl, solo, dress, standing, flower, outdoors, water, white flower, pink flower, scenery, reflection, rain, dark, ripples, yellow flower, puddle, colorful, abstract, standing on liquidi¼\nvery Wide Shot, limited palette,",
|
||||||
"strip_newlines": false
|
"strip_newlines": false
|
||||||
},
|
},
|
||||||
"class_type": "StringConstantMultiline",
|
"class_type": "StringConstantMultiline",
|
||||||
@@ -151,15 +151,19 @@
|
|||||||
"group_mode": true,
|
"group_mode": true,
|
||||||
"toggle_trigger_words": [
|
"toggle_trigger_words": [
|
||||||
{
|
{
|
||||||
"text": "in the style of ck-rw",
|
"text": "xxx667_illu",
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "in the style of cksc",
|
"text": "glowing",
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "artist:moriimee",
|
"text": "glitch",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "15546+456868",
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -173,7 +177,7 @@
|
|||||||
"_isDummy": true
|
"_isDummy": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orinalMessage": "in the style of ck-rw,, in the style of cksc,, artist:moriimee",
|
"orinalMessage": "xxx667_illu,, glowing,, glitch,, 15546+456868",
|
||||||
"trigger_words": [
|
"trigger_words": [
|
||||||
"56",
|
"56",
|
||||||
2
|
2
|
||||||
@@ -186,22 +190,32 @@
|
|||||||
},
|
},
|
||||||
"56": {
|
"56": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "<lora:ck-shadow-circuit-IL-000012:0.78> <lora:MoriiMee_Gothic_Niji_Style_Illustrious_r1:0.45> <lora:ck-nc-cyberpunk-IL-000011:0.4>",
|
"text": "<lora:ponyv6_noobE11_2_adamW-000017:0.3> <lora:XXX667:0.4> <lora:114558v4df2fsdf5:0.6> <lora:illustriousXL_stabilizer_v1.23:0.3> <lora:mon_monmon2133:0.5>",
|
||||||
"loras": [
|
"loras": [
|
||||||
{
|
{
|
||||||
"name": "ck-shadow-circuit-IL-000012",
|
"name": "ponyv6_noobE11_2_adamW-000017",
|
||||||
"strength": 0.78,
|
"strength": 0.3,
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "MoriiMee_Gothic_Niji_Style_Illustrious_r1",
|
"name": "XXX667",
|
||||||
"strength": 0.45,
|
|
||||||
"active": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ck-nc-cyberpunk-IL-000011",
|
|
||||||
"strength": 0.4,
|
"strength": 0.4,
|
||||||
"active": false
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "114558v4df2fsdf5",
|
||||||
|
"strength": 0.6,
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "illustriousXL_stabilizer_v1.23",
|
||||||
|
"strength": 0.3,
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mon_monmon2133",
|
||||||
|
"strength": 0.5,
|
||||||
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "__dummy_item1__",
|
"name": "__dummy_item1__",
|
||||||
@@ -273,7 +287,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ck-neon-retrowave-IL-000012",
|
"name": "ck-neon-retrowave-IL-000012",
|
||||||
"strength": 0.8,
|
"strength": 0.8,
|
||||||
"active": true
|
"active": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "__dummy_item1__",
|
"name": "__dummy_item1__",
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ async function saveRecipeDirectly(widget) {
|
|||||||
try {
|
try {
|
||||||
// Get the workflow data from the ComfyUI app
|
// Get the workflow data from the ComfyUI app
|
||||||
const prompt = await app.graphToPrompt();
|
const prompt = await app.graphToPrompt();
|
||||||
console.log('Prompt:', prompt.output);
|
console.log('Prompt:', prompt);
|
||||||
|
|
||||||
// Show loading toast
|
// Show loading toast
|
||||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||||
|
|||||||
Reference in New Issue
Block a user