Files
numz-Comfyui-FlowChain/workflow.py
T
2025-03-28 08:38:31 +01:00

468 lines
22 KiB
Python

import json
import torch
import uuid
import copy
import os
from enum import Enum
import numpy as np
import hashlib
from torchvision import transforms
import comfy.model_management
from PIL import Image
from nodes import SaveImage
import gc
import folder_paths
from server import PromptServer
from execution import PromptExecutor
class ExecutionResult(Enum):
SUCCESS = 0
FAILURE = 1
PENDING = 2
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __eq__(self, _) -> bool:
return True
def __ne__(self, __value: object) -> bool:
return False
script_list_path = os.path.join(folder_paths.user_directory, "default", "workflows")
def recursive_delete(workflow, to_delete):
# workflow_copy = copy.deepcopy(workflow)
new_delete = []
for node_id in to_delete:
for node_id2, node in workflow.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == node_id:
new_delete.append(node_id2)
if node_id in workflow:
del workflow[node_id]
if len(new_delete) > 0:
workflow = recursive_delete(workflow, new_delete)
return workflow
class Workflow(SaveImage):
def __init__(self):
self.ws = None
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"workflows": ("COMBO", {"values": []}),
"workflow": ("STRING", {"default": ""})
},
"optional": {}
}
RETURN_TYPES = (
AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"),
AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"),
)
FUNCTION = "generate"
CATEGORY = "FlowChain ⛓️"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(s, workflows, workflow, **kwargs):
m = hashlib.sha256()
m.update(workflows.encode())
# Ajouter le contenu du workflow au hash pour détecter les changements de structure
if workflow:
workflow_data = json.loads(workflow)
# Extraire les nœuds de sortie avec leurs positions/types/connexions
outputs = {}
for k, v in workflow_data.items():
if v.get('class_type') == 'WorkflowOutput':
# Capturer le nom, type et la source de données (connexions entrantes)
output_info = {
'name': v['inputs']['Name'],
'type': v['inputs']['type'],
'position': v.get('_meta', {}).get('position', [0, 0]),
}
# Ajouter les connexions d'entrée pour tracer la provenance des données
for input_name, input_value in v['inputs'].items():
if isinstance(input_value, list) and len(input_value) > 0:
# Stocker les IDs des nœuds connectés à cette sortie
output_info[input_name + '_source'] = input_value
outputs[k] = output_info
# Être sûr de préserver l'ordre des sorties dans le hash
# en les triant par position verticale
sorted_outputs = dict(sorted(
outputs.items(),
key=lambda item: item[1].get('position', [0, 0])[1]
))
# Ajouter l'information des sorties au hash
m.update(json.dumps(sorted_outputs, sort_keys=True).encode())
return m.digest().hex()
def generate(self, workflows, workflow, **kwargs):
def populate_inputs(workflow, inputs, kwargs_values):
workflow_inputs = {k: v for k, v in workflow.items() if v["class_type"] == "WorkflowInput"}
for key, value in workflow_inputs.items():
if value["inputs"]["Name"] in inputs:
if type(inputs[value["inputs"]["Name"]]) == list:
if value["inputs"]["Name"] in kwargs_values:
workflow[key]["inputs"]["default"] = kwargs_values[value["inputs"]["Name"]]
else:
workflow[key]["inputs"]["default"] = inputs[value["inputs"]["Name"]]
workflow_inputs_images = {k: v for k, v in workflow.items() if
v["class_type"] == "WorkflowInput" and v["inputs"]["type"] == "IMAGE"}
for key, value in workflow_inputs_images.items():
if "default" not in value["inputs"]:
workflow[key]["inputs"]["default"] = torch.tensor([])
else:
if value["inputs"]["default"].numel() == 0:
workflow[key]["inputs"]["default"] = torch.tensor([])
return workflow
def treat_switch(workflow):
to_delete = []
#do_net_delete = []
switch_to_delete = [-1]
while len(switch_to_delete) > 0:
switch_nodes = {k: v for k, v in workflow.items() if
v["class_type"].startswith("Switch") and v["class_type"].endswith("[Crystools]")}
# order switch nodes by inputs.boolean value
switch_to_delete = []
switch_nodes_copy = copy.deepcopy(switch_nodes)
for switch_id, switch_node in switch_nodes.items():
# create list of inputs who have switch in their inputs
inputs_from_switch = []
for node_ids, node in workflow.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == switch_id:
inputs_from_switch.append({node_ids: input_name})
# convert to dictionary
inputs_from_switch = {k: v for d in inputs_from_switch for k, v in d.items()}
switch = switch_nodes_copy[switch_id]
for node_id, input_name in inputs_from_switch.items():
if type(switch["inputs"]["boolean"]) == list:
switch_boolean_value = workflow[switch["inputs"]["boolean"][0]]["inputs"]
other_input_name = None
if "default" in switch_boolean_value:
other_input_name = "default"
elif "boolean" in switch_boolean_value:
other_input_name = "boolean"
if other_input_name is not None:
if switch_boolean_value[other_input_name] == True:
if type(switch["inputs"]["on_true"]) == list:
workflow[node_id]["inputs"][input_name] = switch["inputs"]["on_true"]
if node_id in switch_nodes_copy:
switch_nodes_copy[node_id]["inputs"][input_name] = switch["inputs"]["on_true"]
else:
to_delete.append(node_id)
else:
if type(switch["inputs"]["on_false"]) == list:
workflow[node_id]["inputs"][input_name] = switch["inputs"]["on_false"]
if node_id in switch_nodes_copy:
switch_nodes_copy[node_id]["inputs"][input_name] = switch["inputs"]["on_false"]
else:
to_delete.append(node_id)
switch_to_delete.append(switch_id)
else:
if switch["inputs"]["boolean"] == True:
if type(switch["inputs"]["on_true"]) == list:
workflow[node_id]["inputs"][input_name] = switch["inputs"]["on_true"]
if node_id in switch_nodes_copy:
switch_nodes_copy[node_id]["inputs"][input_name] = switch["inputs"]["on_true"]
else:
to_delete.append(node_id)
else:
if type(switch["inputs"]["on_false"]) == list:
workflow[node_id]["inputs"][input_name] = switch["inputs"]["on_false"]
if node_id in switch_nodes_copy:
switch_nodes_copy[node_id]["inputs"][input_name] = switch["inputs"]["on_false"]
else:
to_delete.append(node_id)
switch_to_delete.append(switch_id)
print(switch_to_delete)
workflow = {k: v for k, v in workflow.items() if
not (v["class_type"].startswith("Switch") and v["class_type"].endswith(
"[Crystools]") and k in switch_to_delete)}
return workflow, to_delete
def treat_continue(workflow):
to_delete = []
continue_nodes = {k: v for k, v in workflow.items() if
v["class_type"].startswith("WorkflowContinue")}
do_net_delete = []
for continue_node_id, continue_node in continue_nodes.items():
for node_id, node in workflow.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == continue_node_id:
if type(continue_node["inputs"]["continue_workflow"]) == list:
input_other_node = \
workflow[continue_node["inputs"]["continue_workflow"][0]][
"inputs"]
other_input_name = None
if "default" in input_other_node:
other_input_name = "default"
elif "boolean" in input_other_node:
other_input_name = "boolean"
if other_input_name is not None:
if input_other_node[other_input_name]:
workflow[node_id]["inputs"][input_name] = continue_node["inputs"]["input"]
else:
to_delete.append(node_id)
else:
do_net_delete.append(continue_node_id)
else:
if continue_node["inputs"]["continue_workflow"]:
workflow[node_id]["inputs"][input_name] = continue_node["inputs"]["input"]
else:
to_delete.append(node_id)
workflow = {k: v for k, v in workflow.items() if
not (v["class_type"].startswith("WorkflowContinue") and k not in do_net_delete)}
return workflow, to_delete
def redefine_id(subworkflow, max_id):
new_sub_workflow = {}
for k, v in subworkflow.items():
max_id += 1
new_sub_workflow[str(max_id)] = v
# replace old id by new id items in inputs of workflow
for node_id, node in subworkflow.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == k:
subworkflow[node_id]["inputs"][input_name][0] = str(max_id)
for node_id, node in new_sub_workflow.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == k:
new_sub_workflow[node_id]["inputs"][input_name][0] = str(max_id)
return new_sub_workflow, max_id
def change_subnode(subworkflow, node_id_to_find, value):
for node_id, node in subworkflow.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == node_id_to_find:
subworkflow[node_id]["inputs"][input_name] = value
return subworkflow
def merge_inputs_outputs(workflow, workflow_name, subworkflow, workflow_outputs):
# get max workflow id
# coinvert workflow_outputs to list
workflow_outputs = list(workflow_outputs.values())
# prendre le premier workflow
workflow_node = [{"id":k, **v} for k, v in workflow.items() if v["class_type"] == "Workflow" and v["inputs"]["workflows"] == workflow_name][0]
sub_input_nodes = {k: v for k, v in subworkflow.items() if v["class_type"] == "WorkflowInput"}
do_not_delete = []
for sub_id, sub_node in sub_input_nodes.items():
if sub_node["inputs"]["Name"] in workflow_node["inputs"]:
value = workflow_node["inputs"][sub_node["inputs"]["Name"]]
if type(value) == list:
subworkflow = change_subnode(subworkflow, sub_id, value)
else:
subworkflow[sub_id]["inputs"]["default"] = value
do_not_delete.append(sub_id)
# remove input node
subworkflow = {k: v for k, v in subworkflow.items() if not (v["class_type"] == "WorkflowInput" and k not in do_not_delete)}
sub_output_nodes = {k: v for k, v in subworkflow.items() if v["class_type"] == "WorkflowOutput"}
workflow_copy = copy.deepcopy(workflow)
for node_id, node in workflow_copy.items():
for input_name, input_value in node["inputs"].items():
if type(input_value) == list:
if len(input_value) > 0:
if input_value[0] == workflow_node["id"]:
for sub_output_id, sub_output_node in sub_output_nodes.items():
if sub_output_node["inputs"]["Name"] == workflow_outputs[input_value[1]]["inputs"]["Name"]:
workflow[node_id]["inputs"][input_name] = sub_output_node["inputs"]["default"]
# remove output node
subworkflow = {k: v for k, v in subworkflow.items() if not (v["class_type"] == "WorkflowOutput")}
return workflow, subworkflow
def clean_workflow(workflow, inputs=None, kwargs_values=None):
if kwargs_values is None:
kwargs_values = {}
if inputs is None:
inputs = {}
if inputs is not None:
workflow = populate_inputs(workflow, inputs, kwargs_values)
workflow_outputs = {k: v for k, v in workflow.items() if v["class_type"] == "WorkflowOutput"}
for output_id, output_node in workflow_outputs.items():
workflow[output_id]["inputs"]["ui"] = False
workflow, switch_to_delete = treat_switch(workflow)
workflow, continue_to_delete = treat_continue(workflow)
workflow = recursive_delete(workflow, switch_to_delete + continue_to_delete)
return workflow, workflow_outputs
def get_recursive_workflow(workflow_name, workflows, max_id=0):
# if workflows[-5:] == ".json":
# workflow = get_workflow(workflows)
# else:
try:
if workflows == "{}":
raise ValueError("Empty workflow.")
workflow = json.loads(workflows)
except:
raise RuntimeError(f"Error while loading workflow: {workflow_name}, See <a href='https://github.com/numz/Comfyui-FlowChain'> for more information.")
workflow, max_id = redefine_id(workflow, max_id)
sub_workflows = {k: v for k, v in workflow.items() if v["class_type"] == "Workflow"}
for key, sub_workflow_node in sub_workflows.items():
workflow_json = sub_workflow_node["inputs"]["workflow"]
workflow_name = sub_workflow_node["inputs"]["workflows"]
subworkflow, max_id = get_recursive_workflow(workflow_name, workflow_json, max_id)
workflow_outputs_sub = {k: v for k, v in subworkflow.items() if v["class_type"] == "WorkflowOutput"}
workflow, subworkflow = merge_inputs_outputs(workflow, workflow_name, subworkflow, workflow_outputs_sub)
workflow = {k: v for k, v in workflow.items() if k != key}
# add subworkflow to workflow
workflow.update(subworkflow)
return workflow, max_id
server_instance = PromptServer.instance
client_id = server_instance.client_id
if server_instance and hasattr(server_instance, 'prompt_queue'):
current_queue = server_instance.prompt_queue.get_current_queue()
queue_info = {
'queue_running': current_queue[0],
'queue_pending': current_queue[1]
}
# Now you can access the original inputs as before
queue_to_use = queue_info["queue_running"]
original_inputs = [v["inputs"] for k, v in queue_to_use[0][2].items() if
"workflows" in v["inputs"] and v["inputs"]["workflows"] == workflows][0]
else:
# Fallback to empty inputs if server instance not available
original_inputs = {}
workflow, _ = get_recursive_workflow(workflows, workflow, 5000)
workflow, workflow_outputs = clean_workflow(workflow, original_inputs, kwargs)
# Accéder au fichier JSON original pour obtenir les positions correctes
workflow_file_path = os.path.join(folder_paths.user_directory, "default", "workflows", workflows)
original_positions = {}
# Récupérer les positions des noeuds de sortie depuis le fichier original
if os.path.exists(workflow_file_path):
try:
with open(workflow_file_path, "r", encoding="utf-8") as f:
original_workflow = json.load(f)
# Créer un mapping node_id -> position pour les noeuds WorkflowOutput
if "nodes" in original_workflow:
for node in original_workflow["nodes"]:
if node.get("type") == "WorkflowOutput":
node_id = str(node.get("id", "unknown"))
pos_y = node.get("pos", [0, 0])[1]
node_name = node.get("widgets_values", "")["Name"]["value"]
original_positions[node_name] = pos_y
except Exception as e:
print(f"Erreur lors de la lecture du fichier workflow original: {str(e)}")
# Récupérer les nœuds de sortie et les trier par position Y
workflow_outputs_with_position = []
for k, v in workflow_outputs.items():
output_name = v["inputs"]["Name"]
# Utiliser la position du fichier original si disponible, sinon utiliser une position par défaut
y_position = original_positions.get(output_name, 999999)
workflow_outputs_with_position.append((k, y_position))
# Trier par position Y croissante
workflow_outputs_with_position.sort(key=lambda x: x[1])
# Extraire seulement les IDs dans l'ordre trié
workflow_outputs_id = [k for k, _ in workflow_outputs_with_position]
prompt_id = str(uuid.uuid4())
class SimpleServer:
def __init__(self):
self.client_id = client_id
self.last_node_id = None
self.last_prompt_id = prompt_id
def send_sync(self, *args, **kwargs):
pass # No-op implementation
simple_server = SimpleServer()
executor = PromptExecutor(simple_server)
executor.execute(workflow, prompt_id, {"client_id": client_id}, workflow_outputs_id)
history_result = executor.history_result
comfy.model_management.unload_all_models()
gc.collect()
# Remplacer la boucle de génération d'output qui ne respecte pas l'ordre
output = []
for id_node in workflow_outputs_id: # Utiliser l'ordre trié des IDs
if id_node in history_result["outputs"]:
result_value = history_result["outputs"][id_node]["default"]
# Apply formatting based on the expected output type
output.append(result_value[0])
else:
node = workflow_outputs[id_node] # Récupérer le nœud correspondant à l'ID
if node["inputs"]["type"] == "IMAGE" or node["inputs"]["type"] == "MASK":
black_image_np = np.zeros((255, 255, 3), dtype=np.uint8)
black_image_pil = Image.fromarray(black_image_np)
transform = transforms.ToTensor()
image_tensor = transform(black_image_pil)
image_tensor = image_tensor.permute(1, 2, 0)
image_tensor = image_tensor.unsqueeze(0)
output.append(image_tensor)
else:
output.append(None)
return tuple(output)
# return tuple(queue[uid]["outputs"])
NODE_CLASS_MAPPINGS_WORKFLOW = {
"Workflow": Workflow,
}
NODE_DISPLAY_NAME_MAPPINGS_WORKFLOW = {
"Workflow": "Workflow (FlowChain ⛓️)",
}