From 27cbae7b0506550865b632d82850d19a977dc130 Mon Sep 17 00:00:00 2001 From: NumZ Date: Tue, 25 Mar 2025 21:43:34 +0100 Subject: [PATCH] Add live update workflow node from sub workflow inputs/outputs --- README.md | 2 +- py/endpoints.py | 109 +++---- web/js/jsnodes.js | 750 ++++++++++++++++++++++++---------------------- workflow.py | 68 +---- workflow_nodes.py | 22 -- 5 files changed, 423 insertions(+), 528 deletions(-) diff --git a/README.md b/README.md index cf4a112..21e27c2 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Warning and know issue: ## 🔗 requirements -- **comfyui**: Be sure to have a **ComfyUI front-end** version of at least **1.3.11**. If not, update ComfyUI to the latest version. To find the front-end version, go to ComfyUI settings (the gear icon), click "About," and check the version at the top of the page. +- **comfyui**: Be sure to have a **ComfyUI front-end** version of at least **1.11.8**. If not, update ComfyUI to the latest version. To find the front-end version, go to ComfyUI settings (the gear icon), click "About," and check the version at the top of the page. ## 💻 Installation diff --git a/py/endpoints.py b/py/endpoints.py index 4a919c3..855af56 100644 --- a/py/endpoints.py +++ b/py/endpoints.py @@ -5,6 +5,37 @@ import json import folder_paths from app.user_manager import UserManager +def get_workflow_data(workflow_file): + nodes_input = {} + nodes_output = {} + + if "nodes" in workflow_file: + # Extraire les nœuds WorkflowInput et WorkflowOutput + for node in workflow_file["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", {}), + "position": node.get("pos", {})[1] + } + elif node.get("type") == "WorkflowOutput": + node_id = str(node.get("id", "unknown")) + nodes_output[node_id] = { + "class_type": "WorkflowOutput", + "inputs": node.get("widgets_values", {}), + "position": node.get("pos", {})[1] + } + # sort by position + nodes_input = dict(sorted(nodes_input.items(), key=lambda item: item[1]["position"])) + nodes_output = dict(sorted(nodes_output.items(), key=lambda item: item[1]["position"])) + + if nodes_input or nodes_output: + # Créer une clé unique basée sur le chemin relatif + return {"inputs": nodes_input, + "outputs": nodes_output, + 'workflow': workflow_file} @server.PromptServer.instance.routes.get("/flowchain/workflows") async def workflows(request): @@ -24,45 +55,9 @@ async def workflows(request): 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} + relative_path = os.path.relpath(file_path, json_path) + result[relative_path] = get_workflow_data(json_content) + except json.JSONDecodeError: # Ignorer les fichiers JSON mal formés print(f"Ignoring malformed JSON file: {file_path}") @@ -88,45 +83,15 @@ async def workflow(request): unversal_path = original_path.replace("\\", "/") json_path = unversal_path.split("/") if ".json" in json_path[-1]: - file_name = json_path[-1] + #file_name = json_path[-1] 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) - nodes_input = {} - nodes_output = {} - - # Vérifier le format du workflow (API ou standard) - if "nodes" in json_content: - # 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: - result = {"inputs": nodes_input, - "outputs": nodes_output, - 'workflow': json_content} + result = get_workflow_data(json_content) else: result = {"error": "File not found"} diff --git a/web/js/jsnodes.js b/web/js/jsnodes.js index 954edf4..6534c92 100644 --- a/web/js/jsnodes.js +++ b/web/js/jsnodes.js @@ -274,14 +274,7 @@ function addLoadVideoCommon(nodeType, nodeData) { addPreviewOptions(nodeType); } function cleanInputs(root_obj, start_index = 2, 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) { @@ -305,12 +298,6 @@ function cleanInputs(root_obj, start_index = 2, reset_value=true) { root_obj.widgets = root_obj.widgets.splice(0,start_index) if(reset_value){ root_obj.widgets_values = []; - /* - for (let key in root_obj.widgets_values) { - if (key != "workflows"){ - delete root_obj.widgets_values[key]; - } - }*/ const max_node_output = root_obj.outputs.length; for(let i = 0; 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; - if (type == "STRING" || type =="text"){ - 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, - ['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, - ['FLOAT',{default: value.default, "min": 0.00, "max": 2048.00, "step": 0.01},], - app, - ) - 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, 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"); -} - -*/ async function convertWorkflowToApiFormat(standardWorkflow) { try { return new Promise((resolve, reject) => { @@ -552,30 +471,6 @@ async function importWorkflow(root_obj, workflow_path, app, nodeData, reset_valu 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); - - - 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); }) @@ -600,45 +495,45 @@ function addWidgets(root_obj, field_name, value, app){ } if ((type == "STRING" || type =="text") && field_name != "workflow"){ - ComfyWidgets.STRING(root_obj, field_name, ['STRING',{default: value.value,},],app,) + ComfyWidgets.STRING(root_obj, field_name, ['STRING', getDefaultOptions("STRING", value.value),],app,) // Stocker la définition localement au lieu de modifier nodeData - root_obj.local_input_defs.required[field_name] = ["STRING", {}]; + root_obj.local_input_defs.required[field_name] = ["STRING", getDefaultOptions("STRING", value.value)]; } if (type == "INT" || type == "number"){ ComfyWidgets.INT( root_obj, field_name, - ['INT',{default: value.value, "min": 0, "max": 18446744073709551616, "step": 1},], + ['INT', getDefaultOptions("INT", value.value),], app, ) // Stocker la définition localement - root_obj.local_input_defs.required[field_name] = ["INT", {"min": 0, "max": 18446744073709551616, "step": 1}]; + root_obj.local_input_defs.required[field_name] = ["INT", getDefaultOptions("INT", value.value)]; } if (type == "FLOAT"){ ComfyWidgets.FLOAT( root_obj, field_name, - ['FLOAT',{default: value.value, "min": 0.00, "max": 2048.00, "step": 0.01},], + ['FLOAT',getDefaultOptions("FLOAT", value.value),], app, ) // Stocker la définition localement - root_obj.local_input_defs.required[field_name] = ["FLOAT", {"min": 0.00, "max": 2048.00, "step": 0.01}]; + root_obj.local_input_defs.required[field_name] = ["FLOAT", getDefaultOptions("FLOAT", value.value)]; } if (type == "BOOLEAN" || type == "toggle"){ root_obj.addWidget("toggle",field_name, value.value, ()=>{}); // Stocker la définition localement - root_obj.local_input_defs.required[field_name] = ["BOOLEAN", {}]; + root_obj.local_input_defs.required[field_name] = ["BOOLEAN", getDefaultOptions("BOOLEAN", value.value)]; } - + /* 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", {}]; - } + }*/ if (type == "IMAGE") root_obj.addInput(field_name, "IMAGE"); if (type == "LATENT") root_obj.addInput(field_name, "LATENT"); if (type == "MODEL") root_obj.addInput(field_name, "MODEL"); @@ -656,6 +551,7 @@ function addOutputs(root_obj, workflow_name){ root_obj.addOutput(value.inputs.Name.value, value.inputs.type.value); } } + organizeOutputs(root_obj, workflow_name); } // change @@ -699,8 +595,6 @@ function hideWidget( } } - - // Hide any linked widgets, e.g. seed+seedControl if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { @@ -709,27 +603,11 @@ function hideWidget( } } - - 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 + widget ) { hideWidget(node, widget) - - const { type } = getWidgetType(config) - // Add input and store widget config for creating on primitive node const [oldWidth, oldHeight] = node.size @@ -744,34 +622,358 @@ function hideWidget( ]) return node } - -export function addInputs(root_obj, inputs, widgets_values){ - - let start_index = 2; - for (let [key, value] of Object.entries(inputs)){ - const isinput = root_obj.inputs.find(i => i.name == value.inputs[0]); - const isWidget = root_obj.widgets.find(w => w.name == value.inputs[0]); +export function getDefaultOptions(type, value = 0){ + let options = {}; + switch(type){ + case "INT": + options = {default: value, "min": 0, "max": 18446744073709551616, "step": 1}; + break; + case "FLOAT": + options = {default: value, "min": 0.00, "max": 2048.00, "step": 0.01}; + break; + default: + options = {}; + } + return options; +} + + +export function addInputs(node, inputs, widgets_values, initial = false) { + // Phase 1: Préparer les données triées et simplifiées + const mapped_input = Object.entries(inputs) + .sort(([, a], [, b]) => a.position - b.position) + .map(([, input], index) => { + return { + name: input.inputs[0], + type: input.inputs[1], + value: input.inputs.length > 2 ? input.inputs[2] : undefined, + orderIndex: index // Ajouter un index d'ordre basé sur la position + }; + }); + + // Séparer les inputs pour un traitement différencié + const widget_inputs = mapped_input.filter(input => input.value !== undefined && input.value !== null); + const pure_inputs = mapped_input.filter(input => input.value === undefined || input.value === null); + + // S'assurer que le nœud a un objet local_input_defs + if (!node.local_input_defs) { + node.local_input_defs = { + required: {}, + optional: {} + }; + } + + // Phase 2: Ajouter les nouveaux widgets/inputs avec leur index d'ordre + for (const input of mapped_input) { + const { name, type, value, orderIndex } = input; + let isWidget = node.widgets.find(w => w.name === name); + let isinput = node.inputs.find(i => i.name === name); - if (!isWidget && (!isinput||isinput.widget)){ - if (value.inputs[2]){ - const values = widgets_values[start_index] ?? value.inputs[2]; - const widget_param = {value: values, type: value.inputs[1]} - //const widget_param = {value: widgets_values[start_index], type: value.inputs.default.type} - addWidgets(root_obj, value.inputs[0], widget_param, app); - if (isinput){ - //node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)) - const config = [value.inputs[2]] - convertToInput(root_obj, root_obj.widgets[start_index], config) + if (!isWidget && (!isinput || isinput.widget)) { + if (value !== undefined) { + // Ajouter un widget avec sa valeur et son index d'ordre + const widget_param = { value, type }; + addWidgets(node, name, widget_param, app); + isWidget = node.widgets.find(w => w.name === name); + isWidget.orderIndex = orderIndex; // Stocker l'index d'ordre + + if (isinput && !isinput.widget) { + convertToInput(node, isWidget); + // Préserver l'index d'ordre sur l'input converti + const newInput = node.inputs.find(i => i.name === name); + if (newInput) newInput.orderIndex = orderIndex; + newInput.pos = isWidget.pos; + } + } else { + // C'est un input pur + if (!isinput) { + node.addInput(name, type); + const newInput = node.inputs[node.inputs.length - 1]; + newInput.orderIndex = orderIndex; // Stocker l'index d'ordre + } + } + } else { + // Mettre à jour l'index d'ordre des éléments existants + if (isWidget) isWidget.orderIndex = orderIndex; + if (isinput) isinput.orderIndex = orderIndex; + } + } + + // Phase 3: Mettre à jour les types si nécessaires + for (const input of mapped_input) { + const { name, type, value } = input; + let isWidget = node.widgets.find(w => w.name === name); + let isinput = node.inputs.find(i => i.name === name); + + if (isWidget || isinput) { + const localType = node.local_input_defs?.required[name] || null; + + // Vérifier si le type est différent et le mettre à jour + if (localType && localType[0] !== type) { + if (isinput) { + if (isinput.link) { + node.graph.removeLink(isinput.link); + } + isinput.type = type; + } + + if (isWidget) { + isWidget.type = type; + const options = getDefaultOptions(type, value); + node.local_input_defs.required[name] = [type, options]; + isWidget.options = options; + isWidget.value = value !== undefined ? value : isWidget.value; + } + } + } + } + + // Phase 4: Réorganiser les widgets selon leur index d'ordre + // Séparer les widgets système (les 2 premiers) des widgets à trier + const systemWidgets = node.widgets.slice(0, 2); + const sortableWidgets = node.widgets.slice(2); + + // Trier les widgets par index d'ordre + sortableWidgets.sort((a, b) => { + if (a.orderIndex === undefined) return 1; + if (b.orderIndex === undefined) return -1; + return a.orderIndex - b.orderIndex; + }); + + // Reconstruire le tableau des widgets + node.widgets = [...systemWidgets, ...sortableWidgets]; + + // Maintenant ajuster les positions visuelles des widgets + for (let i = 2; i < node.widgets.length; i++) { + node.widgets[i].y = node.widgets[i-1].y + (node.widgets[i-1].computedHeight || LiteGraph.NODE_WIDGET_HEIGHT); + node.widgets[i].last_y = node.widgets[i].y; + } + + // Réorganiser les inputs purs + for (let i = 0; i < pure_inputs.length; i++) { + const targetInput = pure_inputs[i]; + const actualIndex = node.inputs.findIndex(input => input.name === targetInput.name); + + if (actualIndex !== -1) { + // const actualIndex = node.inputs.indexOf(pureNodeInputs[inputIndex]); + const expected_position = i; + + if (actualIndex !== expected_position) { + // Déconnecter temporairement le lien s'il existe + let link = null; + if (node.inputs[actualIndex].link) { + link = node.graph.links[node.inputs[actualIndex].link]; + node.graph.removeLink(node.inputs[actualIndex].link); + } + + // Sauvegarder les propriétés importantes + const inputToMove = node.inputs[actualIndex]; + const inputName = inputToMove.name; + const inputType = inputToMove.type; + const inputWidget = inputToMove.widget; + const inputPos = inputToMove.pos; + + // Supprimer l'input + node.removeInput(actualIndex); + + // Ajouter à la position attendue + node.addInput(inputName, inputType, {pos: inputPos, widget: inputWidget}, expected_position); + + // Reconnecter le lien si nécessaire + if (link && link.origin_id !== null) { + const newIndex = node.inputs.findIndex(input => input.name === targetInput.name); + link.target_slot = newIndex; + node.graph.links[link.id] = link; + node.inputs[newIndex].link = link.id; } - start_index += 1; - }else{ - const input_param = {type: value.inputs[1]}; - addWidgets(root_obj, value.inputs[0], input_param, app); } } } + // Phase 6: S'assurer que les inputs avec widget sont toujours à la fin de la liste + const inputsWithWidgets = []; + const inputsWithoutWidgets = []; + + // 1. Trier les inputs en deux groupes + for (let i = 0; i < node.inputs.length; i++) { + if (node.inputs[i].widget) { + inputsWithWidgets.push(i); + } else { + inputsWithoutWidgets.push(i); + } + } + + // 2. Si des inputs avec widgets ne sont pas déjà à la fin, les réorganiser + if (inputsWithWidgets.length > 0 && inputsWithWidgets[0] < inputsWithoutWidgets[inputsWithoutWidgets.length - 1]) { + // Déplacer tous les inputs avec widgets à la fin + for (let i = 0; i < inputsWithWidgets.length; i++) { + const currentIndex = inputsWithWidgets[i] - i; // Ajuster l'index car la liste change à chaque suppression + + // Sauvegarder les informations de l'input et son lien + let link = null; + if (node.inputs[currentIndex].link) { + let link = null; + if (node.inputs[actualIndex].link) { + link = node.graph.links[node.inputs[actualIndex].link]; + node.graph.removeLink(node.inputs[actualIndex].link); + } + } + + // Sauvegarder les propriétés importantes + const inputToMove = node.inputs[currentIndex]; + const inputName = inputToMove.name; + const inputType = inputToMove.type; + const inputWidget = inputToMove.widget; + const inputPos = inputToMove.pos; + + node.removeInput(currentIndex); + node.addInput(inputName, inputType, {pos: inputPos, widget: inputWidget}); + + // Reconnecter le lien si nécessaire + if (link && link.origin_id !== null) { + const newIndex = node.inputs.findIndex(input => input.name === inputName); + if (node.graph) + node.graph.links[link.id].target_slot = newIndex; + node.inputs[expected_position].link = link.id; + + } + if (link && link.origin_id !== null) { + const newIndex = node.inputs.findIndex(input => input.name === inputName); + link.target_slot = newIndex; + node.graph.links[link.id] = link; + node.inputs[newIndex].link = link.id; + } + + } + + // Rafraîchir le canvas + if (node.graph) { + node.graph.setDirtyCanvas(true); + } + } + + // Rafraîchir le canvas si nécessaire + if (node.graph) { + node.graph.setDirtyCanvas(false, true); + //node.graph.afterChange(); + node.graph.connectionChange(this); + } +} + +export function organizeOutputs(node, workflow_name) { + const outputs = app.lipsync_studio[workflow_name].outputs; + + // Extraire et trier les outputs selon leur position + const sortedOutputs = Object.entries(outputs) + .sort(([, a], [, b]) => a.position - b.position) + .map(([, output]) => ({ + name: output.inputs.Name.value, + type: output.inputs.type.value + })); + + // Réorganiser les outputs selon l'ordre déterminé + for (let i = 0; i < sortedOutputs.length; i++) { + const targetOutput = sortedOutputs[i]; + const actualIndex = node.outputs.findIndex(output => output.name === targetOutput.name); + + if (actualIndex !== -1 && actualIndex !== i) { + // Sauvegarder les liens existants + let links = []; + if (node.outputs[actualIndex].links && node.outputs[actualIndex].links.length > 0) { + for (const linkId of node.outputs[actualIndex].links) { + const linkInfo = node.graph.links[linkId]; + if (linkInfo) { + links.push({ + id: linkId, + target_id: linkInfo.target_id, + target_slot: linkInfo.target_slot + }); + // Déconnecter temporairement + node.graph.removeLink(linkId); + } + } + } + + // Sauvegarder les propriétés importantes + const outputToMove = node.outputs[actualIndex]; + const outputName = outputToMove.name; + const outputType = outputToMove.type; + + // Supprimer l'output + node.removeOutput(actualIndex); + + // Ajouter à la position attendue + node.addOutput(outputName, outputType, i); + + // Reconnecter les liens sauvegardés + for (const link of links) { + node.graph.connect(node.id, i, link.target_id, link.target_slot); + } + } + } + + // Rafraîchir le canvas + if (node.graph) { + node.graph.setDirtyCanvas(true); + } +} + + +export function removeInputs(node, inputs, widgets_values){ + // Ensemble des noms d'entrées valides à partir de inputs + const validInputNames = new Set(); + for (let [key, value] of Object.entries(inputs)) { + if (value.inputs && value.inputs[0]) { + validInputNames.add(value.inputs[0]); + } + } + + // Noms à préserver quoi qu'il arrive + const preserveNames = new Set(["workflows", "workflow"]); + + // Collecter les noms d'entrées actuelles + const currentInputNames = new Set(); + for (let input of node.inputs) + if (!preserveNames.has(input.name)) + currentInputNames.add(input.name); + + // Collecter les noms de widgets actuels (à partir de l'index 2) + const currentWidgetNames = new Set(); + for (let i = 2; i < node.widgets.length; i++) + if (!preserveNames.has(node.widgets[i].name)) + currentWidgetNames.add(node.widgets[i].name); + + // Identifier les éléments à supprimer (ceux qui sont dans current mais pas dans valid) + const inputsToRemove = [...currentInputNames].filter(name => !validInputNames.has(name)); + const widgetsToRemove = [...currentWidgetNames].filter(name => !validInputNames.has(name)); + + // Supprimer les entrées identifiées (en parcourant en sens inverse) + if (inputsToRemove.length > 0) + for (let i = node.inputs.length - 1; i >= 0; i--) + if (inputsToRemove.includes(node.inputs[i].name)){ + // Déconnecter le lien s'il existe + if (node.inputs[i].link != null) + node.graph.removeLink(node.inputs[i].link); + node.removeInput(i); + } + + // Supprimer les widgets identifiés (en parcourant en sens inverse) + if (widgetsToRemove.length > 0) + for (let i = node.widgets.length - 1; i >= 2; i--) + if (widgetsToRemove.includes(node.widgets[i].name)) { + widgetName = node.widgets[i].name; + node.widgets.splice(i, 1); + widgets_values.splice(i, 1); + if (node.local_input_defs && node.local_input_defs.required[widgetName]){ + delete node.local_input_defs.required[widgetName]; + } + // change y and last_y of widgets + for (let j = i; j < node.widgets.length; j++){ + node.widgets[j].y -= node.widgets[j].computedHeight; + node.widgets[j].last_y -= node.widgets[j].computedHeight; + } + } } app.registerExtension({ @@ -785,71 +987,37 @@ app.registerExtension({ case "Workflow": nodeType.prototype.onNodeCreated = function() { chainCallback(this, "onConfigure", function(info) { - //let widgetDict = info.widgets_values if (info.widgets_values[0] != "None"){ - //this.widgets[1].value = importWorkflow(this, info.widgets_values[0], app, nodeData) - //if (this.widgets.length==2){ - const inputs = app.lipsync_studio[info.widgets_values[0]].inputs; - addInputs(this, inputs, info.widgets_values); - addOutputs(this, info.widgets_values[0]); - importWorkflow(this, info.widgets_values[0], app, nodeData) - .then(data => { - if (data){ - this.widgets[1].value = data; - const inputs = app.lipsync_studio[info.widgets_values[0]].inputs; - addInputs(this, inputs, info.widgets_values); - addOutputs(this, info.widgets_values[0]); - //importWorkflow(this, info.widgets_values[0], app, nodeData) - } - }) - .catch(error => { - console.error('Erreur lors de l\'importation:', error); - }); - - /* - if (info.widgets_values[1] != this.widgets[1].value){ - this.widgets[1].value = info.widgets_values[1]; - } - 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} - addWidgets(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; + const inputs = app.lipsync_studio[info.widgets_values[0]].inputs; + + addInputs(this, inputs, info.widgets_values); + addOutputs(this, info.widgets_values[0]); + removeInputs(this, inputs, info.widgets_values); + fitHeight(this); + importWorkflow(this, info.widgets_values[0], app, nodeData) + .then(data => { + if (data){ + this.widgets[1].value = data; + const inputs = app.lipsync_studio[info.widgets_values[0]].inputs; + + addInputs(this, inputs, info.widgets_values); + addOutputs(this, info.widgets_values[0]); + removeInputs(this, inputs, info.widgets_values); + fitHeight(this); + //importWorkflow(this, info.widgets_values[0], app, nodeData) } - }*/ - //} - /* - if(info.widgets_values[0] != "None") - api.fetchApi("/flowchain/workflow?workflow_path="+info.widgets_values[0]) - .then(response => response.json()) - .then(async data => { - cleanInputs(this); - app.lipsync_studio[info.widgets_values[0]] = data; - addInputs(this, info.widgets_values[0], info.widgets_values); - }) - .catch(error => { - console.error('Erreur lors de l\'importation:', error); - });*/ + }) + .catch(error => { + console.error('Erreur lors de l\'importation:', error); + }); } }); 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]; + for (let inp of this.inputs){ + if (inp.widget){ + if (inp.type != this.local_input_defs.required[inp.name][0]) + inp.type = this.local_input_defs.required[inp.name][0]; } } if(this.widgets[0].options.values == "COMBO"){ @@ -857,8 +1025,6 @@ app.registerExtension({ } }); const workflow_reload = this.title.startsWith("Workflow: ")?true:false; - - // nodeData.input.required.workflows = ["COMBO", {values:["None", ...Object.keys(app.lipsync_studio)]}] this.widgets[0].options.values = ["None", ...Object.keys(app.lipsync_studio)] this.widgets[0].callback = ( value ) => { cleanInputs(this); @@ -867,8 +1033,9 @@ app.registerExtension({ }else{ this.widgets[1].value = importWorkflow(this, value, app, nodeData); const inputs = app.lipsync_studio[value].inputs; - addInputs(this, inputs, {}); + addInputs(this, inputs, {}, true); addOutputs(this, value); + fitHeight(this); } }; if (!workflow_reload){ @@ -890,99 +1057,9 @@ app.registerExtension({ inputs["default"] = { inputs: ["default", info.widgets_values[1], info.widgets_values[2]] - /* - default:{value: info.widgets_values[2], type: info.widgets_values[1]}, - Name: {value: "default", type: "STRING"}, - type: {value: info.widgets_values[1], type: "COMBO"}*/ }; addInputs(this, inputs, info.widgets_values); - /* - const inputs = {}; - for (let [k, value] of Object.entries(info.widgets_values)) { - if (k == "default"){ - inputs[k] = { - inputs: { - default:{value: value.value, type: info.widgets_values.default.type}, - Name: {value: k, type: "STRING"}, - type: {value: info.widgets_values.default.type, type: "STRING"} - } - }; - }else{ - inputs[k] = { - inputs: { - default:{value: value.value, type: value.type}, - Name: {value: k, type: value.type}, - type: {value: value.type, type: value.type} - } - }; - } - } - let widgetDict = {}; - if(info.widgets_values.default) - widgetDict = [info.widgets_values.Name.value, info.widgets_values.default.type, info.widgets_values.default.value]; - else - widgetDict = [info.widgets_values.Name.value, info.widgets_values.type.value]; - - addInputs(this, inputs, widgetDict);*/ - /*if (info.widgets_values.length == undefined) { - - - for (let w of this.widgets) { - if (w.name in widgetDict) { - w.value = widgetDict[w.name].value; - } - } - - // check if widgetDict in this.widgets - for (let [key, value] of Object.entries(widgetDict)) { - let widget = this.widgets.find(w => w.name === key); - let type = this.widgets.find(w => w.name === "type"); - if(!widget){ - let widget_param = {} - if (value.name == "default"){ - widget_param = {value: value.value, type: type.value} - }else{ - widget_param = value; - } - // addWidgets(this, key, widget_param, app); - addInputs(this, {inputs: {Name: {value: key}, type: {value: type.value}}}, widgetDict); - widget = this.widgets.find(w => w.name === key); - } - //this.widgets.push(value); - widget.options = info.widgets_values[key].options; - widget.value = info.widgets_values[key].value; - //if value exists in inputs - - for (let input of this.inputs){ - if (input.name == key){ - //find if key exists in inputs array in inputs.Name - if (info.widgets_values[key].type != "converted-widget"){ - this.removeInput(this.inputs.indexOf(input)); - } - break; - } - } - - } - } - // get inputs by name - - for (let w of this.inputs){ - if (w.name=="default"){ - w.type = info.widgets_values.type.value; - } - } - if (info.outputs_values != undefined){ - // deep copy outputs - if(this.id == -1){ - this.outputs[0] = {links: null, name: info.outputs_values.name, type: info.outputs_values.type}; - }else{ - this.outputs[0] = {...info.outputs_values}; - } - } - this.setSize(info.size);*/ - }); chainCallback(this, "onSerialize", function(info) { let widgetDict = info.widgets_values; @@ -998,68 +1075,9 @@ app.registerExtension({ } } } - /* - info.widgets_values = {}; - if (!this.widgets) { - return; - } - - for (let w of this.widgets) { - if (w.name == "default"){ - const default_type = this.inputs.find(i => i.name == "default"); - if (default_type) - info.widgets_values[w.name] = {name: w.name, options : w.options, value: w.value, type: default_type.type}; - else - info.widgets_values[w.name] = {name: w.name, options : w.options, value: w.value, type: info.widgets_values.type.value}; - }else{ - info.widgets_values[w.name] = {name: w.name, options : w.options, value: w.value, type: w.type}; - } - }*/ - /* - for (let w of this.inputs){ - // if w.name exists in info.widgets_values - if (info.widgets_values[w.name]){ - w.type = info.widgets_values[w.name].type; - if(info.widgets_values[w.name].type == "converted-widget"){ - if(info.widgets_values[w.name].origType == "toggle"){ - w.type = "BOOLEAN"; - }else if(info.widgets_values[w.name].origType == "text"){ - w.type = "STRING"; - } - } - } - } - // deep copy outputs without reference - if (this.outputs.length > 0){ - if (this.outputs[0].links == null){ - info.outputs_values = {links: null, name: this.outputs[0].name, type: this.outputs[0].type}; - }else{ - info.outputs_values = {links: [...this.outputs[0].links], name: this.outputs[0].name, slot_index: this.outputs[0].slot_index, type: this.outputs[0].type}; - } - } - - 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);*/ cleanInputs(this); switch(value){ diff --git a/workflow.py b/workflow.py index e13d25e..9b34813 100644 --- a/workflow.py +++ b/workflow.py @@ -91,27 +91,7 @@ class Workflow(SaveImage): return m.digest().hex() def generate(self, workflows, workflow, **kwargs): - # get current file path - """ - def get_workflow(workflow_name): - 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: - 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 - """ 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(): @@ -345,44 +325,7 @@ class Workflow(SaveImage): # 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'): @@ -397,15 +340,6 @@ class Workflow(SaveImage): 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] - # 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 - """ - else: # Fallback to empty inputs if server instance not available original_inputs = {} diff --git a/workflow_nodes.py b/workflow_nodes.py index 2f11efb..c2862c8 100644 --- a/workflow_nodes.py +++ b/workflow_nodes.py @@ -122,7 +122,6 @@ class WorkflowOutput: OUTPUT_NODE = True - @classmethod def IS_CHANGED(s, Name, type, ui=True, **kwargs): m = hashlib.sha256() @@ -158,33 +157,12 @@ class WorkflowOutput: NODE_CLASS_MAPPINGS_NODES = { "WorkflowInput": WorkflowInput, "WorkflowOutput": WorkflowOutput, - - # "WorkflowInputImage": WorkflowInputImage, - # "WorkflowInputString": WorkflowInputString, - # "WorkflowInputBoolean": WorkflowInputBoolean, - # "WorkflowInputInteger": WorkflowInputInteger, - # "WorkflowInputFloat": WorkflowInputFloat, - # "WorkflowOutputImage": WorkflowOutputImage, - # "WorkflowInputSwitch": WorkflowInputSwitch, - # "WorkflowContinueImage": WorkflowContinueImage, - # "WorkflowContinueLatent": WorkflowContinueLatent, "WorkflowContinue": WorkflowContinue, - } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS_NODES = { "WorkflowInput": "Workflow Input (FlowChain ⛓️)", "WorkflowOutput": "Workflow Output (FlowChain ⛓️)", - # "WorkflowInputImage": "Workflow Input Image (Lipsync Studio)", - # "WorkflowInputString": "Workflow Input String (Lipsync Studio)", - # "WorkflowInputBoolean": "Workflow Input Boolean (Lipsync Studio)", - # "WorkflowInputInteger": "Workflow Input Integer (Lipsync Studio)", - # "WorkflowInputFloat": "Workflow Input Float (Lipsync Studio)", - # "WorkflowOutputImage": "Workflow Output Image (Lipsync Studio)", - # "WorkflowInputSwitch": "Workflow Input Switch (Lipsync Studio)", - # "WorkflowContinueImage": "Workflow Continue Image (Lipsync Studio)", - # "WorkflowContinueLatent": "Workflow Continue Latent (Lipsync Studio)", "WorkflowContinue": "Workflow Continue (FlowChain ⛓️)", - # "VisualizeOpticalFlow": "Visualize optical flow", }