review all process

This commit is contained in:
NumZ
2025-03-23 20:50:19 +01:00
parent 0761017a6c
commit 70750a3f27
8 changed files with 1195 additions and 927 deletions
+1 -1
View File
@@ -12,6 +12,7 @@ NODE_CLASS_MAPPINGS.update(NODE_CLASS_MAPPINGS_WORKFLOW)
NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_NODES)
NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_WORKFLOW)
def get_ext_dir(subpath=None, mkdir=False):
dir = os.path.dirname(__file__)
if subpath is not None:
@@ -43,4 +44,3 @@ for file in files:
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
WEB_DIRECTORY = "./web"
View File
+283 -139
View File
@@ -1,45 +1,82 @@
import sys
import uuid
import server
from aiohttp import web
import shutil
import os
import subprocess
import json
import urllib.request
import copy
import folder_paths
from app.user_manager import UserManager
import multiprocessing as mp
import time
import queue
from multiprocessing import Process, Queue
import websocket
from nacl import hashlib
client_id = '5b49a023-b05a-4c53-8dc9-addc3a749911'
server_address = "127.0.0.1:8188"
@server.PromptServer.instance.routes.get("/flowchain/workflows")
async def workflows(request):
user = UserManager().get_request_user_id(request)
json_path = folder_paths.user_directory + "/" + user + "/workflows/api/"
json_path = folder_paths.user_directory + "/" + user + "/workflows/"
result = {}
# Vérifier si le répertoire principal existe
if os.path.exists(json_path):
files = os.listdir(json_path)
for idx, file in enumerate(files):
with open(json_path + file, "r", encoding="utf-8") as f:
json_content = json.load(f)
nodes_input = {k: v for k, v in json_content.items() if v["class_type"] == "WorkflowInput"}
nodes_output = {k: v for k, v in json_content.items() if v["class_type"] == "WorkflowOutput"}
result[file] = {"inputs": nodes_input, "outputs": nodes_output}
# Utiliser os.walk pour parcourir récursivement tous les sous-répertoires
for root, dirs, files in os.walk(json_path):
for file in files:
# Ne traiter que les fichiers JSON
if file.lower().endswith('.json'):
file_path = os.path.join(root, file)
try:
with open(file_path, "r", encoding="utf-8") as f:
json_content = json.load(f)
nodes_input = {}
nodes_output = {}
# Vérifier le format du workflow (API ou standard)
if "nodes" in json_content:
# Format standard (non-API)
is_standard_format = True
# Extraire les nœuds WorkflowInput et WorkflowOutput
for node in json_content["nodes"]:
if node.get("type") == "WorkflowInput":
# Convertir au format compatible pour le client
node_id = str(node.get("id", "unknown"))
nodes_input[node_id] = {
"class_type": "WorkflowInput",
"inputs": node.get("widgets_values", {})
}
elif node.get("type") == "WorkflowOutput":
node_id = str(node.get("id", "unknown"))
nodes_output[node_id] = {
"class_type": "WorkflowOutput",
"inputs": node.get("widgets_values", {})
}
else:
# Format API
nodes_input = {k: v for k, v in json_content.items() if
v.get("class_type") == "WorkflowInput"}
nodes_output = {k: v for k, v in json_content.items() if
v.get("class_type") == "WorkflowOutput"}
# Ajouter au résultat seulement si le fichier contient des nœuds WorkflowInput ou WorkflowOutput
if nodes_input or nodes_output:
# Créer une clé unique basée sur le chemin relatif
relative_path = os.path.relpath(file_path, json_path)
result[relative_path] = {"inputs": nodes_input,
"outputs": nodes_output,
'workflow': json_content}
except json.JSONDecodeError:
# Ignorer les fichiers JSON mal formés
print(f"Ignoring malformed JSON file: {file_path}")
except Exception as e:
print(f"Error processing {file_path}: {str(e)}")
else:
# Créer le répertoire s'il n'existe pas
os.makedirs(json_path)
result["No file in worflows/api folder"] = {"inputs": {}, "outputs": {}}
if result == {}:
result["No file in worflows/api folder"] = {"inputs": {}, "outputs": {}}
result["No file in worflows folder"] = {"inputs": {}, "outputs": {}}
# Si aucun fichier valide n'a été trouvé
if not result:
result["No compatible workflow files found"] = {"inputs": {}, "outputs": {}}
return web.json_response(result, content_type='application/json')
@@ -48,21 +85,29 @@ async def workflow(request):
user = UserManager().get_request_user_id(request)
original_path = request.query.get("workflow_path")
json_path = original_path.replace("\\", "/").split("/")
if ".json" in json_path[0]:
file_name = json_path[0]
json_path = folder_paths.user_directory + "/" + user + "/workflows/api/" + file_name
else:
unversal_path = original_path.replace("\\", "/")
json_path = unversal_path.split("/")
if ".json" in json_path[-1]:
file_name = json_path[-1]
json_path = folder_paths.user_directory + "/" + user + "/workflows/api/" + file_name
if os.path.exists(original_path):
shutil.copy(original_path, json_path)
json_path = folder_paths.user_directory + "/" + user + "/workflows/" + unversal_path
if os.path.exists(json_path):
with open(json_path, "r", encoding="utf-8") as f:
json_content = json.load(f)
err = "none"
"""
if "nodes" in json_content:
err = "Not a Json API format workflow"
try:
api_workflow = convert_standard_to_api_format(json_content)
if api_workflow:
json_content = api_workflow
# Conservons la référence au workflow original si nécessaire
# json_content["_original_standard_format"] = True
else:
err = "Échec de la conversion du workflow standard en format API"
except Exception as e:
err = f"Erreur lors de la conversion: {str(e)}"
"""
result = {"error": err, "workflow": json_content, "file_name": file_name}
else:
result = {"error": "File not found"}
@@ -70,111 +115,210 @@ async def workflow(request):
return web.json_response(result, content_type='application/json')
"""
def generate(workflow_path, kwargs):
workflow = json.load(open(workflow_path, "r", encoding="utf-8"))
outputs = get_outputs(workflow)
workflow_optimized = copy.deepcopy(workflow)
for idx, field in enumerate(kwargs):
for node_id, node in workflow.items():
if "input_" + field["name"] in node["_meta"]["title"]:
# get first key of workflow[node_id]["inputs"]
key = list(workflow[node_id]["inputs"].keys())[0]
workflow[node_id]["inputs"][key] = field["value"]
boolean_values = []
for node_id, node in workflow.items():
if "boolean" in node["inputs"] and "input_" in node["_meta"]["title"]:
boolean_values.append((node_id, node["inputs"]["boolean"]))
for node_id, active in boolean_values:
for node_id2, value2 in workflow_optimized.items():
if "boolean" in value2["inputs"] and (
"on_true" in value2["inputs"] or "on_false" in value2["inputs"]):
if node_id2 in workflow:
if workflow[node_id2]["inputs"]["boolean"] == [node_id, 0]:
input_to_replace = None
if active:
if "on_true" in value2["inputs"]:
input_to_replace = workflow[node_id2]["inputs"]["on_true"]
else:
if "on_false" in value2["inputs"]:
input_to_replace = workflow[node_id2]["inputs"]["on_false"]
worflow_value_to_change = []
for key3, value3 in workflow.items():
for k, v in value3["inputs"].items():
if v == [node_id2, 0]:
worflow_value_to_change.append((key3, k))
# workflow[key3]["inputs"][k] = input_to_replace
for key3, k in worflow_value_to_change:
if input_to_replace:
workflow[key3]["inputs"][k] = input_to_replace
else:
del workflow[key3]["inputs"][k]
del workflow[node_id2]
boolean_values = []
for node_id, value in workflow.items():
if value["class_type"] == "Continue Workflow":
boolean_values.append((node_id, value["inputs"]["boolean"], value["inputs"]["line"]))
for node_id, active, line in boolean_values:
for node_id2, value2 in workflow_optimized.items():
worflow_value_to_change = []
for inp, val in value2["inputs"].items():
if val == [node_id, 0]:
if type(active) == list:
continue_workflow = workflow_optimized[active[0]]['inputs']['boolean']
def convert_standard_to_api_format(standard_workflow):
"""
Convertit un workflow au format standard en format API de manière similaire à graphToPrompt
"""
# Vérifier si c'est un workflow valide
if not standard_workflow or "nodes" not in standard_workflow or not isinstance(standard_workflow["nodes"], list):
print("Format de workflow invalide")
return None
# Créer une structure pour retrouver les nœuds par ID
nodes_by_id = {node["id"]: node for node in standard_workflow["nodes"]}
# Créer une map des liens
links_by_id = {}
if "links" in standard_workflow and isinstance(standard_workflow["links"], list):
for link in standard_workflow["links"]:
# Format du lien: [id, origin_node, origin_slot, target_node, target_slot, type]
if len(link) >= 5:
links_by_id[link[0]] = {
"id": link[0],
"origin_id": link[1],
"origin_slot": link[2],
"target_id": link[3],
"target_slot": link[4],
"type": link[5] if len(link) > 5 else None
}
# Map des liens par cible (nœud + slot)
links_by_target = {}
for link_id, link in links_by_id.items():
key = f"{link['target_id']}-{link['target_slot']}"
links_by_target[key] = link
# Résultat au format API
api_format = {}
# Obtenir un ordre d'exécution approximatif (simplifié)
# Remarque: c'est une simplification, le vrai ordre d'exécution est plus complexe
execution_order = simulate_execution_order(standard_workflow)
# Traiter les nœuds selon l'ordre d'exécution simulé
for node_id in execution_order:
node = nodes_by_id.get(node_id)
if not node:
continue
# Ignorer les nœuds désactivés (mode NEVER ou BYPASS) - similaire à LGraphEventMode
# Le mode 2 correspond généralement à NEVER, le mode 3 à BYPASS dans LiteGraph
if node.get("mode") in [2, 4]:
continue
# Initialiser le nœud au format API
api_node = {
"inputs": {},
"class_type": node.get("type", ""),
"_meta": {
"title": node.get("title", node.get("type", ""))
}
}
# Traiter les widgets (valeurs des paramètres)
if "widgets_values" in node:
# Si c'est un tableau (ancien format)
if isinstance(node["widgets_values"], list):
# Associer chaque valeur à son nom si disponible
if "widgets" in node and isinstance(node["widgets"], list):
for i, widget in enumerate(node["widgets"]):
if i < len(node["widgets_values"]) and "name" in widget:
api_node["inputs"][widget["name"]] = node["widgets_values"][i]
# Si c'est un objet (format plus récent)
elif isinstance(node["widgets_values"], dict):
for name, value in node["widgets_values"].items():
# Si la valeur a une structure spécifique avec une propriété "value"
if isinstance(value, dict) and "value" in value:
api_node["inputs"][name] = value["value"]
else:
continue_workflow = active
if continue_workflow:
worflow_value_to_change.append((node_id2, inp, line))
else:
worflow_value_to_change.append((node_id2, inp, None))
for key3, k, line2 in worflow_value_to_change:
if line2:
workflow[key3]["inputs"][k] = line2
else:
del workflow[key3]["inputs"][k]
queue_prompt(workflow, outputs)
return True
def get_history(prompt_id):
with urllib.request.urlopen("http://{}/history/{}".format(server_address, prompt_id)) as response:
return json.loads(response.read())
api_node["inputs"][name] = value
# Traiter les connexions d'entrée
if "inputs" in node and isinstance(node["inputs"], list):
for slot_index, input_data in enumerate(node["inputs"]):
if not input_data:
continue
input_name = input_data.get("name", f"input_{slot_index}")
# Vérifier si cette entrée a un lien
if "link" in input_data and input_data["link"] is not None:
link = links_by_id.get(input_data["link"])
if link:
# Suivre les redirections (nœuds en bypass)
origin_id, origin_slot = trace_link_through_bypasses(
link["origin_id"],
link["origin_slot"],
nodes_by_id,
links_by_id
)
if origin_id is not None:
# Format API pour une connexion: [node_id, output_slot]
api_node["inputs"][input_name] = [str(origin_id), origin_slot]
# Ajouter le nœud au format API
api_format[str(node_id)] = api_node
# Nettoyer les liens qui pointent vers des nœuds inexistants
for node_id, node_data in api_format.items():
inputs_to_remove = []
for input_name, input_value in node_data["inputs"].items():
if isinstance(input_value, list) and len(input_value) == 2:
source_node_id = input_value[0]
if source_node_id not in api_format:
inputs_to_remove.append(input_name)
for input_name in inputs_to_remove:
del node_data["inputs"][input_name]
return api_format
def get_outputs(workflow):
output_images_path = []
for node_id, node in workflow.items():
if "output_" in node["_meta"]["title"]:
output_images_path.append(node["_meta"]["title"])
return output_images_path
def simulate_execution_order(workflow):
"""
Simule un ordre d'exécution approximatif des nœuds.
C'est une simplification de graph.computeExecutionOrder().
"""
# Fonction simplifiée - dans un vrai graphe, cela serait plus complexe
# en impliquant une analyse topologique du graphe de dépendances
# On construit un graphe de dépendances
nodes_by_id = {node["id"]: node for node in workflow["nodes"]}
dependencies = {node["id"]: set() for node in workflow["nodes"]}
# Trouver les dépendances de chaque nœud
if "links" in workflow and isinstance(workflow["links"], list):
for link in workflow["links"]:
if len(link) >= 5:
target_id = link[3] # Le nœud qui reçoit la connexion
source_id = link[1] # Le nœud d'origine
if target_id in dependencies:
dependencies[target_id].add(source_id)
# Tri topologique simplifié
visited = set()
result = []
def visit(node_id):
if node_id in visited:
return
visited.add(node_id)
for dep_id in dependencies[node_id]:
if dep_id in dependencies: # S'assurer que le nœud existe
visit(dep_id)
result.append(node_id)
# Visiter tous les nœuds
for node_id in dependencies:
if node_id not in visited:
visit(node_id)
return result
def queue_prompt(prompt, outputs):
root_folder = os.path.dirname(__file__)
if not os.path.exists(root_folder + "/../queue"):
os.makedirs(root_folder + "/../queue")
queues = {}
if os.path.exists(root_folder + "/../queue/queue.json"):
queues = json.loads(open(root_folder + "/../queue/queue.json", "r", encoding="utf-8").read())
uid = str(uuid.uuid4())
queues[uid] = {"prompt": prompt, "client_id": client_id, "output_fields": outputs, "status": {"completed": "false"}}
print(uid)
with open(root_folder + "/../queue/queue.json", "w", encoding="utf-8") as f:
json.dump(queues, f)
time.sleep(0.5)
commands = [sys.executable, root_folder + "/../queue/queue.py", uid]
try:
subprocess.Popen(commands, stderr=subprocess.PIPE)
return True
except subprocess.CalledProcessError as exception:
print(exception.stderr.decode().strip(), __name__.upper())
return False
"""
def trace_link_through_bypasses(origin_id, origin_slot, nodes_by_id, links_by_id):
"""
Suit une chaîne de nœuds en mode BYPASS pour trouver la source réelle.
Simule le comportement de la boucle while dans graphToPrompt.
"""
current_id = origin_id
current_slot = origin_slot
# Maximum d'itérations pour éviter les boucles infinies
max_iterations = 100
iterations = 0
while iterations < max_iterations:
iterations += 1
parent = nodes_by_id.get(current_id)
if not parent:
break
# Si le nœud parent n'est pas en mode BYPASS, on a trouvé la source
if parent.get("mode") != 3: # 3 est généralement le mode BYPASS
return current_id, current_slot
# Chercher le lien d'entrée correspondant pour continuer à remonter
if "inputs" in parent and isinstance(parent["inputs"], list):
for i, input_data in enumerate(parent["inputs"]):
if not input_data or "link" not in input_data:
continue
# Vérifier si le type correspond (simplifié)
# Dans le vrai code, il y a une vérification plus complexe des types
link = links_by_id.get(input_data["link"])
if link:
# Mise à jour pour la prochaine itération
current_id = link["origin_id"]
current_slot = link["origin_slot"]
break
else:
# Aucun lien compatible trouvé
break
# Si on arrive ici, soit on a atteint le maximum d'itérations,
# soit on n'a pas trouvé de chemin valide
return origin_id, origin_slot
View File
+209 -33
View File
@@ -1,39 +1,6 @@
import os
import shutil
import subprocess
from .caching import HierarchicalCache, LRUCache, CacheKeySetInputSignature, CacheKeySetID
class CacheSet:
def __init__(self, lru_size=None):
if lru_size is None or lru_size == 0:
self.init_classic_cache()
else:
self.init_lru_cache(lru_size)
self.all = [self.outputs, self.ui, self.objects]
# Useful for those with ample RAM/VRAM -- allows experimenting without
# blowing away the cache every time
def init_lru_cache(self, cache_size):
self.outputs = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
self.ui = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
self.objects = HierarchicalCache(CacheKeySetID)
# Performs like the old cache -- dump data ASAP
def init_classic_cache(self):
self.outputs = HierarchicalCache(CacheKeySetInputSignature)
self.ui = HierarchicalCache(CacheKeySetInputSignature)
self.objects = HierarchicalCache(CacheKeySetID)
def recursive_debug_dump(self):
result = {
"outputs": self.outputs.recursive_debug_dump(),
"ui": self.ui.recursive_debug_dump(),
}
return result
caches = CacheSet(None)
def ffmpeg_suitability(path):
@@ -90,3 +57,212 @@ else:
ffmpeg_path = ffmpeg_paths[0]
else:
ffmpeg_path = max(ffmpeg_paths, key=ffmpeg_suitability)
def convert_standard_to_api_format(standard_workflow):
"""
Convertit un workflow au format standard en format API de manière similaire à graphToPrompt
"""
# Vérifier si c'est un workflow valide
if not standard_workflow or "nodes" not in standard_workflow or not isinstance(standard_workflow["nodes"], list):
print("Format de workflow invalide")
return None
# Créer une structure pour retrouver les nœuds par ID
nodes_by_id = {node["id"]: node for node in standard_workflow["nodes"]}
# Créer une map des liens
links_by_id = {}
if "links" in standard_workflow and isinstance(standard_workflow["links"], list):
for link in standard_workflow["links"]:
# Format du lien: [id, origin_node, origin_slot, target_node, target_slot, type]
if len(link) >= 5:
links_by_id[link[0]] = {
"id": link[0],
"origin_id": link[1],
"origin_slot": link[2],
"target_id": link[3],
"target_slot": link[4],
"type": link[5] if len(link) > 5 else None
}
# Map des liens par cible (nœud + slot)
links_by_target = {}
for link_id, link in links_by_id.items():
key = f"{link['target_id']}-{link['target_slot']}"
links_by_target[key] = link
# Résultat au format API
api_format = {}
# Obtenir un ordre d'exécution approximatif (simplifié)
# Remarque: c'est une simplification, le vrai ordre d'exécution est plus complexe
execution_order = simulate_execution_order(standard_workflow)
# Traiter les nœuds selon l'ordre d'exécution simulé
for node_id in execution_order:
node = nodes_by_id.get(node_id)
if not node:
continue
# Ignorer les nœuds désactivés (mode NEVER ou BYPASS) - similaire à LGraphEventMode
# Le mode 2 correspond généralement à NEVER, le mode 3 à BYPASS dans LiteGraph
if node.get("mode") in [2, 3]:
continue
# Initialiser le nœud au format API
api_node = {
"inputs": {},
"class_type": node.get("type", ""),
"_meta": {
"title": node.get("title", node.get("type", ""))
}
}
# Traiter les widgets (valeurs des paramètres)
if "widgets_values" in node:
# Si c'est un tableau (ancien format)
if isinstance(node["widgets_values"], list):
# Associer chaque valeur à son nom si disponible
if "widgets" in node and isinstance(node["widgets"], list):
for i, widget in enumerate(node["widgets"]):
if i < len(node["widgets_values"]) and "name" in widget:
api_node["inputs"][widget["name"]] = node["widgets_values"][i]
# Si c'est un objet (format plus récent)
elif isinstance(node["widgets_values"], dict):
for name, value in node["widgets_values"].items():
# Si la valeur a une structure spécifique avec une propriété "value"
if isinstance(value, dict) and "value" in value:
api_node["inputs"][name] = value["value"]
else:
api_node["inputs"][name] = value
# Traiter les connexions d'entrée
if "inputs" in node and isinstance(node["inputs"], list):
for slot_index, input_data in enumerate(node["inputs"]):
if not input_data:
continue
input_name = input_data.get("name", f"input_{slot_index}")
# Vérifier si cette entrée a un lien
if "link" in input_data and input_data["link"] is not None:
link = links_by_id.get(input_data["link"])
if link:
# Suivre les redirections (nœuds en bypass)
origin_id, origin_slot = trace_link_through_bypasses(
link["origin_id"],
link["origin_slot"],
nodes_by_id,
links_by_id
)
if origin_id is not None:
# Format API pour une connexion: [node_id, output_slot]
api_node["inputs"][input_name] = [str(origin_id), origin_slot]
# Ajouter le nœud au format API
api_format[str(node_id)] = api_node
# Nettoyer les liens qui pointent vers des nœuds inexistants
for node_id, node_data in api_format.items():
inputs_to_remove = []
for input_name, input_value in node_data["inputs"].items():
if isinstance(input_value, list) and len(input_value) == 2:
source_node_id = input_value[0]
if source_node_id not in api_format:
inputs_to_remove.append(input_name)
for input_name in inputs_to_remove:
del node_data["inputs"][input_name]
return api_format
def simulate_execution_order(workflow):
"""
Simule un ordre d'exécution approximatif des nœuds.
C'est une simplification de graph.computeExecutionOrder().
"""
# Fonction simplifiée - dans un vrai graphe, cela serait plus complexe
# en impliquant une analyse topologique du graphe de dépendances
# On construit un graphe de dépendances
nodes_by_id = {node["id"]: node for node in workflow["nodes"]}
dependencies = {node["id"]: set() for node in workflow["nodes"]}
# Trouver les dépendances de chaque nœud
if "links" in workflow and isinstance(workflow["links"], list):
for link in workflow["links"]:
if len(link) >= 5:
target_id = link[3] # Le nœud qui reçoit la connexion
source_id = link[1] # Le nœud d'origine
if target_id in dependencies:
dependencies[target_id].add(source_id)
# Tri topologique simplifié
visited = set()
result = []
def visit(node_id):
if node_id in visited:
return
visited.add(node_id)
for dep_id in dependencies[node_id]:
if dep_id in dependencies: # S'assurer que le nœud existe
visit(dep_id)
result.append(node_id)
# Visiter tous les nœuds
for node_id in dependencies:
if node_id not in visited:
visit(node_id)
return result
def trace_link_through_bypasses(origin_id, origin_slot, nodes_by_id, links_by_id):
"""
Suit une chaîne de nœuds en mode BYPASS pour trouver la source réelle.
Simule le comportement de la boucle while dans graphToPrompt.
"""
current_id = origin_id
current_slot = origin_slot
# Maximum d'itérations pour éviter les boucles infinies
max_iterations = 100
iterations = 0
while iterations < max_iterations:
iterations += 1
parent = nodes_by_id.get(current_id)
if not parent:
break
# Si le nœud parent n'est pas en mode BYPASS, on a trouvé la source
if parent.get("mode") != 3: # 3 est généralement le mode BYPASS
return current_id, current_slot
# Chercher le lien d'entrée correspondant pour continuer à remonter
if "inputs" in parent and isinstance(parent["inputs"], list):
for i, input_data in enumerate(parent["inputs"]):
if not input_data or "link" not in input_data:
continue
# Vérifier si le type correspond (simplifié)
# Dans le vrai code, il y a une vérification plus complexe des types
link = links_by_id.get(input_data["link"])
if link:
# Mise à jour pour la prochaine itération
current_id = link["origin_id"]
current_slot = link["origin_slot"]
break
else:
# Aucun lien compatible trouvé
break
# Si on arrive ici, soit on a atteint le maximum d'itérations,
# soit on n'a pas trouvé de chemin valide
return origin_id, origin_slot
+584 -174
View File
@@ -1,7 +1,6 @@
import { app } from "../../../scripts/app.js";
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
const client_id = '5b49a023-b05a-4c53-8dc9-addc3a749911'
const colors = ["#222222", "#5940bb", "#FFFFFF", "#7cbb1a", "#29699c", "#777788", "#268bd2", "#2ab7ca", "#d33682", "#dc322f", "#facfad","#77ff77", "#5940bb"]
const bg_colors = ["#000000", "#392978", "#89888d", "#496c12", "#19466a", "#4b4b56", "#165481", "#176974", "#851f50", "#911e1c", "#9f826b", "#499f49", "#392978"]
@@ -274,25 +273,39 @@ function addLoadVideoCommon(nodeType, nodeData) {
addVideoPreview(nodeType);
addPreviewOptions(nodeType);
}
function cleanInputs(root_obj, reset_value=true) {
if (!root_obj.inputs) {
root_obj.inputs = [];
function cleanInputs(root_obj, nodeData, reset_value=true) {
//nodeData.input.required = {};
/*
if (!root_obj.inputs) root_obj.inputs = [];
if (!root_obj.outputs) root_obj.outputs = [];
if (!root_obj.widgets) root_obj.widgets = [];
if (!root_obj.widgets_values) root_obj.widgets_values = [];
*/
// Disconnect all links first
for (let i = 0; i < root_obj.outputs.length; i++) {
const output = root_obj.outputs[i];
if (output.links && output.links.length) {
// Make a copy of the links array because it will be modified during disconnection
const links = output.links.slice();
for (const linkId of links) {
root_obj.graph.removeLink(linkId);
}
}
}
if (!root_obj.outputs) {
root_obj.outputs = [];
}
if (!root_obj.widgets) {
root_obj.widgets = [];
//root_obj.widgets_values = [];
}
if (!root_obj.widgets_values) {
root_obj.widgets_values = [];
// Same for inputs
for (let i = 0; i < root_obj.inputs.length; i++) {
const input = root_obj.inputs[i];
if (input.link) {
root_obj.graph.removeLink(input.link);
}
}
root_obj.widgets = root_obj.widgets.splice(0,3)
root_obj.widgets = root_obj.widgets.splice(0,2)
if(reset_value){
for (let key in root_obj.widgets_values) {
if (key != "workflows" && key != "workflow_api_path" && key != "Import Workflow"){
if (key != "workflows"){
delete root_obj.widgets_values[key];
}
}
@@ -304,20 +317,37 @@ function cleanInputs(root_obj, reset_value=true) {
const max_node_input = root_obj.inputs.length;
for(let i = 0; i<max_node_input; i++)
root_obj.removeInput(0)
/*
if (root_obj.graph) {
root_obj.graph.setDirtyCanvas(true);
root_obj.graph.change();
}*/
}
function clearInputs(root_obj, reset_value=true) {
if (!root_obj.inputs) {
root_obj.inputs = [];
//nodeData.input.required = {};
if (!root_obj.inputs) root_obj.inputs = [];
if (!root_obj.outputs) root_obj.outputs = [];
if (!root_obj.widgets) root_obj.widgets = [];
if (!root_obj.widgets_values) root_obj.widgets_values = [];
// Déconnecter tous les liens d'abord pour les entrées
for (let i = 0; i < root_obj.outputs.length; i++) {
const output = root_obj.outputs[i];
if (output.links && output.links.length) {
const links = output.links.slice();
for (const linkId of links) {
root_obj.graph.removeLink(linkId);
}
}
}
if (!root_obj.outputs) {
root_obj.outputs = [];
}
if (!root_obj.widgets) {
root_obj.widgets = [];
}
if (!root_obj.widgets_values) {
root_obj.widgets_values = [];
// Parcourir les entrées en sens inverse pour éviter les problèmes d'indice
for (let i = root_obj.inputs.length - 1; i >= 0; i--) {
const input = root_obj.inputs[i];
if ((input.name === "default" || input.name === "input") && input.link) {
root_obj.graph.removeLink(input.link);
}
}
for(let i = 0; i<root_obj.inputs.length; i++){
@@ -336,11 +366,16 @@ function clearInputs(root_obj, reset_value=true) {
}
const max_node_input = root_obj.outputs.length;
for(let i = 0; i<max_node_input; i++)
root_obj.removeOutput(0)
for(let i = root_obj.outputs.length - 1; i >= 0; i--)
root_obj.removeOutput(i);
/*
if (root_obj.graph) {
root_obj.graph.setDirtyCanvas(true);
root_obj.graph.change();
}*/
}
function addWidgetType(root_obj, value){
function addWidgetType(root_obj, value, nodeData){
if (!root_obj.widgets) {
root_obj.widgets = [];
}
@@ -351,75 +386,229 @@ function addWidgetType(root_obj, value){
const type = value.type
if (type == "IMAGE" && root_obj.inputs.filter(i => i.name === field_name).length == 0){
root_obj.addInput(field_name, "IMAGE");
}
const input_value = root_obj.widgets.length + 1 < root_obj.widgets_values.length?root_obj.widgets_values[root_obj.widgets.length]:value.default;
//const input_value = root_obj.widgets.length + 1 < root_obj.widgets_values.length?root_obj.widgets_values[root_obj.widgets.length]:value.default;
if (type == "STRING" || type =="text"){
ComfyWidgets.STRING(root_obj, field_name, ['STRING',{default: value.default,callback: () => {},},],app,)
ComfyWidgets.STRING(root_obj, field_name, ['STRING', {default: value.default},], app,)
if (nodeData.input.required == undefined) nodeData.input.required = {};
//nodeData.input.required[field_name] = ["STRING", {"min": 0, "max": 18446744073709551616, "step": 1}]
root_obj.local_input_defs.required[field_name] = ["STRING", {}];
}
if (type == "INT"){
ComfyWidgets.INT(
root_obj,
field_name,
['',{default: input_value,callback: () => {},},], app,)
['INT',{default: value.default},], app,)
if (nodeData.input.required == undefined) nodeData.input.required = {};
//nodeData.input.required[field_name] = ["INT", {"min": 0, "max": 18446744073709551616, "step": 1}]
root_obj.local_input_defs.required[field_name] = ["INT", {"min": 0, "max": 18446744073709551616, "step": 1}];
}
if (type == "FLOAT"){
ComfyWidgets.FLOAT(
root_obj,
field_name,
['',{default: input_value,callback: (val) => console.log('VALUE', val), "min": 0.00, "max": 1.00, "step": 0.01},],
['FLOAT',{default: value.default, "min": 0.00, "max": 2048.00, "step": 0.01},],
app,
)
const widget = root_obj.inputs.filter(i => i.name === field_name);
if(widget.length > 0)
app.convertToWidget(root_obj, widget[0]);
if (nodeData.input.required == undefined) nodeData.input.required = {};
//nodeData.input.required[field_name] = ["FLOAT", {"min": 0.00, "max": 2048.00, "step": 0.01}]
root_obj.local_input_defs.required[field_name] = ["FLOAT", {"min": 0.00, "max": 2048.00, "step": 0.01}];
}
if (type == "BOOLEAN"){
root_obj.addWidget("toggle",field_name, input_value, ()=>{});
root_obj.addWidget("toggle",field_name, value.default, ()=>{});
const widget = root_obj.inputs.filter(i => i.name === field_name);
if(widget.length > 0)
app.convertToWidget(root_obj, widget[0]);
}
if (type == "LATENT"){
root_obj.addInput(field_name, "LATENT");
}
if (type == "MODEL"){
root_obj.addInput(field_name, "MODEL");
}
if (type == "CLIP"){
root_obj.addInput(field_name, "CLIP");
}
if (type == "MASK"){
root_obj.addInput(field_name, "MASK");
}
if (type == "CONDITIONING"){
root_obj.addInput(field_name, "CONDITIONING");
}
if (type == "VAE"){
root_obj.addInput(field_name, "VAE");
}
if (type == "LATENT") root_obj.addInput(field_name, "LATENT");
if (type == "MODEL") root_obj.addInput(field_name, "MODEL");
if (type == "CLIP") root_obj.addInput(field_name, "CLIP");
if (type == "MASK") root_obj.addInput(field_name, "MASK");
if (type == "CONDITIONING") root_obj.addInput(field_name, "CONDITIONING");
if (type == "VAE") root_obj.addInput(field_name, "VAE");
}
async function convertWorkflowToApiFormat(standardWorkflow) {
try {
return new Promise((resolve, reject) => {
// Sauvegarder les prototypes originaux de onConfigure pour tous les types de nœuds
const originalCallbacks = new Map();
// Temporairement désactiver tous les callbacks onConfigure
for (const nodeTypeName in LiteGraph.registered_node_types) {
const nodeType = LiteGraph.registered_node_types[nodeTypeName];
if (nodeType.prototype.onConfigure) {
originalCallbacks.set(nodeTypeName, nodeType.prototype.onConfigure);
nodeType.prototype.onConfigure = function() {}; // Fonction vide
}
}
// Sauvegarder l'état des callbacks du graphe principal
const originalOnConfigure = LGraph.prototype.onConfigure;
LGraph.prototype.onConfigure = function() {}; // Désactiver temporairement
try {
// Créer un graph temporaire isolé
const tempGraph = new LGraph();
// Configurer sans déclencher de callbacks
tempGraph.configure(standardWorkflow);
// Sauvegarder la référence du graphe original
const originalGraph = app.graph;
// Utiliser graphToPrompt en mode isolé
app.graph = tempGraph;
app.graphToPrompt(tempGraph)
.then(apiData => {
// Restaurer le graphe original
app.graph = originalGraph;
// Résoudre avec le format API
resolve(apiData.output);
})
.catch(error => {
console.error("Erreur lors de la conversion:", error);
reject(error);
})
.finally(() => {
// Nettoyer le graphe temporaire
tempGraph.clear();
// Assurer que toutes les références sont supprimées
if (tempGraph._nodes) {
while (tempGraph._nodes.length > 0) {
tempGraph.remove(tempGraph._nodes[0]);
}
tempGraph._nodes = null;
}
// Supprimer les écouteurs d'événements
tempGraph.removeAllListeners && tempGraph.removeAllListeners();
tempGraph._links = null;
// Restaurer tous les callbacks originaux
for (const [nodeTypeName, callback] of originalCallbacks.entries()) {
LiteGraph.registered_node_types[nodeTypeName].prototype.onConfigure = callback;
}
// Restaurer le callback du graphe
LGraph.prototype.onConfigure = originalOnConfigure;
console.log("Conversion terminée et sandbox nettoyée");
});
} catch (error) {
// En cas d'erreur, restaurer les callbacks et rejeter
for (const [nodeTypeName, callback] of originalCallbacks.entries()) {
LiteGraph.registered_node_types[nodeTypeName].prototype.onConfigure = callback;
}
LGraph.prototype.onConfigure = originalOnConfigure;
reject(error);
}
});
} catch (error) {
console.error("Erreur lors de la préparation du graph:", error);
throw error;
}
}
async function importWorkflow(root_obj, workflow_path, app, nodeData, reset_values=true){
const filename = workflow_path.replace(/\\/g, '/').split("/");
root_obj.title = "Workflow: "+filename[filename.length-1].replace(".json", "").replace(/_/g, " ");
root_obj.local_input_defs = {
required: {},
optional: {}
};
/*
return api.fetchApi("/flowchain/workflow?workflow_path="+workflow_path)
.then(response => response.json())
.then(async data => {*/
cleanInputs(root_obj,nodeData, reset_values);
/*
if (data.error != "none"){
return false;
}*/
//let workflow = data.workflow;
let workflow = app.lipsync_studio[workflow_path].workflow;
// Si c'est un format standard, le convertir en format API
if ("nodes" in workflow) {
try {
//app.loading_bling = true;
workflow = await convertWorkflowToApiFormat(workflow);
//app.loading_bling = false;
} catch (error) {
console.error("Échec de la conversion du workflow:", error);
return false;
}
}
if (!workflow) {
console.error('Workflow invalide ou échec de conversion');
return false;
}
// Traiter le workflow API
const nodes_input = Object.fromEntries(
Object.entries(workflow).filter(([k, v]) => v.class_type == "WorkflowInput")
);
const nodes_output = Object.fromEntries(
Object.entries(workflow).filter(([k, v]) => v.class_type == "WorkflowOutput")
);
root_obj.widgets[1].value = JSON.stringify(workflow);
//ComfyWidgets.STRING(root_obj, "workflow", ['STRING',{default: JSON.stringify(workflow)},],app,)
//app.hideWidget(root_obj, root_obj.widgets[root_obj.widgets.length - 1], {holdSpace: false});
// root_obj.widgets[root_obj.widgets.length - 1].hidden = true;
// Ajouter les widgets d'entrée
Object.entries(nodes_input).forEach(node => {
addWidgetType(root_obj, node[1].inputs, nodeData);
});
// Ajouter les sorties
Object.entries(nodes_output).forEach(node => {
root_obj.addOutput(`${node[1].inputs.Name}`, node[1].inputs.type);
});
root_obj.size[0] = 400;
return JSON.stringify(workflow);
/*
})
.catch(error => {
console.error('Erreur lors de l\'importation:', error);
return false;
});*/
}
/*
function importWorkflow(root_obj, workflow_path, app, reset_values=true){
const filename = workflow_path.replace(/\\/g, '/').split("/");
root_obj.title = "Workflow: "+filename[filename.length-1].replace(".json", "").replace(/_/g, " ");
api.fetchApi("/flowchain/workflow?workflow_path="+workflow_path)
.then(response => response.json())
.then(data => {
.then(data =>{
cleanInputs(root_obj, reset_values);
if (data.error != "none"){
return false
}else{
const workflow = data.workflow;
let workflow = data.workflow;
console.log('Workflow:', workflow);
const nodes_input = Object.fromEntries(
Object.entries(workflow).filter(([k, v]) => v.class_type == "WorkflowInput")
@@ -448,36 +637,182 @@ function importWorkflow(root_obj, workflow_path, app, reset_values=true){
throw error; // Rilancia l'errore per consentire al chiamante di gestirlo
});
}
function addWidgetInfo(root_obj, field_name, value, app){
*/
function addWidgetInfo(root_obj, field_name, value, app, nodeData){
let type = value.type;
if (type == "converted-widget"){
type = value.origType;
}
if (type == "STRING" || type =="text"){
ComfyWidgets.STRING(root_obj, field_name, ['STRING',{default: value.value,callback: () => {},},],app,)
// S'assurer que le nœud a un objet de stockage local pour ses définitions d'entrée
if (!root_obj.local_input_defs) {
root_obj.local_input_defs = {
required: {},
optional: {}
};
}
if ((type == "STRING" || type =="text") && field_name != "workflow"){
ComfyWidgets.STRING(root_obj, field_name, ['STRING',{default: value.value,},],app,)
// Stocker la définition localement au lieu de modifier nodeData
root_obj.local_input_defs.required[field_name] = ["STRING", {}];
}
if (type == "INT" || type == "number"){
ComfyWidgets.INT(
root_obj,
field_name,
['',{default: value.value, callback: () => {},},],
['',{default: value.value, "min": 0, "max": 18446744073709551616, "step": 1},],
app,
)
// Stocker la définition localement
root_obj.local_input_defs.required[field_name] = ["INT", {"min": 0, "max": 18446744073709551616, "step": 1}];
}
if (type == "FLOAT"){
ComfyWidgets.FLOAT(
root_obj,
field_name,
['',{default: value.value, callback: (val) => console.log('VALUE', val),},],
['',{default: value.value, callback: (val) => console.log('VALUE', val), "min": 0.00, "max": 2048.00, "step": 0.01},],
app,
)
// Stocker la définition localement
root_obj.local_input_defs.required[field_name] = ["FLOAT", {"min": 0.00, "max": 2048.00, "step": 0.01}];
}
if (type == "BOOLEAN" || type == "toggle"){
root_obj.addWidget("toggle",field_name, value.value, ()=>{});
root_obj.addWidget("toggle",field_name, value.value, ()=>{});
// Stocker la définition localement
root_obj.local_input_defs.required[field_name] = ["BOOLEAN", {}];
}
if (field_name == "workflow"){
root_obj.addWidget("STRING", field_name, value.value, ()=>{});
root_obj.widgets[root_obj.widgets.length - 1].hidden = true;
// Stocker la définition localement
root_obj.local_input_defs.required[field_name] = ["STRING", {}];
}
}
// change
function hideWidget(
node,
widget,
options = {}
) {
const { suffix = '', holdSpace = true } = options
if (widget.type?.startsWith("converted-widget")) return
widget.origType = widget.type
widget.origComputeSize = widget.computeSize
widget.origSerializeValue = widget.serializeValue
// @ts-expect-error custom widget type
widget.type = "converted-widget" + suffix
if (holdSpace) {
widget.computeSize = () => [0, LiteGraph.NODE_WIDGET_HEIGHT]
} else {
// -4 is due to the gap litegraph adds between widgets automatically
widget.computeSize = () => [0, -4]
}
widget.serializeValue = (node, index) => {
// Prevent serializing the widget if we have no input linked
if (!node.inputs) {
return undefined
}
let node_input = node.inputs.find((i) => i.widget?.name === widget.name)
if (!node_input || !node_input.link) {
return undefined
}
return widget.origSerializeValue
? widget.origSerializeValue(node, index)
: widget.value
}
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, { suffix: ':' + widget.name, holdSpace: false })
}
}
}
function showWidget(widget) {
// @ts-expect-error custom widget type
widget.type = widget.origType
widget.computeSize = widget.origComputeSize
widget.serializeValue = widget.origSerializeValue
delete widget.origType
delete widget.origComputeSize
delete widget.origSerializeValue
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
showWidget(w)
}
}
}
function getWidgetType(config) {
// Special handling for COMBO so we restrict links based on the entries
let type = config[0]
if (type instanceof Array) {
type = 'COMBO'
}
return { type }
}
const GET_CONFIG = Symbol()
export function convertToInput(
node,
widget,
config
) {
hideWidget(node, widget)
const { type } = getWidgetType(config)
// Add input and store widget config for creating on primitive node
const [oldWidth, oldHeight] = node.size
/*const inputIsOptional = !!widget.options?.inputIsOptional
const input = node.addInput(widget.name, type, {
// @ts-expect-error [GET_CONFIG] is not a valid property of IWidget
widget: { name: widget.name, [GET_CONFIG]: () => config },
...(inputIsOptional ? { shape: LiteGraph.SlotShape.HollowCircle } : {})
})
*/
for (const widget of node.widgets) {
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT
}
// Restore original size but grow if needed
node.setSize([
Math.max(oldWidth, node.size[0]),
Math.max(oldHeight, node.size[1])
])
return node
}
function convertToWidget(node, widget) {
showWidget(widget)
const [oldWidth, oldHeight] = node.size
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
for (const widget of node.widgets) {
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT
}
// Restore original size but grow if needed
node.setSize([
Math.max(oldWidth, node.size[0]),
Math.max(oldHeight, node.size[1])
])
}
app.registerExtension({
name: "FlowChain.jsnodes",
@@ -490,10 +825,44 @@ app.registerExtension({
case "Workflow":
nodeType.prototype.onNodeCreated = function() {
chainCallback(this, "onConfigure", function(info) {
let widgetDict = info.widgets_values
if (info.widgets_values.length == undefined) {
//let widgetDict = info.widgets_values
if (!info.widgets_values[0] != "None" && !app.loading_bling){
if (this.widgets.length==2){
if (info.widgets_values[1] != this.widgets[1].value){
this.widgets[1].value = info.widgets_values[1];
//hideWidget(this, this.widgets[1])
//ComfyWidgets.STRING(this, "workflow", ['STRING',{default: workflow},],app,);
//this.widgets[1].hidden = true;
}
//this.widgets[0].value = info.widgets_values[0];
const inputs = app.lipsync_studio[info.widgets_values[0]].inputs;
let start_index = 2;
for (let [key, value] of Object.entries(inputs)){
const isinput = this.inputs.find(i => i.name == value.inputs.Name.value);
const isWidget = this.widgets.find(w => w.name == value.inputs.Name.value);
if (!isWidget && (!isinput||isinput.widget)){
const widget_param = {value: info.widgets_values[start_index], type: value.inputs.default.type}
addWidgetInfo(this, value.inputs.Name.value, widget_param, app, nodeData);
if (isinput){
//node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
const config = [value.inputs.default.type]
convertToInput(this, this.widgets[start_index], config)
}
start_index += 1;
}
}
}
}
/*
if(info.widgets_values.workflows.value != "None"){
const workflow_name = info.widgets_values.workflows.value;
this.widgets[0].value = workflow_name;
console.log("workflow_name", workflow_name)
console.log(app.lipsync_studio[workflow_name])
const inputs = app.lipsync_studio[workflow_name].inputs;
@@ -506,7 +875,7 @@ app.registerExtension({
for (let [key, value] of Object.entries(widgetDict)) {
let widget = this.widgets.find(w => w.name === key);
if(!widget){
addWidgetInfo(this, key, value, app);
addWidgetInfo(this, key, value, app, nodeData);
widget = this.widgets.find(w => w.name === key);
}
@@ -517,30 +886,11 @@ app.registerExtension({
console.log(info.widgets_values[key].options)
widget.options = info.widgets_values[key].options;
widget.value = info.widgets_values[key].value;
/*
for (let input of this.inputs)
if (input.name == key){
for (let [key2, value2] of Object.entries(inputs)){
if (value2.inputs.Name == key){
input.type = value2.inputs.type;
widget.type = "converted-widget"
widget.origType = info.widgets_values[key].origType;
widget.origComputeSize = undefined;
widget.last_y = info.widgets_values[key].last_y;
widget.origSerializeValue = nodeType.prototype.serializeValue;
widget.value = info.widgets_values[key].value;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
break;
}
}
break;
}
*/
}
}
}
}
if (this.id == -1){
for (let i = 0; i < this.outputs.length; i++) {
@@ -551,12 +901,50 @@ app.registerExtension({
this.outputs = [...info.outputs_values];
}
}
this.setSize(info.size);
this.setSize(info.size);*/
});
chainCallback(this, "onSerialize", function(info) {
for (let w of this.inputs){
// if w.name exists in info.widgets_values
if (w.widget){
if (w.type != this.local_input_defs.required[w.name][0])
w.type = this.local_input_defs.required[w.name][0];
}
}
// Créer la structure info.inputs si elle n'existe pas
/*
if (!info.inputs)
info.inputs = {};
// Ajouter les valeurs des widgets dans info.inputs
if (this.widgets) {
for (let w of this.widgets) {
// Stocker uniquement la valeur dans info.inputs
info.inputs[w.name] = w.value;
}
}
// Le reste de votre code existant pour info.widgets_values reste inchangé
info.widgets_values = {};
if (this.widgets) {
for (let w of this.widgets) {
info.widgets_values[w.name] = {
name: w.name,
options: w.options,
value: w.value,
type: w.type,
origType: w.origType,
last_y: w.last_y
};
}
}
*/
/*
let inps = {};
if (info.widgets_values[2] != "None"){
const workflow_name = info.widgets_values[2];
if (info.widgets_values[0] != "None"){
const workflow_name = info.widgets_values[0];
inps = app.lipsync_studio[workflow_name].inputs
}
info.widgets_values = {};
@@ -581,70 +969,29 @@ app.registerExtension({
}
}
this.setSize(info.size);
*/
});
const workflow_reload = this.title.startsWith("Workflow: ")?true:false;
const filename = this.title.replace("Workflow: ", "");
this.addWidget("STRING", "workflow_api_path", "", ()=>{});
this.addWidget("button", "Import Workflow", null, () => {
const workflow_path = this.widgets.find(w => w.name === "workflow_api_path")["value"];
const filename = workflow_path.replace(/\\/g, '/').split("/");
this.title = "Workflow: "+filename[filename.length-1].replace(".json", "").replace(/_/g, " ");
cleanInputs(this);
if (workflow_path != "" && workflow_path != "None")
api.fetchApi("/flowchain/workflow?workflow_path="+workflow_path)
.then(response => response.json())
.then(data => {
// Eseguire l'elaborazione dei dati
const workflow = data.workflow;
//console.log('Workflow:', workflow);
if (data.error == "none"){
const combo = this.widgets.find(w => w.name === "workflows");
if (!combo.options.values.includes(data.file_name)){
combo.options.values.push(data.file_name);
}
const nodes_input = Object.fromEntries(
Object.entries(workflow).filter(([k, v]) => v.class_type == "WorkflowInput")
);
const nodes_output = Object.fromEntries(
Object.entries(workflow).filter(([k, v]) => v.class_type == "WorkflowOutput")
);
//remove a values from app.lipsync_studio
if (data.file_name in app.lipsync_studio){
delete app.lipsync_studio[data.file_name];
}
if ("No file in worflows/api folder" in app.lipsync_studio){
delete app.lipsync_studio["No file in worflows/api folder"];
}
app.lipsync_studio[data.file_name] = {inputs: nodes_input, outputs: nodes_output};
combo.value = data.file_name;
importWorkflow(this, data.file_name, app)
}else{
alert(data.error)
}
})
.catch(error => {
console.error('Error:', error);
throw error; // Rilancia l'errore per consentire al chiamante di gestirlo
});
});
this.addWidget("combo", "workflows", "None", (value) => {
if (value == "None" || value == ""){
this.title = "Workflow (FlowChain ⛓️)";
cleanInputs(this);
}else{
importWorkflow(this, value, app)
const workflow_reload = this.title.startsWith("Workflow: ")?true:false;
if (!app.loading_bling){
this.widgets[0].options.values = ["None", ...Object.keys(app.lipsync_studio)]
this.widgets[0].callback = ( value ) => {
if (value == "None" || value == ""){
this.title = "Workflow (FlowChain ⛓️)";
cleanInputs(this, nodeData);
}else{
this.widgets[1].value = importWorkflow(this, value, app, nodeData)
}
};
if (!workflow_reload){
this.widgets[0].value = "None";
this.widgets[1].value = "";
}
},{
values: ["None", ...Object.keys(app.lipsync_studio)]
});
if(!workflow_reload || !filename in app.lipsync_studio){
cleanInputs(this);
//hideWidget(this, this.widgets[1], { holdSpace: false })
cleanInputs(this, nodeData);
this.color = "#004670";
this.bgcolor = "#002942";
}
this.color = "#004670";
this.bgcolor = "#002942";
}
break;
case "WorkflowInput":
@@ -664,7 +1011,7 @@ app.registerExtension({
let widget = this.widgets.find(w => w.name === key);
let type = this.widgets.find(w => w.name === "type");
if(!widget){
addWidgetInfo(this, key, value, app);
addWidgetInfo(this, key, value, app, nodeData);
widget = this.widgets.find(w => w.name === key);
}
//this.widgets.push(value);
@@ -677,13 +1024,6 @@ app.registerExtension({
//find if key exists in inputs array in inputs.Name
if (info.widgets_values[key].type != "converted-widget"){
this.removeInput(this.inputs.indexOf(input));
//input.type = info.widgets_values.type.value;
/*widget.type = "converted-widget"
widget.origType = info.widgets_values[key].origType;
widget.origComputeSize = undefined;
widget.last_y = info.widgets_values[key].last_y;
widget.origSerializeValue = nodeType.prototype.serializeValue;
widget.computeSize = () => [0, -4];*/
}
break;
}
@@ -746,6 +1086,23 @@ app.registerExtension({
});
this.widgets[1].callback = ( value ) => {
// D'abord, déconnecter tous les liens existants
for (let i = 0; i < this.outputs.length; i++) {
const output = this.outputs[i];
if (output.links && output.links.length) {
const links = output.links.slice();
for (const linkId of links) {
this.graph.removeLink(linkId);
}
}
}
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
if (input.link) {
this.graph.removeLink(input.link);
}
}
clearInputs(this);
switch(value){
case "STRING":
@@ -756,13 +1113,15 @@ app.registerExtension({
["STRING",{default: "",callback: (val) => console.log('VALUE', val),},],
app,
)
break;
case "INT":
this.addOutput("output", "INT");
ComfyWidgets.INT(
this,
"default",
['',{default: 0,callback: (val) => console.log('VALUE', val),},],
//['',{default: 0, "min": 0, "max": 18446744073709551616, "step": 1},],
['',{default: 0},],
app,
)
break;
@@ -771,7 +1130,7 @@ app.registerExtension({
ComfyWidgets.FLOAT(
this,
"default",
['',{default: 0,callback: (val) => console.log('VALUE', val), "min": 0.00, "max": 1.00, "step": 0.01},],
['',{default: 0,callback: (val) => console.log('VALUE', val), "min": 0.00, "max": 2048.00, "step": 0.01},],
app,
)
break;
@@ -779,6 +1138,8 @@ app.registerExtension({
this.addOutput("output", "BOOLEAN");
this.addWidget("toggle", "default", false, ()=>{});
break;
case "none":
break;
default:
this.addOutput("output", value);
this.addInput("default", value);
@@ -787,6 +1148,8 @@ app.registerExtension({
this.color = colors[node_type_list.indexOf(value)];
this.bgcolor = bg_colors[node_type_list.indexOf(value)];
};
if (this.widgets[1].value == "none")
clearInputs(this);
this.color = colors[node_type_list.indexOf("none")];
this.bgcolor = bg_colors[node_type_list.indexOf("none")];
}
@@ -807,7 +1170,7 @@ app.registerExtension({
let widget = this.widgets.find(w => w.name === key);
let type = this.widgets.find(w => w.name === "type");
if(!widget){
addWidgetInfo(this, key, value, app);
addWidgetInfo(this, key, value, app, nodeData);
widget = this.widgets.find(w => w.name === key);
}
//this.widgets.push(value);
@@ -906,7 +1269,7 @@ app.registerExtension({
let widget = this.widgets.find(w => w.name === key);
let type = this.widgets.find(w => w.name === "type");
if(!widget){
addWidgetInfo(this, key, value, app);
addWidgetInfo(this, key, value, app, nodeData);
widget = this.widgets.find(w => w.name === key);
}
//this.widgets.push(value);
@@ -988,14 +1351,36 @@ app.registerExtension({
this.setSize(info.size);
});
this.widgets[1].callback = ( value ) => {
// D'abord, déconnecter tous les liens existants
for (let i = 0; i < this.outputs.length; i++) {
const output = this.outputs[i];
if (output.links && output.links.length) {
const links = output.links.slice();
for (const linkId of links) {
this.graph.removeLink(linkId);
}
}
}
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
if (input.link) {
this.graph.removeLink(input.link);
}
}
clearInputs(this);
this.addOutput("output", value);
this.addInput("default", value);
switch(value){
case "none":
break;
default:
this.addOutput("output", value);
this.addInput("default", value);
break;
}
this.color = colors[node_type_list.indexOf(value)];
this.bgcolor = bg_colors[node_type_list.indexOf(value)];
};
clearInputs(this);
if (this.widgets[1].value == "none") clearInputs(this);
this.color = colors[node_type_list.indexOf("none")];
this.bgcolor = bg_colors[node_type_list.indexOf("none")];
}
@@ -1043,5 +1428,30 @@ app.registerExtension({
console.error('Error:', error);
throw error;
});
const origRemoveNode = LGraphCanvas.prototype.removeNode;
LGraphCanvas.prototype.removeNode = function(node) {
if (node && node.inputs && node.outputs) {
// Assurer que tous les liens sont déconnectés avant de supprimer le nœud
for (let i = 0; i < node.inputs.length; i++) {
const input = node.inputs[i];
if (input.link != null) {
this.graph.removeLink(input.link);
}
}
for (let i = 0; i < node.outputs.length; i++) {
const output = node.outputs[i];
if (output.links && output.links.length) {
const links = output.links.slice(); // Copier pour éviter les problèmes lors de la modification
for (const linkId of links) {
this.graph.removeLink(linkId);
}
}
}
}
// Appeler la méthode originale
return origRemoveNode.call(this, node);
};
}
});
});
+115 -577
View File
@@ -1,32 +1,22 @@
import json
import urllib.request
import urllib.parse
import torch
import logging
import time
import uuid
import traceback
import nodes
import copy
import os
import asyncio
from enum import Enum
import numpy as np
import server
import hashlib
from torchvision import transforms
from .utils.logger import Logger
from .utils.utils import caches
from comfy_execution.graph import get_input_info, ExecutionList, DynamicPrompt, ExecutionBlocker
import comfy.model_management
import sys
from PIL import Image
from comfy_execution.graph_utils import is_link, GraphBuilder
from nodes import SaveImage
import gc
import folder_paths
from server import PromptServer
from execution import PromptExecutor
from .utils.utils import convert_standard_to_api_format
class ExecutionResult(Enum):
SUCCESS = 0
@@ -44,515 +34,8 @@ class AnyType(str):
return False
client_id = '5b49a023-b05a-4c53-8dc9-addc3a749911'
server_address = "127.0.0.1:8188"
script_list_path = os.path.join(folder_paths.user_directory, "default", "workflows", "api")
def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execution_block_cb=None, pre_execute_cb=None):
# check if node wants the lists
input_is_list = getattr(obj, "INPUT_IS_LIST", False)
if len(input_data_all) == 0:
max_len_input = 0
else:
max_len_input = max(len(x) for x in input_data_all.values())
# get a slice of inputs, repeat last input when list isn't long enough
def slice_dict(d, i):
return {k: v[i if len(v) > i else -1] for k, v in d.items()}
results = []
def process_inputs(inputs, index=None):
if allow_interrupt:
nodes.before_node_execution()
execution_block = None
for k, v in inputs.items():
if isinstance(v, ExecutionBlocker):
execution_block = execution_block_cb(v) if execution_block_cb else v
break
if execution_block is None:
if pre_execute_cb is not None and index is not None:
pre_execute_cb(index)
results.append(getattr(obj, func)(**inputs))
else:
results.append(execution_block)
if input_is_list:
process_inputs(input_data_all, 0)
elif max_len_input == 0:
process_inputs({})
else:
for i in range(max_len_input):
input_dict = slice_dict(input_data_all, i)
process_inputs(input_dict, i)
return results
def merge_result_data(results, obj):
# check which outputs need concatenating
output = []
output_is_list = [False] * len(results[0])
if hasattr(obj, "OUTPUT_IS_LIST"):
output_is_list = obj.OUTPUT_IS_LIST
# merge node execution results
for i, is_list in zip(range(len(results[0])), output_is_list):
if is_list:
output.append([x for o in results for x in o[i]])
else:
output.append([o[i] for o in results])
return output
def get_output_data(obj, input_data_all, execution_block_cb=None, pre_execute_cb=None):
results = []
uis = []
subgraph_results = []
return_values = _map_node_over_list(obj, input_data_all, obj.FUNCTION, allow_interrupt=True,
execution_block_cb=execution_block_cb, pre_execute_cb=pre_execute_cb)
has_subgraph = False
for i in range(len(return_values)):
r = return_values[i]
if isinstance(r, dict):
if 'ui' in r:
uis.append(r['ui'])
if 'expand' in r:
# Perform an expansion, but do not append results
has_subgraph = True
new_graph = r['expand']
result = r.get("result", None)
if isinstance(result, ExecutionBlocker):
result = tuple([result] * len(obj.RETURN_TYPES))
subgraph_results.append((new_graph, result))
elif 'result' in r:
result = r.get("result", None)
if isinstance(result, ExecutionBlocker):
result = tuple([result] * len(obj.RETURN_TYPES))
results.append(result)
subgraph_results.append((None, result))
else:
if isinstance(r, ExecutionBlocker):
r = tuple([r] * len(obj.RETURN_TYPES))
results.append(r)
subgraph_results.append((None, r))
if has_subgraph:
output = subgraph_results
elif len(results) > 0:
output = merge_result_data(results, obj)
else:
output = []
ui = dict()
if len(uis) > 0:
# ui = {k: [y for x in uis for y in x[k]] for k in uis[0].keys()}
for k in uis[0].keys():
for x in uis:
ui[k] = x[k]
# ui = {k: uis[0]["images"] for k in uis[0].keys()}
return output, ui, has_subgraph
def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, extra_data=None):
if extra_data is None:
extra_data = {}
valid_inputs = class_def.INPUT_TYPES()
input_data_all = {}
missing_keys = {}
for x in inputs:
input_data = inputs[x]
input_type, input_category, input_info = get_input_info(class_def, x)
def mark_missing():
missing_keys[x] = True
input_data_all[x] = (None,)
if is_link(input_data) and (not input_info or not input_info.get("rawLink", False)):
input_unique_id = input_data[0]
output_index = input_data[1]
if outputs is None:
mark_missing()
continue # This might be a lazily-evaluated input
cached_output = outputs.get(input_unique_id)
if cached_output is None:
mark_missing()
continue
if output_index >= len(cached_output):
mark_missing()
continue
obj = cached_output[output_index]
input_data_all[x] = obj
elif input_category is not None:
input_data_all[x] = [input_data]
if "hidden" in valid_inputs:
h = valid_inputs["hidden"]
for x in h:
if h[x] == "PROMPT":
input_data_all[x] = [dynprompt.get_original_prompt() if dynprompt is not None else {}]
if h[x] == "DYNPROMPT":
input_data_all[x] = [dynprompt]
if h[x] == "EXTRA_PNGINFO":
input_data_all[x] = [extra_data.get('extra_pnginfo', None)]
if h[x] == "UNIQUE_ID":
input_data_all[x] = [unique_id]
return input_data_all, missing_keys
def full_type_name(klass):
module = klass.__module__
if module == 'builtins':
return klass.__qualname__
return module + '.' + klass.__qualname__
def format_value(x):
if x is None:
return None
elif isinstance(x, (int, float, bool, str)):
return x
else:
return str(x)
def executes(server, dynprompt, caches, current_item, extra_data, executed, prompt_id, execution_list,
pending_subgraph_results):
unique_id = current_item
real_node_id = dynprompt.get_real_node_id(unique_id)
display_node_id = dynprompt.get_display_node_id(unique_id)
parent_node_id = dynprompt.get_parent_node_id(unique_id)
inputs = dynprompt.get_node(unique_id)['inputs']
class_type = dynprompt.get_node(unique_id)['class_type']
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
if caches.outputs.get(unique_id) is not None:
if server.client_id is not None:
cached_output = caches.ui.get(unique_id) or {}
server.send_sync("executed", {"node": unique_id, "display_node": display_node_id,
"output": cached_output.get("output", None), "prompt_id": prompt_id},
server.client_id)
return (ExecutionResult.SUCCESS, None, None)
input_data_all = None
try:
if unique_id in pending_subgraph_results:
cached_results = pending_subgraph_results[unique_id]
resolved_outputs = []
for is_subgraph, result in cached_results:
if not is_subgraph:
resolved_outputs.append(result)
else:
resolved_output = []
for r in result:
if is_link(r):
source_node, source_output = r[0], r[1]
node_output = caches.outputs.get(source_node)[source_output]
for o in node_output:
resolved_output.append(o)
else:
resolved_output.append(r)
resolved_outputs.append(tuple(resolved_output))
output_data = merge_result_data(resolved_outputs, class_def)
output_ui = []
has_subgraph = False
else:
input_data_all, missing_keys = get_input_data(inputs, class_def, unique_id, caches.outputs, dynprompt,
extra_data)
if server.client_id is not None:
server.last_node_id = display_node_id
server.send_sync("executing",
{"node": unique_id, "display_node": display_node_id, "prompt_id": prompt_id},
server.client_id)
obj = caches.objects.get(unique_id)
if obj is None:
obj = class_def()
caches.objects.set(unique_id, obj)
if hasattr(obj, "check_lazy_status"):
required_inputs = _map_node_over_list(obj, input_data_all, "check_lazy_status", allow_interrupt=True)
required_inputs = set(sum([r for r in required_inputs if isinstance(r, list)], []))
required_inputs = [x for x in required_inputs if isinstance(x, str) and (
x not in input_data_all or x in missing_keys
)]
if len(required_inputs) > 0:
for i in required_inputs:
execution_list.make_input_strong_link(unique_id, i)
return (ExecutionResult.PENDING, None, None)
def execution_block_cb(block):
if block.message is not None:
"""mes = {
"prompt_id": prompt_id,
"node_id": unique_id,
"node_type": class_type,
"executed": list(executed),
"exception_message": f"Execution Blocked: {block.message}",
"exception_type": "ExecutionBlocked",
"traceback": [],
"current_inputs": [],
"current_outputs": [],
}"""
"""server.send_sync("execution_error", mes, server.client_id)"""
return ExecutionBlocker(None)
else:
return block
def pre_execute_cb(call_index):
GraphBuilder.set_default_prefix(unique_id, call_index, 0)
output_data, output_ui, has_subgraph = get_output_data(obj, input_data_all,
execution_block_cb=execution_block_cb,
pre_execute_cb=pre_execute_cb)
if len(output_ui) > 0:
caches.ui.set(unique_id, {
"meta": {
"node_id": unique_id,
"display_node": display_node_id,
"parent_node": parent_node_id,
"real_node_id": real_node_id,
},
"output": output_ui
})
if server.client_id is not None:
server.send_sync("executed", {"node": unique_id, "display_node": display_node_id, "output": output_ui,
"prompt_id": prompt_id}, server.client_id)
if has_subgraph:
cached_outputs = []
new_node_ids = []
new_output_ids = []
new_output_links = []
for i in range(len(output_data)):
new_graph, node_outputs = output_data[i]
if new_graph is None:
cached_outputs.append((False, node_outputs))
else:
# Check for conflicts
for node_id, node_info in new_graph.items():
new_node_ids.append(node_id)
display_id = node_info.get("override_display_id", unique_id)
dynprompt.add_ephemeral_node(node_id, node_info, unique_id, display_id)
# Figure out if the newly created node is an output node
class_type = node_info["class_type"]
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
if hasattr(class_def, 'OUTPUT_NODE') and class_def.OUTPUT_NODE == True:
new_output_ids.append(node_id)
for i in range(len(node_outputs)):
if is_link(node_outputs[i]):
from_node_id, from_socket = node_outputs[i][0], node_outputs[i][1]
new_output_links.append((from_node_id, from_socket))
cached_outputs.append((True, node_outputs))
new_node_ids = set(new_node_ids)
for cache in caches.all:
cache.ensure_subcache_for(unique_id, new_node_ids).clean_unused()
for node_id in new_output_ids:
execution_list.add_node(node_id)
for link in new_output_links:
execution_list.add_strong_link(link[0], link[1], unique_id)
pending_subgraph_results[unique_id] = cached_outputs
return (ExecutionResult.PENDING, None, None)
caches.outputs.set(unique_id, output_data)
except comfy.model_management.InterruptProcessingException as iex:
logging.info("Processing interrupted")
# skip formatting inputs/outputs
error_details = {
"node_id": real_node_id,
}
return (ExecutionResult.FAILURE, error_details, iex)
except Exception as ex:
typ, _, tb = sys.exc_info()
exception_type = full_type_name(typ)
input_data_formatted = {}
if input_data_all is not None:
input_data_formatted = {}
for name, inputs in input_data_all.items():
input_data_formatted[name] = [format_value(x) for x in inputs]
logging.error(f"!!! Exception during processing !!! {ex}")
logging.error(traceback.format_exc())
error_details = {
"node_id": real_node_id,
"exception_message": str(ex),
"exception_type": exception_type,
"traceback": traceback.format_tb(tb),
"current_inputs": input_data_formatted
}
if isinstance(ex, comfy.model_management.OOM_EXCEPTION):
logging.error("Got an OOM, unloading all loaded models.")
comfy.model_management.unload_all_models()
return (ExecutionResult.FAILURE, error_details, ex)
executed.add(unique_id)
return (ExecutionResult.SUCCESS, None, None)
class IsChangedCache:
def __init__(self, dynprompt, outputs_cache):
self.dynprompt = dynprompt
self.outputs_cache = outputs_cache
self.is_changed = {}
def get(self, node_id):
if node_id in self.is_changed:
return self.is_changed[node_id]
node = self.dynprompt.get_node(node_id)
class_type = node["class_type"]
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
if not hasattr(class_def, "IS_CHANGED"):
self.is_changed[node_id] = False
return self.is_changed[node_id]
if "is_changed" in node:
self.is_changed[node_id] = node["is_changed"]
return self.is_changed[node_id]
# Intentionally do not use cached outputs here. We only want constants in IS_CHANGED
input_data_all, _ = get_input_data(node["inputs"], class_def, node_id, None)
try:
is_changed = _map_node_over_list(class_def, input_data_all, "IS_CHANGED")
node["is_changed"] = [None if isinstance(x, ExecutionBlocker) else x for x in is_changed]
except Exception as e:
logging.warning("WARNING: {}".format(e))
node["is_changed"] = float("NaN")
finally:
self.is_changed[node_id] = node["is_changed"]
return self.is_changed[node_id]
status_messages = []
def add_message(servers, event, data: dict, broadcast: bool):
data = {
**data,
"timestamp": int(time.time() * 1000),
}
status_messages.append((event, data))
"""if servers.client_id is not None or broadcast:
servers.send_sync(event, data, servers.client_id)"""
def handle_execution_error(servers, prompt_id, prompt, current_outputs, executed, error, ex):
node_id = error["node_id"]
class_type = prompt[node_id]["class_type"]
# First, send back the status to the frontend depending
# on the exception type
if isinstance(ex, comfy.model_management.InterruptProcessingException):
mes = {
"prompt_id": prompt_id,
"node_id": node_id,
"node_type": class_type,
"executed": list(executed),
}
add_message(servers, "execution_interrupted", mes, broadcast=True)
else:
mes = {
"prompt_id": prompt_id,
"node_id": node_id,
"node_type": class_type,
"executed": list(executed),
"exception_message": error["exception_message"],
"exception_type": error["exception_type"],
"traceback": error["traceback"],
"current_inputs": error["current_inputs"],
"current_outputs": list(current_outputs),
}
add_message(servers, "execution_error", mes, broadcast=False)
def execute(server, prompt, prompt_id, extra_data={}, execute_outputs=[]):
nodes.interrupt_processing(False)
if "client_id" in extra_data:
server.client_id = extra_data["client_id"]
status_messages = []
add_message(server,"execution_start", {"prompt_id": prompt_id}, broadcast=False)
with torch.inference_mode():
dynamic_prompt = DynamicPrompt(prompt)
is_changed_cache = IsChangedCache(dynamic_prompt, caches.outputs)
for cache in caches.all:
cache.set_prompt(dynamic_prompt, prompt.keys(), is_changed_cache)
cache.clean_unused()
cached_nodes = []
for node_id in prompt:
if caches.outputs.get(node_id) is not None:
cached_nodes.append(node_id)
comfy.model_management.cleanup_models()
add_message(server, "execution_cached",{"nodes": cached_nodes, "prompt_id": prompt_id}, broadcast=False)
pending_subgraph_results = {}
executed = set()
execution_list = ExecutionList(dynamic_prompt, caches.outputs)
current_outputs = caches.outputs.all_node_ids()
for node_id in list(execute_outputs):
execution_list.add_node(node_id)
while not execution_list.is_empty():
node_id, error, ex = execution_list.stage_node_execution()
if error is not None:
handle_execution_error(server, prompt_id, dynamic_prompt.original_prompt, current_outputs, executed,
error, ex)
break
if "type" in prompt[node_id]["inputs"] and prompt[node_id]["inputs"]["type"] in ["IMAGE", "LATENT"]:
logging.info("node : {} {} image_count => {}".format(node_id, prompt[node_id]["class_type"],
len(prompt[node_id]["inputs"]["default"])))
else:
logging.info(
"node : {} {} {}".format(node_id, prompt[node_id]["class_type"], prompt[node_id]["inputs"]))
result, error, ex = executes(server, dynamic_prompt, caches, node_id, extra_data, executed,
prompt_id, execution_list, pending_subgraph_results)
success = result != ExecutionResult.FAILURE
if result == ExecutionResult.FAILURE:
handle_execution_error(server, prompt_id, dynamic_prompt.original_prompt, current_outputs, executed,
error, ex)
break
elif result == ExecutionResult.PENDING:
execution_list.unstage_node_execution()
else: # result == ExecutionResult.SUCCESS:
execution_list.complete_node_execution()
else:
# Only execute when the while-loop ends without break
#print("execution_success", prompt_id)
add_message(server, "execution_success", {"prompt_id": prompt_id}, broadcast=False)
ui_outputs = {}
meta_outputs = {}
all_node_ids = caches.ui.all_node_ids()
for node_id in all_node_ids:
ui_info = caches.ui.get(node_id)
if ui_info is not None:
ui_outputs[node_id] = ui_info["output"]
meta_outputs[node_id] = ui_info["meta"]
history_result = {"outputs": ui_outputs, "meta": meta_outputs,}
for node_id in history_result["outputs"]:
for output in history_result["outputs"][node_id]:
if type(history_result["outputs"][node_id][output]) == torch.Tensor:
logging.info("output : {} {} image_count => {}".format(node_id, prompt[node_id]["class_type"],
len(history_result["outputs"][node_id][output])))
elif len(str(history_result["outputs"][node_id][output])) > 100:
logging.info("output : {} {} {}".format(node_id, prompt[node_id]["class_type"],
str(history_result["outputs"][node_id][output])[:100]))
else:
logging.info("output : {} {}".format(node_id, history_result["outputs"][node_id][output]))
server.last_node_id = None
"""if comfy.model_management.DISABLE_SMART_MEMORY:
comfy.model_management.unload_all_models()"""
return history_result
#client_id = '5b49a023-b05a-4c53-8dc9-addc3a749911'
script_list_path = os.path.join(folder_paths.user_directory, "default", "workflows")
def recursive_delete(workflow, to_delete):
@@ -580,15 +63,22 @@ class Workflow(SaveImage):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"workflows": ("COMBO", {"values": []}),
"workflow": ("STRING", {"default": ""})
},
"optional": {
"hidden": {
"workflows": ("STRING", {"default": ""})
}}
# "workflows": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = (
AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"),
AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"),
)
FUNCTION = "generate"
CATEGORY = "FlowChain ⛓️"
@@ -600,25 +90,28 @@ class Workflow(SaveImage):
m.update(workflows.encode())
return m.digest().hex()
def generate(self, workflows, **kwargs):
def generate(self, workflows, workflow, **kwargs):
# get current file path
"""
def get_workflow(workflow_name):
"""
with urllib.request.urlopen(
"http://{}/flowchain/workflow?workflow_path={}".format(server_address, workflow_name)) as response:
workflow = json.loads(response.read())
"""
json_path = os.path.join(script_list_path, workflow_name)
if os.path.exists(json_path):
with open(json_path, "r", encoding="utf-8") as f:
json_content = json.load(f)
if "nodes" in json_content:
print("json is not an api worflow:",workflow_name)
try:
api_workflow = convert_standard_to_api_format(json_content)
if api_workflow:
json_content = api_workflow
else:
print("Échec de la conversion du workflow standard en format API")
except Exception as e:
print(f"Erreur lors de la conversion: {str(e)}")
else:
print("File not found:",workflow_name)
return json_content
return json_content
"""
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():
@@ -635,7 +128,7 @@ class Workflow(SaveImage):
if "default" not in value["inputs"]:
workflow[key]["inputs"]["default"] = torch.tensor([])
else:
if value["inputs"]["default"] == []:
if not value["inputs"]["default"]:
workflow[key]["inputs"]["default"] = torch.tensor([])
return workflow
@@ -651,8 +144,7 @@ class Workflow(SaveImage):
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 = {node_id: node for node_id, node in workflow.items() if any(
input_value[0] == switch_id for input_value in node["inputs"].values() if type(input_value) == list)}"""
inputs_from_switch = []
for node_ids, node in workflow.items():
for input_name, input_value in node["inputs"].items():
@@ -835,71 +327,117 @@ class Workflow(SaveImage):
return workflow, workflow_outputs
def get_recursive_workflow(workflows, max_id=0):
workflow = get_workflow(workflows)
# if workflows[-5:] == ".json":
# workflow = get_workflow(workflows)
# else:
workflow = json.loads(workflows)
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, max_id)
subworkflow, max_id = get_recursive_workflow(workflow_json, max_id)
#subworkflow = get_workflow(workflow_name)
#max_id = max([int(k) for k in workflow.keys() if k.isdigit()])
# change all id in subworkflow
#subworkflow = redefine_id(subworkflow["workflow"], 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)
# sub_workflow, workflow_outputs_sub = treat_workflow(subworkflow)
workflow = {k: v for k, v in workflow.items() if
not (v["class_type"] == "Workflow" and v["inputs"]["workflows"] == workflow_name)}
# add subworkflow to workflow
workflow.update(subworkflow)
return workflow, max_id
"""
def format_output_value(value, type_name):
if value is None:
return None
if type_name == "STRING":
# Convert list of characters or bytes to string
if isinstance(value, list) and all(isinstance(x, (str, bytes)) for x in value):
return ''.join(value)
elif isinstance(value, (bytes, bytearray)):
return value.decode('utf-8')
elif type_name == "INT":
# Ensure integer conversion
try:
return int(value)
except (ValueError, TypeError):
pass
elif type_name == "FLOAT":
# Ensure float conversion
try:
return float(value)
except (ValueError, TypeError):
pass
elif type_name == "BOOLEAN":
# Convert to proper boolean
if isinstance(value, (str, list)):
if isinstance(value, str):
return value.lower() in ('true', 'yes', '1', 't', 'y')
elif len(value) > 0:
return bool(value[0])
return bool(value)
# For other types (LATENT, CLIP, MODEL, etc.), return as-is
return value
"""
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 = len(queue_info["queue_pending"]) > 0 and queue_info["queue_pending"] or 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]
with urllib.request.urlopen("http://{}/queue".format(server_address)) as response:
queue_info = json.loads(response.read())
# find "workflow" node in queue_info["queue_pending"][-1][3]["extra_png_info"]["workflow"]["nodes"]
for node in queue_to_use[-1][3]["extra_pnginfo"]["workflow"]["nodes"]:
workflow_name = 'Workflow: ' + original_inputs["workflows"].replace(".json", "").replace("_", " ")
if node["type"] == "Workflow" and node["title"] == workflow_name:
workflow = node["widgets_values"][1]
break
original_inputs = [v["inputs"] for k, v in queue_info["queue_running"][0][2].items() if
"workflows" in v["inputs"] and v["inputs"]["workflows"] == workflows][0]
workflow, _ = get_recursive_workflow(workflows, 5000)
else:
# Fallback to empty inputs if server instance not available
original_inputs = {}
workflow, _ = get_recursive_workflow(workflow, 5000)
workflow, workflow_outputs = clean_workflow(workflow, original_inputs, kwargs)
workflow_outputs_id = [k for k, v in workflow.items() if v["class_type"] == "WorkflowOutput"]
prompt_id = str(uuid.uuid4())
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
servers = server.PromptServer(loop)
servers.last_prompt_id = prompt_id
servers.client_id = client_id
execution_start_time = time.perf_counter()
logging.info("workflow : {}".format(workflows))
#p_executor = PromptExecutor(servers)
#p_executor.execute(workflow, prompt_id, {}, workflow_outputs_id)
#history_result = p_executor.history_result
history_result = execute(servers, workflow, prompt_id, {}, workflow_outputs_id)
current_time = time.perf_counter()
execution_time = current_time - execution_start_time
logging.info("Prompt executed in {:.2f} seconds".format(execution_time))
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()
del servers
gc.collect()
output = []
for id_node, node in workflow_outputs.items():
if id_node in history_result["outputs"]:
mask = history_result["outputs"][id_node]["default"]
# create hash from mask + node name
"""hash = hashlib.sha256(mask
hash = hash.update(node["inputs"]["Name"].encode())
filename_prefix = node["inputs"]["Name"]+"/"+hash
if node["inputs"]["type"] == "IMAGE":
self.save_images(history_result["outputs"][id_node]["default"], filename_prefix)
elif node["inputs"]["type"] == "MASK":
preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
self.save_images(preview, filename_prefix)"""
output.append(history_result["outputs"][id_node]["default"])
result_value = history_result["outputs"][id_node]["default"]
# Apply formatting based on the expected output type
# formatted_value = format_output_value(result_value, node["inputs"]["type"])
output.append(result_value[0])
else:
if node["inputs"]["type"] == "IMAGE" or node["inputs"]["type"] == "MASK":
black_image_np = np.zeros((255, 255, 3), dtype=np.uint8)
+3 -3
View File
@@ -144,14 +144,14 @@ class WorkflowOutput:
image_tensor = image_tensor.permute(1, 2, 0)
image_tensor = image_tensor.unsqueeze(0)
return {"ui": {"default": image_tensor}}
return {"ui": {"default": kwargs["default"]}}
return {"ui": {"default": [kwargs["default"]]}}
elif type == "LATENT":
if kwargs["default"] is None:
return {"ui": {"default": torch.tensor([])}}
return {"ui": {"default": kwargs["default"]}}
return {"ui": {"default": [kwargs["default"]]}}
else:
ui = {"ui": {}}
ui["ui"]["default"] = kwargs["default"]
ui["ui"]["default"] = [kwargs["default"]]
return ui