review all process
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
|
||||
+283
-139
@@ -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
|
||||
+209
-33
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user