diff --git a/README.md b/README.md
index d66373a..ba1c371 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
# ⛓️ Comfyui-FlowChain
## 💡 Description
+
This repository includes a set of custom nodes for ComfyUI that allow you to:
- - Convert your workflows into nodes
- - Chain your workflows together
- - Bonus: a node to integrate [LipSync Studio v0.6](https://www.patreon.com/Wav2LipStudio) via API (third-party application)
+- Convert your workflows into nodes
+- Chain your workflows together
+- Bonus: a node to integrate [LipSync Studio v0.6](https://www.patreon.com/Wav2LipStudio) via API (third-party application)
@@ -14,52 +15,62 @@ This repository includes a set of custom nodes for ComfyUI that allow you to:
## 📖 Quick Index
-* [🤔 Why, when the 'convert to group node' option already exists?](#-why-when-the-convert-to-group-node-option-already-exists)
-* [🚀 Updates](#-updates)
-* [🔗 requirements](#-requirements)
-* [💻 Installation](#-installation)
-* [🕸️ Nodes](#-nodes)
-* [🐍 Usage](#-usage)
-* [💪 Special things to know](#-special-things-to-know)
-* [📺 Examples](#-examples)
-* [😎 Contributing](#-contributing)
-* [🙏 Appreciation](#-appreciation)
-* [📜 License](#-license)
-* [☕ Support](#-support)
+
+- [🤔 Why, when the 'convert to group node' option already exists?](#-why-when-the-convert-to-group-node-option-already-exists)
+- [🚀 Updates](#-updates)
+- [🔗 requirements](#-requirements)
+- [💻 Installation](#-installation)
+- [🕸️ Nodes](#-nodes)
+- [🐍 Usage](#-usage)
+- [💪 Special things to know](#-special-things-to-know)
+- [📺 Examples](#-examples)
+- [😎 Contributing](#-contributing)
+- [🙏 Appreciation](#-appreciation)
+- [📜 License](#-license)
+- [☕ Support](#-support)
## 🤔 Why, when the 'convert to group node' option already exists?
+
The idea came from the frustration users experienced with the many limitations and bugs in ComfyUI’s group nodes. While group nodes can seem useful for organizing nodes hierarchically, their actual use often leads to issues. Here are some of the major flaws I aimed to address:
- - **Instability with primitive nodes**: Primitive nodes are often dropped when grouped, making them ineffective in complex workflows.
- - **Issues with dynamic nodes**: Dynamic nodes, such as those with switches or conditional behaviors, tend to lose connections when grouped.
- - **Incompatibility with ControlNet**: When integrating ControlNet nodes into group nodes, the order of inputs and outputs is crucial to avoid reversing results.
- - **Complex node maintenance**: Managing nodes in ComfyUI can quickly become overwhelming, especially when you need to modify the same function multiple times.
+
+- **Instability with primitive nodes**: Primitive nodes are often dropped when grouped, making them ineffective in complex workflows.
+- **Issues with dynamic nodes**: Dynamic nodes, such as those with switches or conditional behaviors, tend to lose connections when grouped.
+- **Incompatibility with ControlNet**: When integrating ControlNet nodes into group nodes, the order of inputs and outputs is crucial to avoid reversing results.
+- **Complex node maintenance**: Managing nodes in ComfyUI can quickly become overwhelming, especially when you need to modify the same function multiple times.
By solving these problems, my tool makes node management more intuitive, stable, and efficient, allowing you to focus on what matters: creating.
## 🚀 Updates
+
+**2025.05.12**
+
+- 🛠️ Fixed instabilities for comfyui version > 0.3.27 (thanks to [o0oradaro0o](https://github.com/o0oradaro0o))
+
**2025.03.26**
- - 💪 Live update of workflow when modifying a subworkflow, just 1 second after saving the subworkflow. But you need to back and forth to see the changes. if you work on one screen no problem, but if you work on two screens you need to back and forth to see the changes.
- - ↕️ Capability to order inputs and outputs in the workflow node. Order depends on the order of the input/output from top to bottom in the subworkflow.
- - 🛠️ Fixed some instabilities.
- - 🆙 Must work with ComfyUI 1.11.8 and above.
+- 💪 Live update of workflow when modifying a subworkflow, just 1 second after saving the subworkflow. But you need to back and forth to see the changes. if you work on one screen no problem, but if you work on two screens you need to back and forth to see the changes.
+- ↕️ Capability to order inputs and outputs in the workflow node. Order depends on the order of the input/output from top to bottom in the subworkflow.
+- 🛠️ Fixed some instabilities.
+- 🆙 Must work with ComfyUI 1.11.8 and above.
**2025.03.23**
First of all, I apologize for not maintaining the code regularly. ComfyUI has evolved significantly since my last version, so I've completely revised the code to ensure better integration with ComfyUI and avoid future issues.
Features in this version:
+
- 💪 No need to convert your workflows to API format anymore - you can use original workflows directly.
- 💪 Better cache management
- 🗑️ remove "import workflow", no longer usefull since working on standart workflow.
--
+-
Warning and know issue:
+
- 🛑 old version will not be compatible, sorry about that
- ⚠️ The word "seed" in input as "INT" will break the node so if you wan't use "seed" as input prefer to give a name like "seed_input" or whatever...
-
**2024.11.01 Initial version features :**
1 Convert your workflows into nodes
+
- ⛓️ Chain your workflow
- 👄 Extra Node that use [LipSync Studio v0.6](https://www.patreon.com/Wav2LipStudio)
@@ -70,10 +81,12 @@ Warning and know issue:
## 💻 Installation
### Method 1: Automatic Installation
+
1. Go to the **ComfyUI Manager** and click **Custom Nodes Manager**.
2. search for **FlowChain** and click **Install**.
### Method 2: Manual Installation
+
1. Install [Git](https://git-scm.com/)
2. Go to folder ..\ComfyUI\custom_nodes
3. Run cmd.exe
@@ -82,36 +95,36 @@ Warning and know issue:
> > **Variant 1:** In folder click panel current path and input **cmd** and press **Enter** on keyboard
> >
> > **Variant 2:** Press on keyboard Windows+R, and enter cmd.exe open window cmd, enter **cd /d your_path_to_custom_nodes**, **Enter** on keyboard
-4. Then do :
+4. Then do :
-```git clone https://github.com/numz/Comfyui-FlowChain.git```
+`git clone https://github.com/numz/Comfyui-FlowChain.git`
After this command be created folder Comfyui-FlowChain
-8. Go to the folder:
+8. Go to the folder:
-```cd Comfyui-FlowChain```
+`cd Comfyui-FlowChain`
8. Then do:
-```pip install -r requirements.txt```
+`pip install -r requirements.txt`
7. Run Comfyui...
## 🕸️ Nodes:
-| | Name | Description | ComfyUI category |
-|:-------------------------------------------------:|:--------------------|:------------------------------------------------------------------------------------------------------------:|:----------------:|
-|
| _Workflow_ | Node that allows loading workflows. It will show Inputs and Outputs into the loaded Workflows | FlowChain ⛓️ |
-|
| _Workflow Input_ | Node used to declare the inputs of your workflows. | FlowChain ⛓️ |
-|
| _Workflow Output_ | Node used to declare the outputs of your workflows. | FlowChain ⛓️ |
-|
| _Workflow Continue_ | Node to stop/Continue the workflow process. | FlowChain ⛓️ |
-|
| _Workflow Lipsync_ | Extra Node to use LipSync Studio via API | FlowChain ⛓️ |
-
+| | Name | Description | ComfyUI category |
+| :-----------------------------------------------: | :------------------ | :-------------------------------------------------------------------------------------------: | :--------------: |
+|
| _Workflow_ | Node that allows loading workflows. It will show Inputs and Outputs into the loaded Workflows | FlowChain ⛓️ |
+|
| _Workflow Input_ | Node used to declare the inputs of your workflows. | FlowChain ⛓️ |
+|
| _Workflow Output_ | Node used to declare the outputs of your workflows. | FlowChain ⛓️ |
+|
| _Workflow Continue_ | Node to stop/Continue the workflow process. | FlowChain ⛓️ |
+|
| _Workflow Lipsync_ | Extra Node to use LipSync Studio via API | FlowChain ⛓️ |
# 🐍 Usage
## ⛓️ Workflow Node
+

Select a workflow from the **workflows** dropdown menu. This list displays all compatible workflows saved in your ComfyUI user directory:
@@ -121,34 +134,37 @@ Select a workflow from the **workflows** dropdown menu. This list displays all c
For a workflow to be considered **compatible**, it must contain at least one "Workflow Input" or "Workflow Output" node. This ensures the workflow can properly interface with the FlowChain system.
**Note:** After adding new workflows to this directory, you'll need to refresh the ComfyUI interface (press F5) to see them appear in the dropdown list. But once loaded, it will refresh the parent node automatically
-
+
## ⛓️ Input Node
+

- Allow to declare inputs in your workflow.
-- Types available : **"IMAGE", "MASK", "STRING", "INT", "FLOAT", "LATENT", "BOOLEAN", "CLIP", "CONDITIONING", "MODEL", "VAE"**
+- Types available : **"IMAGE", "MASK", "STRING", "INT", "FLOAT", "LATENT", "BOOLEAN", "CLIP", "CONDITIONING", "MODEL", "VAE"**
- Give a Name and select the type.
- **Default** value is used when debugging your workflow or if you don't plug an input into the **Workflow** node.
- 
## ⛓️ Output Node
+

- Allow to declare outputs in your workflow.
-- Types available : **"IMAGE", "MASK", "STRING", "INT", "FLOAT", "LATENT", "BOOLEAN", "CLIP", "CONDITIONING", "MODEL", "VAE"**
+- Types available : **"IMAGE", "MASK", "STRING", "INT", "FLOAT", "LATENT", "BOOLEAN", "CLIP", "CONDITIONING", "MODEL", "VAE"**
- Give a Name and select the type.
- **Default** value is used to connect the output.

## ⛓️ Continue Node
+

- Usually associated with a **boolean** input plugged on **"continue_workflow"**, allow to "Stop" a workflow if **"continue_workflow"** is False.
-- Types available : **"IMAGE", "LATENT"**
+- Types available : **"IMAGE", "LATENT"**
- Give a Name and select the type.
-- During development of your workflow, If **continue_worflow" is False it will let pass only 1 image/latent, and if True it will let pass all images/latents.
+- During development of your workflow, If \*\*continue_worflow" is False it will let pass only 1 image/latent, and if True it will let pass all images/latents.

@@ -157,23 +173,24 @@ For a workflow to be considered **compatible**, it must contain at least one "Wo

## 🔉👄 Workflow LipSync Node
+

- Extra Node that allow to use third-party app **[Lipsync Studio v0.6](https://www.patreon.com/Wav2LipStudio)** Via it's API
- Inputs:
- - **frames**: Images to compute.
- - **audio**: Audio to add.
- - **faceswap_image**: An image with a face to swap.
- - **lipsync_studio_url**: usually http://127.0.0.1:7860
- - **project_name**: name of your project.
- - **face_id**: id of the face you want to lipsync and faceswap.
- - **fps**: frame per second.
- - **avatar**: Will be used create a driving video, 10 avatars are available, each give different output result.
- - **close mouth before Lip sync**: Allow to close the mouth before create the lip sync.
- - **quality**: Can be **Low, Medium, High**, in High gfpgan will be used to enhance quality output.
- - **skip_first_frame**: number of frames to remove at the beginning of the video.
- - **load_cap**: number of frames to load.
- - **low vram**: allow to decrease VRAM consumption for low pc configuration.
+ - **frames**: Images to compute.
+ - **audio**: Audio to add.
+ - **faceswap_image**: An image with a face to swap.
+ - **lipsync_studio_url**: usually http://127.0.0.1:7860
+ - **project_name**: name of your project.
+ - **face_id**: id of the face you want to lipsync and faceswap.
+ - **fps**: frame per second.
+ - **avatar**: Will be used create a driving video, 10 avatars are available, each give different output result.
+ - **close mouth before Lip sync**: Allow to close the mouth before create the lip sync.
+ - **quality**: Can be **Low, Medium, High**, in High gfpgan will be used to enhance quality output.
+ - **skip_first_frame**: number of frames to remove at the beginning of the video.
+ - **load_cap**: number of frames to load.
+ - **low vram**: allow to decrease VRAM consumption for low pc configuration.
Project will be automatically created into your Lipsync Studio **projects** folder. You can then load it into studio and work directly from studio if the output not good enough for you.
@@ -181,7 +198,7 @@ Project will be automatically created into your Lipsync Studio **projects** fold
## 💪 Special things to know
- the **"🪛 Switch"** nodes from [Crystools](https://github.com/crystian/ComfyUI-Crystools) have a particular place in **workflow Node**
+the **"🪛 Switch"** nodes from [Crystools](https://github.com/crystian/ComfyUI-Crystools) have a particular place in **workflow Node**

@@ -203,9 +220,9 @@ Coming soon
We welcome contributions to this project. When submitting pull requests, please provide a detailed description of the changes. see [CONTRIBUTING](CONTRIBUTING.md) for more information.
-# 🙏 Appreciation
-- [Jedrzej Kosinski](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) : For the code quality that really inspired me during development.
+# 🙏 Appreciation
+- [Jedrzej Kosinski](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) : For the code quality that really inspired me during development.
# ☕ Support
@@ -218,4 +235,5 @@ Your support helps me cover the costs of development and maintenance, and allows
[patreon page](https://www.patreon.com/Wav2LipStudio)
# 📜 License
-* The code in this repository is released under the MIT license as found in the [LICENSE file](LICENSE).
+
+- The code in this repository is released under the MIT license as found in the [LICENSE file](LICENSE).
diff --git a/web/js/constants.js b/web/js/constants.js
index 8ac1050..d8c59c6 100644
--- a/web/js/constants.js
+++ b/web/js/constants.js
@@ -1,3 +1,63 @@
-export const colors = ["#222222", "#5940bb", "#FFFFFF", "#7cbb1a", "#29699c", "#777788", "#268bd2", "#2ab7ca", "#d33682", "#dc322f", "#facfad","#77ff77", "#5940bb"]
-export const bg_colors = ["#000000", "#392978", "#89888d", "#496c12", "#19466a", "#4b4b56", "#165481", "#176974", "#851f50", "#911e1c", "#9f826b", "#499f49", "#392978"]
-export const node_type_list = ["none", "IMAGE", "MASK", "STRING", "INT", "FLOAT", "LATENT", "CLIP", "CONDITIONING", "MODEL", "VAE", "BOOLEAN", "SWITCH"]
+export const colors = [
+ "#222222",
+ "#5940bb",
+ "#AFFFFF",
+ "#7cbb1a",
+ "#29699c",
+ "#777788",
+ "#268bd2",
+ "#2ab7ca",
+ "#d33682",
+ "#dc322f",
+ "#facfad",
+ "#77ff77",
+ "#6940bb",
+ "#165481",
+ "#176974",
+ "#851f50",
+ "#911e1c",
+ "#9f826b",
+ "#499f49",
+ "#392978",
+];
+export const bg_colors = [
+ "#000000",
+ "#392978",
+ "#59888d",
+ "#496c12",
+ "#19466a",
+ "#4b4b56",
+ "#165481",
+ "#176974",
+ "#851f50",
+ "#911e1c",
+ "#9f826b",
+ "#499f49",
+ "#192978",
+ "#065481",
+ "#076974",
+ "#551f50",
+ "#611e1c",
+ "#7f826b",
+ "#199f49",
+ "#092978",
+];
+export const node_type_list = [
+ "none",
+ "IMAGE",
+ "MASK",
+ "STRING",
+ "INT",
+ "FLOAT",
+ "LATENT",
+ "CLIP",
+ "CONDITIONING",
+ "MODEL",
+ "VAE",
+ "BOOLEAN",
+ "SWITCH",
+ "AUDIO"
+];
+export const string_widget = [
+ "STRING"
+];
diff --git a/web/js/inputs.js b/web/js/inputs.js
index 8d99c01..8c881cf 100644
--- a/web/js/inputs.js
+++ b/web/js/inputs.js
@@ -1,454 +1,489 @@
import { addWidgets, convertToInput, getDefaultOptions } from "./widgets.js";
+import { string_widget } from "./constants.js";
-export function cleanInputs(root_obj, start_index = 2, reset_value=true) {
-
- 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);
- }
- }
+export function cleanInputs(root_obj, start_index = 2, reset_value = true) {
+ 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);
+ }
}
-
- // 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);
- }
+ }
+
+ // 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, start_index);
+ if (reset_value) {
+ root_obj.widgets_values = [];
+ const max_node_output = root_obj.outputs.length;
+ for (let i = 0; i < max_node_output; i++) root_obj.removeOutput(0);
+ }
- root_obj.widgets = root_obj.widgets.splice(0,start_index)
- if(reset_value){
- root_obj.widgets_values = [];
- const max_node_output = root_obj.outputs.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);
- }
+ // Parcourir les entrées en sens inverse pour éviter les problèmes d'indice
+ for (let i = root_obj.inputs.length - 1; i >= 0; i--) {
+ const input = root_obj.inputs[i];
+ if ((input.name === "default" || input.name === "input") && input.link) {
+ root_obj.graph.removeLink(input.link);
}
+ }
- for(let i = 0; i= 0; i--)
- root_obj.removeOutput(i);
- /*
+ const max_node_input = root_obj.outputs.length;
+ for (let i = root_obj.outputs.length - 1; i >= 0; i--)
+ root_obj.removeOutput(i);
+ /*
if (root_obj.graph) {
root_obj.graph.setDirtyCanvas(true);
root_obj.graph.change();
}*/
-
}
-
-export function addOutputs(root_obj, workflow_name){
- const outputs = app.lipsync_studio[workflow_name].outputs;
- for (let [key, value] of Object.entries(outputs)){
- const isoutput = root_obj.outputs.find(i => i.name == value.inputs.Name.value);
- if (!isoutput){
- root_obj.addOutput(value.inputs.Name.value, value.inputs.type.value);
- }
+export function addOutputs(root_obj, workflow_name) {
+ const outputs = app.lipsync_studio[workflow_name].outputs;
+ for (let [key, value] of Object.entries(outputs)) {
+ const isoutput = root_obj.outputs.find(
+ (i) => i.name == value.inputs.Name.value
+ );
+ if (!isoutput) {
+ root_obj.addOutput(value.inputs.Name.value, value.inputs.type.value);
}
- organizeOutputs(root_obj, workflow_name);
+ }
+ organizeOutputs(root_obj, workflow_name);
}
-
export function addInputs(node, inputs, widgets_values) {
- // 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);
- // replace value in widget_inputs with the value in widgets_values
- if (widgets_values){
- for (let i = 0; i < widget_inputs.length; i++) {
- const widget_input = widget_inputs[i];
- const widget_value = widgets_values[2 + i];
- //find name in mapped_input and replace value
- if (widget_value)
- for (let j = 0; j < mapped_input.length; j++)
- if (mapped_input[j].name == widget_input.name){
- mapped_input[j].value = widget_value;
- break;
- }
- }
- }
-
- 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 !== 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;
+ // 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
+ };
});
-
- // 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;
+
+ // Séparer les inputs pour un traitement différencié
+ const widget_inputs = mapped_input.filter(
+ (input) => input.value !== undefined && input.value !== null
+ );
+ // replace value in widget_inputs with the value in widgets_values
+ if (widgets_values) {
+ for (let i = 0; i < widget_inputs.length; i++) {
+ const widget_input = widget_inputs[i];
+ const widget_value = widgets_values[2 + i];
+ //find name in mapped_input and replace value
+ if (widget_value)
+ for (let j = 0; j < mapped_input.length; j++)
+ if (mapped_input[j].name == widget_input.name) {
+ mapped_input[j].value = widget_value;
+ break;
+ }
}
-
- // 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;
- }
- }
+ }
+
+ 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 !== undefined) {
+ // Ajouter un widget avec sa valeur et son index d'ordre
+ let widget_param = { value, type };
+ if (string_widget.includes(type))
+ widget_param = { value, type: "STRING" };
+ addWidgets(node, name, widget_param, app);
+ isWidget = node.widgets.find((w) => w.name === name);
+ if (orderIndex !== undefined) isWidget.orderIndex = orderIndex; // Stocker l'index d'ordre// 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 && orderIndex !== undefined)
+ newInput.orderIndex = orderIndex;
+ newInput.pos = isWidget.pos;
}
- }
-
- // 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);
+ } 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;
}
+ }
- // 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
- const currentWidget = node.inputs[currentIndex];
- // Sauvegarder les informations de l'input et son lien
- let link = null;
- if (currentWidget.link) {
- let link = null;
- if (currentWidget.link) {
- link = node.graph.links[currentWidget.link];
- node.graph.removeLink(currentWidget.link);
- }
- }
-
- // Sauvegarder les propriétés importantes
- const inputToMove = currentWidget;
- 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);
- node.graph.links[link.id].target_slot = newIndex;
- node.inputs[newIndex].link = link.id;
- }
- /*if (link && link.origin_id !== null) {
+ // 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;
+ const typeToUse = string_widget.includes(type) ? "STRING" : type;
+ // Vérifier si le type est différent et le mettre à jour
+ if (localType && localType[0] !== typeToUse) {
+ if (isinput) {
+ if (isinput.link) {
+ node.graph.removeLink(isinput.link);
+ }
+ isinput.type = typeToUse;
+ }
+
+ if (isWidget) {
+ isWidget.type = typeToUse;
+ const options = getDefaultOptions(typeToUse, value);
+ node.local_input_defs.required[name] = [typeToUse, 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;
+ }
+ }
+ }
+ }
+
+ // 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
+ const currentWidget = node.inputs[currentIndex];
+ // Sauvegarder les informations de l'input et son lien
+ let link = null;
+ if (currentWidget.link) {
+ let link = null;
+ if (currentWidget.link) {
+ link = node.graph.links[currentWidget.link];
+ node.graph.removeLink(currentWidget.link);
+ }
+ }
+
+ // Sauvegarder les propriétés importantes
+ const inputToMove = currentWidget;
+ 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
+ );
+ node.graph.links[link.id].target_slot = newIndex;
+ node.inputs[newIndex].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
+ // Rafraîchir le canvas
if (node.graph) {
- node.graph.setDirtyCanvas(false, true);
- //node.graph.afterChange();
- //node.graph.connectionChange(node);
+ 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(node);
+ }
}
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,
- origin_id: linkInfo.origin_id,
- origin_slot: linkInfo.origin_slot,
- 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);
- const newOutput = node.outputs.findIndex(output => output.name === outputName);
-
- // Reconnecter les liens sauvegardés
- for (const link of links) {
- const targetNode = node.graph.getNodeById(link.target_id);
- node.connect(newOutput, targetNode, link.target_slot);
- }
+ 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,
+ origin_id: linkInfo.origin_id,
+ origin_slot: linkInfo.origin_slot,
+ 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);
+ const newOutput = node.outputs.findIndex(
+ (output) => output.name === outputName
+ );
+
+ // Reconnecter les liens sauvegardés
+ for (const link of links) {
+ const targetNode = node.graph.getNodeById(link.target_id);
+ node.connect(newOutput, targetNode, link.target_slot);
+ }
}
-
- // Rafraîchir le canvas
- if (node.graph) {
- node.graph.setDirtyCanvas(true);
- }
+ }
+
+ // 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]);
- }
+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;
- }
- }
-}
\ No newline at end of file
+ }
+
+ // 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;
+ }
+ }
+}
diff --git a/web/js/nodetype_workflow.js b/web/js/nodetype_workflow.js
index 3b1fbc9..a36a761 100644
--- a/web/js/nodetype_workflow.js
+++ b/web/js/nodetype_workflow.js
@@ -5,229 +5,295 @@ import { hideWidget } from "./widgets.js";
// Graph-independent part of initialization (called onNodeCreated)
function initialisation_preGraph(node) {
- node.color = "#004670";
- node.bgcolor = "#002942";
+ node.color = "#004670";
+ node.bgcolor = "#002942";
- // Basic widget setup that doesn't depend on the graph
- if (node.widgets && node.widgets[0] && node.widgets[0].options) {
- if (app && app.lipsync_studio) {
- node.widgets[0].options.values = ["None", ...Object.keys(app.lipsync_studio)];
- } else {
- console.warn("App or lipsync_studio not available during preGraph setup for workflow node.");
- node.widgets[0].options.values = ["None"];
+ // Basic widget setup that doesn't depend on the graph
+ if (node.widgets && node.widgets[0] && node.widgets[0].options) {
+ if (app && app.lipsync_studio) {
+ node.widgets[0].options.values = [
+ "None",
+ ...Object.keys(app.lipsync_studio),
+ ];
+ } else {
+ console.warn(
+ "App or lipsync_studio not available during preGraph setup for workflow node."
+ );
+ node.widgets[0].options.values = ["None"];
+ }
+ }
+
+ const isReloading = node.title && node.title.startsWith("Workflow: ");
+ if (!isReloading && node.widgets && node.widgets[0]) {
+ node.widgets[0].value = "None"; // Default to "None"
+ if (node.widgets[1]) {
+ node.widgets[1].value = ""; // Clear workflow JSON
+ }
+ }
+
+ if (node.widgets && node.widgets[1]) {
+ hideWidget(node, node.widgets[1], { holdSpace: false });
+ }
+
+ // Setup the primary widget callback.
+ // Operations inside this callback that need the graph (like cleanInputs, addInputs, addOutputs)
+ // will be guarded by checking node.graph.
+ if (node.widgets && node.widgets[0]) {
+ node.widgets[0].callback = async (value) => {
+ if (!node.graph) {
+ console.warn(
+ "Workflow widget callback triggered, but node.graph is not yet available. Action might be deferred or skipped."
+ );
+ // If critical, you might need a flag to re-process this in onAdded
+ return;
+ }
+
+ cleanInputs(node); // Requires node.graph
+
+ if (value === "None") {
+ node.title = "Workflow (FlowChain ⛓️)";
+ // Further cleanup of dynamic inputs/outputs might be needed here
+ // For example, explicitly removing all but the essential widgets/inputs/outputs.
+ } else if (app && app.lipsync_studio && app.lipsync_studio[value]) {
+ try {
+ const workflowJSON = await importWorkflow(node, value, app); // importWorkflow updates node.title
+ if (node.widgets && node.widgets[1]) {
+ node.widgets[1].value = workflowJSON;
+ }
+
+ // Ensure app.lipsync_studio[value] (and its .inputs) is still valid after await
+ if (app.lipsync_studio[value] && app.lipsync_studio[value].inputs) {
+ const inputs = app.lipsync_studio[value].inputs;
+ addInputs(node, inputs, []); // Requires node.graph
+ addOutputs(node, value); // Requires node.graph
+ fitHeight(node);
+ } else {
+ console.error(
+ "Workflow data or inputs became unavailable after import for:",
+ value
+ );
+ node.title = "Workflow (FlowChain ⛓️)"; // Reset title on error
+ }
+ } catch (error) {
+ console.error(
+ "Error processing workflow selection in callback:",
+ error
+ );
+ node.title = "Workflow (FlowChain ⛓️)"; // Reset title on error
}
- }
-
- const isReloading = node.title && node.title.startsWith("Workflow: ");
- if (!isReloading && node.widgets && node.widgets[0]) {
- node.widgets[0].value = "None"; // Default to "None"
- if (node.widgets[1]) {
- node.widgets[1].value = ""; // Clear workflow JSON
- }
- }
-
- if (node.widgets && node.widgets[1]) {
- hideWidget(node, node.widgets[1], { holdSpace: false });
- }
-
- // Setup the primary widget callback.
- // Operations inside this callback that need the graph (like cleanInputs, addInputs, addOutputs)
- // will be guarded by checking node.graph.
- if (node.widgets && node.widgets[0]) {
- node.widgets[0].callback = async (value) => {
- if (!node.graph) {
- console.warn("Workflow widget callback triggered, but node.graph is not yet available. Action might be deferred or skipped.");
- // If critical, you might need a flag to re-process this in onAdded
- return;
- }
-
- cleanInputs(node); // Requires node.graph
-
- if (value === "None") {
- node.title = "Workflow (FlowChain ⛓️)";
- // Further cleanup of dynamic inputs/outputs might be needed here
- // For example, explicitly removing all but the essential widgets/inputs/outputs.
- } else if (app && app.lipsync_studio && app.lipsync_studio[value]) {
- try {
- const workflowJSON = await importWorkflow(node, value, app); // importWorkflow updates node.title
- if (node.widgets && node.widgets[1]) {
- node.widgets[1].value = workflowJSON;
- }
-
- // Ensure app.lipsync_studio[value] (and its .inputs) is still valid after await
- if (app.lipsync_studio[value] && app.lipsync_studio[value].inputs) {
- const inputs = app.lipsync_studio[value].inputs;
- addInputs(node, inputs, []); // Requires node.graph
- addOutputs(node, value); // Requires node.graph
- fitHeight(node);
- } else {
- console.error("Workflow data or inputs became unavailable after import for:", value);
- node.title = "Workflow (FlowChain ⛓️)"; // Reset title on error
- }
- } catch (error) {
- console.error("Error processing workflow selection in callback:", error);
- node.title = "Workflow (FlowChain ⛓️)"; // Reset title on error
- }
- } else {
- node.title = "Workflow (FlowChain ⛓️)";
- }
- };
- }
+ } else {
+ node.title = "Workflow (FlowChain ⛓️)";
+ }
+ };
+ }
}
// Graph-dependent part of initialization (called onAdded)
function initialisation_onAdded(node) {
- if (!node.graph) {
- console.error("CRITICAL: initialisation_onAdded called for workflow node, but node.graph is not set.");
- return;
- }
+ if (!node.graph) {
+ console.error(
+ "CRITICAL: initialisation_onAdded called for workflow node, but node.graph is not set."
+ );
+ return;
+ }
- // Perform an initial cleanInputs now that we are sure the graph exists.
- // This is important if the node was configured (e.g. from graph load) before being added.
- cleanInputs(node);
+ // Perform an initial cleanInputs now that we are sure the graph exists.
+ // This is important if the node was configured (e.g. from graph load) before being added.
+ cleanInputs(node);
- // If a workflow was already selected (e.g. loading a saved graph),
- // ensure its inputs/outputs are correctly set up.
- // The widget callback might have already run if `configure` set its value.
- // We re-evaluate or trigger necessary setup steps.
- if (node.widgets && node.widgets[0]) {
- const selectedWorkflow = node.widgets[0].value;
- if (selectedWorkflow && selectedWorkflow !== "None") {
- if (app && app.lipsync_studio && app.lipsync_studio[selectedWorkflow] && app.lipsync_studio[selectedWorkflow].inputs) {
- // If workflow JSON is loaded and inputs are known, ensure UI is consistent
- if (node.widgets[1] && (node.widgets[1].value === "" || typeof node.widgets[1].value !== 'string' || !node.widgets[1].value.startsWith('{'))) {
- // Workflow selected, but JSON not loaded in widget[1] or inputs/outputs not added by callback yet.
- // Trigger the callback logic.
- console.log(`Workflow node ${node.id}: Re-evaluating selected workflow '${selectedWorkflow}' onAdded.`);
- node.widgets[0].callback(selectedWorkflow);
- } else {
- // Workflow JSON likely loaded, ensure inputs/outputs are present
- // This can be a fallback if the callback didn't fully setup due to timing
- const inputs = app.lipsync_studio[selectedWorkflow].inputs;
- const currentWidgetValues = node.widgets_values || []; // from node.configure
- addInputs(node, inputs, currentWidgetValues);
- addOutputs(node, selectedWorkflow);
- removeInputs(node, inputs, currentWidgetValues); // Ensure this logic is sound for onAdded
- fitHeight(node);
- }
- } else if (selectedWorkflow !== "None") {
- // Workflow selected, but its definition isn't in app.lipsync_studio. Try to load it.
- console.warn(`Workflow node ${node.id}: '${selectedWorkflow}' selected but not in lipsync_studio. Attempting import via callback.`);
- node.widgets[0].callback(selectedWorkflow); // This will attempt importWorkflow
- }
+ // If a workflow was already selected (e.g. loading a saved graph),
+ // ensure its inputs/outputs are correctly set up.
+ // The widget callback might have already run if `configure` set its value.
+ // We re-evaluate or trigger necessary setup steps.
+ if (node.widgets && node.widgets[0]) {
+ const selectedWorkflow = node.widgets[0].value;
+ if (selectedWorkflow && selectedWorkflow !== "None") {
+ if (
+ app &&
+ app.lipsync_studio &&
+ app.lipsync_studio[selectedWorkflow] &&
+ app.lipsync_studio[selectedWorkflow].inputs
+ ) {
+ // If workflow JSON is loaded and inputs are known, ensure UI is consistent
+ if (
+ node.widgets[1] &&
+ (node.widgets[1].value === "" ||
+ typeof node.widgets[1].value !== "string" ||
+ !node.widgets[1].value.startsWith("{"))
+ ) {
+ // Workflow selected, but JSON not loaded in widget[1] or inputs/outputs not added by callback yet.
+ // Trigger the callback logic.
+ console.log(
+ `Workflow node ${node.id}: Re-evaluating selected workflow '${selectedWorkflow}' onAdded.`
+ );
+ node.widgets[0].callback(selectedWorkflow);
} else {
- node.title = "Workflow (FlowChain ⛓️)";
+ // Workflow JSON likely loaded, ensure inputs/outputs are present
+ // This can be a fallback if the callback didn't fully setup due to timing
+ const inputs = app.lipsync_studio[selectedWorkflow].inputs;
+ const currentWidgetValues = node.widgets_values || []; // from node.configure
+ addInputs(node, inputs, currentWidgetValues);
+ addOutputs(node, selectedWorkflow);
+ removeInputs(node, inputs, currentWidgetValues); // Ensure this logic is sound for onAdded
+ fitHeight(node);
}
+ } else if (selectedWorkflow !== "None") {
+ // Workflow selected, but its definition isn't in app.lipsync_studio. Try to load it.
+ console.warn(
+ `Workflow node ${node.id}: '${selectedWorkflow}' selected but not in lipsync_studio. Attempting import via callback.`
+ );
+ node.widgets[0].callback(selectedWorkflow); // This will attempt importWorkflow
+ }
+ } else {
+ node.title = "Workflow (FlowChain ⛓️)";
}
+ }
}
function configure(info) {
- // `this` is the node. Called when loading graph data.
- // `info.widgets_values` contains the saved values for widgets.
- if (!app || !app.lipsync_studio) {
- console.error("App or lipsync_studio not available during workflow node configure.");
- return;
- }
+ // `this` is the node. Called when loading graph data.
+ // `info.widgets_values` contains the saved values for widgets.
+ if (!app || !app.lipsync_studio) {
+ console.error(
+ "App or lipsync_studio not available during workflow node configure."
+ );
+ return;
+ }
- // Update widget options first, in case lipsync_studio has changed since last save
- if (this.widgets && this.widgets[0] && this.widgets[0].options) {
- this.widgets[0].options.values = ["None", ...Object.keys(app.lipsync_studio)];
- }
+ // Update widget options first, in case lipsync_studio has changed since last save
+ if (this.widgets && this.widgets[0] && this.widgets[0].options) {
+ this.widgets[0].options.values = [
+ "None",
+ ...Object.keys(app.lipsync_studio),
+ ];
+ }
- const selectedWorkflowName = info.widgets_values ? info.widgets_values[0] : "None";
+ let selectedWorkflowName = info.widgets_values
+ ? info.widgets_values[0]
+ : "None";
- if (this.widgets && this.widgets[0]) {
- this.widgets[0].value = selectedWorkflowName; // Set the widget value from saved data
- }
- if (this.widgets && this.widgets[1] && info.widgets_values && info.widgets_values[1]) {
- this.widgets[1].value = info.widgets_values[1]; // Set the hidden workflow JSON
- }
+ if (this.widgets && this.widgets[0]) {
+ this.widgets[0].value = selectedWorkflowName; // Set the widget value from saved data
+ }
+ if (
+ this.widgets &&
+ this.widgets[1] &&
+ info.widgets_values &&
+ info.widgets_values[1]
+ ) {
+ this.widgets[1].value = info.widgets_values[1]; // Set the hidden workflow JSON
+ }
-
- if (selectedWorkflowName === "None") {
- this.title = "Workflow (FlowChain ⛓️)";
- if (this.graph) { // Only clean if graph is available
- cleanInputs(this);
- }
- return;
- }
-
- // If the node is already on a graph, we can proceed with fuller setup.
- // If not, onAdded will handle the rest.
+ if (selectedWorkflowName === "None") {
+ this.title = "Workflow (FlowChain ⛓️)";
if (this.graph) {
- if (app.lipsync_studio[selectedWorkflowName] && app.lipsync_studio[selectedWorkflowName].inputs) {
- this.title = app.lipsync_studio[selectedWorkflowName].title || `Workflow: ${selectedWorkflowName}`;
- const inputs = app.lipsync_studio[selectedWorkflowName].inputs;
- addInputs(this, inputs, info.widgets_values || []);
- addOutputs(this, selectedWorkflowName);
- removeInputs(this, inputs, info.widgets_values || []);
- fitHeight(this);
- } else {
- // Data not yet in lipsync_studio, try to import.
- // The callback of widget[0] will handle this if triggered by value change,
- // or onAdded will pick it up.
- // For configure, we might just set the title and let onAdded handle full setup.
- this.title = `Workflow: ${selectedWorkflowName} (loading...)`;
- // Avoid calling importWorkflow directly here if onAdded will robustly handle it,
- // to prevent multiple calls.
- }
- } else {
- // Graph not yet available, title will be set, onAdded will do the heavy lifting.
- this.title = `Workflow: ${selectedWorkflowName}`;
+ // Only clean if graph is available
+ cleanInputs(this);
}
+ return;
+ }
+
+ // If the node is already on a graph, we can proceed with fuller setup.
+ // If not, onAdded will handle the rest.
+ if (this.graph) {
+ let inputs = [];
+ if (selectedWorkflowName in app.lipsync_studio) {
+ inputs = app.lipsync_studio[selectedWorkflowName].inputs;
+ } else {
+ if (selectedWorkflowName.replaceAll("\\", "/") in app.lipsync_studio) {
+ selectedWorkflowName = selectedWorkflowName.replaceAll("\\", "/");
+ info.widgets_values[0] = selectedWorkflowName;
+ this.widgets[0].value = selectedWorkflowName;
+ inputs = app.lipsync_studio[selectedWorkflowName].inputs;
+ } else {
+ selectedWorkflowName = selectedWorkflowName.replaceAll("/", "\\");
+ info.widgets_values[0] = selectedWorkflowName;
+ this.widgets[0].value = selectedWorkflowName;
+ inputs = app.lipsync_studio[selectedWorkflowName].inputs;
+ }
+ }
+
+ if (inputs) {
+ this.title =
+ app.lipsync_studio[selectedWorkflowName].title ||
+ `Workflow: ${selectedWorkflowName}`;
+ //const inputs = app.lipsync_studio[selectedWorkflowName].inputs;
+ addInputs(this, inputs, info.widgets_values || []);
+ addOutputs(this, selectedWorkflowName);
+ removeInputs(this, inputs, info.widgets_values || []);
+ fitHeight(this);
+ } else {
+ // Data not yet in lipsync_studio, try to import.
+ // The callback of widget[0] will handle this if triggered by value change,
+ // or onAdded will pick it up.
+ // For configure, we might just set the title and let onAdded handle full setup.
+ this.title = `Workflow: ${selectedWorkflowName} (loading...)`;
+ // Avoid calling importWorkflow directly here if onAdded will robustly handle it,
+ // to prevent multiple calls.
+ }
+ } else {
+ // Graph not yet available, title will be set, onAdded will do the heavy lifting.
+ this.title = `Workflow: ${selectedWorkflowName}`;
+ }
}
function serialize(info) {
- // Standard serialization of widget values is usually handled by LiteGraph.
- // This custom serialize can ensure specific data is captured if needed.
- // info.widgets_values will be populated by LiteGraph based on current widget values.
+ // Standard serialization of widget values is usually handled by LiteGraph.
+ // This custom serialize can ensure specific data is captured if needed.
+ // info.widgets_values will be populated by LiteGraph based on current widget values.
- // If local_input_defs logic is still needed for type adjustments:
- if (this.inputs && this.local_input_defs && this.local_input_defs.required) {
- for (let inp of this.inputs) {
- if (inp.widget &&
- this.local_input_defs.required[inp.name] &&
- this.local_input_defs.required[inp.name][0] !== undefined &&
- inp.type !== this.local_input_defs.required[inp.name][0]) {
- inp.type = this.local_input_defs.required[inp.name][0];
- }
- }
+ // If local_input_defs logic is still needed for type adjustments:
+ if (this.inputs && this.local_input_defs && this.local_input_defs.required) {
+ for (let inp of this.inputs) {
+ if (
+ inp.widget &&
+ this.local_input_defs.required[inp.name] &&
+ this.local_input_defs.required[inp.name][0] !== undefined &&
+ inp.type !== this.local_input_defs.required[inp.name][0]
+ ) {
+ inp.type = this.local_input_defs.required[inp.name][0];
+ }
}
+ }
}
export function setupWorkflowNode(nodeType) {
- const originalOnAdded = nodeType.prototype.onAdded;
- nodeType.prototype.onAdded = function(graph) {
- if (originalOnAdded) {
- originalOnAdded.apply(this, arguments);
- }
- initialisation_onAdded(this); // Our graph-dependent setup
- };
+ const originalOnAdded = nodeType.prototype.onAdded;
+ nodeType.prototype.onAdded = function (graph) {
+ if (originalOnAdded) {
+ originalOnAdded.apply(this, arguments);
+ }
+ initialisation_onAdded(this); // Our graph-dependent setup
+ };
- const originalOnRemoved = nodeType.prototype.onRemoved;
- nodeType.prototype.onRemoved = function() {
- // Perform any cleanup specific to this node when removed from graph
- // For example, disconnecting callbacks or releasing resources
- if (originalOnRemoved) {
- originalOnRemoved.apply(this, arguments);
- }
- };
+ const originalOnRemoved = nodeType.prototype.onRemoved;
+ nodeType.prototype.onRemoved = function () {
+ // Perform any cleanup specific to this node when removed from graph
+ // For example, disconnecting callbacks or releasing resources
+ if (originalOnRemoved) {
+ originalOnRemoved.apply(this, arguments);
+ }
+ };
- nodeType.prototype.onNodeCreated = function() {
- // This is one of the first lifecycle hooks.
- // Initialize properties that don't depend on the graph.
- this.local_input_defs = this.local_input_defs || { required: {} };
-
- // Add essential widgets if not already present (LiteGraph usually handles this from type definition)
- // For a "COMBO" and a "STRING" (hidden)
- if (!this.widgets || this.widgets.length < 2) {
- this.addWidget("combo", "Workflow", "None", () => {}, { values: ["None"] });
- this.addWidget("string", "workflow_json", "", () => {}, { multiline: true });
- }
-
- initialisation_preGraph(this); // Our graph-independent setup
+ nodeType.prototype.onNodeCreated = function () {
+ // This is one of the first lifecycle hooks.
+ // Initialize properties that don't depend on the graph.
+ this.local_input_defs = this.local_input_defs || { required: {} };
- chainCallback(this, "onConfigure", configure);
- chainCallback(this, "onSerialize", serialize);
- };
-}
\ No newline at end of file
+ // Add essential widgets if not already present (LiteGraph usually handles this from type definition)
+ // For a "COMBO" and a "STRING" (hidden)
+ if (!this.widgets || this.widgets.length < 2) {
+ this.addWidget("combo", "Workflow", "None", () => {}, {
+ values: ["None"],
+ });
+ this.addWidget("string", "workflow_json", "", () => {}, {
+ multiline: true,
+ });
+ }
+
+ initialisation_preGraph(this); // Our graph-independent setup
+
+ chainCallback(this, "onConfigure", configure);
+ chainCallback(this, "onSerialize", serialize);
+ };
+}