diff --git a/__init__.py b/__init__.py index 3b982b7..9dfeee2 100644 --- a/__init__.py +++ b/__init__.py @@ -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" - diff --git a/py/__init__.py b/py/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/endpoints.py b/py/endpoints.py index 74675bb..1b36b81 100644 --- a/py/endpoints.py +++ b/py/endpoints.py @@ -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 \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/utils.py b/utils/utils.py index cdbc979..d013d55 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -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 \ No newline at end of file diff --git a/web/js/jsnodes.js b/web/js/jsnodes.js index d2466db..1a011da 100644 --- a/web/js/jsnodes.js +++ b/web/js/jsnodes.js @@ -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= 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= 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); + }; } -}); +}); \ No newline at end of file diff --git a/workflow.py b/workflow.py index 5a8a3d2..0e081ef 100644 --- a/workflow.py +++ b/workflow.py @@ -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) diff --git a/workflow_nodes.py b/workflow_nodes.py index 2a04323..2f11efb 100644 --- a/workflow_nodes.py +++ b/workflow_nodes.py @@ -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