diff --git a/py/endpoints.py b/py/endpoints.py index 1de17ee..9befc9b 100644 --- a/py/endpoints.py +++ b/py/endpoints.py @@ -9,7 +9,6 @@ def get_workflow_data(workflow_file): nodes = {"WorkflowInput": {}, "WorkflowOutput": {}} # Extraire les nœuds WorkflowInput et WorkflowOutput - # print(workflow_file["nodes"]); for node in workflow_file["nodes"]: node_type = node.get("type") diff --git a/web/js/inputs.js b/web/js/inputs.js index ea3c6e7..33ff23f 100644 --- a/web/js/inputs.js +++ b/web/js/inputs.js @@ -168,12 +168,14 @@ export function addInputs(node, inputs, widgets_values) { (input) => input.value !== undefined && input.value !== null ); // replace value in widget_inputs with the value in widgets_values - if (widgets_values) { + if (widgets_values && widgets_values.length > 2) { + // widgets_values[0] = workflow name, widgets_values[1] = workflow JSON + // Input values start from index 2 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) + if (widget_value !== undefined && widget_value !== null) for (let j = 0; j < mapped_input.length; j++) if (mapped_input[j].name == widget_input.name) { mapped_input[j].value = widget_value; @@ -421,6 +423,35 @@ export function addInputs(node, inputs, widgets_values) { }*/ } + // Update widgets_values array to reflect current widget values + if (node.widgets && node.widgets.length > 0) { + // Ensure widgets_values array exists and has correct length + if (!node.widgets_values) { + node.widgets_values = []; + } + + // Update widgets_values with current widget values + // widgets_values[0] = workflow name, widgets_values[1] = workflow JSON + // Input values start from index 2 + for (let i = 0; i < node.widgets.length; i++) { + const widget = node.widgets[i]; + const valueIndex = 2 + i; + + // Ensure the array is long enough + while (node.widgets_values.length <= valueIndex) { + node.widgets_values.push(undefined); + } + + // Update the value, but don't overwrite workflow JSON if it's "false" + if (valueIndex === 1 && widget.value === "false") { + // Don't update the workflow JSON if it's "false" + continue; + } + + node.widgets_values[valueIndex] = widget.value; + } + } + // Rafraîchir le canvas si nécessaire if (node.graph) { node.graph.setDirtyCanvas(false, true); diff --git a/web/js/nodetype_workflow.js b/web/js/nodetype_workflow.js index acf7638..f6ae209 100644 --- a/web/js/nodetype_workflow.js +++ b/web/js/nodetype_workflow.js @@ -101,37 +101,24 @@ function initialisation_preGraph(node) { } else if (app && app.lipsync_studio && app.lipsync_studio[value]) { try { let workflowJSON = await importWorkflow(node, value, app); // importWorkflow updates node.title + + // Check if importWorkflow returned false (error case) + if (workflowJSON === false) { + console.error(`Failed to import workflow: ${value}`); + node.title = "Workflow (FlowChain ⛓️)"; // Reset title on error + return; + } + workflowJSON = JSON.parse(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; - const outputs = app.lipsync_studio[value].outputs; - for (let [key, value] of Object.entries(inputs)) { - if ( - value["inputs"][2]?.values && - value["inputs"][2].values.length > 0 - ) { - workflowJSON[key]["inputs"]["type"] = "COMBO"; - } else { - workflowJSON[key]["inputs"]["type"] = value["inputs"][1]; - } - } - for (let [key, value] of Object.entries(outputs)) { - if (value["inputs"].length === undefined) { - workflowJSON[key]["inputs"]["type"] = - value["inputs"].type.value; - } else { - workflowJSON[key]["inputs"]["type"] = value["inputs"][1]; - } - } - workflowJSON = JSON.stringify(workflowJSON); - + // We no longer mutate workflowJSON by numeric keys; IDs may differ after conversion if (node.widgets && node.widgets[1]) { - node.widgets[1].value = workflowJSON; + node.widgets[1].value = ""; // keep hidden field empty to avoid bloat } - - addInputs(node, inputs, []); // Requires node.graph - addOutputs(node, value); // Requires node.graph + addInputs(node, inputs, node.widgets_values || []); + addOutputs(node, value); fitHeight(node); } else { console.error( @@ -184,6 +171,7 @@ function initialisation_onAdded(node) { if ( node.widgets[1] && (node.widgets[1].value === "" || + node.widgets[1].value === "false" || typeof node.widgets[1].value !== "string" || !node.widgets[1].value.startsWith("{")) ) { @@ -245,9 +233,13 @@ function configure(info) { this.widgets && this.widgets[1] && info.widgets_values && - info.widgets_values[1] + info.widgets_values[1] && + info.widgets_values[1] !== "false" ) { this.widgets[1].value = info.widgets_values[1]; // Set the hidden workflow JSON + } else if (info.widgets_values && info.widgets_values[1] === "false") { + // If the stored value is "false", clear it and trigger re-import + this.widgets[1].value = ""; } if (selectedWorkflowName === "None") { @@ -296,20 +288,30 @@ function configure(info) { const outputs = app.lipsync_studio[selectedWorkflowName].outputs; for (let [key, value] of Object.entries(inputs)) { - data_json[key]["inputs"]["type"] = value["inputs"][1]; + // Check if the key exists in data_json before accessing it + if (data_json[key] && data_json[key]["inputs"]) { + data_json[key]["inputs"]["type"] = value["inputs"][1]; + } else { + console.warn(`Key '${key}' not found in data_json for inputs processing`); + } } for (let [key, value] of Object.entries(outputs)) { - if (value["inputs"].length === undefined) { - data_json[key]["inputs"]["type"] = value["inputs"].type.value; - } else { - if ( - value["inputs"][2]?.values && - value["inputs"][2].values.length > 0 - ) { - data_json[key]["inputs"]["type"] = "COMBO"; + // Check if the key exists in data_json before accessing it + if (data_json[key] && data_json[key]["inputs"]) { + if (value["inputs"].length === undefined) { + data_json[key]["inputs"]["type"] = value["inputs"].type.value; } else { - data_json[key]["inputs"]["type"] = value["inputs"][1]; + if ( + value["inputs"][2]?.values && + value["inputs"][2].values.length > 0 + ) { + data_json[key]["inputs"]["type"] = "COMBO"; + } else { + data_json[key]["inputs"]["type"] = value["inputs"][1]; + } } + } else { + console.warn(`Key '${key}' not found in data_json for outputs processing`); } } this.widgets[1].value = JSON.stringify(data_json); @@ -395,6 +397,12 @@ export function setupWorkflowNode(nodeType) { initialisation_preGraph(this); // Our graph-independent setup chainCallback(this, "onConfigure", configure); - chainCallback(this, "onSerialize", serialize); + chainCallback(this, "onSerialize", function(info){ + // Ensure workflow_json is not persisted to avoid runaway nested JSON + if (info.widgets_values && info.widgets_values.length > 1) { + info.widgets_values[1] = ""; + } + serialize.call(this, info); + }); }; } diff --git a/web/js/workflows.js b/web/js/workflows.js index a378000..c77d051 100644 --- a/web/js/workflows.js +++ b/web/js/workflows.js @@ -27,14 +27,13 @@ async function convertWorkflowToApiFormat(standardWorkflow) { const originalGraph = app.graph; // Utiliser graphToPrompt en mode isolé - app.graph = tempGraph; + // Instead of setting app.graph directly, we'll pass the tempGraph to graphToPrompt // Configurer sans déclencher de callbacks tempGraph.configure(standardWorkflow); app.graphToPrompt(tempGraph) .then(apiData => { - // Restaurer le graphe original - app.graph = originalGraph; + // No need to restore app.graph since we didn't change it // Résoudre avec le format API resolve(apiData.output); @@ -89,7 +88,6 @@ export async function importWorkflow(root_obj, workflow_path, app){ const filename = workflow_path.replace(/\\/g, '/').split("/"); root_obj.title = "Workflow: "+filename[filename.length-1].replace(".json", "").replace(/_/g, " "); - return api.fetchApi("/flowchain/workflow?workflow_path="+workflow_path) .then(response => response.json()) .then(async data => { diff --git a/workflow.py b/workflow.py index 88a5f5d..5f0b987 100644 --- a/workflow.py +++ b/workflow.py @@ -119,7 +119,7 @@ def IS_CHANGED(s, workflows, workflow, **kwargs): return m.digest().hex() - def generate(self, workflows, workflow, unique_id, **kwargs): + async def generate(self, workflows, workflow, unique_id, **kwargs): def populate_inputs(workflow, inputs, kwargs_values): workflow_inputs = {k: v for k, v in workflow.items() if "class_type" in v and v["class_type"] == "WorkflowInput"} @@ -354,39 +354,142 @@ def merge_inputs_outputs(workflow, workflow_name, subworkflow, workflow_outputs, if sub_output_node["inputs"]["Name"] == workflow_outputs[input_value[1]]["inputs"]["Name"]: workflow[node_id]["inputs"][input_name] = sub_output_node["inputs"]["default"] - # remove output node - subworkflow = {k: v for k, v in subworkflow.items() if not ("class_type" in v and v["class_type"] == "WorkflowOutput")} - return workflow, subworkflow, do_not_delete def clean_workflow(workflow, inputs=None, kwargs_values=None): + # Ensure workflow is a dict (not a raw JSON string) + if isinstance(workflow, str): + try: + workflow = json.loads(workflow) + except Exception: + workflow = {} if kwargs_values is None: kwargs_values = {} if inputs is None: inputs = {} if inputs is not None: workflow = populate_inputs(workflow, inputs, kwargs_values) + + # From here on, perform cleaning and return results + # Log current workflow for debugging + print(f"Workflow: {workflow}") workflow_outputs = {k: v for k, v in workflow.items() if "class_type" in v and v["class_type"] == "WorkflowOutput"} - for output_id, output_node in workflow_outputs.items(): + # Keep UI nodes so we can still collect outputs; just hide them + for output_id, _output_node in workflow_outputs.items(): workflow[output_id]["inputs"]["ui"] = False + # Remove switch/continue structures and their dead branches workflow, switch_to_delete = treat_switch(workflow) workflow, continue_to_delete = treat_continue(workflow) workflow = recursive_delete(workflow, switch_to_delete + continue_to_delete) + + # Recompute outputs after pruning just in case + workflow_outputs = {k: v for k, v in workflow.items() if "class_type" in v and v["class_type"] == "WorkflowOutput"} return workflow, workflow_outputs def get_recursive_workflow(workflow_name, workflows, max_id=0,do_not_delete = []): - # if workflows[-5:] == ".json": - # workflow = get_workflow(workflows) - # else: - try: - if workflows == "{}": - raise ValueError("Empty workflow.") - workflow = json.loads(workflows) - except: - raise RuntimeError(f"Error while loading workflow: {workflow_name}, See for more information.") + # Decide whether to load from file or parse JSON content + # If the provided workflows content is empty, use the selected workflow_name file + workflows_source = workflows + if workflows_source is None or (isinstance(workflows_source, str) and workflows_source.strip() in ("", "{}")): + workflows_source = workflow_name + + # Check if workflows_source is a filename (contains .json) or JSON content + # A filename should be a simple path, not JSON content + is_filename = ( + isinstance(workflows_source, str) + and workflows_source.endswith('.json') + and not workflows_source.startswith('{') + and not workflows_source.startswith('[') + and len(workflows_source) < 200 + ) + + if is_filename: + # It's a filename, load the file content + workflow_file_path = os.path.join(folder_paths.user_directory, "default", "workflows", workflows_source) + if not os.path.exists(workflow_file_path): + raise RuntimeError(f"Workflow file not found: {workflow_file_path}") + + try: + with open(workflow_file_path, "r", encoding="utf-8") as f: + workflow_data = json.load(f) + + # Convert from ComfyUI format to FlowChain format if needed + if "nodes" in workflow_data: + # Convert ComfyUI format to FlowChain format + workflow = {} + # Build a map of node_id -> ordered input names + input_names_by_node = {} + for node in workflow_data["nodes"]: + input_names_by_node[str(node["id"])] = [i.get("name") for i in node.get("inputs", [])] + for node in workflow_data["nodes"]: + node_id = str(node["id"]) + workflow[node_id] = { + "class_type": node["type"], + "inputs": {} + } + + # Map widget values positionally for inputs that have widgets and are not linked + widget_values = node.get("widgets_values", []) + widget_idx = 0 + for input_def in node.get("inputs", []): + input_name = input_def["name"] + has_widget = "widget" in input_def and input_def["widget"] is not None + is_connected = input_def.get("link") is not None + if has_widget: + if not is_connected: + if widget_idx < len(widget_values): + workflow[node_id]["inputs"][input_name] = widget_values[widget_idx] + # advance index regardless to keep alignment with widgets_values ordering + widget_idx += 1 + + # Handle links between nodes - ComfyUI format + if "links" in workflow_data: + for link in workflow_data["links"]: + # ComfyUI link format: [link_id, source_node, source_slot, target_node, target_slot, type] + source_node = str(link[1]) + source_slot = int(link[2]) if isinstance(link[2], int) else 0 + target_node = str(link[3]) + target_slot = link[4] + # Translate target slot index to the actual input name if needed + if isinstance(target_slot, int): + input_names = input_names_by_node.get(target_node, []) + target_input_name = input_names[target_slot] if target_slot < len(input_names) else str(target_slot) + else: + target_input_name = target_slot + + if source_node in workflow and target_node in workflow: + # Set connection as [source_node, source_slot] + workflow[target_node]["inputs"][target_input_name] = [source_node, source_slot] + + # Add widget values for WorkflowInput and WorkflowOutput nodes + for node in workflow_data.get("nodes", []): + node_id = str(node["id"]) + if node["type"] in ["WorkflowInput", "WorkflowOutput"] and "widgets_values" in node: + widget_values = node["widgets_values"] + if node["type"] == "WorkflowInput": + if len(widget_values) > 0: + workflow[node_id]["inputs"]["Name"] = widget_values[0] + elif node["type"] == "WorkflowOutput": + if len(widget_values) > 0: + workflow[node_id]["inputs"]["Name"] = widget_values[0] + + except Exception as e: + raise RuntimeError(f"Error while loading workflow file {workflow_file_path}: {str(e)}") + else: + # It's JSON content, parse it directly + try: + if workflows_source == "{}" or workflows_source == "": + raise ValueError("Empty workflow.") + # If it's already a dict, keep as is + if isinstance(workflows_source, dict): + workflow = workflows_source + else: + workflow = json.loads(workflows_source) + except Exception as e: + raise RuntimeError(f"Error while parsing workflow JSON: {str(e)}") workflow, max_id = redefine_id(workflow, max_id) sub_workflows = {k: v for k, v in workflow.items() if "class_type" in v and v["class_type"] == "Workflow"} @@ -394,7 +497,7 @@ def get_recursive_workflow(workflow_name, workflows, max_id=0,do_not_delete = [] for key, sub_workflow_node in sub_workflows.items(): workflow_json = sub_workflow_node["inputs"]["workflow"] sub_workflow_name = sub_workflow_node["inputs"]["workflows"] - subworkflow, max_id, do_not_delete = get_recursive_workflow(sub_workflow_name, workflow_json, max_id, do_not_delete) + subworkflow, max_id, do_not_delete = get_recursive_workflow(sub_workflow_name, sub_workflow_name, max_id, do_not_delete) # get sub workflow file path sub_workflow_file_path = os.path.join(folder_paths.user_directory, "default", "workflows", sub_workflow_name) @@ -455,7 +558,17 @@ def get_recursive_workflow(workflow_name, workflows, max_id=0,do_not_delete = [] original_inputs = {} workflow, _, _ = get_recursive_workflow(workflows, workflow, 5000, []) + # Safety: ensure we have a dict workflow (not a raw string) + if isinstance(workflow, str): + try: + workflow = json.loads(workflow) + except Exception: + workflow = {} + print(f"Converted workflow has {len(workflow)} nodes") + print(f"Workflow node types: {[node.get('class_type', 'unknown') for node in workflow.values()]}") + workflow, workflow_outputs = clean_workflow(workflow, original_inputs, kwargs) + print(f"After cleaning: {len(workflow)} nodes, {len(workflow_outputs)} outputs") # Accéder au fichier JSON original pour obtenir les positions correctes workflow_file_path = os.path.join(folder_paths.user_directory, "default", "workflows", workflows) @@ -513,9 +626,17 @@ def send_sync(self, *args, **kwargs): simple_server = SimpleServer() executor = PromptExecutor(simple_server) - executor.execute(workflow, prompt_id, {"client_id": client_id}, workflow_outputs_id) + print(f"Executing workflow with {len(workflow)} nodes") + print(f"Workflow outputs to execute: {workflow_outputs_id}") + + await executor.execute_async(workflow, prompt_id, {"client_id": client_id}, workflow_outputs_id) history_result = executor.history_result + print(f"Execution completed. History result keys: {list(history_result.keys())}") + if "outputs" in history_result: + print(f"Outputs: {list(history_result['outputs'].keys())}") + for output_id, output_data in history_result["outputs"].items(): + print(f"Output {output_id}: {type(output_data.get('default', 'No default'))}") comfy.model_management.unload_all_models() gc.collect()